{"sources":{"src/ProxyAdminExecutor.sol":{"content":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\n/// @dev Minimal ERC721 interface — only the selector {ProxyAdminExecutor.sendNfts} needs.\ninterface IERC721 {\n    function transferFrom(address from, address to, uint256 tokenId) external;\n}\n\n/// @dev Minimal ERC1155 interface — only the selector {ProxyAdminExecutor.sendNfts} needs.\n///      ERC1155 has no plain `transferFrom`; `safeBatchTransferFrom` is the only batch primitive\n///      and it always invokes the recipient's `onERC1155BatchReceived` hook when it is a contract.\ninterface IERC1155 {\n    function safeBatchTransferFrom(\n        address from,\n        address to,\n        uint256[] calldata ids,\n        uint256[] calldata amounts,\n        bytes calldata data\n    ) external;\n}\n\n/// @title ProxyAdminExecutor\n/// @notice Minimal implementation for the `OwnedUpgradabilityProxy` (ZeppelinOS)\n///         at 0x0BABA1Ad5bE3a5C0a66E7ac838a129Bf948f1eA4.\n/// @dev Exposes two entry points, {transact} and {sendNfts}, both of which run from the proxy's\n///      own context (i.e. spending the proxy's own ETH and token balances). They are:\n///\n///        - owner-gated: the caller must equal the proxy's own owner, read live\n///          from the ZeppelinOS proxy-owner storage slot. There is a single\n///          source of truth for ownership and no separate admin state to\n///          initialize, so the implementation cannot be left half-configured.\n///\n///        - non-reentrant: guarded with EIP-1153 transient storage. The guard\n///          writes NO persistent storage and is cleared automatically at the end\n///          of the transaction, so it can never collide with — or corrupt — the\n///          proxy's implementation/owner slots or any token balances.\n///\n///      This contract declares no persistent state and writes no persistent\n///      storage. The proxy's upgrade and ownership machinery — `upgradeTo`,\n///      `upgradeToAndCall`, `proxyOwner`, `transferProxyOwnership`,\n///      `implementation` — lives in the proxy contract's own bytecode (those\n///      selectors are dispatched by the proxy before its `fallback`, never by\n///      this implementation), so upgradeability and ownership are retained no\n///      matter what this implementation does. After upgrading to this contract\n///      the proxy exposes only those native proxy functions plus {transact} and\n///      {sendNfts}; the old `getImplementation()` decoy no longer resolves.\ncontract ProxyAdminExecutor {\n    /// @dev keccak256(\"org.zeppelinos.proxy.owner\"): the exact slot the proxy\n    ///      reads in `proxyOwner()` / `onlyProxyOwner`.\n    bytes32 private constant _PROXY_OWNER_SLOT = 0x337c729c04082e3bdd94ba7d2b5a8a642f3a138702366a91707825373a2029ba;\n\n    /// @dev Transient (EIP-1153) reentrancy-lock slot:\n    ///      keccak256(\"proxy.admin.executor.reentrancy.lock\").\n    bytes32 private constant _REENTRANCY_LOCK_SLOT = 0x7ad6fd455985f1f61742d1f8074b916eb0b953b15044a0f9774e942ffa2cf239;\n\n    /// @dev Gas forwarded to each NFT transfer call in {sendNfts}. Because a token contract forwards\n    ///      (nearly) all remaining gas to its receiver hook, capping the outer call is the only lever\n    ///      that bounds `onERC721Received` / `onERC1155BatchReceived`: it stops one griefing recipient\n    ///      from burning the whole batch's gas, which is what lets `tolerant` mode continue past a bad\n    ///      entry. Sized to cover a cold ERC721/1155 transfer plus a heavy receiver hook; an ERC1155\n    ///      `safeBatchTransferFrom` is granted this allowance per token id in the batch.\n    uint256 private constant GAS_LIMIT_NFT_TRANSFER = 200_000;\n\n    /// @notice One recipient's sub-batch within a collection (inner level of batching).\n    /// @param to      Recipient of every id in this sub-batch.\n    /// @param ids     Token ids to send to `to`.\n    /// @param amounts ERC1155 amounts, parallel to `ids`. Ignored for ERC721 groups (may be empty).\n    struct Recipient {\n        address to;\n        uint256[] ids;\n        uint256[] amounts;\n    }\n\n    /// @notice One collection and all of its recipient sub-batches (outer level of batching).\n    /// @param token      The ERC721 or ERC1155 collection.\n    /// @param isERC1155  false = ERC721 (`transferFrom` per id); true = ERC1155 (`safeBatchTransferFrom`).\n    /// @param recipients Per-recipient sub-batches.\n    struct NftGroup {\n        address token;\n        bool isERC1155;\n        Recipient[] recipients;\n    }\n\n    /// @notice Emitted on every successful {transact}.\n    event Transacted(address indexed to, uint256 value, bytes data, bytes result);\n\n    /// @notice Emitted once at the end of {sendNfts} with the tally of individual NFTs moved.\n    event NftsSent(uint256 succeeded, uint256 failed);\n\n    /// @notice Emitted (tolerant mode only) when a single ERC721 `transferFrom` fails; the batch continues.\n    event NftTransferFailed(address indexed token, address indexed to, uint256 id);\n\n    /// @notice Emitted (tolerant mode only) when a recipient's ERC1155 batch fails; the batch continues.\n    event NftBatchTransferFailed(address indexed token, address indexed to, uint256[] ids, uint256[] amounts);\n\n    /// @notice Thrown when the caller is not the proxy owner.\n    error NotProxyOwner(address caller, address owner);\n\n    /// @notice Thrown on a reentrant {transact} / {sendNfts} call.\n    error Reentrancy();\n\n    /// @notice Thrown when the inner call reverts; `returndata` bubbles the reason.\n    error CallReverted(bytes returndata);\n\n    /// @notice Thrown when a {sendNfts} group's `token` has no contract code (e.g. a mistyped address).\n    error NotAContract(address token);\n\n    /// @notice Thrown when an ERC1155 recipient's `ids` and `amounts` arrays differ in length.\n    error LengthMismatch(address to, uint256 idsLength, uint256 amountsLength);\n\n    /// @notice Execute one arbitrary call from the proxy's own context.\n    /// @param to     Target address (a token contract, an EOA/contract for ETH, etc.).\n    /// @param value  Wei to send with the call, drawn from the proxy's own balance\n    ///               (plus any ETH attached to this call). Use 0 for token calls.\n    /// @param data   Calldata for the target; empty for a plain ETH transfer.\n    /// @return result Raw return data from the call.\n    function transact(address to, uint256 value, bytes calldata data) external payable returns (bytes memory result) {\n        // --- ownership gate: compare msg.sender against the proxy's owner slot ---\n        address owner;\n        assembly {\n            owner := sload(_PROXY_OWNER_SLOT)\n        }\n        if (msg.sender != owner) {\n            revert NotProxyOwner(msg.sender, owner);\n        }\n\n        // --- reentrancy gate via transient storage (no persistent writes) ---\n        uint256 locked;\n        assembly {\n            locked := tload(_REENTRANCY_LOCK_SLOT)\n        }\n        if (locked != 0) {\n            revert Reentrancy();\n        }\n        assembly {\n            tstore(_REENTRANCY_LOCK_SLOT, 1)\n        }\n\n        bool ok;\n        (ok, result) = to.call{ value: value }(data);\n        if (!ok) {\n            revert CallReverted(result);\n        }\n\n        // Clear the transient lock (also auto-discarded at end of transaction).\n        assembly {\n            tstore(_REENTRANCY_LOCK_SLOT, 0)\n        }\n\n        emit Transacted(to, value, data, result);\n    }\n\n    /// @notice Batch-transfer ERC721 and/or ERC1155 tokens out of the proxy to many recipients.\n    /// @dev Runs in the proxy's context (delegatecall), so `address(this)` is the proxy — the token\n    ///      holder — and every transfer moves the proxy's own NFTs. Owner-gated and non-reentrant,\n    ///      exactly like {transact}. Two levels of batching minimise calldata: `token` is stated once\n    ///      per collection and `to` once per recipient. ERC721 uses `transferFrom` (no receiver hook)\n    ///      once per id; ERC1155 uses a single `safeBatchTransferFrom` per recipient (one hook, one\n    ///      event). Each call is gas-capped by {GAS_LIMIT_NFT_TRANSFER} to bound receiver callbacks.\n    ///      Success details come from the tokens' own `Transfer` / `TransferBatch` events; only\n    ///      failures (and the final tally) are emitted here.\n    /// @param groups   Collections (outer) each holding recipient sub-batches (inner).\n    /// @param tolerant When true, a failed transfer emits a `*Failed` event and the batch continues;\n    ///                 when false, the first failure reverts the whole batch (revert reason bubbled).\n    ///                 An ERC1155 sub-batch is atomic: one bad id fails that whole recipient sub-batch.\n    /// @return succeeded Count of token-id transfers that succeeded (each ERC1155 id moves its full amount).\n    /// @return failed    Count of token-id transfers that failed (nonzero only when `tolerant`).\n    function sendNfts(NftGroup[] calldata groups, bool tolerant) external returns (uint256 succeeded, uint256 failed) {\n        // --- ownership gate: compare msg.sender against the proxy's owner slot ---\n        address owner;\n        assembly {\n            owner := sload(_PROXY_OWNER_SLOT)\n        }\n        if (msg.sender != owner) {\n            revert NotProxyOwner(msg.sender, owner);\n        }\n\n        // --- reentrancy gate via transient storage (no persistent writes) ---\n        uint256 locked;\n        assembly {\n            locked := tload(_REENTRANCY_LOCK_SLOT)\n        }\n        if (locked != 0) {\n            revert Reentrancy();\n        }\n        assembly {\n            tstore(_REENTRANCY_LOCK_SLOT, 1)\n        }\n\n        for (uint256 g; g < groups.length; ++g) {\n            NftGroup calldata group = groups[g];\n            address token = group.token;\n            // Guard the common footgun (a mistyped/EOA address): a low-level call to an account with\n            // no code returns success without moving anything, which would be miscounted as a transfer.\n            if (token.code.length == 0) {\n                revert NotAContract(token);\n            }\n\n            Recipient[] calldata recipients = group.recipients;\n            for (uint256 r; r < recipients.length; ++r) {\n                Recipient calldata rc = recipients[r];\n                (uint256 ok, uint256 bad) =\n                    group.isERC1155 ? _sendErc1155(token, rc, tolerant) : _sendErc721(token, rc, tolerant);\n                succeeded += ok;\n                failed += bad;\n            }\n        }\n\n        // Clear the transient lock (also auto-discarded at end of transaction).\n        assembly {\n            tstore(_REENTRANCY_LOCK_SLOT, 0)\n        }\n\n        emit NftsSent(succeeded, failed);\n    }\n\n    /// @dev Send one recipient's ERC721 ids, one gas-capped `transferFrom` each.\n    function _sendErc721(address token, Recipient calldata rc, bool tolerant)\n        private\n        returns (uint256 succeeded, uint256 failed)\n    {\n        uint256 n = rc.ids.length;\n        for (uint256 i; i < n; ++i) {\n            uint256 id = rc.ids[i];\n            bytes memory cd = abi.encodeCall(IERC721.transferFrom, (address(this), rc.to, id));\n            (bool ok, bytes memory ret) = token.call{ gas: GAS_LIMIT_NFT_TRANSFER }(cd);\n            if (ok) {\n                ++succeeded;\n            } else if (tolerant) {\n                emit NftTransferFailed(token, rc.to, id);\n                ++failed;\n            } else {\n                revert CallReverted(ret);\n            }\n        }\n    }\n\n    /// @dev Send one recipient's ERC1155 ids in a single gas-capped `safeBatchTransferFrom`. The call\n    ///      is granted {GAS_LIMIT_NFT_TRANSFER} per id so large legitimate batches are not starved,\n    ///      while a griefing receiver stays bounded per NFT. The batch is atomic: any failure fails\n    ///      all `n` ids for this recipient.\n    function _sendErc1155(address token, Recipient calldata rc, bool tolerant)\n        private\n        returns (uint256 succeeded, uint256 failed)\n    {\n        uint256 n = rc.ids.length;\n        // Enforce the ids/amounts length invariant first, so a malformed sub-batch (e.g. empty `ids`\n        // with non-empty `amounts`) reverts loudly rather than being silently skipped as a no-op.\n        if (rc.amounts.length != n) {\n            revert LengthMismatch(rc.to, n, rc.amounts.length);\n        }\n        if (n == 0) {\n            return (0, 0);\n        }\n        bytes memory cd = abi.encodeCall(IERC1155.safeBatchTransferFrom, (address(this), rc.to, rc.ids, rc.amounts, ''));\n        (bool ok, bytes memory ret) = token.call{ gas: GAS_LIMIT_NFT_TRANSFER * n }(cd);\n        if (ok) {\n            succeeded = n;\n        } else if (tolerant) {\n            emit NftBatchTransferFailed(token, rc.to, rc.ids, rc.amounts);\n            failed = n;\n        } else {\n            revert CallReverted(ret);\n        }\n    }\n}\n"}},"matchId":"43187660","creationMatch":"exact_match","runtimeMatch":"exact_match","verifiedAt":"2026-08-02T04:34:14Z","match":"exact_match","chainId":"1","address":"0xB2de2bff671b03612a0Fa3340E0e0eE7e1C0a388"}