ETH Price: $2,282.65 (-5.81%)
Gas: 0.3 Gwei

Transaction Decoder

Block:
18325328 at Oct-11-2023 06:09:59 AM +UTC
Transaction Fee:
0.00033551526794292 ETH $0.77
Gas Used:
61,080 Gas / 5.493046299 Gwei

Emitted Events:

314 TrainingGround.Claim( metaToken=1766847064778384329583297500742918515827483896875618958121606201292623479, programme=16, level=3 )
315 TrainingGround.StartTraining( metaToken=1766847064778384329583297500742918515827483896875618958121606201292623479, programme=5, level=0 )

Account State Difference:

  Address   Before After State Difference Code
0x6015C527...b64bf4b7F
1.654213764079436809 Eth
Nonce: 16
1.653878248811493889 Eth
Nonce: 17
0.00033551526794292
(beaverbuild)
6.356324137898963528 Eth6.356330245898963528 Eth0.000006108
0xd9e6db06...72f4cAFcC

Execution Trace

TrainingGround.switchProgramme( tokenAddress=0x5d2BF3b4264EFADE95fC89348f1367fCa0552861, tokenId=3703, programme=5 )
  • JumpPort.isDeposited( tokenAddress=0x5d2BF3b4264EFADE95fC89348f1367fCa0552861, tokenId=3703 ) => ( True )
  • JumpPort.ownerOf( tokenAddress=0x5d2BF3b4264EFADE95fC89348f1367fCa0552861, tokenId=3703 ) => ( owner=0x6015C5273A9730A4c4823230AFB2a3ab64bf4b7F )
    File 1 of 2: TrainingGround
    // SPDX-License-Identifier: AGPL-3.0
    pragma solidity ^0.8.9;
    import {Portal} from "./Portal.sol";
    interface ILegionFighter {
        function isCore(uint256 tokenId) external view returns (bool, uint256);
        function Equipped(uint256 tokenId) external view returns (uint8);
    }
    contract TrainingGround is Portal {
        bool public trainingActive = false;
        struct Permission {
            uint8 allowLevel;
            uint248 index;
        }
        mapping(address => Permission) allowedCollections; // collection address => Permission struct
        mapping(uint256 => address) allowedCollectionsLookup; // allowedCollections index => collection address (reverse lookup)
        uint256 allowedCollectionsCount = 0;
        struct State {
            uint8 status; // 0 or 1 - Boolean for whether currently training
            uint8 programme; // Programme Id
            uint208 progress; // Progress (104 x 2 bits)
            uint32 startBlock; // Block started training in current programme
        }
        mapping(uint256 => uint32[3]) public durationsByProgramme; // programme id => durations[]
        mapping(uint256 => uint256) public participantsByProgramme; // programme id => count of participants
        uint256 public programmesCount = 0;
        mapping(uint256 => State) stateByMetaToken; // metaToken => State struct
        event NewProgramme(uint8 indexed programme, uint32[3] durations);
        event StartTraining(uint256 indexed metaToken, uint8 indexed programme, uint8 level);
        event Claim(uint256 indexed metaToken, uint8 indexed programme, uint8 level);
        constructor(address jumpPortAddress) Portal(jumpPortAddress) {}
        /* Helper / View functions */
        /**
         * @dev Get a metaToken (a unique id representing a specific tokenId in a specific collection)
         * @param tokenAddress the collection contract address
         * @param tokenId the fighter tokenId
         */
        function getMetaToken(address tokenAddress, uint256 tokenId) public view returns (uint256 metaToken) {
            uint256 tokenAddressIndex = uint256(allowedCollections[tokenAddress].index);
            metaToken = (tokenAddressIndex << 240) | tokenId;
        }
        /**
         * @dev Get the contract address and tokenId of a given metaToken
         * @param metaToken a unique id representing a specific tokenId in a specific collection
         */
        function getTokenDetails(uint256 metaToken) public view returns (address tokenAddress, uint256 tokenId) {
            uint256 tokenAddressIndex = metaToken >> 240;
            tokenAddress = allowedCollectionsLookup[tokenAddressIndex];
            tokenId = metaToken & 0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff;
        }
        /**
         * @dev Check whether a given collection address is allowed into Training Ground
         */
        function isAllowed(address tokenAddress) external view returns (bool allowed) {
            return allowedCollections[tokenAddress].allowLevel > 0;
        }
        /**
         * @dev Check whether a given fighter is currently training (by metaToken)
         */
        function isTraining(uint256 metaToken) public view returns (bool training) {
            return stateByMetaToken[metaToken].status > 0;
        }
        /**
         * @dev Check whether a given fighter is currently training (by contract address and tokenId)
         */
        function isTraining(address tokenAddress, uint256 tokenId) public view returns (bool training) {
            return isTraining(getMetaToken(tokenAddress, tokenId));
        }
        /**
         * @dev Get the full progress (represented by a uint208) of a given fighter (by metaToken)
         */
        function getFullProgress(uint256 metaToken) public view returns (uint208 progress) {
            return stateByMetaToken[metaToken].progress;
        }
        /**
         * @dev Get the full progress (represented by a uint208) of a given fighter (by contract address and tokenId)
         */
        function getFullProgress(address tokenAddress, uint256 tokenId) public view returns (uint208 progress) {
            return getFullProgress(getMetaToken(tokenAddress, tokenId));
        }
        /**
         * @dev Get a count of each level achieved for a given fighter (by metaToken)
         * Returns level counts as named Taunts, Fighting styles and Combos
         */
        function getSkillCounts(uint256 metaToken)
            public
            view
            returns (
                uint256 taunts,
                uint256 styles,
                uint256 combos
            )
        {
            unchecked {
                State storage state = stateByMetaToken[metaToken];
                for (uint8 i = 1; i <= programmesCount; i++) {
                    uint256 level = uint8(state.progress >> (208 - (i * 2))) & 3;
                    if (level >= 1) {
                        taunts++;
                    }
                    if (level >= 2) {
                        styles++;
                    }
                    if (level == 3) {
                        combos++;
                    }
                }
            }
        }
        /**
         * @dev Get a count of each level achieved for a given fighter (by contract address and tokenId)
         * Returns level counts as named Taunts, Fighting styles and Combos
         */
        function getSkillCounts(address tokenAddress, uint256 tokenId)
            public
            view
            returns (
                uint256 taunts,
                uint256 styles,
                uint256 combos
            )
        {
            return getSkillCounts(getMetaToken(tokenAddress, tokenId));
        }
        /**
         * @dev Get the status of a fighter during training in their current programme (by metaToken)
         * Returns the current programme id, the block they started and the duration
         */
        function getTrainingStatus(uint256 metaToken)
            public
            view
            returns (
                uint8 programme,
                uint32 startBlock,
                uint32 duration
            )
        {
            require(stateByMetaToken[metaToken].status > 0, "Token is not training");
            State storage state = stateByMetaToken[metaToken];
            programme = state.programme;
            startBlock = state.startBlock;
            duration = uint32(block.number - uint256(state.startBlock));
        }
        /**
         * @dev Get the status of a fighter during training in their current programme (by contract address and tokenId)
         * Returns the current programme id, the block they started and the duration
         */
        function getTrainingStatus(address tokenAddress, uint256 tokenId)
            public
            view
            returns (
                uint8 programme,
                uint32 startBlock,
                uint32 duration
            )
        {
            return getTrainingStatus(getMetaToken(tokenAddress, tokenId));
        }
        /**
         * @dev Get the current level achieved by a fighter in their current programme (by metaToken)
         */
        function getCurrentLevel(uint256 metaToken) public view returns (uint8 level) {
            unchecked {
                (uint8 programme, , uint32 blocksDuration) = getTrainingStatus(metaToken);
                uint32[3] storage durations = durationsByProgramme[programme];
                if (durations[2] <= blocksDuration) {
                    return 3;
                } else if (durations[1] <= blocksDuration) {
                    return 2;
                } else if (durations[0] <= blocksDuration) {
                    return 1;
                } else {
                    return 0;
                }
            }
        }
        /**
         * @dev Get the current level achieved by a fighter in their current programme (by contract address and tokenId)
         */
        function getCurrentLevel(address tokenAddress, uint256 tokenId) public view returns (uint8 level) {
            level = getCurrentLevel(getMetaToken(tokenAddress, tokenId));
        }
        /**
         * @dev Get the level claimed by a fighter in a given programme (by metaToken)
         */
        function getClaimedLevel(uint256 metaToken, uint8 programme) public view returns (uint8 level) {
            State storage state = stateByMetaToken[metaToken];
            level = uint8(state.progress >> (208 - (programme * 2))) & 3;
        }
        /**
         * @dev Get the level claimed by a fighter in a given programme (by contract address and tokenId)
         */
        function getClaimedLevel(
            address tokenAddress,
            uint256 tokenId,
            uint8 programme
        ) public view returns (uint8 level) {
            return getClaimedLevel(getMetaToken(tokenAddress, tokenId), programme);
        }
        /* Programme actions */
        /**
         * @dev Internal helper function used for starting a fighter on a new programme
         */
        function _startProgramme(uint256 metaToken, uint8 programme) internal {
            State storage state = stateByMetaToken[metaToken];
            uint8 startLevel = getClaimedLevel(metaToken, programme);
            require(startLevel < 3, "Programme already completed");
            state.programme = programme;
            unchecked {
                state.startBlock = uint32(block.number);
                if (startLevel > 0) {
                    uint32[3] storage durations = durationsByProgramme[programme];
                    state.startBlock = uint32(block.number) - durations[startLevel - 1];
                }
                participantsByProgramme[programme]++;
            }
            emit StartTraining(metaToken, programme, startLevel);
        }
        /**
         * @dev Join a fighter onto a training programme
         * @param tokenAddress the collection contract address
         * @param tokenId the fighter tokenId
         * @param programme the id of the programme to join
         */
        function joinProgramme(
            address tokenAddress,
            uint256 tokenId,
            uint8 programme
        ) public isActive onlyOperator(tokenAddress, tokenId) tokenAllowed(tokenAddress, tokenId) {
            require(programme > 0 && programme <= programmesCount, "Programme does not exist");
            uint256 metaToken = getMetaToken(tokenAddress, tokenId);
            State storage state = stateByMetaToken[metaToken];
            require(state.status == 0, "Already training");
            _startProgramme(metaToken, programme);
            state.status = 1;
            JumpPort.lockToken(tokenAddress, tokenId);
        }
        /**
         * @dev Switch a fighter onto a different training programme
         * @param tokenAddress the collection contract address
         * @param tokenId the fighter tokenId
         * @param programme the id of the programme to join
         */
        function switchProgramme(
            address tokenAddress,
            uint256 tokenId,
            uint8 programme
        ) public isActive onlyOperator(tokenAddress, tokenId) {
            require(allowedCollections[tokenAddress].allowLevel > 0, "Token not allowed");
            require(programme > 0 && programme <= programmesCount, "Programme does not exist");
            uint256 metaToken = getMetaToken(tokenAddress, tokenId);
            State storage state = stateByMetaToken[metaToken];
            uint8 currentProgramme = state.programme;
            require(state.status == 1, "Token is not training");
            require(currentProgramme != programme, "Token is already in the programme");
            claimLevel(metaToken);
            participantsByProgramme[currentProgramme]--;
            _startProgramme(metaToken, programme);
        }
        /**
         * @dev Remove a fighter from their current programme
         * @param tokenAddress the collection contract address
         * @param tokenId the fighter tokenId
         */
        function leaveCurrentProgramme(address tokenAddress, uint256 tokenId) public onlyOperator(tokenAddress, tokenId) {
            uint256 metaToken = getMetaToken(tokenAddress, tokenId);
            State storage state = stateByMetaToken[metaToken];
            require(state.status == 1, "Token is not training");
            claimLevel(metaToken);
            participantsByProgramme[state.programme]--;
            state.status = 0;
            state.programme = 0;
            JumpPort.unlockToken(tokenAddress, tokenId);
        }
        /**
         * @dev Claim a level achieved for a given fighter in their current programme (by metaToken)
         * This function is public so can be called anytime by anyone if they wish to pay the gas
         */
        function claimLevel(uint256 metaToken) public {
            State storage state = stateByMetaToken[metaToken];
            uint8 currentProgramme = state.programme;
            uint8 claimedLevel = getClaimedLevel(metaToken, currentProgramme);
            uint8 currentLevel = getCurrentLevel(metaToken);
            if (currentLevel > claimedLevel) {
                uint208 mask = uint208(claimedLevel ^ currentLevel) << (208 - currentProgramme * 2);
                state.progress = (state.progress ^ mask);
                emit Claim(metaToken, currentProgramme, currentLevel);
            }
        }
        /**
         * @dev Claim a level achieved for a given fighter in their current programme (by contract address and tokenId)
         * This function is public so can be called anytime by anyone if they wish to pay the gas
         */
        function claimLevel(address tokenAddress, uint256 tokenId) public {
            claimLevel(getMetaToken(tokenAddress, tokenId));
        }
        /* Modifiers */
        /**
         * @dev Prevent execution if training is not currently active
         */
        modifier isActive() {
            require(trainingActive == true, "Training not active");
            _;
        }
        /**
         * @dev Prevent execution if the specified token is not in the JumpPort or msg.sender is not owner or approved
         */
        modifier onlyOperator(address tokenAddress, uint256 tokenId) {
            require(JumpPort.isDeposited(tokenAddress, tokenId) == true, "Token not in JumpPort");
            address tokenOwner = JumpPort.ownerOf(tokenAddress, tokenId);
            require(tokenOwner == msg.sender || JumpPort.getApproved(tokenAddress, tokenId) == msg.sender || JumpPort.isApprovedForAll(tokenOwner, msg.sender) == true, "Not an operator of that token");
            _;
        }
        /**
         * @dev Prevent execution if the Legion like fighter is a core or is not equipped
         */
        modifier tokenAllowed(address tokenAddress, uint256 tokenId) {
            require(allowedCollections[tokenAddress].allowLevel > 0, "Token not allowed");
            ILegionFighter LF = ILegionFighter(tokenAddress);
            (bool core, ) = LF.isCore(tokenId);
            require(core == false, "Not a Legion Fighter");
            require(LF.Equipped(tokenId) > 0, "Fighter not equipped");
            _;
        }
        /* Administration */
        /**
         * @dev Toggle training active state
         * @param active desired state of training active (true/false)
         */
        function setTraining(bool active) external onlyRole(ADMIN_ROLE) {
            trainingActive = active;
        }
        /**
         * @dev Add a new token collection to the allowed list
         * @param tokenAddress the collection contract address
         */
        function addAllowedCollection(address tokenAddress) external onlyRole(ADMIN_ROLE) {
            require(allowedCollections[tokenAddress].index == 0, "Collection permissions already exist");
            allowedCollectionsCount++;
            allowedCollections[tokenAddress] = Permission(1, uint248(allowedCollectionsCount));
            allowedCollectionsLookup[allowedCollectionsCount] = tokenAddress;
        }
        /**
         * @dev Update a token collections permission level
         * @param tokenAddress the collection contract address
         * @param allowLevel an integer (0-255) for allowed permission level. Anything greater than 0 is allowed.
         */
        function updateAllowedCollection(address tokenAddress, uint8 allowLevel) external onlyRole(ADMIN_ROLE) {
            require(allowedCollections[tokenAddress].index > 0, "Collection permissions do not exist");
            allowedCollections[tokenAddress].allowLevel = allowLevel;
        }
        /**
         * @dev Add a new programme with associated level durations
         * @param durations an array of block heights for each level duration
         */
        function addProgramme(uint32[3] calldata durations) public onlyRole(ADMIN_ROLE) {
            require(durations[1] > durations[0] && durations[2] > durations[1], "Durations not in ascending order");
            require(programmesCount < 104, "Max programmes exist");
            programmesCount++;
            durationsByProgramme[programmesCount] = durations;
            emit NewProgramme(uint8(programmesCount), durations);
        }
        /**
         * @dev Add a batch of new programmes with associated level durations
         * @param programmes an array of programme duration arrays (block heights for each level duration)
         */
        function addProgrammes(uint32[3][] calldata programmes) external onlyRole(ADMIN_ROLE) {
            for (uint8 i = 0; i < programmes.length; i++) {
                addProgramme(programmes[i]);
            }
        }
        /**
         * @dev Update an existing programmes level durations
         * @param programme the id of the programme
         * @param durations an array of block heights for each level duration
         */
        function updateProgramme(uint8 programme, uint32[3] calldata durations) external onlyRole(ADMIN_ROLE) {
            require(programme > 0 && programme <= programmesCount, "Programme does not exist");
            require(durations[1] > durations[0] && durations[2] > durations[1], "Durations not in ascending order");
            durationsByProgramme[programme] = durations;
        }
        /**
         * @dev Eject a fighter from their current programme
         * @param tokenAddress the collection contract address
         * @param tokenId the fighter tokenId
         */
        function ejectFighter(address tokenAddress, uint256 tokenId) external onlyRole(ADMIN_ROLE) {
            uint256 metaToken = getMetaToken(tokenAddress, tokenId);
            State storage state = stateByMetaToken[metaToken];
            require(state.status == 1, "Token is not training");
            participantsByProgramme[state.programme]--;
            state.status = 0;
            state.programme = 0;
            JumpPort.unlockToken(tokenAddress, tokenId);
        }
    }
    // SPDX-License-Identifier: AGPL-3.0
    pragma solidity ^0.8.9;
    import {OwnableBase} from "./OwnableBase.sol";
    interface IJumpPort {
        function ownerOf(address tokenAddress, uint256 tokenId) external view returns (address owner);
        function isDeposited(address tokenAddress, uint256 tokenId) external view returns (bool);
        function getApproved(address tokenAddress, uint256 tokenId) external view returns (address copilot);
        function isApprovedForAll(address owner, address operator) external view returns (bool);
        function lockToken(address tokenAddress, uint256 tokenId) external;
        function unlockToken(address tokenAddress, uint256 tokenId) external;
        function unlockAllTokens(bool isOverridden) external;
        function blockExecution(bool isBlocked) external;
    }
    abstract contract Portal is OwnableBase {
        IJumpPort public JumpPort;
        bytes32 public constant UNLOCK_ROLE = keccak256("UNLOCK_ROLE");
        constructor(address jumpPortAddress) {
            JumpPort = IJumpPort(jumpPortAddress);
        }
        /**
         * @dev Allow current administrators to be able to grant/revoke unlock role to other addresses.
         */
        function setUnlockRole(address account, bool canUnlock) public onlyRole(ADMIN_ROLE) {
            roles[UNLOCK_ROLE][account] = canUnlock;
            emit RoleChange(UNLOCK_ROLE, account, canUnlock, msg.sender);
        }
        /**
         * @dev Mark locks held by this portal as void or not.
         * Allows for portals to have a degree of self-governance; if the administrator(s) of a portal
         * realize something is wrong and wish to allow all tokens locked by that portal as void, they're
         * able to indicate that to the JumpPort, without needing to invlove JumpPort governance.
         */
        function unlockAllTokens(bool isOverridden) public onlyRole(ADMIN_ROLE) {
            JumpPort.unlockAllTokens(isOverridden);
        }
        /**
         * @dev Prevent this Portal from calling `executeAction` on the JumpPort.
         * Intended to be called in the situation of a large failure of an individual Portal's operation,
         * as a way for the Portal itself to indicate it has failed, and arbitrary contract calls should not
         * be allowed to originate from it.
         *
         * This function only allows Portals to enable/disable their own execution right.
         */
        function blockExecution(bool isBlocked) public onlyRole(ADMIN_ROLE) {
            JumpPort.blockExecution(isBlocked);
        }
    }
    // SPDX-License-Identifier: AGPL-3.0
    pragma solidity ^0.8.9;
    interface IReverseResolver {
        function claim(address owner) external returns (bytes32);
    }
    interface IERC20 {
        function balanceOf(address account) external view returns (uint256);
        function transfer(address recipient, uint256 amount) external returns (bool);
    }
    interface IERC721 {
        function safeTransferFrom(
            address from,
            address to,
            uint256 tokenId
        ) external;
    }
    interface IDocumentationRepository {
        function doc(address contractAddress)
            external
            view
            returns (
                string memory name,
                string memory description,
                string memory details
            );
    }
    error MissingRole(bytes32 role, address operator);
    abstract contract OwnableBase {
        bytes32 public constant ADMIN_ROLE = 0x00;
        mapping(bytes32 => mapping(address => bool)) internal roles; // role => operator => hasRole
        mapping(bytes32 => uint256) internal validSignatures; // message hash => expiration block height
        IDocumentationRepository public DocumentationRepository;
        event RoleChange(bytes32 indexed role, address indexed account, bool indexed isGranted, address sender);
        constructor() {
            roles[ADMIN_ROLE][msg.sender] = true;
        }
        function doc()
            public
            view
            returns (
                string memory name,
                string memory description,
                string memory details
            )
        {
            return DocumentationRepository.doc(address(this));
        }
        /**
         * @dev See {ERC1271-isValidSignature}.
         */
        function isValidSignature(bytes32 hash, bytes memory) external view returns (bytes4 magicValue) {
            if (validSignatures[hash] >= block.number) {
                return 0x1626ba7e; // bytes4(keccak256("isValidSignature(bytes32,bytes)")
            } else {
                return 0xffffffff;
            }
        }
        /**
         * @dev Inspect whether a specific address has a specific role.
         */
        function hasRole(bytes32 role, address account) public view returns (bool) {
            return roles[role][account];
        }
        /* Modifiers */
        modifier onlyRole(bytes32 role) {
            if (roles[role][msg.sender] != true) revert MissingRole(role, msg.sender);
            _;
        }
        /* Administration */
        /**
         * @dev Allow current administrators to be able to grant/revoke admin role to other addresses.
         */
        function setAdmin(address account, bool isAdmin) public onlyRole(ADMIN_ROLE) {
            roles[ADMIN_ROLE][account] = isAdmin;
            emit RoleChange(ADMIN_ROLE, account, isAdmin, msg.sender);
        }
        /**
         * @dev Claim ENS reverse-resolver rights for this contract.
         * https://docs.ens.domains/contract-api-reference/reverseregistrar#claim-address
         */
        function setReverseResolver(address registrar) public onlyRole(ADMIN_ROLE) {
            IReverseResolver(registrar).claim(msg.sender);
        }
        /**
         * @dev Update address for on-chain documentation lookup.
         */
        function setDocumentationRepository(address documentationAddress) public onlyRole(ADMIN_ROLE) {
            DocumentationRepository = IDocumentationRepository(documentationAddress);
        }
        /**
         * @dev Set a message as valid, to be queried by ERC1271 clients.
         */
        function markMessageSigned(bytes32 hash, uint256 expirationLength) public onlyRole(ADMIN_ROLE) {
            validSignatures[hash] = block.number + expirationLength;
        }
        /**
         * @dev Rescue ERC20 assets sent directly to this contract.
         */
        function withdrawForeignERC20(address tokenContract) public onlyRole(ADMIN_ROLE) {
            IERC20 token = IERC20(tokenContract);
            token.transfer(msg.sender, token.balanceOf(address(this)));
        }
        /**
         * @dev Rescue ERC721 assets sent directly to this contract.
         */
        function withdrawForeignERC721(address tokenContract, uint256 tokenId) public virtual onlyRole(ADMIN_ROLE) {
            IERC721(tokenContract).safeTransferFrom(address(this), msg.sender, tokenId);
        }
        function withdrawEth() public onlyRole(ADMIN_ROLE) {
            payable(msg.sender).transfer(address(this).balance);
        }
    }
    

    File 2 of 2: JumpPort
    // SPDX-License-Identifier: AGPL-3.0
    pragma solidity ^0.8.9;
    import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
    import "./OwnableBase.sol";
    interface IERC721Transfers {
      function safeTransferFrom (
        address from,
        address to,
        uint256 tokenId
      ) external;
      function safeTransferFrom (
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
      ) external;
      function transferFrom (
        address from,
        address to,
        uint256 tokenId
      ) external;
    }
    contract JumpPort is IERC721Receiver, OwnableBase {
      bool public depositPaused;
      bytes32 public constant PORTAL_ROLE = keccak256("PORTAL_ROLE");
      struct LockRecord {
        address parentLock;
        bool isLocked;
      }
      /**
       * @dev Record an Owner's collection balance, and the block it was last updated.
       *
       * Stored as less than uint256 values to fit into one storage slot.
       *
       * This structure will work until approximately the year 2510840694154672305534315769283066566440942177785607
       * (when the block height becomes too large for a uint192), and for oners who don't have more than
       * 18,​446,​744,​073,​709,​551,​615 items from a single collection deposited.
       */
      struct BalanceRecord {
        uint64 balance;
        uint192 blockHeight;
      }
      mapping(address => bool) public lockOverride;
      mapping(address => bool) public executionBlocked;
      mapping(address => mapping(uint256 => mapping(address => LockRecord))) internal portalLocks; // collection address => token ID => portal address => LockRecord
      mapping(address => mapping(uint256 => address)) private currentLock; // collection address => token ID => portal address
      mapping(address => mapping(uint256 => address)) private Owners; // collection address => token ID => owner address
      mapping(address => mapping(address => BalanceRecord)) private OwnerBalances; // collection address => owner Address => count
      mapping(address => mapping(uint256 => uint256)) private DepositBlock; // collection address => token ID => block height
      mapping(address => mapping(uint256 => uint256)) private PingRequestBlock; // collection address => token ID => block height
      mapping(address => mapping(uint256 => address)) private Copilots; // collection address => token ID => copilot address
      mapping(address => mapping(address => bool)) private CopilotApprovals; // owner address => copilot address => is approved
      event Deposit(address indexed owner, address indexed tokenAddress, uint256 indexed tokenId);
      event Withdraw(address indexed owner, address indexed tokenAddress, uint256 indexed tokenId, uint256 duration);
      event Approval(address indexed owner, address indexed approved, address indexed tokenAddress, uint256 tokenId);
      event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
      event Lock(address indexed portalAddress, address indexed owner, address indexed tokenAddress, uint256 tokenId);
      event Unlock(address indexed portalAddress, address indexed owner, address indexed tokenAddress, uint256 tokenId);
      event ActionExecuted(address indexed tokenAddress, uint256 indexed tokenId, address target, bytes data);
      constructor (address documentationAddress) OwnableBase(documentationAddress) {}
      /* Deposit Tokens */
      /**
       * @dev Receive a token directly; transferred with the `safeTransferFrom` method of another ERC721 token.
       * @param operator the _msgSender of the transaction
       * @param from the address of the former owner of the incoming token
       * @param tokenId the ID of the incoming token
       * @param data additional metdata
       */
      function onERC721Received (
        address operator,
        address from,
        uint256 tokenId,
        bytes calldata data
      ) public override whenDepositNotPaused returns (bytes4) {
        Owners[msg.sender][tokenId] = from;
        unchecked {
          OwnerBalances[msg.sender][from].balance++;
          OwnerBalances[msg.sender][from].blockHeight = uint192(block.number);
        }
        DepositBlock[msg.sender][tokenId] = block.number;
        PingRequestBlock[msg.sender][tokenId] = 0;
        emit Deposit(from, msg.sender, tokenId);
        return IERC721Receiver.onERC721Received.selector;
      }
      /**
       * @dev Deposit an individual token from a specific collection.
       * To be successful, the JumpPort contract must be "Approved" to move this token on behalf
       * of the current owner, in the token's contract.
       */
      function deposit (address tokenAddress, uint256 tokenId) public whenDepositNotPaused {
        IERC721Transfers(tokenAddress).transferFrom(msg.sender, address(this), tokenId);
        Owners[tokenAddress][tokenId] = msg.sender;
        unchecked {
          OwnerBalances[tokenAddress][msg.sender].balance++;
          OwnerBalances[tokenAddress][msg.sender].blockHeight = uint192(block.number);
        }
        DepositBlock[tokenAddress][tokenId] = block.number;
        PingRequestBlock[tokenAddress][tokenId] = 0;
        emit Deposit(msg.sender, tokenAddress, tokenId);
      }
      /**
       * @dev Deposit multiple tokens from a single collection.
       * To be successful, the JumpPort contract must be "Approved" to move these tokens on behalf
       * of the current owner, in the token's contract.
       */
      function deposit (address tokenAddress, uint256[] calldata tokenIds) public {
        unchecked {
          for (uint256 i; i < tokenIds.length; i++) {
            deposit(tokenAddress, tokenIds[i]);
          }
        }
      }
      /**
       * @dev Deposit multiple tokens from multiple different collections.
       * To be successful, the JumpPort contract must be "Approved" to move these tokens on behalf
       * of the current owner, in the token's contract.
       */
      function deposit (address[] calldata tokenAddresses, uint256[] calldata tokenIds) public {
        require(tokenAddresses.length == tokenIds.length, "Mismatched inputs");
        unchecked {
          for (uint256 i; i < tokenIds.length; i++) {
            deposit(tokenAddresses[i], tokenIds[i]);
          }
        }
      }
      /* Withdraw Tokens */
      /**
       * @dev Internal helper function that clears out the tracked metadata for the token.
       * Does not do any permission checks, and does not do the actual transferring of the token.
       */
      function _withdraw (address tokenAddress, uint256 tokenId) internal {
        address currentOwner = Owners[tokenAddress][tokenId];
        emit Withdraw(
          currentOwner,
          tokenAddress,
          tokenId,
          block.number - DepositBlock[tokenAddress][tokenId]
        );
        unchecked {
          OwnerBalances[tokenAddress][currentOwner].balance--;
          OwnerBalances[tokenAddress][currentOwner].blockHeight = uint192(block.number);
        }
        Owners[tokenAddress][tokenId] = address(0);
        DepositBlock[tokenAddress][tokenId] = 0;
        Copilots[tokenAddress][tokenId] = address(0);
      }
      /**
       * @dev Withdraw a token, to the owner's address, using `safeTransferFrom`, with no additional data.
       */
      function safeWithdraw (address tokenAddress, uint256 tokenId)
        public
        isPilot(tokenAddress, tokenId)
        withdrawAllowed(tokenAddress, tokenId)
      {
        address ownerAddress = Owners[tokenAddress][tokenId];
        _withdraw(tokenAddress, tokenId);
        IERC721Transfers(tokenAddress).safeTransferFrom(address(this), ownerAddress, tokenId);
      }
      /**
       * @dev Withdraw a token, to the owner's address, using `safeTransferFrom`, with additional calldata.
       */
      function safeWithdraw (address tokenAddress, uint256 tokenId, bytes calldata data)
        public
        isPilot(tokenAddress, tokenId)
        withdrawAllowed(tokenAddress, tokenId)
      {
        address ownerAddress = Owners[tokenAddress][tokenId];
        _withdraw(tokenAddress, tokenId);
        IERC721Transfers(tokenAddress).safeTransferFrom(address(this), ownerAddress, tokenId, data);
      }
      /**
       * @dev Bulk withdraw multiple tokens, using `safeTransferFrom`, with no additional data.
       */
      function safeWithdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds) public {
        require(tokenAddresses.length == tokenIds.length, "Inputs mismatched");
        for(uint256 i = 0; i < tokenAddresses.length; i++) {
          safeWithdraw(tokenAddresses[i], tokenIds[i]);
        }
      }
      /**
       * @dev Bulk withdraw multiple tokens, using `safeTransferFrom`, with additional calldata.
       */
      function safeWithdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds, bytes[] calldata data) public {
        require(tokenAddresses.length == tokenIds.length, "Inputs mismatched");
        for(uint256 i = 0; i < tokenAddresses.length; i++) {
          safeWithdraw(tokenAddresses[i], tokenIds[i], data[i]);
        }
      }
      /**
       * @dev Withdraw a token, to the owner's address, using `transferFrom`.
       * USING `transferFrom` RATHER THAN `safeTransferFrom` COULD RESULT IN LOST TOKENS. USE `safeWithdraw` FUNCTIONS
       * WHERE POSSIBLE, OR DOUBLE-CHECK RECEIVING ADDRESSES CAN HOLD TOKENS IF USING THIS FUNCTION.
       */
      function withdraw (address tokenAddress, uint256 tokenId)
        public
        isPilot(tokenAddress, tokenId)
        withdrawAllowed(tokenAddress, tokenId)
      {
        address ownerAddress = Owners[tokenAddress][tokenId];
        _withdraw(tokenAddress, tokenId);
        IERC721Transfers(tokenAddress).transferFrom(address(this), ownerAddress, tokenId);
      }
      /**
       * @dev Bulk withdraw multiple tokens, to specific addresses, using `transferFrom`.
       * USING `transferFrom` RATHER THAN `safeTransferFrom` COULD RESULT IN LOST TOKENS. USE `safeWithdraw` FUNCTIONS
       * WHERE POSSIBLE, OR DOUBLE-CHECK RECEIVING ADDRESSES CAN HOLD TOKENS IF USING THIS FUNCTION.
       */
      function withdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds) public {
        require(tokenAddresses.length == tokenIds.length, "Inputs mismatched");
        for(uint256 i = 0; i < tokenAddresses.length; i++) {
          withdraw(tokenAddresses[i], tokenIds[i]);
        }
      }
      /**
       * @dev Designate another address that can act on behalf of this token.
       * This allows the Copliot address to withdraw the token from the JumpPort, and interact with any Portal
       * on behalf of this token.
       */
      function setCopilot (address copilot, address tokenAddress, uint256 tokenId) public {
        require(Owners[tokenAddress][tokenId] == msg.sender, "Not the owner of that token");
        require(msg.sender != copilot, "approve to caller");
        Copilots[tokenAddress][tokenId] = copilot;
        emit Approval(msg.sender, copilot, tokenAddress, tokenId);
      }
      /**
       * @dev Designate another address that can act on behalf of all tokens owned by the sender's address.
       * This allows the Copliot address to withdraw the token from the JumpPort, and interact with any Portal
       * on behalf of any token owned by the sender.
       */
      function setCopilotForAll (address copilot, bool approved) public {
        require(msg.sender != copilot, "approve to caller");
        CopilotApprovals[msg.sender][copilot] = approved;
        emit ApprovalForAll(msg.sender, copilot, approved);
      }
      /* Receive actions from Portals */
      /**
       * @dev Lock a token into the JumpPort.
       * Causes a token to not be able to be withdrawn by its Owner, until the same Portal contract calls the `unlockToken` function for it,
       * or the locks for that portal are all marked as invalid (either by JumpPort administrators, or the Portal itself).
       */
      function lockToken (address tokenAddress, uint256 tokenId) public tokenDeposited(tokenAddress, tokenId) onlyRole(PORTAL_ROLE) {
        if (portalLocks[tokenAddress][tokenId][msg.sender].isLocked) return; // Already locked; nothing to do
        // Check if this lock is already in the chain of "active" locks
        address checkPortal = currentLock[tokenAddress][tokenId];
        while (checkPortal != address(0)) {
          if (checkPortal == msg.sender) {
            // This portal is already in the chain of active locks
            portalLocks[tokenAddress][tokenId][msg.sender].isLocked = true;
            emit Lock(msg.sender, Owners[tokenAddress][tokenId], tokenAddress, tokenId);
            return;
          }
          checkPortal = portalLocks[tokenAddress][tokenId][checkPortal].parentLock;
        }
        // Looped through all active locks and didn't find this Portal. So, add it as the new head
        portalLocks[tokenAddress][tokenId][msg.sender] = LockRecord(currentLock[tokenAddress][tokenId], true);
        currentLock[tokenAddress][tokenId] = msg.sender;
        emit Lock(msg.sender, Owners[tokenAddress][tokenId], tokenAddress, tokenId);
      }
      /**
       * @dev Unlocks a token held in the JumpPort.
       * Does not withdraw the token from the JumpPort, but makes it available for withdraw whenever the Owner wishes to.
       */
      function unlockToken (address tokenAddress, uint256 tokenId) public tokenDeposited(tokenAddress, tokenId) onlyRole(PORTAL_ROLE) {
        portalLocks[tokenAddress][tokenId][msg.sender].isLocked = false;
        emit Unlock(msg.sender, Owners[tokenAddress][tokenId], tokenAddress, tokenId);
        if (!isLocked(tokenAddress, tokenId)) {
          currentLock[tokenAddress][tokenId] = address(0);
        }
      }
      /**
       * @dev Take an action as the JumpPort (the owner of the tokens within it), as directed by a Portal.
       * This is a powerful function and Portals that wish to use it NEEDS TO MAKE SURE its execution is guarded by
       * checks to ensure the address passed as `operator` to this function is the one authorizing the action
       * (in most cases, it should be the `msg.sender` communicating to the Portal), and that the `payload`
       * being passed in operates on the `tokenId` indicated, and no other tokens.
       *
       * Here on the JumpPort side, it verifies that the `operator` passed in is the current owner or a Copilot of the
       * token being operated upon, but has to trust the Portal that the passed-in `tokenId` matches what token
       * will get acted upon in the `payload`.
       */
      function executeAction (address operator, address tokenAddress, uint256 tokenId, address targetAddress, bytes calldata payload)
        public
        payable
        tokenDeposited(tokenAddress, tokenId)
        onlyRole(PORTAL_ROLE)
        returns(bytes memory result)
      {
        require(executionBlocked[msg.sender] == false, "Execution blocked for that Portal");
        // Check if operator is allowed to act for this token
        address owner = Owners[tokenAddress][tokenId];
        require(
          operator == owner || operator == Copilots[tokenAddress][tokenId] || CopilotApprovals[owner][operator] == true,
          "Not an operator of that token"
        );
        // Make the external call
        (bool success, bytes memory returnData) = targetAddress.call{ value: msg.value }(payload);
        if (success == false) {
          if (returnData.length == 0) {
            revert("Executing action on other contract failed");
          } else {
            assembly {
              revert(add(32, returnData), mload(returnData))
            }
          }
        } else {
          emit ActionExecuted(tokenAddress, tokenId, targetAddress, payload);
          return returnData;
        }
      }
      /**
       * @dev Unlocks all locks held by a Portal.
       * Intended to be called in the situation of a large failure of an individual Portal's operation,
       * as a way for the Portal itself to indicate it has failed, and all tokens that were previously
       * locked by it should be allowed to exit.
       *
       * This function only allows Portals to enable/disable the locks they created. The function `setAdminLockOverride`
       * is similar, but allows JumpPort administrators to set/clear the lock ability for any Portal contract.
       */
      function unlockAllTokens (bool isOverridden) public onlyRole(PORTAL_ROLE) {
        lockOverride[msg.sender] = isOverridden;
      }
      /**
       * @dev Prevent a Portal from executing calls to other contracts.
       * Intended to be called in the situation of a large failure of an individual Portal's operation,
       * as a way for the Portal itself to indicate it has failed, and arbitrary contract calls should not
       * be allowed to originate from it.
       *
       * This function only allows Portals to enable/disable their own execution right. The function `setAdminExecutionBlocked`
       * is similar, but allows JumpPort administrators to set/clear the execution block for any Portal contract.
       */
      function blockExecution (bool isBlocked) public onlyRole(PORTAL_ROLE) {
        executionBlocked[msg.sender] = isBlocked;
      }
      /* View functions */
      /**
       * @dev Is the specified token currently deposited in the JumpPort?
       */
      function isDeposited (address tokenAddress, uint256 tokenId) public view returns (bool) {
        return Owners[tokenAddress][tokenId] != address(0);
      }
      /**
       * @dev When was the specified token deposited in the JumpPort?
       */
      function depositedSince (address tokenAddress, uint256 tokenId) public view tokenDeposited(tokenAddress, tokenId) returns (uint256 blockNumber) {
        blockNumber = DepositBlock[tokenAddress][tokenId];
      }
      /**
       * @dev Is the specified token currently locked in the JumpPort?
       * If any Portal contract has a valid lock (the Portal has indicated the token should be locked,
       * and the Portal's locking rights haven't been overridden) on the token, this function will return true.
       */
      function isLocked (address tokenAddress, uint256 tokenId) public view tokenDeposited(tokenAddress, tokenId) returns (bool) {
        address checkPortal = currentLock[tokenAddress][tokenId];
        while (checkPortal != address(0)) {
          if (portalLocks[tokenAddress][tokenId][checkPortal].isLocked && lockOverride[checkPortal] == false) return true;
          checkPortal = portalLocks[tokenAddress][tokenId][checkPortal].parentLock;
        }
        return false;
      }
      /**
       * @dev Get a list of all Portal contract addresses that hold a valid lock (the Portal has indicated the token should be locked,
       * and the Portal's locking rights haven't been overridden) on the token.
       */
      function lockedBy (address tokenAddress, uint256 tokenId) public view returns (address[] memory) {
        address[] memory lockedRaw = new address[](500);
        uint256 index = 0;
        address checkPortal = currentLock[tokenAddress][tokenId];
        while (checkPortal != address(0)) {
          if (portalLocks[tokenAddress][tokenId][checkPortal].isLocked && lockOverride[checkPortal] == false) {
            lockedRaw[index] = checkPortal;
            index++;
          }
          checkPortal = portalLocks[tokenAddress][tokenId][checkPortal].parentLock;
        }
        address[] memory lockedFinal = new address[](index);
        unchecked {
          for (uint256 i = 0; i < index; i++) {
            lockedFinal[i] = lockedRaw[i];
          }
        }
        return lockedFinal;
      }
      /**
       * @dev Who is the owner of the specified token that is deposited in the JumpPort?
       * A core tenent of the JumpPort is that this value will not change while the token is deposited;
       * a token cannot change owners while in the JumpPort, though they can add/remove Copilots.
       */
      function ownerOf (address tokenAddress, uint256 tokenId) public view tokenDeposited(tokenAddress, tokenId) returns (address owner) {
        owner = Owners[tokenAddress][tokenId];
      }
      /**
       * @dev Who are the owners of a specified range of tokens in a collection?
       * Bulk query function, to be able to enumerate a whole token collection more easily on the client end
       */
      function ownersOf (address tokenAddress, uint256 tokenSearchStart, uint256 tokenSearchEnd) public view returns (address[] memory tokenOwners) {
        unchecked {
          require(tokenSearchEnd >= tokenSearchStart, "Search parameters out of order");
          tokenOwners = new address[](tokenSearchEnd - tokenSearchStart + 1);
          for (uint256 i = tokenSearchStart; i <= tokenSearchEnd; i++) {
            tokenOwners[i - tokenSearchStart] = Owners[tokenAddress][i];
          }
        }
      }
      /**
       * @dev For a specified owner address, what tokens in the specified range do they own?
       * Bulk query function, to be able to enumerate a specific address' collection more easily on the client end
       */
      function ownedTokens (address tokenAddress, address owner, uint256 tokenSearchStart, uint256 tokenSearchEnd) public view returns (uint256[] memory tokenIds) {
        unchecked {
          require(tokenSearchEnd >= tokenSearchStart, "Search parameters out of order");
          require(owner != address(0), "Balance query for the zero address");
          uint256[] memory ownedRaw = new uint256[](tokenSearchEnd - tokenSearchStart);
          uint256 index = 0;
          for (uint256 i = tokenSearchStart; i <= tokenSearchEnd; i++) {
            if (Owners[tokenAddress][i] == owner) {
              ownedRaw[index] = i;
              index++;
            }
          }
          uint256[] memory ownedFinal = new uint256[](index);
          for (uint256 i = 0; i < index; i++) {
            ownedFinal[i] = ownedRaw[i];
          }
          return ownedFinal;
        }
      }
      /**
       * @dev For a specific token collection, how many tokens in that collection does a specific owning address own in the JumpPort?
       */
      function balanceOf (address tokenAddress, address owner) public view returns (BalanceRecord memory) {
        require(owner != address(0), "Balance query for the zero address");
        return OwnerBalances[tokenAddress][owner];
      }
      /**
       * @dev For a specific set of token collections, how many tokens total does a specific owning address own in the JumpPort?
       * Bulk query function, to be able to enumerate a specific address' collection more easily on the client end
       */
      function balanceOf (address[] calldata tokenAddresses, address owner) public view returns (uint256) {
        require(owner != address(0), "Balance query for the zero address");
        uint256 totalBalance = 0;
        unchecked {
          for (uint256 i = 0; i < tokenAddresses.length; i++) {
            totalBalance += OwnerBalances[tokenAddresses[i]][owner].balance;
          }
        }
        return totalBalance;
      }
      /**
       * @dev For a specific token, which other address is approved to act as the owner of that token for actions pertaining to the JumpPort?
       */
      function getApproved (address tokenAddress, uint256 tokenId) public view tokenDeposited(tokenAddress, tokenId) returns (address copilot) {
        copilot = Copilots[tokenAddress][tokenId];
      }
      /**
       * @dev For a specific owner's address, is the specified operator address allowed to act as the owner for actions pertaining to the JumpPort?
       */
      function isApprovedForAll (address owner, address operator) public view returns (bool) {
        return CopilotApprovals[owner][operator];
      }
      /* Modifiers */
      /**
       * @dev Prevent execution if the specified token is not currently deposited in the JumpPort.
       */
      modifier tokenDeposited (address tokenAddress, uint256 tokenId) {
        require(Owners[tokenAddress][tokenId] != address(0), "Not currently deposited");
        _;
      }
      /**
       * @dev Prevent execution if deposits to the JumpPort overall are paused.
       */
      modifier whenDepositNotPaused () {
        require(depositPaused == false, "Paused");
        _;
      }
      /**
       * @dev Prevent execution if the specified token is locked by any Portal currently.
       */
      modifier withdrawAllowed (address tokenAddress, uint256 tokenId) {
        require(!isLocked(tokenAddress, tokenId), "Token is locked");
        _;
      }
      /**
       * @dev Prevent execution if the transaction sender is not the owner nor Copliot for the specified token.
       */
      modifier isPilot (address tokenAddress, uint256 tokenId) {
        address owner = Owners[tokenAddress][tokenId];
        require(
          msg.sender == owner || msg.sender == Copilots[tokenAddress][tokenId] || CopilotApprovals[owner][msg.sender] == true,
          "Not an operator of that token"
        );
        _;
      }
      /* Administration */
      /**
       * @dev Add or remove the "Portal" role to a specified address.
       */
      function setPortalValidation (address portalAddress, bool isValid) public onlyRole(ADMIN_ROLE) {
        roles[PORTAL_ROLE][portalAddress] = isValid;
        emit RoleChange(PORTAL_ROLE, portalAddress, isValid, msg.sender);
      }
      /**
       * @dev Prevent new tokens from being added to the JumpPort.
       */
      function setPaused (bool isDepositPaused)
        public
        onlyRole(ADMIN_ROLE)
      {
        depositPaused = isDepositPaused;
      }
      /**
       * @dev As an administrator of the JumpPort contract, set a Portal's locks to be valid or not.
       *
       * This function allows JumpPort administrators to set/clear the override for any Portal contract.
       * The `unlockAllTokens` function is similar (allowing Portal addresses to set/clear lock overrides as well)
       * but only for their own Portal address.
       */
      function setAdminLockOverride (address portal, bool isOverridden) public onlyRole(ADMIN_ROLE) {
        lockOverride[portal] = isOverridden;
      }
      /**
       * @dev As an administrator of the JumpPort contract, set a Portal to be able to execute other functions or not.
       *
       * This function allows JumpPort administrators to set/clear the execution block for any Portal contract.
       * The `blockExecution` function is similar (allowing Portal addresses to set/clear lock overrides as well)
       * but only for their own Portal address.
       */
      function setAdminExecutionBlocked (address portal, bool isBlocked) public onlyRole(ADMIN_ROLE) {
        executionBlocked[portal] = isBlocked;
      }
      /**
       * @dev Contract owner requesting the owner of a token check in.
       * This starts the process of the owner of the contract being able to remove any token, after a time delay.
       * If the current owner does not want the token removed, they have 2,400,000 blocks (about one year)
       * to trigger the `ownerPong` method, which will abort the withdraw
       */
      function adminWithdrawPing (address tokenAddress, uint256 tokenId)
        public
        onlyRole(ADMIN_ROLE)
      {
        require(Owners[tokenAddress][tokenId] != address(0), "Token not deposited");
        PingRequestBlock[tokenAddress][tokenId] = block.number;
      }
      /**
       * @dev As the owner of a token, abort an attempt to force-remove it.
       * The owner of the contract can remove any token from the JumpPort, if they trigger the `adminWithdrawPing` function
       * for that token, and the owner does not respond by calling this function within 2,400,000 blocks (about one year)
       */
      function ownerPong (address tokenAddress, uint256 tokenId) public isPilot(tokenAddress, tokenId) {
        PingRequestBlock[tokenAddress][tokenId] = 0;
      }
      /**
       * @dev As an Administrator, abort an attempt to force-remove a token.
       * This is a means for the Administration to change its mind about a force-withdraw, or to correct the actions of a rogue Administrator.
       */
      function adminPong (address tokenAddress, uint256 tokenId) public onlyRole(ADMIN_ROLE) {
        PingRequestBlock[tokenAddress][tokenId] = 0;
      }
      /**
       * @dev Check if a token has a ping from the contract Administration pending, and if so, what block it was requested at
       * Returns zero if there is no request pending.
       */
      function tokenPingRequestBlock (address tokenAddress, uint256 tokenId) public view returns (uint256 blockNumber) {
        return PingRequestBlock[tokenAddress][tokenId];
      }
      /**
       * @dev Check if a set of tokens have a ping from the contract Administration pending, and if so, what block it was requested at
       * Returns zero for a token if there is no request pending for that token.
       */
      function tokenPingRequestBlocks (address[] calldata tokenAddresses, uint256[] calldata tokenIds) public view returns(uint256[] memory blockNumbers) {
        require(tokenAddresses.length == tokenIds.length, "Inputs mismatched");
        unchecked {
          blockNumbers = new uint256[](tokenAddresses.length);
          for (uint256 i = 0; i < tokenAddresses.length; i++) {
            blockNumbers[i] = PingRequestBlock[tokenAddresses[i]][tokenIds[i]];
          }
        }
      }
      /**
       * @dev Rescue ERC721 assets sent directly to this contract.
       */
      function withdrawForeignERC721 (address tokenContract, uint256 tokenId)
        public
        override
        onlyRole(ADMIN_ROLE)
      {
        if (Owners[tokenContract][tokenId] == address(0)) {
          // This token got here without being properly recorded; allow withdraw immediately
          DepositBlock[tokenContract][tokenId] = 0;
          Copilots[tokenContract][tokenId] = address(0);
          IERC721(tokenContract).safeTransferFrom(
            address(this),
            msg.sender,
            tokenId
          );
          return;
        }
        // This token is deposited into the JumpPort in a valid manner.
        // Only allow contract-owner withdraw if owner does not respond to ping
        unchecked {
          require(PingRequestBlock[tokenContract][tokenId] > 0 && PingRequestBlock[tokenContract][tokenId] < block.number - 2_400_000, "Owner ping has not expired");
        }
        currentLock[tokenContract][tokenId] = address(0); // Remove all locks on this token
        _withdraw(tokenContract, tokenId);
        IERC721(tokenContract).safeTransferFrom(
          address(this),
          msg.sender,
          tokenId
        );
      }
    }
    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts v4.4.1 (token/ERC721/IERC721Receiver.sol)
    pragma solidity ^0.8.0;
    /**
     * @title ERC721 token receiver interface
     * @dev Interface for any contract that wants to support safeTransfers
     * from ERC721 asset contracts.
     */
    interface IERC721Receiver {
        /**
         * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
         * by `operator` from `from`, this function is called.
         *
         * It must return its Solidity selector to confirm the token transfer.
         * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
         *
         * The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.
         */
        function onERC721Received(
            address operator,
            address from,
            uint256 tokenId,
            bytes calldata data
        ) external returns (bytes4);
    }
    // SPDX-License-Identifier: AGPL-3.0
    pragma solidity ^0.8.9;
    interface IReverseResolver {
      function claim (address owner) external returns (bytes32);
    }
    interface IERC20 {
      function balanceOf (address account) external view returns (uint256);
      function transfer (address recipient, uint256 amount) external returns (bool);
    }
    interface IERC721 {
      function safeTransferFrom (address from, address to, uint256 tokenId ) external;
    }
    interface IDocumentationRepository {
      function doc (address contractAddress) external view returns (string memory name, string memory description, string memory details);
    }
    error MissingRole(bytes32 role, address operator);
    abstract contract OwnableBase {
      bytes32 public constant ADMIN_ROLE = 0x00;
      mapping(bytes32 => mapping(address => bool)) internal roles; // role => operator => hasRole
      mapping(bytes32 => uint256) internal validSignatures; // message hash => expiration block height
      IDocumentationRepository public DocumentationRepository;
      event RoleChange (bytes32 indexed role, address indexed account, bool indexed isGranted, address sender);
      constructor (address documentationAddress) {
        roles[ADMIN_ROLE][msg.sender] = true;
        DocumentationRepository = IDocumentationRepository(documentationAddress);
      }
      function doc () public view returns (string memory name, string memory description, string memory details) {
        return DocumentationRepository.doc(address(this));
      }
      /**
       * @dev See {ERC1271-isValidSignature}.
       */
      function isValidSignature(bytes32 hash, bytes memory)
        external
        view
        returns (bytes4 magicValue)
      {
        if (validSignatures[hash] >= block.number) {
          return 0x1626ba7e; // bytes4(keccak256("isValidSignature(bytes32,bytes)")
        } else {
          return 0xffffffff;
        }
      }
      /**
       * @dev Inspect whether a specific address has a specific role.
       */
      function hasRole (bytes32 role, address account) public view returns (bool) {
        return roles[role][account];
      }
      /* Modifiers */
      modifier onlyRole (bytes32 role) {
        if (roles[role][msg.sender] != true) revert MissingRole(role, msg.sender);
        _;
      }
      /* Administration */
      /**
       * @dev Allow current administrators to be able to grant/revoke admin role to other addresses.
       */
      function setAdmin (address account, bool isAdmin) public onlyRole(ADMIN_ROLE) {
        roles[ADMIN_ROLE][account] = isAdmin;
        emit RoleChange(ADMIN_ROLE, account, isAdmin, msg.sender);
      }
      /**
       * @dev Claim ENS reverse-resolver rights for this contract.
       * https://docs.ens.domains/contract-api-reference/reverseregistrar#claim-address
       */
      function setReverseResolver (address registrar) public onlyRole(ADMIN_ROLE) {
        IReverseResolver(registrar).claim(msg.sender);
      }
      /**
       * @dev Update address for on-chain documentation lookup.
       */
      function setDocumentationRepository (address documentationAddress) public onlyRole(ADMIN_ROLE) {
        DocumentationRepository = IDocumentationRepository(documentationAddress);
      }
      /**
       * @dev Set a message as valid, to be queried by ERC1271 clients.
       */
      function markMessageSigned (bytes32 hash, uint256 expirationLength) public onlyRole(ADMIN_ROLE) {
        validSignatures[hash] = block.number + expirationLength;
      }
      /**
       * @dev Rescue ERC20 assets sent directly to this contract.
       */
      function withdrawForeignERC20 (address tokenContract) public onlyRole(ADMIN_ROLE) {
        IERC20 token = IERC20(tokenContract);
        token.transfer(msg.sender, token.balanceOf(address(this)));
      }
      /**
       * @dev Rescue ERC721 assets sent directly to this contract.
       */
      function withdrawForeignERC721 (address tokenContract, uint256 tokenId)
        public
        virtual
        onlyRole(ADMIN_ROLE)
      {
        IERC721(tokenContract).safeTransferFrom(
          address(this),
          msg.sender,
          tokenId
        );
      }
      function withdrawEth () public onlyRole(ADMIN_ROLE) {
        payable(msg.sender).transfer(address(this).balance);
      }
    }