...

Developers Guide

Extend the Network

The JumpPort contract is designed to be used by many different token projects. This guide steps through the technical backing, and provides information that is useful for developers looking to integrate with the system. If you're an end-user who would like to know how to use the JumpPort and its related Portals, you'd probably be more interested in this guide.


JumpPort

The JumpPort is designed to be a central contract that any ERC721-compliant token can use as a custodial staking solution. The ERC721 standard does not have a built-in standard for how to "lock" a token in place (prevent transfers). However, token holders can voluntarily lock a token in order to signal that they do not currently intend to sell that token. Token holders will instead be holding it as to use it. Some collections have built a "staking" option into their collection contract (and can be a non-custodial solution). As for tokens that don't have that option built-in, the JumpPort can be a way to easily add that functionality to a collection.

From an end-user perspective, the JumpPort allows them to do two key actions: Deposit and Withdraw from the JumpPort. If a token-owner does not interact with any of the Portal contracts, they may Deposit and Withdraw at will, and do not need prior authorization/allow-listing from the JumpPort Administrators nor the token project owners.

If a token is fully ERC721-compliant, it should be able to successfully deposit and withdraw from the JumpPort. The specific functions of the ERC721 standard that the JumpPort uses are safeTransferFrom and transferFrom. These functions are used to withdraw tokens from the JumpPort to their original owners. There are withdraw functions that target each of those transfer methods to aid in the creation of your own apps. When building your own apps, you can select the most appropriate transfer function for your token.

Depositing

The JumpPort is an "ERC721-receiver" (it implements the onERC721Received function). Any token that properly implements a safeTransferFrom function of their own that looks for and calls the onERC721Received function on the receiving contract when transferring can use that method to send tokens individually to the JumpPort.

If a user would like to transfer multiple tokens at once into the JumpPort, the JumpPort has batch-transfer functions that can be used for that. If the JumpPort is authorized to act upon a user's tokens (the token has properly implemented "Approve" and/or "Approve for All" functions), then that user can call the deposit function on the JumpPort, and it will use the transferFrom function on the incoming token's collection contract to grab the token. The deposit function needs, as input, the address of the incoming token's collection, and the incoming token's identifier in that collection. There are multiple variations of the deposit function that will accept individual versions of those parameters, or arrays of those pairs (deposit (address tokenAddress, uint256 tokenId) for single tokens, plus deposit (address tokenAddress, uint256[] calldata tokenIds) and deposit (address[] calldata tokenAddresses, uint256[] calldata tokenIds) for multiple at once).

When a token is deposited into the JumpPort, a "Deposit" event is fired for each token, which includes which Owner was recorded for that token.

Withdrawing

One of the core tenants of the JumpPort contract is that a token cannot change ownership while in the JumpPort. The logic of withdrawing is therefore set to ensure that the address that deposited the token is the only address that token can be withdrawn to. This also helps prevent the real owner of a token from being tricked into signing a transaction that withdraws their token to an attacker's address.

The JumpPort has six "withdraw" function options. These function options relate to the ERC721 standard "Safe Transfer From", "Safe Transfer From (with data)" and "(unsafe) Transfer From" functions, with both a single-token and multi-token form. All six do the same action from the JumpPort's perspective: they give the token back and mark it as withdrawn. The key difference is which function on the token collection contract gets called to move the token out:

safeWithdraw (address tokenAddress, uint256 tokenId) and
safeWithdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds)
use the "Safe Transfer From" (with no additional "data" parameter) token function.
safeWithdraw (address tokenAddress, uint256 tokenId, bytes calldata data) and
safeWithdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds, bytes[] calldata data)
use the "Safe Transfer From (with data)"" token function.
withdraw (address tokenAddress, uint256 tokenId) and
withdraw (address[] calldata tokenAddresses, uint256[] calldata tokenIds)
use the "(unsafe) Transfer From".

When a token is withdrawn from the JumpPort, a "Withdraw" event is fired for each token. This event includes how many blocks the token spent in the JumpPort for the stay that just ended.

Copilots

While the JumpPort requires only one owner for each token and can only withdraw to that original owner, it includes a system by which the owner can grant other addresses the ability to operate on their token. Portals can optionally use this as additional permissions to allow actions on their application. This is similar to the "Approval" system that is part of the ERC721 standard. In order to keep with the lore of the JumpPort, they are termed "Copilots." Enumerating which addresses have been granted access to be Copilots for a token share the same function names as the ERC721 standard:

  • getApproved: If a Copilot has been granted access to a specific token, return that address
  • isApprovedForAll: If a Copilot has been granted access to all tokens owned by a specific other address, return true.

For a token owner to set an address as having Copilot rights, the functions to call are:

  • setCopilot: Grant a Copilot access to a specific token.
  • setCopilotForAll: Granted a Copilot access to all tokens owned by the owner's address.

Token Metadata

The JumpPort has several view functions that return metadata about individual tokens. Each of these functions generally require as input the tokenAddress (the address of the ERC721 collection contract), and the tokenId being inquired about:

  • isDeposited: Existence-check for a token in the JumpPort; returns true if that token is currently deposited.
  • depositedSince: What block was the token deposited into the JumpPort? Reverts if the token isn't deposited currently.
  • isLocked: Is the token locked in place (unable to be withdrawn)?
  • lockedBy: For tokens that are locked, which Portal(s) are locking it? Returns an array of addresses that are each Portal contracts locking the token.
  • ownerOf: Which address owns this token? This is the address the token will be returned to when it's Withdrawn from the JumpPort.

Ownership Enumeration

In the prior section, the ownerOf function was mentioned as a way to find out who owns a token if you already know the token you care about. But what about going the other direction? If you have an address and want to know all the tokens in the JumpPort they own, how to find those? In the ERC721 standard, recording that sort of metadata falls under the "Enumerable" optional feature, which means not all ERC721 tokens implement that. Notably, projects using the ERC721A implementation forgo owner enumeration to save gas on minting/transferring. The JumpPort follows that rubric and does not have a function to step through a list of all tokens owned by a particular address. For clients that do wish to discover all tokens an address owns within the JumpPort, they can scan for Deposit and Withdraw Events emitted from the JumpPort, or there are some additional functions on the JumpPort contract that can help:

When starting to enumerate an specific address' holdings in the JumpPort, the balanceOf function will help get a total count. There are two variations of that function, one that takes a single token collection address as input and one that takes an array of collection addresses. When requesting the total for a single collection address, the result is a structured data object with both the total count of owned tokens for that owner, as well as the block height it was most recently updated. This can be used for cache-checking on the client side, to know if it's necessary to refresh the list of tokens the client app thinks are deposited by that owner.

For example, if querying the balanceOf function for Alice's address, requesting her count of MoonCats deposited in the JumpPort returns that she has five deposited as of block 14927000, a client app could use the other bulk enumeration tools and find which five MoonCat IDs Alice owns and cache it. If then at a future time they query Alice's balance again and find it's still five MoonCats, but the block updated to 14927500, the client knows the cache is likely not valid any further because even though her balance is still "five", because the JumpPort noted the block height of last change is different, that indicates Alice did something (likely added a MoonCat and withdrew a different MoonCat) and which specific MoonCats she has deposited should be re-fetched.

Once a client app knows that a given owner has a non-zero balance for a specific collection, they could loop through and call ownerOf on every token in the collection. But doing that looping on the client side means that to scan across a 10,000 item NFT collection would take 10,000 remote calls through an Ethereum node's API, and the network overhead of all those calls would cause it to take quite a long time to iterate through. To reduce the number of network calls that would need to be done for a client to enumerate a full collection, the JumpPort has a few bulk query functions to help with that:

ownedTokens (address tokenAddress, address owner, uint256 tokenSearchStart, uint256 tokenSearchEnd)
This function is used when looking to find only tokens owned by a specific address. It will return a filtered array of only the token IDs owned by that owner. The function allows entering a "start" and "end" identifier, which it loops through inclusively, allowing client apps to break up the token collection iterating however they want. A client could call ownedTokens(MOONCATS_ADDRESS, ALICE_ADDRESS, 0, 25439) to iterate through the whole MoonCat collection in one call, but that would take several seconds of computation for the Ethereum node that got queried to return a result. Most public Ethereum nodes have a hard limit on how long a computation call can take, so that sort of interaction might be possible if the client app is running its own Ethereum Node and can set that configuration higher, but otherwise it would still need to be split up into chunks. Submitting this function call with a 1,000 item gap between "start" and "end" values should execute in enough time for a public node to not reject it. Going through a collection 1,000 items at a time will still require a loop on the client side to step through the collection, but is a significantly less number of network calls to make, and therefore should take under a minute for typical collections.
ownersOf (address tokenAddress, uint256 tokenSearchStart, uint256 tokenSearchEnd)
This function doesn't filter for a specific owner, and instead returns an array of addresses, where the address at index zero is the owner of the token with ID tokenSearchStart, the address at index one is the owner of token tokenSearchStart + 1, etc. If a token is not deposited into the JumpPort, this function will return the Zero Address for that token ID's position.

Governance

The JumpPort contract has built-in role-based governance, where any address that is marked as having an Administrator role, in a manner inspired by OppenZeppelin's Role-based Access Control structure. The JumpPort uses two different "role" identifiers, each of which is a bytes32 identifier. The values for those identifiers can be fetched from the ADMIN_ROLE and PORTAL_ROLE properties on the JumpPort contract. When addresses are granted/revoked access to a specific role, a "RoleChange" event is emitted from the JumpPort contract.

JumpPort Administrators are able to withdraw ERC20 tokens sent to the JumpPort at any time (as there's no reason for the JumpPort to hold ERC20 tokens, any tokens that end up there is likely a mistake, and the Administration can "rescue" them and either take them as a donation or attempt to deliver them back to their original owner).

JumpPort Administrators can also withdraw ERC721 tokens that are sent to the JumpPort, as long as certain circumstances are met. If an ERC721 token is owned by the JumpPort, but the JumpPort itself didn't record any "owner" for it in its own metadata (that token was likely sent to the JumpPort via an "(unsafe) Transfer From" call), that is an indication a mistake was made, and an Administrator can rescue the token immediately. If an ERC721 token is owned by someone in the JumpPort, in most circumstances only the address who deposited that token should be allowed to interact with it. But if a situation arises where that owner loses access to their private key or other circumstance prevents them from interacting using that account, that user could petition the Administrators to start a force-withdrawal of the token. If the Administrators are convinced that a properly-owned token should be removed, they can trigger the adminWithdrawPing function on the JumpPort. That function is similar to a "dead-man switch"/"Operator Presence" check. If adminWithdrawPing gets called for a specific token, the owner of that token has one year to respond (call ownerPong from their owner address). If they don't respond, the JumpPort Administrators can withdraw the token. Note that Administrators can withdraw a token in this manner even if it is locked by a Portal contract.

This one year delay prevents users requesting that Administrators use this ability frivolously. For example, if a user wanted their token right now, they might be tempted to try to bully the Administrators into acting more quickly, if Administrators had that right. This prevents a rogue/compromised Administrator from being able to drain the JumpPort contract swiftly. This delay ensures that if a user has convinced the Administrators to start the process, but they are not actually the owner of a token, then the true owner could step forward and assert that they are still in control of the address that owns the token in question.

JumpPort Administrators are also able to designate whether an address is allowed to act as a Portal, thereby granting it the right to lock and unlock tokens. This action is done through the setPortalValidation function, which will trigger a "RoleChange" event for the Portal address that got promoted/demoted.

A smart contract that has the role of Portal can then lock and unlock tokens. If something goes wrong with a Portal contract. One example of this would be if the Portal is designed as a "bridge" contract to bring tokens to another chain, the "unlock" action would require triggering from the other chain. If the other chain were to have a catastrophic failure, there would be no way for users to trigger the "unlock" action. In this case, the Administrators could use the setAdminLockOverride function to designate a specific Portal has failed and any "locks" it currently has on tokens should be considered voided. This would then allow owners of those tokens to withdraw them (assuming no other Portal held a lock on those tokens).

As a final, global control mechanism, Administrators have the ability to pause deposits going into the JumpPort via the setPaused function. This can be used if there is a systemic flaw discovered in the JumpPort and users would be stopped from depositing into the JumpPort to keep their assets safe. Administrators can not pause withdrawals, which helps avoid a situation in which a rogue/compromised Administrator might attempt to hold all tokens hostage in the JumpPort.

Initially, the ponderware corporate address will be the sole Administrator of the JumpPort contract. If other communities begin using the JumpPort, Administration would then shift to a multi-sig/DAO contract for the combined project team leaders. This multi-sig/DAO contract would then be transitioned to exist as the sole Administrator of the JumpPort.


Portals

Portal contracts are a classification of smart contracts that use metadata from the JumpPort. These Portal contracts are granted the rights to Lock and Unlock tokens within the JumpPort in order to assist in the development of applications that require time-delayed interactions to guarantee that a token does not move during that time.

Any smart contract can use JumpPort metadata, but if a contract requires the ability to lock and unlock tokens, then the developers of that smart contract will need to get the JumpPort Administrators to grant their smart contract a Portal role.

Locking

To lock a token into the JumpPort, a Portal contract can call lockToken, which will emit a "Lock" event that indicates which token was locked and by which Portal. One token can be locked by more than one Portal at a time.

Unlocking

To unlock a token in the JumpPort that was previously locked by a Portal, the appropriate Portal contract can call unlockToken. This will emit an "Unlock" event indicating which token was unlocked and by which Portal. A Portal will not be able to "withdraw" a token from the JumpPort after unlocking if other Portals still have a lock on that token, or if the Portal itself is not the owner of the token that was unlocked.

Governance

Portal contracts can call the function unlockAllTokens on the JumpPort. This function can only be called by Portal contracts, and when called, marks all locks held by that specific Portal as void. This has the same effect as the JumpPort Administrators calling setAdminLockOverride on that Portal. The difference is that Administrators can mark any Portal's locks as void, while an individual Portal can only mark its own locks as void. This is intended to be used if the Portal itself wants to have some governance process within itself and ensures that even if all the Administrators of the JumpPort went dark/unresponsive, a Portal could still react to an emergency under its own domain.