Borrow against RWAs

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

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);

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,
            );
        }
    };

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 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