账户
0xf4...4532
0xF4...4532

0xF4...4532

$500
此合同的源代码已经过验证!
合同元数据
编译器
0.8.9+commit.e5eed63a
语言
Solidity
合同源代码
文件 1 的 3:IERC721Receiver.sol
// 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);
}
合同源代码
文件 2 的 3:JumpPort.sol
// 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
    );
  }

}
合同源代码
文件 3 的 3:OwnableBase.sol
// 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);
  }

}
设置
{
  "compilationTarget": {
    "contracts/JumpPort.sol": "JumpPort"
  },
  "evmVersion": "london",
  "libraries": {},
  "metadata": {
    "bytecodeHash": "ipfs"
  },
  "optimizer": {
    "enabled": true,
    "runs": 200
  },
  "remappings": []
}
ABI
[{"inputs":[{"internalType":"address","name":"documentationAddress","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"operator","type":"address"}],"name":"MissingRole","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"tokenAddress","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"address","name":"target","type":"address"},{"indexed":false,"internalType":"bytes","name":"data","type":"bytes"}],"name":"ActionExecuted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"approved","type":"address"},{"indexed":true,"internalType":"address","name":"tokenAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"operator","type":"address"},{"indexed":false,"internalType":"bool","name":"approved","type":"bool"}],"name":"ApprovalForAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"tokenAddress","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"portalAddress","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"tokenAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Lock","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"role","type":"bytes32"},{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":true,"internalType":"bool","name":"isGranted","type":"bool"},{"indexed":false,"internalType":"address","name":"sender","type":"address"}],"name":"RoleChange","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"portalAddress","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"tokenAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"Unlock","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"tokenAddress","type":"address"},{"indexed":true,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"duration","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[],"name":"ADMIN_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"DocumentationRepository","outputs":[{"internalType":"contract IDocumentationRepository","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PORTAL_ROLE","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"adminPong","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"adminWithdrawPing","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"tokenAddresses","type":"address[]"},{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"address","name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"components":[{"internalType":"uint64","name":"balance","type":"uint64"},{"internalType":"uint192","name":"blockHeight","type":"uint192"}],"internalType":"struct JumpPort.BalanceRecord","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"isBlocked","type":"bool"}],"name":"blockExecution","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"tokenAddresses","type":"address[]"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"deposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"depositPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"depositedSince","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"doc","outputs":[{"internalType":"string","name":"name","type":"string"},{"internalType":"string","name":"description","type":"string"},{"internalType":"string","name":"details","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"address","name":"targetAddress","type":"address"},{"internalType":"bytes","name":"payload","type":"bytes"}],"name":"executeAction","outputs":[{"internalType":"bytes","name":"result","type":"bytes"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"executionBlocked","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"getApproved","outputs":[{"internalType":"address","name":"copilot","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"role","type":"bytes32"},{"internalType":"address","name":"account","type":"address"}],"name":"hasRole","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"operator","type":"address"}],"name":"isApprovedForAll","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"isDeposited","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"isLocked","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"bytes","name":"","type":"bytes"}],"name":"isValidSignature","outputs":[{"internalType":"bytes4","name":"magicValue","type":"bytes4"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"lockOverride","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"lockToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"lockedBy","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"},{"internalType":"uint256","name":"expirationLength","type":"uint256"}],"name":"markMessageSigned","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"operator","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"onERC721Received","outputs":[{"internalType":"bytes4","name":"","type":"bytes4"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"address","name":"owner","type":"address"},{"internalType":"uint256","name":"tokenSearchStart","type":"uint256"},{"internalType":"uint256","name":"tokenSearchEnd","type":"uint256"}],"name":"ownedTokens","outputs":[{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerOf","outputs":[{"internalType":"address","name":"owner","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"ownerPong","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenSearchStart","type":"uint256"},{"internalType":"uint256","name":"tokenSearchEnd","type":"uint256"}],"name":"ownersOf","outputs":[{"internalType":"address[]","name":"tokenOwners","type":"address[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address[]","name":"tokenAddresses","type":"address[]"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"},{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"safeWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"tokenAddresses","type":"address[]"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"safeWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"safeWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"safeWithdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"},{"internalType":"bool","name":"isAdmin","type":"bool"}],"name":"setAdmin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"portal","type":"address"},{"internalType":"bool","name":"isBlocked","type":"bool"}],"name":"setAdminExecutionBlocked","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"portal","type":"address"},{"internalType":"bool","name":"isOverridden","type":"bool"}],"name":"setAdminLockOverride","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"copilot","type":"address"},{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"setCopilot","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"copilot","type":"address"},{"internalType":"bool","name":"approved","type":"bool"}],"name":"setCopilotForAll","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"documentationAddress","type":"address"}],"name":"setDocumentationRepository","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"isDepositPaused","type":"bool"}],"name":"setPaused","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"portalAddress","type":"address"},{"internalType":"bool","name":"isValid","type":"bool"}],"name":"setPortalValidation","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"registrar","type":"address"}],"name":"setReverseResolver","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"tokenPingRequestBlock","outputs":[{"internalType":"uint256","name":"blockNumber","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address[]","name":"tokenAddresses","type":"address[]"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"tokenPingRequestBlocks","outputs":[{"internalType":"uint256[]","name":"blockNumbers","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bool","name":"isOverridden","type":"bool"}],"name":"unlockAllTokens","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"unlockToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address[]","name":"tokenAddresses","type":"address[]"},{"internalType":"uint256[]","name":"tokenIds","type":"uint256[]"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenAddress","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawEth","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenContract","type":"address"}],"name":"withdrawForeignERC20","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"tokenContract","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"}],"name":"withdrawForeignERC721","outputs":[],"stateMutability":"nonpayable","type":"function"}]