RWA fraction market

This guide outlines the process of deploying a new market using Evergonlab's api-client. In this use case, ERC-721 or ERC-1155 assets are locked in the vault as collateral to issue fractionalized ERC-20 tokens, which are then sold to investors. The assets held in the vault represent real-world twin NFTs.

Once investors purchase the fractionalized tokens, they must stake them in the staking pool to receive dividends generated by the real-world assets. For detailed instructions on deploying the staking platform and running a staking campaign to fully implement the RWA market use case, refer to the Profit Sharing Pools guide.

📘

Note

This guide relies on the viem library, but you are free to use any other Web3 provider such as web3.js or ethers.js.

Preface

There are three entities involved in the process:

  • Admin - Deploys the fraction market.
  • Issuer - Creates the sale.
  • Investor - Purchases fractions.

Below is a simplified diagram outlining the entire process, along with the key actions of each entity:

  1. Admin deploys the Fraction Market.
  2. The Issuer submits a request to the Fraction Platform to approve the spending of collateral that will be locked in the Vault.
  3. The Issuer submits a request to the Fraction Platform to create a new market.
  4. The Fraction Platform processes the request and creates a new Market with the specified number of fractions to be sold to investors. During this process, the platform sets both the softcap and hardcap. If the softcap is not reached, users can claim their invested funds, and the Issuer can unwrap their collateral from the Vault.
  5. The Investor purchases a specified number of fractions.
  6. The Investor stakes the purchased fractions into the Staking Pool.
  7. The Investor can claim rewards after a specified period, as defined in the Staking Pool configuration.

The goal of this guide is to deploy the Fraction Platform, where a Market will be launched to sell fractions to investors.

🚧

Important

Note that this guide does not cover the staking steps 6 and 7 outlined in the diagram. For a complete RWA market use case implementation, refer to the Profit Sharing Pools guide.

Additionally, you must deploy a new smart contract and mint the corresponding ERC-721/1155 NFTs, which will represent real-world assets to be locked in the Vault.

Imports

Create a new typescript project, use the following imports and install the required dependencies.

import { api, handleApiResponse, getEnv, Transaction, ApiClient } from "@evergonlabs/tmi-protocol-api-client";  
import { Address, createWalletClient, Hash, http} from "viem";  
import { privateKeyToAccount } from "viem/accounts";  
import { sepolia } from "viem/chains";
ImportDescription
"evergonlabs/tmi-protocol-api-client"This library is required to construct data objects that are passed as input when executing transactions via the viem.writeContract function and to handle the API responses.
"viem"The viem library is used to create the wallet client, which is then used to send transactions to deploy a fraction platform, market, create fractions, approve the sale, buy fractions, and perform other actions.
"viem/accounts"The privateKeyToAccount import is used when defining a wallet client via the createWalletClient call.
"sepolia"This import is required because the platform will be deployed to the Sepolia testnet.

Defining the API client

The @evergonlab/tmi-protocol-api-client library uses an API client to send transactions. The env property sets the environment to either "test" or "prod". Begin by defining the API client in your code:

  const apiClient = api.createClient({
    env: getEnv("test"),
  });

Deploying the Fraction Platform

To begin the deployment you must first create the Admin client by using the createWalletClient function:

const adminClient = createWalletClient({
  account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
  chain: sepolia,
  transport: http("https://your-provider.com/api-key"),
});
PropertyDescription
accountSpecify your private key using the privateKeyToAccount function.
chainSet the chain property to sepolia.
transportPass your RPC provider URL to the transport property to interact with the EVM network (e.g., Alchemy ).

🚧

Important

The process of creating Admin, Issuer, and Investor wallet accounts is identical across this material. As a result, the descriptions of parameters and similar input values apply to all wallet instances.

Define the sendTransaction function that will be responsible for sending transactions using the writeContract function of the viem library.

The transaction argument expects a Transaction type which must be a data object containing transaction details. The viemClient argument expects a client instance created using the createWalletClient function.

The returned Hash is used throughout this guide to get the transaction response.

async function sendTransaction(viemClient:any, transaction: Transaction): Promise<Hash> {
  return await viemClient.writeContract({
    args: transaction.details.args,
    functionName: transaction.details.functionName,
    abi: transaction.details.abi,
    address: transaction.details.address as Address,
  });
}

Define the deployPlatform function that will:

  1. Create a data object for the transaction using the await api.fractions.market.deployMarket call.
  2. Pass the data object to the sendTransaction function to deploy the fraction platform.
  3. Handle the returned transaction hash using the handleApiResponse function.
  4. Return the platformDetails object, which contains the address of the fraction platform and the Vault to be used in subsequent function calls. This operation requires the use of the getMarketDeployedEvent function, which is included in the API client; It retrieves the platform details by using the transaction hash.
async function deployPlatform(apiClient: ApiClient, viemClient: any) {
  const transactionData = handleApiResponse(
    await api.fractions.market.deployMarket({
      client: apiClient,
      body: {
          chainId: `${sepolia.id}`,
          adminAddress: viemClient.account.address,
          discount: {
            burnForDiscount: {
              burnableTokenAddress: "0xffffffffffffffffffffffffffffffffff"
            }
          },
          cap: {
            maxHardCap: 1000000n
          },
          fee: {
            percent: 10
          }
      },
}))

  const platformDetails = handleApiResponse(
    await api.fractions.market.getMarketDeployedEvent({
      query: {
        chainId: `${sepolia.id}`,
        hash: await sendTransaction(adminClient, transactionData),
      },
    }),
  );
  return platformDetails;
}
PropertyDescription
clientExpects an instance of the api.createClient to make API calls.
chainIdSpecify the chain id using the imported sepolia.
adminAddressRepresents an account address that must be granted the 'Admin' role for the fraction platform.
burnableTokenAddressSpecifies the address of a token that can be burned (sent to the address) in order to receive a discount on platform fees or services.
feeDenotes the discount amount to be provided to a user.

Deploy the platform by passing the apiClient and adminClient to the deployPlatform function call and store the result in the platformReceipt variable. This will provide the address of the fraction platform for use in subsequent calls.

const platformReceipt = await deployPlatform(apiClient, adminClient)

📘

Roles

When deploying, the Admin entity becomes the owner of the platform, with the 'ADMIN' role assigned automatically. Only one 'ADMIN' is allowed per platform.

The Admin entity can then grant the following roles to other entities: : 'ISSUER', and 'INVESTOR'.

'ISSUER'pool - Grants permission to deploy a market.
'INVESTOR' - Grants an account the ability to purchase fractions.

At this stage, we have successfully deployed the fraction platform. The address of the platform can be obtained via the diamondAddress property:

const diamondAddress = platformReceipt.diamondAddress

When the fraction platform is created, a Vault/Wrapper is deployed alongside it, responsible for wrapping and locking various assets as collateral. The Vault's address is required by Issuers who will create their sales. The wrapper address is returned in the platformReceipt variable:

const wrapperAddress = platformReceipt.wrapperAddress

Creating the market

Before the Issuer creates a new market, they must go through the KYC/KYB process using a wallet supported by ComPilot's service. For example, this could be MetaMask. The Issuer's address will then be allowed to create a new market.

After the successful KYC/KYB, the Issuer must approve the Vault to allow the spending of ERC-721 assets, which will be locked as collateral.

Create the wallet client for the Issuer entity:

const issuerClient = createWalletClient({
  account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
  chain: sepolia,
  transport: http("https://your-provider.com/api-key"),
});

Next, define the approvePlatformWrapper function, which will be responsible for sending a transaction to approve the wrapper. This function depends on the approveWrapper function provided by the API client:

async function approvePlatformWrapper(apiClient: ApiClient, viemClient: any, diamondAddress: string, wrapperAddress: string) {
    const transactionData = handleApiResponse(
      await api.fractions.approvals.approveWrapper({
        client: apiClient,
        body: {
          marketAddress: diamondAddress,
          wrapperAddress: wrapperAddress
        }}));

    const transactionResult = await sendTransaction(viemClient, transactionData)

    return transactionResult;
}

Call the approvePlatformWrapper operation to approve the Vault. The result of this operation is optional:

const approveResult = approvePlatformWrapper(apiClient, issuerClient, diamondAddress, wrapperAddress);

After approving the Vault, you must specify the asset and to be wrapped as the collateral. This guide relies on the ERC-721 as the digital-twin NFT, representing the RWA asset. Therefore, we must use the approveWrapperToSpendErc721 function provided by the API client.

Define the approveCollateral function to create the necessary logic for asset approval:

async function approveCollateral(apiClient: ApiClient, viemClient: any, wrapperAddress: string, rwaTokenAddress: string) {
    const transactionData = handleApiResponse(
      await api.fractions.approvals.approveWrapperToSpendErc721({
        client: apiClient,
        body: {
          wrapperAddress: wrapperAddress,
          erc721Address: rwaTokenAddress
        }
      }))

    const transactionResult = await sendTransaction(viemClient, transactionData)

    return transactionResult;
}

Then invoke the approveCollateral operation to approve the spending of the ERC-721 asset by the Vault:

const approveAssetResult = approveCollateral(apiClient, issuerClient, wrapperAddress, "0xfffffffffffffffffffffffffffffffffffffffffff");

To allow Issuer deploying a new market, the Admin entity must grant the 'ISSUER' role.

Define the grantIssuerRole function, which generates a new transaction object using the grantRole function, and then sends it via the sendTransaction invocation.

async function grantIssuerRole(client: ApiClient, viemClient: any, diamondAddress: string) {
  const transactionData = handleApiResponse(
    await api.fractions.roles.grantRole({
      body: {
        chainId: `${sepolia.id}`,
        marketAddress: diamondAddress,
        users: [issuerClient.account.address],
        role: 'ISSUER'
      }
    })
  )

  const transactionResult = await sendTransaction(viemClient, transactionData)
  
  return transactionResult;
}
PropertyDescription
clientExpects an instance of the api.createClient to make API calls.
viemClientMust specify the adminClient which will grant the 'ISSUER' role to the Issuer entity.
marketAddressSet the address of the platform by specifying the diamondAddress variable.
usersSpecify the Issuer address in the array that must be granted with the 'ISSUER' role.
roleSelect the role to grant. For this code example, set it to 'ISSUER'.

Pass the apiClient, adminClient, and diamondAddress as arguments to the grantCreatorRole function call:

const marketReceipt = await grantIssuerRole(apiClient, adminClient, diamondAddress)

After granting the 'ISSUER' role to the Issuer you can begin deploying the market.

Define the createMarket function with viemClient, apiClient, and diamondAddress as arguments to create a new data object for a transaction and send it using the sendTransaction function. To create a new market we use the createFractions method:

async function createMarket(client: ApiClient, viemClient: any, diamondAddress: string) {
  const transactionData = handleApiResponse(
    await api.fractions.createFractions({
      client: apiClient,
      body: {
        chainId: `${sepolia.id}`,
        market: diamondAddress,
        wrappedTokens: [{
            type: 'ERC721',
            address: '0xffffffffffffffffffffffffffffffffffff',
            tokenIds: [11111n, 22222n, 33333n, 44444n],
        }],
        funding: {
            pricePerFraction: 5n,
            address: '0xfffffffffffffffffffffffffffffffffff',
        },
        fractions: {
            token: '0xfffffffffffffffffffffffffffffffffff',
            symbol: 'TST',
            name: 'TEST'
        },
        timeBoundary: {
            start: 1741683643801,
            end: 1751683643801
        },
        cap: {
            soft: 1000000000n,
            hard: 2000000000n
        }
      }
    })
  )

  const marketDetails = handleApiResponse(
    await api.fractions.getFractionsCreatedEvent({
      query: {
        chainId: `${sepolia.id}`,
        hash: await sendTransaction(viemClient, transactionData),
      },
    }),
  );
  return marketDetails;
}
PropertyDescription
clientExpects an instance of the api.createClient to make API calls.
viemClientMust specify the issuerClient which will create a new market.
chainIdSpecify the chain id using the imported sepolia.
marketSet the address of the platform by specifying the diamondAddress variable.
wrappedTokensAn object where you have to specify details for NFTs that will be locked in the vault.
typeExpects the standard of the asset to be locked; Can be: 'ERC20','ERC721'or 'ERC1155'.
addressThe smart contract address of the asset to be locked.
tokenIdsAn array of unique identifiers of the NFTs to be locked.
fundingAn object containing details about the market funding.
pricePerFractionSets the price to be paid for one fraction in the specified currency; For instance, USDC.
addressSpecifies the smart contract address whose tokens will be used as the payment for fractions.
fractionsAn object containing details about the fraction tokens.
tokenRepresents the smart contract address of the fractions to be sold on the market.
symbolThe symbol of the fraction.
nameThe name of the fraction.
timeBoundaryAn object that sets the time boundary for the market.
startThe start time of the market. Must be specified in the UNIX milliseconds.
endThe end time of the market. Must be specified in the UNIX milliseconds.
capAn object that sets the softcap and the hardcap for the market in the currency specified in the funding object.
softCapThe softcap of the market.
hardcapThe hardcap of the market.

Pass the issuerClient, apiClient and diamondAddress arguments to the createMarket function call to create a new market.

const marketDetails = await createMarket(issuerClient, apiClient, diamondAddress)

🚧

Important

The result of this operation provides details about the created market. These details are necessary to obtain the unique market identifier, campaignId, which is required for all entities that will interact with the market later in this tutorial.

For ease of use, we create a new campaignId variable and assign to it the value obtained from the marketDetails:

const campaignId = marketDetails.campaignId

At this point, the market has been successfully deployed, but it has not yet been activated for investors to purchase fractions. Before any fractions can be sold, each newly created market must be approved by the Admin.

Define the approveMarket function, which will take the adminClient, apiClient, and diamondAddress as arguments. As shown, we specify the campaignId that was retrieved in the previous step:

async function approveMarket(viemClient: any, apiClient: ApiClient, diamondAddress: string, campaignId: bigInt) {
  const transactionData = handleApiResponse(
    await api.fractions.approveSale({
      client: apiClient,
      body: {
        chainId: `${sepolia.id}`,
        marketAddress: diamondAddress,
        campaignId: campaignId,
      }})
)

const transactionResult = await sendTransaction(viemClient, transactionData)

return transactionResult
}

Invoke the approveMarket function to activate the token sale. The result of this operation is optional:

const marketApproveReceipt = await approveMarket(adminClient, apiClient, diamondAddress, campaignId)

The next steps will involve the Investor interacting with it and purchasing fractions.

Purchasing fractions

To start buying the fractions, you must create the wallet client for the Investor entity:

const investorClient = createWalletClient({
    account: privateKeyToAccount("0xffffffffffffffffffffffffffffffffffffffff"),
    chain: sepolia,
    transport: http("https://your-provider.com/api-key"),
  });

To enable Investor interacting with the market, Admin must grant the 'INVESTOR' role to it.

Define the grantInvestorRole function whose invocation will rely on the investorClient wallet data, apiClient and the address(diamondAddress), of the fraction platform.

The result of this operation is optional:

async function grantInvestorRole(client: ApiClient, viemClient: any, diamondAddress: string) {
  const transactionData = handleApiResponse(
    await api.fractions.grantRole({
      body: {
        chainId: `${sepolia.id}`,
        marketAddress: diamondAddress,
        users: [investorClient.account.address],
        role: 'INVESTOR'
      }
    })
  )

  const roleResult = handleApiResponse(
    await api.fractions.getMarketDeployEvent({
      query: {
        chainId: `${sepolia.id}`,
        hash: await sendTransaction(viemClient, transactionData),
      },
    }),
  );
  return roleResult;
}

Pass the adminClient, apiClient and diamondAddress arguments to the grantInvestorRole function call. The successful invocation grants the Investor the 'INVESTOR' role.

const investorRoleReceipt = await grantInvestorRole(adminClient, apiClient, diamondAddress)

Once the role has been successfully granted, the Investor can begin purchasing fractions.

Define the buyFractions function, which will take the investorClient, apiClient, diamondAddress, campaignId, and amountToBuy as arguments. The function generates the data object and sends the transaction using the sendTransaction .

async function buyFractions(viemClient: any, apiClient: ApiClient, diamondAddress: string, campaignId: bigInt, amountToBuy: bigInt) {
  const transactionData = handleApiResponse(
    await api.fractions.purchaseSale({
      client: apiClient,
      body: {
        chainId: `${sepolia.id}`,
        market: diamondAddress,
        campaignId: campaignId,
        amountToBuy: amountToBuy,
      }})
)

const transactionResult = await sendTransaction(viemClient, transactionData)

return transactionResult
}

Invoke the buyFractions function to purchase 100 tokens, passing all the required arguments. The return value of this operation is optional:

const buyFractionsReceipt = await buyFractions(investorClient, apiClient, diamondAddress, campaignId, 100n)

After purchasing fractions, the Investor must withdraw them from the platform to his address.

Define the claimFractions function that will rely on the withdrawSale call. It expects the investorClient, apiClient, diamondAddress, and campaignId. The function generates the data object and sends the transaction using the sendTransaction .

async function claimFractions(viemClient: any, apiClient: ApiClient, diamondAddress: string, campaignId: bigint) {
  const transactionData = handleApiResponse(
    await api.fractions.withdrawSale({
      client: apiClient,
      body: {
        market: diamondAddress,
        campaignId: campaignId
      }})
)

const transactionResult = await sendTransaction(viemClient, transactionData)

return transactionResult
}

Pass the required arguments to the claimFractions function call to withdraw all available fractions from the platform to the Investor's address:

const claimResult = await claimFractions(investorClient, apiClient, diamondAddress, campaignId)

Completing the sale preemptively

In the event that the softcap is reached while the hardcap is not yet reached, the Admin can preemptively complete the sale by executing the completeSale function. This will release the funds collected from the fraction sales to the Issuer's address.

Define the terminateSale function, which will accept the issuerClient, apiClient, diamondAddress, and campaignId as arguments. The function generates the data object and sends the transaction using sendTransaction.

async function terminateSale(viemClient: any, apiClient: ApiClient, diamondAddress: string, campaignId: bigint) {
  const transactionData = handleApiResponse(
    await api.fractions.comleteSale({
      client: apiClient,
      body: {
        chainId: `${sepolia.id}`,
        marketAddress: diamondAddress,
        campaignId: campaignId
      }})
)

const transactionResult = await sendTransaction(viemClient, transactionData)

return transactionResult
}

Pass the required arguments in the terminateSale invocation to complete the sale and withdraw the collected funds to the Issuer's wallet address:

const terminateResult = await terminateSale(issuerClient, apiClient, diamondAddress, campaignId)

Closing remarks

Well done! You've gained a strong grasp of the essential concepts behind the RWA Fraction API, and we've explored all the methods available in the RWA template. At this point, you should have a clear understanding of how each process operates and how to manage them efficiently.