USDGBuyerDistributor

0x2459dedb3012d1e929edd17df26620120bdf11bf

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
12,906 bytes
ABI entries
85
28 read · 17 write
License
none

Contract information

Address
0x2459dedb3012d1e929edd17df26620120bdf11bf
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x0972817736…0ef8f57808

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (28)

canStart()bool
cursor()uint256
cycleActive()bool
eligible()uint256
indexToken()address
interval()uint256
isStock(address)bool
keeper(address)bool
maxSpendPerBuy()uint256
nextDistribution()uint256
owner()address
paused()bool
poolManager()address
registry()address
registryTokenId()uint256
remaining()uint256
resolvedRouterSafe()address
router()address
snapCount()uint256
snapPending()bool
snapshotRemaining()uint256
stockTokenAt(uint256)address
stocks(uint256)address, uint24, int24, address
stocksLength()uint256
usdg()address
usdgEthFee()uint24
usdgEthHooks()address
usdgEthTickSpacing()int24

Events (18)

BatchPaidCycleStartedDistributedForeignSweptHoldersSnapshottedIntervalSetKeeperSetMaxSpendPerBuySetOwnershipTransferredPausedPayoutSkippedRialtoBoughtRialtoBuyFailedStockAddedStockRemovedUnpausedUsdgEthPoolSetV4FallbackBought

ABI

[
  {
    "inputs": [
      {
        "internalType": "contract IIndexToken",
        "name": "indexToken_",
        "type": "address"
      },
      {
        "internalType": "contract IPoolManager",
        "name": "pm_",
        "type": "address"
      },
      {
        "internalType": "contract IERC20",
        "name": "usdg_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "registry_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "registryTokenId_",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "owner_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "interval_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "maxSpendPerBuy_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadAmount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "CycleInProgress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "DuplicateStock",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "EnforcedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ExpectedPause",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LengthMismatch",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoCycle",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoDistributionPot",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoEligibleHolders",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotSweepable",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyKeeper",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlySelf",
    "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": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "out",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minOut",
        "type": "uint256"
      }
    ],
    "name": "Slippage",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "SnapshotIncomplete",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TooEarly",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "router",
        "type": "address"
      }
    ],
    "name": "UnsafeRouter",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "from",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "to",
        "type": "uint256"
      }
    ],
    "name": "BatchPaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "holders",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "eligible",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "stocks",
        "type": "uint256"
      }
    ],
    "name": "CycleStarted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "stock",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "holders",
        "type": "uint256"
      }
    ],
    "name": "Distributed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ForeignSwept",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "snapCount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "holderCount",
        "type": "uint256"
      }
    ],
    "name": "HoldersSnapshotted",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "IntervalSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "keeper",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "on",
        "type": "bool"
      }
    ],
    "name": "KeeperSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "MaxSpendPerBuySet",
    "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": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Paused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "stock",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "holder",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "PayoutSkipped",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "stockIndex",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "usdgSpent",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "out",
        "type": "uint256"
      }
    ],
    "name": "RialtoBought",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "stockIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bytes",
        "name": "reason",
        "type": "bytes"
      }
    ],
    "name": "RialtoBuyFailed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "hooks",
        "type": "address"
      }
    ],
    "name": "StockAdded",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "StockRemoved",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "Unpaused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "indexed": false,
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "hooks",
        "type": "address"
      }
    ],
    "name": "UsdgEthPoolSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "stockIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "usdgIn",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "out",
        "type": "uint256"
      }
    ],
    "name": "V4FallbackBought",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "router",
        "type": "address"
      },
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "stockIndex",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "sellAmount",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minOut",
            "type": "uint256"
          },
          {
            "internalType": "bytes",
            "name": "data",
            "type": "bytes"
          }
        ],
        "internalType": "struct USDGBuyerDistributor.BuyOrder",
        "name": "o",
        "type": "tuple"
      }
    ],
    "name": "_rialtoBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "out",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "internalType": "address",
        "name": "hooks",
        "type": "address"
      }
    ],
    "name": "addStock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "components": [
          {
            "internalType": "uint256",
            "name": "stockIndex",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "sellAmount",
            "type": "uint256"
          },
          {
            "internalType": "uint256",
            "name": "minOut",
            "type": "uint256"
          },
          {
            "internalType": "bytes",
            "name": "data",
            "type": "bytes"
          }
        ],
        "internalType": "struct USDGBuyerDistributor.BuyOrder[]",
        "name": "orders",
        "type": "tuple[]"
      }
    ],
    "name": "buyStocksRialto",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256[]",
        "name": "stockIndices",
        "type": "uint256[]"
      },
      {
        "internalType": "uint256[]",
        "name": "usdgAmounts",
        "type": "uint256[]"
      },
      {
        "internalType": "uint256[]",
        "name": "minOuts",
        "type": "uint256[]"
      }
    ],
    "name": "buyStocksV4Fallback",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "canStart",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cursor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "cycleActive",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "distributeBatch",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "eligible",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "indexToken",
    "outputs": [
      {
        "internalType": "contract IIndexToken",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "interval",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isStock",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "keeper",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxSpendPerBuy",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nextDistribution",
    "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": "paused",
    "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": "registry",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "registryTokenId",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "remaining",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "i",
        "type": "uint256"
      }
    ],
    "name": "removeStock",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "resolvedRouterSafe",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "router",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setInterval",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "k",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "on",
        "type": "bool"
      }
    ],
    "name": "setKeeper",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "v",
        "type": "uint256"
      }
    ],
    "name": "setMaxSpendPerBuy",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bool",
        "name": "p",
        "type": "bool"
      }
    ],
    "name": "setPaused",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "internalType": "address",
        "name": "hooks",
        "type": "address"
      }
    ],
    "name": "setUsdgEthPool",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "snapCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "snapPending",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "snapshotHolders",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "snapshotRemaining",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "startCycle",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "i",
        "type": "uint256"
      }
    ],
    "name": "stockTokenAt",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "stocks",
    "outputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "fee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "tickSpacing",
        "type": "int24"
      },
      {
        "internalType": "address",
        "name": "hooks",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "stocksLength",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      }
    ],
    "name": "sweepForeign",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "usdg",
    "outputs": [
      {
        "internalType": "contract IERC20",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "usdgEthFee",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "usdgEthHooks",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "usdgEthTickSpacing",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

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

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
import {Currency} from "v4-core/src/types/Currency.sol";
import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol";
import {SwapParams} from "v4-core/src/types/PoolOperation.sol";

interface IRouterRegistry {
    function ownerOf(uint256 featureId) external view returns (address);
}

/// Minimal view of the $INDEX ReflectionToken — the holder registry this distributor pays out to.
interface IIndexToken {
    function holderCount() external view returns (uint256);
    function holderAt(uint256 i) external view returns (address);
    function balanceOf(address a) external view returns (uint256);
}

/// Buys the tokenized-stock basket with USDG through Rialto, then distributes the bought stock
/// pro-rata to $INDEX holders. Set as `treasury.distributor`, so the treasury (reconfigured to buy
/// only USDG on the deep 0.046% ETH/USDG pool) deposits USDG here.
///
/// The load-bearing property: a keeper supplies opaque router calldata, which this contract executes
/// against an EXTERNAL router. That raw-call surface is locked down (see the SAFETY INVARIANTS in
/// `_resolveRouter` + `_rialtoBuy`) so a fully-compromised keeper can only price-grief a buy within
/// its own minOut through the REAL registry-resolved router — never reach the stock pot, the ETH,
/// the treasury, or any unrelated token, and never move funds off pro-rata to holders.
///
/// There is NO owner or keeper fund-extraction path: every USDG exit is a locked buy of a registered
/// stock that then goes pro-rata to holders. `sweepForeign` reverts on USDG and every registered
/// stock. See docs/RIALTO_HELPER_SPEC.md for the full design + the immutable-registry disclosure.
contract USDGBuyerDistributor is Ownable, Pausable, ReentrancyGuard, IUnlockCallback {
    using SafeERC20 for IERC20;

    // ── immutable trust anchors (never injectable) ──────────────────────────────────────────────
    IIndexToken public immutable indexToken;
    IPoolManager public immutable poolManager;
    IERC20 public immutable usdg; // 6-dp sell token
    address public immutable registry; // Rialto router registry
    uint256 public immutable registryTokenId; // = 2 (ownerOf(2) -> live taker-submitted router)

    // ── owner-settable config ───────────────────────────────────────────────────────────────────
    struct Stock { address token; uint24 fee; int24 tickSpacing; address hooks; }
    Stock[] public stocks;
    mapping(address => bool) public isStock;
    mapping(address => bool) public keeper;
    uint256 public maxSpendPerBuy; // USDG (6-dp) cap per order — bounds keeper-compromise blast radius
    uint256 public interval;
    // USDG/ETH fallback hop-A pool params (default the 0.046% pool: 460 / 9 / 0x0)
    uint24 public usdgEthFee = 460;
    int24 public usdgEthTickSpacing = 9;
    address public usdgEthHooks = address(0);

    // ── distribution snapshot state (verbatim StockDistributorV2) ───────────────────────────────
    address[] private _holders;
    uint256[] private _bals;
    address[] private _stock;
    uint256[] private _pot;
    uint256 public eligible;
    uint256 public cursor;
    bool public cycleActive;
    uint256 public nextDistribution;
    uint256 public snapCount;   // holders captured in the pending paginated snapshot
    bool public snapPending;    // a snapshot is being built (between snapshotHolders and startCycle)

    // ── events ──────────────────────────────────────────────────────────────────────────────────
    event StockAdded(address indexed token, uint24 fee, int24 tickSpacing, address hooks);
    event StockRemoved(address indexed token);
    event KeeperSet(address indexed keeper, bool on);
    event MaxSpendPerBuySet(uint256 v);
    event IntervalSet(uint256 v);
    event UsdgEthPoolSet(uint24 fee, int24 tickSpacing, address hooks);
    event ForeignSwept(address indexed token, address indexed to, uint256 amount);
    event RialtoBought(uint256 indexed stockIndex, address indexed router, uint256 usdgSpent, uint256 out);
    event RialtoBuyFailed(uint256 indexed stockIndex, bytes reason);
    event V4FallbackBought(uint256 indexed stockIndex, uint256 usdgIn, uint256 out);
    event CycleStarted(uint256 holders, uint256 eligible, uint256 stocks);
    event HoldersSnapshotted(uint256 snapCount, uint256 holderCount);
    event BatchPaid(uint256 from, uint256 to);
    event Distributed(address indexed stock, uint256 amount, uint256 holders);
    event PayoutSkipped(address indexed stock, address indexed holder, uint256 amount);

    error OnlyKeeper();
    error OnlySelf();
    error OnlyPoolManager();
    error UnsafeRouter(address router);
    error BadAmount();
    error Slippage(uint256 out, uint256 minOut);
    error NotSweepable();
    error TooEarly();
    error CycleInProgress();
    error NoCycle();
    error LengthMismatch();
    error DuplicateStock();
    error SnapshotIncomplete();
    error NoDistributionPot();
    error NoEligibleHolders();

    modifier onlyKeeper() {
        if (!keeper[msg.sender] && msg.sender != owner()) revert OnlyKeeper();
        _;
    }

    constructor(
        IIndexToken indexToken_,
        IPoolManager pm_,
        IERC20 usdg_,
        address registry_,
        uint256 registryTokenId_,
        address owner_,
        uint256 interval_,
        uint256 maxSpendPerBuy_
    ) Ownable(owner_) {
        indexToken = indexToken_;
        poolManager = pm_;
        usdg = usdg_;
        registry = registry_;
        registryTokenId = registryTokenId_;
        interval = interval_;
        maxSpendPerBuy = maxSpendPerBuy_;
    }

    // ── Rialto buys ─────────────────────────────────────────────────────────────────────────────

    struct BuyOrder { uint256 stockIndex; uint256 sellAmount; uint256 minOut; bytes data; }

    /// Keeper batch buy. Each order runs in an isolated try/catch self-call, so one stale/failed/
    /// unavailable Rialto quote is skipped (emits RialtoBuyFailed) while the others still settle in
    /// this tx. Route any skipped stock through buyStocksV4Fallback.
    function buyStocksRialto(BuyOrder[] calldata orders) external nonReentrant whenNotPaused onlyKeeper {
        address router = _resolveRouter(); // INVARIANTS 1-3, once, on-chain
        for (uint256 i; i < orders.length; ++i) {
            try this._rialtoBuy(router, orders[i]) returns (uint256 out) {
                emit RialtoBought(orders[i].stockIndex, router, orders[i].sellAmount, out);
            } catch (bytes memory reason) {
                emit RialtoBuyFailed(orders[i].stockIndex, reason);
            }
        }
    }

    /// Execute ONE order under the full safety invariants. `onlySelf` — no external caller can inject
    /// `router`; it is always the on-chain-resolved value from buyStocksRialto. Runs under the outer
    /// nonReentrant lock, so it is deliberately not itself nonReentrant (that would revert try/catch).
    function _rialtoBuy(address router, BuyOrder calldata o) external returns (uint256 out) {
        if (msg.sender != address(this)) revert OnlySelf();

        Stock storage s = stocks[o.stockIndex]; // INVARIANT 4: token by index (OOB reverts)
        IERC20 stock = IERC20(s.token);

        // INVARIANT 5: amount bounds
        uint256 bal = usdg.balanceOf(address(this));
        if (o.sellAmount == 0 || o.sellAmount > maxSpendPerBuy || o.sellAmount > bal || o.minOut == 0) revert BadAmount();

        uint256 beforeStock = stock.balanceOf(address(this));
        uint256 beforeUsdg = bal;

        // INVARIANT 7: exact approve on USDG ONLY (never a stock), reset to 0 after — unconditional
        usdg.forceApprove(router, o.sellAmount);
        // INVARIANT 6 + 8: value hardcoded 0; require success, bubble reason
        (bool ok, bytes memory ret) = router.call{value: 0}(o.data);
        usdg.forceApprove(router, 0);
        if (!ok) {
            assembly { revert(add(ret, 0x20), mload(ret)) }
        }

        // INVARIANT 9: output = measured balance delta of the EXPECTED stock, not router return data
        out = stock.balanceOf(address(this)) - beforeStock;
        if (out < o.minOut) revert Slippage(out, o.minOut);
        // INVARIANT 10: input spend sanity (allowance already caps this; defensive)
        uint256 spent = beforeUsdg - usdg.balanceOf(address(this));
        if (spent > o.sellAmount) revert BadAmount();
    }

    /// Resolve the live router and enforce INVARIANTS 1-3 before any allowance is granted.
    function _resolveRouter() internal view returns (address router) {
        // INVARIANT 1: target locked to the registry-resolved router (keeper cannot supply it)
        router = IRouterRegistry(registry).ownerOf(registryTokenId);
        // INVARIANT 2: router has code (rejects bricked / migrated / EOA / zero)
        // INVARIANT 3: safe-router reject-list (kills poison-registry -> approve+call-as-transfer)
        if (
            router == address(0) || router.code.length == 0 || router == address(this)
                || router == address(usdg) || router == address(poolManager) || router == address(indexToken)
                || isStock[router]
        ) revert UnsafeRouter(router);
    }

    /// Router the buys would use right now, with the full safety check applied — for keeper preflight.
    function resolvedRouterSafe() external view returns (address) {
        return _resolveRouter();
    }

    /// Raw registry read (no safety check) — informational.
    function router() external view returns (address) {
        return IRouterRegistry(registry).ownerOf(registryTokenId);
    }

    // ── v4 two-hop fallback (USDG -> ETH -> stock), no raw calldata ──────────────────────────────

    /// Liquidity-of-last-resort for stocks with no Rialto quote. All pool keys come from stored
    /// config; the keeper supplies only indices, amounts, and minOuts.
    function buyStocksV4Fallback(uint256[] calldata stockIndices, uint256[] calldata usdgAmounts, uint256[] calldata minOuts)
        external nonReentrant whenNotPaused onlyKeeper
    {
        if (stockIndices.length != usdgAmounts.length || stockIndices.length != minOuts.length) revert LengthMismatch();
        poolManager.unlock(abi.encode(stockIndices, usdgAmounts, minOuts));
    }

    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert OnlyPoolManager();
        (uint256[] memory idx, uint256[] memory amts, uint256[] memory minOuts) =
            abi.decode(data, (uint256[], uint256[], uint256[]));

        PoolKey memory usdgKey = PoolKey({
            currency0: Currency.wrap(address(0)),
            currency1: Currency.wrap(address(usdg)),
            fee: usdgEthFee,
            tickSpacing: usdgEthTickSpacing,
            hooks: IHooks(usdgEthHooks)
        });

        uint256 totalUsdg;
        for (uint256 i; i < idx.length; ++i) {
            // minOut > 0 required, mirroring the Rialto path — a zero floor on a public v4 pool is
            // fully sandwichable.
            if (amts[i] == 0 || amts[i] > maxSpendPerBuy || minOuts[i] == 0) revert BadAmount();
            Stock storage s = stocks[idx[i]];
            totalUsdg += amts[i];

            // Hop A: USDG (currency1) -> ETH (currency0). exact input of USDG.
            BalanceDelta dA = poolManager.swap(
                usdgKey,
                SwapParams({zeroForOne: false, amountSpecified: -int256(amts[i]), sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1}),
                ""
            );
            uint256 ethMid = uint256(uint128(dA.amount0())); // ETH received

            // Hop B: ETH -> stock. exact input of the ETH from hop A (nets ETH to 0).
            BalanceDelta dB = poolManager.swap(
                PoolKey({currency0: Currency.wrap(address(0)), currency1: Currency.wrap(s.token), fee: s.fee, tickSpacing: s.tickSpacing, hooks: IHooks(s.hooks)}),
                SwapParams({zeroForOne: true, amountSpecified: -int256(ethMid), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1}),
                ""
            );
            uint256 out = uint256(uint128(dB.amount1()));
            if (out < minOuts[i]) revert Slippage(out, minOuts[i]);
            poolManager.take(Currency.wrap(s.token), address(this), out);
            emit V4FallbackBought(idx[i], amts[i], out);
        }

        // settle the USDG we owe (ETH nets to zero across the paired hops)
        poolManager.sync(Currency.wrap(address(usdg)));
        usdg.safeTransfer(address(poolManager), totalUsdg);
        poolManager.settle();
        return "";
    }

    // ── paginated snapshot + distribution ────────────────────────────────────────────────────────

    /// Capture up to `count` more holders into the pending cycle snapshot. Permissionless. The first
    /// cold snapshot of the holder registry is ~2 storage writes/holder and would exceed the chain's
    /// per-tx gas cap past ~700 holders, so a large registry is snapshotted across several txs, then
    /// finalized by startCycle(). The first call (snapPending == false) resets and starts fresh.
    ///
    /// Registry churn during the snapshot is bounded and safe — no double-pay, no brick. The registry
    /// swap-pops on holder removal, so a holder that fully exits mid-snapshot is either already captured
    /// (paid its snapshot balance, which it held) or skipped and rolled to next cycle; a holder that
    /// joins is captured if it lands before the cursor, else rolled over. Each index is read exactly
    /// once, and total payout is always bounded by the frozen pot.
    function snapshotHolders(uint256 count) external nonReentrant {
        if (cycleActive) revert CycleInProgress();
        if (block.timestamp < nextDistribution) revert TooEarly();
        if (!snapPending) {
            // Don't begin a snapshot (permissionless, and O(holders) gas) for an empty cycle — there
            // must be stock to distribute. Stock can only leave via distributeBatch (needs cycleActive)
            // so the pot can't drain between here and startCycle's finalize.
            if (!_hasPot()) revert NoDistributionPot();
            delete _holders;
            delete _bals;
            delete _stock;
            delete _pot;
            eligible = 0;
            snapCount = 0;
            snapPending = true;
        }
        uint256 n = indexToken.holderCount();
        uint256 end = snapCount + count;
        if (end > n) end = n;
        uint256 elig = eligible;
        for (uint256 i = snapCount; i < end; ++i) {
            address h = indexToken.holderAt(i);
            uint256 b = indexToken.balanceOf(h);
            _holders.push(h);
            _bals.push(b);
            elig += b;
        }
        eligible = elig;
        snapCount = end;
        emit HoldersSnapshotted(snapCount, n);
    }

    /// How many holders still need snapshotting before startCycle can finalize (keeper convenience).
    function snapshotRemaining() external view returns (uint256) {
        uint256 n = indexToken.holderCount();
        uint256 done = snapPending ? snapCount : 0;
        return n > done ? n - done : 0;
    }

    function startCycle() external nonReentrant {
        if (cycleActive) revert CycleInProgress();
        if (block.timestamp < nextDistribution) revert TooEarly();

        if (snapPending) {
            // paginated path: the snapshot must cover every current holder (a grown registry needs
            // more snapshotHolders() calls first; a shrunk one is fine — the missing tail rolls over).
            if (snapCount < indexToken.holderCount()) revert SnapshotIncomplete();
            snapPending = false;
        } else {
            // single-shot path (small registry / tests): snapshot every holder inline.
            delete _holders;
            delete _bals;
            delete _stock;
            delete _pot;
            uint256 n = indexToken.holderCount();
            uint256 elig;
            for (uint256 i; i < n; ++i) {
                address h = indexToken.holderAt(i);
                uint256 b = indexToken.balanceOf(h);
                _holders.push(h);
                _bals.push(b);
                elig += b;
            }
            eligible = elig;
        }

        // No empty cycle: there must be eligible supply AND a nonzero pot before we advance
        // nextDistribution and open a cycle (a caller-agnostic guard; the keeper also checks off-chain).
        if (eligible == 0) revert NoEligibleHolders();
        uint256 m = stocks.length;
        uint256 totalPot;
        for (uint256 k; k < m; ++k) {
            address tok = stocks[k].token;
            uint256 p = _safeBalanceOf(tok); // a token whose balanceOf reverts/bombs => pot 0, never bricks the snapshot
            _stock.push(tok);
            _pot.push(p);
            totalPot += p;
        }
        if (totalPot == 0) revert NoDistributionPot();

        nextDistribution = block.timestamp + interval;
        cursor = 0;
        cycleActive = true;
        emit CycleStarted(_holders.length, eligible, m);
    }

    function distributeBatch(uint256 count) external nonReentrant {
        if (!cycleActive) revert NoCycle();
        uint256 n = _holders.length;
        uint256 end = cursor + count;
        if (end > n) end = n;

        uint256 elig = eligible;
        uint256 m = _stock.length;
        for (uint256 i = cursor; i < end; ++i) {
            uint256 b = _bals[i];
            if (b == 0) continue;
            address h = _holders[i];
            for (uint256 k; k < m; ++k) {
                uint256 amt = (_pot[k] * b) / elig; // dust stays for next cycle
                if (amt != 0) _trySend(_stock[k], h, amt);
            }
        }
        emit BatchPaid(cursor, end);
        cursor = end;

        if (end == n) {
            cycleActive = false;
            for (uint256 k; k < m; ++k) emit Distributed(_stock[k], _pot[k], n);
        }
    }

    /// Non-reverting stock transfer. A holder that cannot receive a (regulated / allowlisted) stock
    /// is SKIPPED, never allowed to revert the batch and brick the cycle. The skipped share stays in
    /// the pot and rolls into the next cycle's snapshot.
    ///
    /// Done in assembly (the SafeTransferLib pattern), NOT `token.call(...)` returning `bytes memory`,
    /// for two reasons — both bricks a malformed/upgraded regulated stock token could otherwise cause:
    ///   1. High-level `.call` copies the FULL returndata into memory before any check, so a token
    ///      returning megabytes would OOG this call under the 63/64 rule (a returndata bomb). We cap
    ///      the copy at 32 bytes, so the caller's cost is O(1) in the return size.
    ///   2. `abi.decode(ret,(bool))` itself reverts on a return shorter than 32 bytes or a non-clean
    ///      bool; here a malformed return is simply treated as unpaid.
    /// Paid iff the call succeeded AND the token returned nothing (USDT-style) or a >=32-byte value
    /// whose first word is nonzero (a truthy bool). Anything else => skip, never brick.
    ///
    /// Residual (accepted): a maliciously-UPGRADED stock token could still brick a cycle by burning
    /// all forwarded gas. We deliberately do NOT cap forwarded gas with a stipend — a fixed stipend
    /// would silently skip EVERY holder if the issuer ever legitimately raised transfer cost. The
    /// token issuer (Robinhood) is trusted and can pause / adminBurn regardless, so gas-exhaustion is
    /// out of scope; this guard covers every NON-malicious behavior (revert, false, odd return shape).
    function _trySend(address token, address to, uint256 amt) private {
        bytes memory payload = abi.encodeCall(IERC20.transfer, (to, amt));
        bool paid;
        assembly {
            // retLength = 32: copy at most one word of returndata (bomb-proof); value = 0.
            let ok := call(gas(), token, 0, add(payload, 0x20), mload(payload), 0x00, 0x20)
            let rds := returndatasize()
            // paid = ok AND ( rds == 0 OR ( rds >= 32 AND first word != 0 ) )
            paid := and(ok, or(iszero(rds), and(iszero(lt(rds, 32)), iszero(iszero(mload(0x00))))))
        }
        if (!paid) emit PayoutSkipped(token, to, amt);
    }

    /// balanceOf that cannot brick startCycle: a token whose balanceOf reverts, returns malformed data,
    /// or returndata-bombs yields 0 (its pot is treated as empty and skipped this cycle) instead of
    /// reverting the whole snapshot and stranding every stock. Same 32-byte returndata cap as _trySend;
    /// gas-exhaustion by a maliciously-upgraded token remains the accepted residual, and the owner can
    /// removeStock a persistently-broken token.
    function _safeBalanceOf(address token) private view returns (uint256 bal) {
        bytes memory payload = abi.encodeCall(IERC20.balanceOf, (address(this)));
        assembly {
            let ok := staticcall(gas(), token, add(payload, 0x20), mload(payload), 0x00, 0x20)
            if and(ok, iszero(lt(returndatasize(), 32))) { bal := mload(0x00) }
        }
    }

    /// True if any registered stock has a nonzero balance here — i.e. there is something to distribute.
    function _hasPot() private view returns (bool) {
        uint256 m = stocks.length;
        for (uint256 k; k < m; ++k) {
            if (_safeBalanceOf(stocks[k].token) != 0) return true;
        }
        return false;
    }

    // ── owner config (no fund-extraction path) ──────────────────────────────────────────────────

    function addStock(address token, uint24 fee, int24 tickSpacing, address hooks) external onlyOwner {
        // USDG is the sell token, never a distributed stock; address(0) is nonsense. A token already
        // present in stocks[] would be snapshotted twice in startCycle and over-subscribe the pot
        // (the single physical balance paid out 2x), so bar duplicates. Re-adding a *removed* stock
        // is fine — it is no longer in stocks[] — even though isStock stays true for the sweep bar.
        if (token == address(0) || token == address(usdg)) revert BadAmount();
        uint256 m = stocks.length;
        for (uint256 i; i < m; ++i) {
            if (stocks[i].token == token) revert DuplicateStock();
        }
        stocks.push(Stock(token, fee, tickSpacing, hooks));
        isStock[token] = true;
        emit StockAdded(token, fee, tickSpacing, hooks);
    }

    function removeStock(uint256 i) external onlyOwner {
        address token = stocks[i].token;
        stocks[i] = stocks[stocks.length - 1];
        stocks.pop();
        // isStock[token] is deliberately NOT cleared. It only gates (a) sweepForeign, which MUST stay
        // permanently barred from any token ever registered as a stock — else removeStock()+
        // sweepForeign() would drain a de-listed stock's still-held pot — and (b) the router
        // reject-list, where a stale-true entry is strictly safer. Once a stock, never sweepable.
        emit StockRemoved(token);
    }

    function setKeeper(address k, bool on) external onlyOwner { keeper[k] = on; emit KeeperSet(k, on); }
    function setPaused(bool p) external onlyOwner { if (p) _pause(); else _unpause(); }
    function setMaxSpendPerBuy(uint256 v) external onlyOwner { maxSpendPerBuy = v; emit MaxSpendPerBuySet(v); }
    function setInterval(uint256 v) external onlyOwner { interval = v; emit IntervalSet(v); }

    function setUsdgEthPool(uint24 fee, int24 tickSpacing, address hooks) external onlyOwner {
        usdgEthFee = fee;
        usdgEthTickSpacing = tickSpacing;
        usdgEthHooks = hooks;
        emit UsdgEthPoolSet(fee, tickSpacing, hooks);
    }

    /// Rescue accidentally-sent junk tokens ONLY. Reverts on USDG and any registered stock, so the
    /// protocol's own funds can never be swept.
    function sweepForeign(address token, address to) external onlyOwner {
        if (token == address(usdg) || isStock[token]) revert NotSweepable();
        uint256 bal = IERC20(token).balanceOf(address(this));
        IERC20(token).safeTransfer(to, bal);
        emit ForeignSwept(token, to, bal);
    }

    // ── views ───────────────────────────────────────────────────────────────────────────────────
    function canStart() external view returns (bool) { return !cycleActive && block.timestamp >= nextDistribution; }
    function remaining() external view returns (uint256) { return cycleActive ? _holders.length - cursor : 0; }
    function stocksLength() external view returns (uint256) { return stocks.length; }
    function stockTokenAt(uint256 i) external view returns (address) { return stocks[i].token; }

    // No receive()/fallback: the v4 fallback nets ETH to zero inside the unlock (nothing is ever
    // taken as native ETH), so the contract never needs to hold ETH. A stray ETH transfer therefore
    // reverts, which protects senders rather than locking their ETH here with no rescue path.
}
Chain explorer1925msChain node73ms