Skip to main content
Version: 0.1.0

Staking-Bearing NFTs as your collectibles

sTokens are NFTs automatically minted when the user stakes DEV into one of the Property Tokens and function as a key to recognizing the staking position. Therefore, as long as you have sTokens, the staking position associated with it is yours. If you transfer sTokens to someone else, your staking position will also be transferred entirely.

tokenURI.image of sTokens returns the preset SVG image by default, but the Property Tokens author can change that value with a few options. If you are the author of Property Tokens, you can create NFT collectibles based on staking by rewriting the tokenURI.image in sTokens to a unique image.

Dynamic sTokens​

The most recommended way to make sTokens collectibles is to generate tokenURI.image for sTokens with Dynamic sTokens dynamically.

To enable Dynamic sTokens, create a Descriptor contract inherited from ITokenURIDescriptor and pass that contract address to STokensManager.setTokenURIDescriptor.

Create a Descriptor​

dynamic-s-tokens-simple-tiers can be used as an example of a Descriptor.

ITokenURIDescriptor is exposed as an npm package, so you can add it to your project's dependencies as follows:

npm i -D @devprotocol/i-s-tokens

The Descriptor must needs to implement the following interface:

function image(
uint256 _tokenId,
address _owner,
ISTokenManagerStruct.StakingPositionV1 memory _positions,
ISTokenManagerStruct.RewardsV1 memory _rewards
) external view returns (string memory);

The image function takes the following arguments and returns an image used as tokenURI.image. The function is expected to return a base64 encoded data URI, IPFS or HTTP URI.

address property; // 0xF4f8a63bc2A7608757407aA9923eea8DeA9279c0
uint256 amount; // 100000000000000000000
uint256 price; // 435847878060443323
uint256 cumulativeReward; // 0
uint256 pendingReward; // 0
uint256 entireReward; // 40634326095432410200
uint256 cumulativeReward; // 0
uint256 withdrawableReward; // 40634326095432410200

And those values have the following meanings:

_tokenIdID of sTokens. Remember that your Property Tokens sTokens ID is not necessarily a serial number, as the ID is unique across all Property Tokens.
_ownerThe address of the owner of sTokens.
_positions.propertyThe address of Property Tokens as a staking destination.
_positions.amountThe amount of staked DEV tokens. The number is an integer multiplied by 10^18, similar to a typical ERC-20 amounts.
_positions.priceThis value is a snapshot of price, the value that the Lockup contract uses to calculate the staking reward, and is a value that is constantly increasing. The difference between the third return value of Lockup.calculateCumulativeRewardPrices and this value increases over time from the last update date of the staking position. However, remember that the difference does not indicate the time series.
_positions.cumulativeRewardThe cumulative sum of DEV rewards claimed to the staking position. The number is an integer multiplied by 10^18, similar to a typical ERC-20 amounts.
_positions.pendingRewardThe unclaimed DEV reward. This value is updated when the staking position is updated. The number is an integer multiplied by 10^18, similar to a typical ERC-20 amounts.
_rewards.entireRewardThe cumulative sum of all rewards of the staking position, and this value is the sum of claimed reward, claimable reward, and pending reward. The number is an integer multiplied by 10^18, similar to a typical ERC-20 amounts.
_rewards.cumulativeRewardSame value as _positions.cumulativeReward.
_rewards.withdrawableRewardThe current claimable DEV reward of the staking position. And this value is the sum of the claimable reward and pending reward. The number is an integer multiplied by 10^18, similar to a typical ERC-20 amounts.

Return value example​

Base64-encoded SVG​





Apply the developed Descriptor to your Property Tokens​

After deploying the Descriptor, associate the Descriptor with the Property Tokens by calling STokensManager.setTokenURIDescriptor. This function call is to be successful only if the caller's address is the same as the author of the passed Property Tokens.

NetworkExplorer page for STokensManager
Arbitrum One
Arbitrum Rinkeby
Polygon Mumbai

Dev Kit JS​

Using Dev Kit JS, you can call sTokens.tokenURI from your JavaScript app.

import { clientsSTokens } from '@devprotocol/dev-kit/agent'

const sTokens = clientsSTokens(provider) // `provider` means BaseProvider of Ethers.js
const tokenURI = sTokens.tokenURI(1)
// {
// name: string
// description: string
// image: string
// }

You can also simulate the return value of the Descriptor by calling STokensManager.tokenURISim as follows and pre-calculate the image of unminted sTokens. This is useful for implementing a collectibles sales page as an NFT list UI.

import { clientsSTokens } from '@devprotocol/dev-kit/agent'
import { utils } from 'ethers'

const sTokens = clientsSTokens(provider) // `provider` means BaseProvider of Ethers.js
const options = {
positions: {
property: '0xF4f8a63bc2A7608757407aA9923eea8DeA9279c0'
amount: utils.parseUnits('100', 18).toString(),
const tokenURI = sTokens.tokenURISim(options)
// {
// name: string
// description: string
// image: string
// }

tokenURISim takes the same arguments as Descriptor.image. See the source code for the full of typing.


By calling Dev Kit JS, you can mint sTokens through DEV staking as follows.

import { positionsCreate } from '@devprotocol/dev-kit/agent'
import { whenDefined } from '@devprotocol/util-ts'
import { utils } from 'ethers'

const start = await positionsCreate({
from: '<USER_ADDRESS>',
destination: '0xF4f8a63bc2A7608757407aA9923eea8DeA9279c0',
amount: utils.parseUnits('100', 18).toString(),
// When approval is needed, start.approvalNeeded is true, and start.approveIfNeeded does send the approval transaction
// When approval is not needed, start.approvalNeeded is false, and start.approveIfNeeded does not send anything
const approveIfNeeded = await whenDefined(start, (x) => x.approveIfNeeded())
// When approval is needed, approveIfNeeded.waitNeeded is true, and start.waitOrSkipApproval does wait until confirmed the transaction
// When approval is not needed, approveIfNeeded.waitNeeded is false, and start.waitOrSkipApproval does not do anything and resolve immediately
const waitOrSkipApproval = await whenDefined(approveIfNeeded, (x) =>
const stake = await whenDefined(waitOrSkipApproval, (x) =>
const staked = await whenDefined(stake, (x) => x.wait())