Borrow against RWAs

This guide provides an initial overview of using the Unlockd SDK for borrowing against RWAs functionalities. As we're in the early stages of documentation, your feedback is crucial. We encourage developers to explore this use case and help shape these materials.

We're committed to supporting your integration journey. If you need personalized assistance or have suggestions, please reach out to us directly. Your input will help us create more comprehensive and useful documentation for the developer community.

Overview

Borrowing against Real-World Assets (RWAs) is a powerful feature of the Unlockd protocol. By leveraging the Unlockd SDK, developers can enable users to collateralize their tokenized assets and obtain instant loans.

This feature can be applied to loans with a single asset or even a portfolio of assets from different classes.

If your product allows users to access an inventory or portfolio, you can natively display their Available To Borrow amount and allow them to bundle all items into a single or different loans without leaving your interface

How-To Guide

This guide will walk you through the process of implementing a borrowing feature using the Unlockd SDK, allowing users to borrow against RWAs.

Prerequisites

Before you begin, ensure you have the following set up:

  • A React-based project

  • The Unlockd SDK installed (@verislabs/unlockd-sdk)

  • Wagmi library and its dependencies (viem, react-query) for Ethereum interactions. You can Create a Project or Install it manually into your Project.

  • Basic understanding of React hooks and Ethereum transactions

This example is written in TypeScript and uses the wagmi library along with its dependencies. However, it's important to note that wagmi is not required to use the Unlockd SDK. The SDK itself is only dependent on viem.

Developers are free to use their preferred tools and libraries for wallet connection and blockchain interactions. This example serves as one possible implementation approach.

Implementation Steps

  1. Set up the React component with necessary state variables and hooks.

  2. Implement login, connection and authentication

  3. Fetch NFTs and prices from connected wallets.

  4. Create or refresh Unlockd wallet.

  5. Implement NFT selection functionality.

  6. Implement NFT approval and transfer to Unlockd wallet.

  7. Set up borrowing functionality against selected NFTs.

  8. Render the user interface with all interactive elements.

Code Overview

1. Set up the React component

In your component, first set up all required state variables (using useState, Context, or your own state manager) and wagmi hooks. This includes states for managing user accounts, NFT selections, approval statuses, and other essential data. For the Unlockd SDK you only need to initialize the api object.

"use client";

import { useState, useEffect } from "react";
import {
    useAccount,
    useConnect,
    useDisconnect,
    useConfig,
    useWriteContract,
    useReadContracts,
} from "wagmi";
import {
    signMessage,
    readContract,
    writeContract,
    waitForTransactionReceipt,
    multicall,
    getBalance,
} from "wagmi/actions";
import { sepolia } from "wagmi/chains";
import { injected } from "wagmi/connectors";
import { parseUnits, Address, isAddress } from "viem";
import {
    UnlockdApi,
    Chains,
    UnderlyingsAsset,
    borrow,
    underlyingsAssets,
    createWallet,
    getWallet,
} from "@verislabs/unlockd-sdk";
import { nftAbi } from "../../abis/ERC721Abi";
import { nftBatchTransferAbi } from "../../abis/NftBatchTransferAbi";

interface NFTPriceData {
    collection: Address;
    tokenId: string;
    underlyingAsset: Address;
    price?: string;
}

const ALLOWED_COLLECTIONS: Address[] = [
    "0xa6a9acfdd1f64ec324ee936344cdb1457bdbddf0",
    "0x388043e55a388e07a75e9a1412fe2d64e48343a5",
];

const nftBatchTransferAddress =
    "0xaba905eba39b9a55fd0f910a6415ba91c3e9353d" as Address;

// The idea is to create a simple example of how to use the Unlockd SDK to borrow against NFTs.
// The user can connect their wallet, log in to Unlockd, approve a collection, select NFTs to transfer to their Unlockd wallet, and borrow against them.
// We are not demonstrating frontend code, or handling states, just the logic to interact with the Unlockd SDK and create a borrow transaction.
function App() {
    // Variables
    const userAccount = useAccount();
    const { connect, connectors } = useConnect();
    const { disconnect } = useDisconnect();
    const config = useConfig();

    const [unlockdAccount, setUnlockdAccount] = useState<Address>();
    const [token, setToken] = useState<string | null>(null);
    const [selectedNFTs, setSelectedNFTs] = useState<NFTPriceData[]>([]);
    const [selectedUnderlyingAsset, setSelectedUnderlyingAsset] =
        useState<Address>(underlyingsAssets(Chains.Sepolia)[UnderlyingsAsset.USDC]);
    const [approvalStatus, setApprovalStatus] = useState<
        Record<Address, "not-approved" | "approved" | "pending">
    >({});
    const [injectedWalletNFTs, setInjectedWalletNFTs] = useState<NFTPriceData[]>(
        [],
    );
    const [unlockdWalletNFTs, setUnlockdWalletNFTs] = useState<NFTPriceData[]>(
        [],
    );
    const [selectedUnlockdNFTs, setSelectedUnlockdNFTs] = useState<
        NFTPriceData[]
    >([]);
    const [borrowAmount, setBorrowAmount] = useState<string>("");

    const api = new UnlockdApi(Chains.Sepolia);

In this example, all the code is contained within the root component (App) for simplicity. However, this functionality can be implemented in any React component.

The hooks used (useAccount, useDisconnect, etc.) are imported from wagmi, while useState is from React. These reactive variables can be stored and managed using any state management solution you prefer.

It's important to note the initialization of the Unlockd SDK, which is a crucial step:

const api = new UnlockdApi(Chains.Sepolia);

This line, found at the end of the component, sets up the Unlockd API instance for interacting with the Sepolia testnet. Make sure to initialize the SDK appropriately in your implementation.

2. Implement login, connection and authentication

Set up functionality authenticating with Unlockd. This step calls the Unlockd SDK for obtaining an authentication token for the user's currently connected address by signing a blockchain message. The auth token is essential for user's interaction with the Unlockd SDK

    // The login should call the unlockd API using the signatureMessage method to get the message to sign.
    // We get the message to sign, and then we trigger it to the user so he can sign it (signMessage).
    // After the user signs the message, we validate it using the validateMessage method, and returning the auth token we will need to interact with the protocol.
    const logIn = async () => {
        if (!userAccount.address) {
            return;
        }
        try {
            const message = await api.signatureMessage(userAccount.address);
            const signature = await signMessage(config, {
                account: userAccount.address,
                message: message.message,
            });
            const authToken = await api.validateMessage(
                userAccount.address,
                signature,
            );
            setToken(authToken.token);
        } catch (error) {
            console.error("Login failed:", error);
        }
    };

3. Fetch and display NFTs from connected wallets.

Implement logic to fetch NFTs (RWAs) from both the user's connected wallet and their Unlockd wallet, if the wallet was created before (see #4 below).

This involves querying blockchain data for NFT balances and token IDs, then passing this data to the Unlockd API to retrive price data for all NFTs. In this example, we are quering the blockchain directly to obtain the NFTs data (using the multicall action from wagmi for efficiency) but you can obtain this data from any means (such as using the Alchemy SDK).

What you need to be aware of is the data structure expected by the Unlockd API when calling the prices method, as see in the priceParams.

    // In order to borrow, we 1st should check if the user has any Allowed Collections in his wallet. If yes: we shouldt get the balance of it, and then get the token ID of each NFT in order to provide the price. If no: it ends here.
    // The fetchNftsAndPrices should be called from the injected wallet and the unlockd wallet, since both will hold assets.
    const fetchNFTsAndPrices = async (
        walletAddress: Address,
        selectedUnderlyingAsset: Address,
        setNFTs: React.Dispatch<React.SetStateAction<NFTPriceData[]>>,
    ) => {
        if (!walletAddress || !isAddress(walletAddress)) {
            console.error(`Invalid wallet address: ${walletAddress}`);
            return;
        }

        if (!selectedUnderlyingAsset || !isAddress(selectedUnderlyingAsset)) {
            console.error(
                `Invalid selected underlying asset: ${selectedUnderlyingAsset}`,
            );
            return;
        }

        try {
            const balanceContracts = ALLOWED_COLLECTIONS.map((collection) => ({
                address: collection,
                abi: nftAbi.abi,
                functionName: "balanceOf",
                args: [walletAddress],
            } as const));

            const balances = await multicall(config, { contracts: balanceContracts });

            const tokenIdContracts = balances.flatMap((balance, index) => {
                if (balance.status !== 'success') return [];

                const numTokens = Number(balance.result || 0);
                if (numTokens === 0) return [];

                return Array.from({ length: numTokens }, (_, i) => ({
                    address: ALLOWED_COLLECTIONS[index],
                    abi: nftAbi.abi,
                    functionName: "tokenOfOwnerByIndex",
                    args: [walletAddress, BigInt(i)],
                } as const));
            });

            const tokenIds = await multicall(config, { contracts: tokenIdContracts });

            const priceParams = tokenIds
                .filter((tokenIdResult) => tokenIdResult.status === 'success')
                .map((tokenIdResult, index) => ({
                    collection: tokenIdContracts[index].address,
                    tokenId: tokenIdResult.result.toString(),
                    underlyingAsset: selectedUnderlyingAsset,
                }));
            const pricesResponse = await api.prices(priceParams);

            const nftsPriceData = pricesResponse.map((result) => ({
                collection: result.collection as Address,
                tokenId: result.tokenId,
                underlyingAsset: selectedUnderlyingAsset,
                price: result.valuation,
            }));

            setNFTs(nftsPriceData);
        } catch (error) {
            console.error(
                `Error fetching NFTs and prices for wallet ${walletAddress}:`,
                error,
            );
        }
    };

For each NFT, you need to create an object with the following structure:

{ 
    collection: string, // NFT's collection address 
    tokenId: string, // NFT's ERC721 token id 
    underlyingAsset: string // the asset ETH or USDC you want to receive the price evaluation in 
} 

Form an array with similar objects containing data about all the NFTs you want to evaluate, and pass it to the prices() method.

4. Create or retrieve Unlockd wallet

Set up functionality to create a new Unlockd wallet (Unlockd Account) if the user doesn't have one, or refresh an existing one. This step is crucial for storing the assets that will be used as collateral for borrowing.

    // In order to create a loan, you will need to own an unlockd wallet.
    // The assets (nfts) will be locked in your unlockd wallet.
    // If you don't have an unlockd wallet, it will be created.
    const createOrRefreshUnlockdWallet = async () => {
        try {
            let unlockdWallet = await getWallet({ network: Chains.Sepolia });
            if (!unlockdWallet) {
                await createWallet({ network: Chains.Sepolia });
                unlockdWallet = await getWallet({ network: Chains.Sepolia });
            }
            if (unlockdWallet) {
                setUnlockdAccount(unlockdWallet);
                await fetchNFTsAndPrices(unlockdWallet, selectedUnderlyingAsset, setUnlockdWalletNFTs);
            }
        } catch (error) {
            console.error("Error with Unlockd wallet:", error);
        }
    };

5. Implement NFT selection functionality

Create a mechanism for users to select one or multiple NFTs, either for transferring to the Unlockd wallet or for using as collateral when borrowing. This function is reused in selecting NFTs in both the user's wallet and the Unlockd wallet.

    // Allows the user to select 1 or multiple NFTs to transfer to the unlockd wallet or to borrow agains them, if used from the unlockd wallet.
    const toggleNFTSelection = (nft: NFTPriceData, isUnlockdWallet: boolean) => {
        const setNFTs = isUnlockdWallet ? setSelectedUnlockdNFTs : setSelectedNFTs;
        setNFTs((prev) =>
            prev.some(
                (item) =>
                    item.collection === nft.collection && item.tokenId === nft.tokenId,
            )
                ? prev.filter(
                    (item) =>
                        !(
                            item.collection === nft.collection &&
                            item.tokenId === nft.tokenId
                        ),
                )
                : [...prev, nft],
        );
    };

6. Implement NFT approval and transfer to Unlockd wallet

Set up the process for approving NFT collections and transferring selected NFTs to the Unlockd wallet. This includes checking the current approval status for all selected NFTs, sending approval transactions for each collection, and transferring the selected NFTs in batch to the Unlockd wallet.

    // This will validate if the user has approved the BatchNFTTransfer contract to transfer the NFTs from his injected wallet to the unlockd wallet.
    // If the approvals exist you dont need to approve again. If not, the user SHOULD APPROVE.
    // The SDK should provide a function to approve!
    const handleApprove = async (collectionAddress: Address) => {
        if (!userAccount.address) {
            return;
        }

        try {
            const isApproved = await readContract(config, {
                address: collectionAddress,
                abi: nftAbi.abi,
                functionName: "isApprovedForAll",
                args: [userAccount.address, nftBatchTransferAddress],
            });

            if (isApproved) {
                setApprovalStatus((prev) => ({
                    ...prev,
                    [collectionAddress]: "approved",
                }));
                return;
            }

            const response = await writeContract(config, {
                address: collectionAddress,
                abi: nftAbi.abi,
                functionName: "setApprovalForAll",
                args: [nftBatchTransferAddress, true],
            });

            setApprovalStatus((prev) => ({
                ...prev,
                [collectionAddress]: "pending",
            }));

            const transactionReceipt = await waitForTransactionReceipt(config, {
                hash: response,
            });

            if (transactionReceipt.status === "success") {
                setApprovalStatus((prev) => ({
                    ...prev,
                    [collectionAddress]: "approved",
                }));
            } else {
                setApprovalStatus((prev) => ({
                    ...prev,
                    [collectionAddress]: "not-approved",
                }));
            }
        } catch (error) {
            console.error("Approval failed:", error);
            setApprovalStatus((prev) => ({
                ...prev,
                [collectionAddress]: "not-approved",
            }));
        }
    };

    const { writeContract: batchTransfer } = useWriteContract();

    // This function will transfer the selected NFTs from the injected wallet to the unlockd wallet.
    // The approve of this contract and its usage is not mandatory.
    // The important is the the unlockd wallet has the NFTs.
    // This contract was developed and audited by us, so we use it, feel free to use it, or change to other preferred method.
    const handleBatchTransfer = async () => {
        if (!userAccount.address || !unlockdAccount || selectedNFTs.length === 0)
            return;
        try {
            const nftTransfers = selectedNFTs.map((nft) => ({
                contractAddress: nft.collection,
                tokenId: BigInt(nft.tokenId),
            }));

            batchTransfer({
                address: nftBatchTransferAddress,
                abi: nftBatchTransferAbi.abi,
                functionName: "batchTransferFrom",
                args: [nftTransfers, unlockdAccount],
            });

            await fetchNFTsAndPrices(userAccount.address, selectedUnderlyingAsset, setInjectedWalletNFTs);
            await fetchNFTsAndPrices(unlockdAccount, selectedUnderlyingAsset, setUnlockdWalletNFTs);
        } catch (error) {
            console.error("Batch transfer failed:", error);
        }
    };

7. Set up borrowing functionality against selected NFTs.

Implement the core borrowing feature of the Unlockd protocol. This includes selecting NFTs from the Unlockd wallet, specifying borrow amounts, obtaining borrow signatures, and executing borrow transactions.

This example demonstrates how all the previously described functionalities are implemented within a single component for simplicity. However, this is just one approach. Feel free to adapt and restructure this implementation to best suit your own application's architecture and requirements.

    // This function will create a borrow transaction.
    // The user should select the NFTs he wants to borrow against (limit: 100), and the amount he wants to borrow.
    // If the user has an unlockd wallet with allowed NFTs, he can borrow against them.
    // We 1st generate an array of nfts (collection and tokenId), after, we build the params we need to call the backend for a borrow signature, by providing the nfts and the underlying asset.
    // The backend will return a signature, and we will use it to call the borrow function from the SDK, adding the amount, nfts and signature.
    // we use 6 decimals since the borrow amount is in USDC.
    const handleBorrow = async () => {
        if (selectedUnlockdNFTs.length === 0 || !token || !unlockdAccount) {
            return;
        }

        try {
            const nftsArray = selectedUnlockdNFTs.map((nft) => ({
                collection: nft.collection,
                tokenId: nft.tokenId,
            }));

            const params = {
                nfts: nftsArray,
                underlyingAsset: selectedUnderlyingAsset,
            };

            const signature = await api.borrowSignature(token, params);

            await borrow(BigInt(parseUnits(borrowAmount, 6)), nftsArray, signature, {
                network: Chains.Sepolia,
            });

            await fetchNFTsAndPrices(unlockdAccount, selectedUnderlyingAsset, setUnlockdWalletNFTs);
        } catch (error) {
            console.error("Borrow failed:", error);
        }
    };

    useEffect(() => {
        if (
            userAccount.isConnected &&
            userAccount.address &&
            isAddress(userAccount.address)
        ) {
            fetchNFTsAndPrices(userAccount.address, selectedUnderlyingAsset, setInjectedWalletNFTs);
        }
    }, [userAccount.isConnected, userAccount.address, selectedUnderlyingAsset]);

    useEffect(() => {
        if (token) {
            createOrRefreshUnlockdWallet();
        }
    }, [token]);

    const isAuth = userAccount.isConnected && !!token;

8. Render the user interface with all interactive elements

Create and render the user interface components that tie all the functionality together. This includes elements for wallet connection, NFT selection, approval buttons, transfer initiation, borrow amount input, and the borrow action button.

    return (
        <div>
            <h1>Unlockd Borrow Example</h1>

            {/* Connect wallet */}
            {(() => {
                if (!userAccount.isConnected) {
                    return (
                        <div>
                            <button onClick={() => connect({ connector: injected() })}>
                                Connect
                            </button>
                        </div>
                    );
                }

                return (
                    <div>
                        <p>Connected to {userAccount.address}</p>
                        <button onClick={() => disconnect()}>Disconnect</button>
                    </div>
                );
            })()}

            {/* Check current network */}
            {(() => {
                if (userAccount.isConnected && userAccount.chainId !== sepolia.id) {
                    return (
                        <div>
                            <p>Please switch to Sepolia network</p>
                        </div>
                    );
                }
                return null;
            })()}

            {/* Unlockd Log In */}
            {(() => {
                if (userAccount.isConnected && !isAuth) {
                    return (
                        <div>
                            <button onClick={logIn}>Log In to Unlockd</button>
                        </div>
                    );
                }
                return null;
            })()}

            {/* Select asset */}
            {(() => {
                if (isAuth) {
                    return (
                        <select
                            value={selectedUnderlyingAsset}
                            onChange={(e) =>
                                setSelectedUnderlyingAsset(e.target.value as Address)
                            }
                        >
                            {Object.entries(UnderlyingsAsset).map(([key, value]) => (
                                <option
                                    key={key}
                                    value={underlyingsAssets(Chains.Sepolia)[value]}
                                >
                                    {key}
                                </option>
                            ))}
                        </select>
                    );
                }
            })()}

            {/* Display collections */}
            {(() => {
                if (isAuth) {
                    return (
                        <>
                            {ALLOWED_COLLECTIONS.map((collectionAddress) => (
                                <div key={collectionAddress}>
                                    <h3>Collection: {collectionAddress}</h3>
                                    <button
                                        onClick={() => handleApprove(collectionAddress)}
                                        disabled={
                                            approvalStatus[collectionAddress] === "approved" ||
                                            approvalStatus[collectionAddress] === "pending"
                                        }
                                    >
                                        {approvalStatus[collectionAddress] === "approved"
                                            ? "Approved"
                                            : approvalStatus[collectionAddress] === "pending"
                                                ? "Approving..."
                                                : "Approve Collection"}
                                    </button>
                                    <p>
                                        Total NFTs in this collection:{" "}
                                        {
                                            injectedWalletNFTs.filter(
                                                (nft) => nft.collection === collectionAddress,
                                            ).length
                                        }
                                    </p>

                                    {injectedWalletNFTs
                                        .filter((nft) => nft.collection === collectionAddress)
                                        .map((nft) => (
                                            <div key={`${nft.collection}-${nft.tokenId}`}>
                                                <input
                                                    type="checkbox"
                                                    checked={selectedNFTs.some(
                                                        (item) =>
                                                            item.collection === nft.collection &&
                                                            item.tokenId === nft.tokenId,
                                                    )}
                                                    onChange={() => toggleNFTSelection(nft, false)}
                                                />
                                                <span>Token ID: {nft.tokenId}</span>
                                                <span>Price: {nft.price}</span>
                                            </div>
                                        ))}
                                    {injectedWalletNFTs.filter(
                                        (nft) => nft.collection === collectionAddress,
                                    ).length === 0 && <p>No NFTs found in this collection</p>}
                                </div>
                            ))}
                        </>
                    );
                }
            })()}

            {/* Transfer NFTs to Unlockd wallet */}
            {(() => {
                if (isAuth) {
                    return (
                        <button
                            onClick={handleBatchTransfer}
                            disabled={selectedNFTs.length === 0 || !unlockdAccount}
                        >
                            Transfer Selected NFTs to Unlockd Wallet
                        </button>
                    );
                }
            })()}

            {/* Create & display Unlockd wallet and the supported collections */}
            {(() => {
                if (isAuth) {
                    return (
                        <>
                            <h2>Unlockd Wallet</h2>
                            <p>Unlockd Wallet: {unlockdAccount || "Not created"}</p>
                            <button onClick={createOrRefreshUnlockdWallet}>
                                Create/Refresh Unlockd Wallet
                            </button>
                            {unlockdAccount && (
                                <>
                                    <h3>NFTs in Unlockd Wallet</h3>
                                    {unlockdWalletNFTs.map((nft) => (
                                        <div key={`${nft.collection}-${nft.tokenId}`}>
                                            <input
                                                type="checkbox"
                                                checked={selectedUnlockdNFTs.some(
                                                    (item) =>
                                                        item.collection === nft.collection &&
                                                        item.tokenId === nft.tokenId,
                                                )}
                                                onChange={() => toggleNFTSelection(nft, true)}
                                            />
                                            <span>Collection: {nft.collection}</span>
                                            <span>Token ID: {nft.tokenId}</span>
                                            <span>Price: {nft.price}</span>
                                        </div>
                                    ))}

                                    <div>
                                        <input
                                            type="text"
                                            value={borrowAmount}
                                            onChange={(e) => setBorrowAmount(e.target.value)}
                                            placeholder="Borrow Amount"
                                        />
                                        <button
                                            onClick={handleBorrow}
                                            disabled={
                                                selectedUnlockdNFTs.length === 0 || !borrowAmount
                                            }
                                        >
                                            Borrow Against Selected NFT
                                        </button>
                                    </div>
                                </>
                            )}
                        </>
                    );
                }
            })()}
        </div>
    );
}

export default App

Last updated