Step-by-step Guides

Buyback Fraction Market

This guide provides a step-by-step overview for setting up a fraction platform with buyback functionality using the Fraction Protocol. It outlines the necessary facets for platform configuration and explains how to correctly initialize the proxy’s storage for Buyback Fraction Market applications.

The buyback is the process in which a borrower (issuer) creates fraction tokens (ERC-20/1155) to raise capital by selling them to investors. The borrower must then repay the debt, with interest, within a specified time window.

When you create a new campaign you can set the buyback duration and interest:

  • Interest – An additional amount the borrower (issuer) must repay to on-chain investors upon buyback, calculated based on a predetermined interest rate as a return for providing capital.
  • Buyback duration – A time window during which a borrower (issuer) must repay the debt provided by on-chain investors. If the debt is not repaid within the specified time window, the borrower is liquidated and their real-world assets are sold for fiat currency. The realized gains from the liquidation are converted into the campaign’s supported cryptocurrency and returned to investors.

Buyback Flow

The buyback flow represents a set of scenarios where a campaign can navigate through, where each scenario yields a specific result or destination point. These scenarios are dictated by the selected facets and their setup when deploying a platform.

Scenarios of the campaign are regulated by:

  1. The buying power of the on-chain investors — how much capital is provided to the borrower (issuer).
  2. The ability of the borrower (issuer) to repay the debt in the specified time window.
Successful Scenario
  1. Twin NFT ERC-721 representing a real-world asset is used as collateral on-chain.

  2. Issuer creates a campaign, wraps the ERC-721 twin NFT, and creates fractions (ERC-1155).

  3. Once the campaign starts, the funds provision duration begins, and on-chain investors buy fractions up until the purchase phase ends.

  4. After the purchase phase ends, if the campaign has reached at least the soft cap (funding goal), the borrower can receive the raised capital.

  5. At the moment the purchase phase ends, the campaign initiates the countdown or a time frame in which the borrower (issuer) must return the debt he has received, plus the interest that is calculated based on the formula specified during the creation of the campaign. For instance: Initial Amount * Interest Rate + Initial Amount.

  6. Upon repayment of the debt, the borrower unlocks their ERC-721 twin NFT (collateral). At this point, the fractions cease to be shares of the twin NFT and instead represent shares of the total capital and interest repaid by the borrower.

  7. Investors redeem their fractions through the platform and receive their original investment plus their proportional share of the interest.

Liquidation Scenario
  1. Twin NFT ERC-721 representing a real-world asset is used as collateral on-chain.

  2. Issuer creates a campaign, wraps the ERC-721 twin NFT, and creates fractions (ERC-1155).

  3. Once the campaign starts, the funds provision duration begins, and on-chain investors buy fractions up until the purchase phase ends.

  4. After the purchase phase ends, if the campaign has reached at least the soft cap (funding goal), the borrower can receive the raised capital.

  5. At the moment the purchase phase ends, the campaign initiates the countdown or a time frame in which the borrower (issuer) must return the debt he has received, plus the interest that is calculated based on the formula specified during the creation of the campaign. For instance: Initial Amount * Interest Rate + Initial Amount.

  6. At this stage, there are two sub-scenarios that can take place:

Subscenario 1: Liquidation due to failed Margin Call

When an NFT asset is used as collateral for the campaign, its initial real-world price is assessed and then provided by the oracle to the platform.

The price of the real-world asset is continuously recalculated and provided to the oracle at specific intervals, in order to determine if the current campaign must be forcefully transitioned into a margin call.

Suppose the initial assessed price of the real-world asset is $200, but the borrower might receive only a portion of that amount - for example, $150. Typically, the borrower only receives a percentage of the initial asset price because the full assessed value of the collateral is considered risky.

The initial asset price acts as a point of reference for the campaign. The platform implements a threshold mechanism that determines when a campaign transitions to a margin call state and when it enters the liquidation phase.

If the asset’s market price drops below a certain threshold, for instance $150, then the campaign forcefully enters the liquidation phase to protect the capital provided by investors. The Admin of the platform or another custom role (Liquidator) with sufficient rights initiates the liquidation phase. Then the Liquidator sells the real-world asset for fiat.

Note that if the collateral price drops below the value received as funds and a margin call is not initiated, and if the borrower never performs a buyback, investors could lose their funds, as the liquidator may not be able to sell the real-world asset at a value higher than its assessed price.

Subscenario 2: Liquidation due to failed Buyback

Borrower does not repay the debt in the specified time window. The Admin of the platform or another custom role (Liquidator) with sufficient rights initiates the liquidation phase.

The ERC-721 twin NFT gets forcefully unlocked from the vault, and the real-world object associated with that twin NFT gets liquidated (sold) for fiat currency.

If the Liquidator is able to sell the real-world asset at a higher price compared to the initial assessment, then the higher amount of funds is returned to on-chain investors. For example, the originally agreed-upon debt is $110, but the Liquidator was able to sell the real-world asset for $150. Then at least $110 or more is returned to on-chain investors.

  1. The realized gains from the liquidation are converted into the campaign’s supported cryptocurrency and sent to the platform.

  2. Investors sell their fractions to the platform and recover the initially invested capital.

Non-funded Scenario
  1. Twin NFT ERC-721 representing a real-world asset is used as collateral on-chain.

  2. Issuer creates a campaign, wraps the ERC-721 twin NFT, and creates fractions (ERC-1155).

  3. Once the campaign starts, the funds provision duration begins, and on-chain investors buy fractions up until the purchase phase ends.

  4. The campaign does not reach the soft-cap. It progresses to the NON_FUNDED state, the Twin ERC-721 NFT is automatically unlocked and returned to the issuer, and all unpurchased fractions held by the platform are burned. Only fractions purchased by investors remain in circulation.

  5. Investors redeem their fractions through the NonFundedSubskeleton and receive only the initial amount that they paid for them. Hence, there is no interest because the campaign has not progressed to the proper state.

Facets

The following table outlines the required facets to integrate into the Diamond proxy for enabling the Buyback Fraction Market use case. For details on each facet’s functionality, refer to the respective documentation below.

ContractAddress
AccessControlFacet0xFd01Dd59308F9Ca6266d885f7DcED7520230074A
StateFacet0x5bef016eA1e5DA7b3B8741FfcE70f7E907c174BF
FundingErc20PacketsFacet0x51B845edAD9312C7FeB389D64C1891a3C2E7650A
StablePriceFacet0x1D87ada51F2bD11681e8C03a9ADc44d6d98C6340
LendingOracleFacet0x5A981Bc93Bc434906C5DfbBb4B3fB7D0b8763854
CreateFractionsSkeleton0xc6D4Dd501741d8066329b699F2A8051d5295f1dd
PurchaseSkeletonNoDiscount0x28256994b72e383B9bc1A6CbD13cffaeDf977982
ReceiveSkeleton0x4DEd580cA392704dcf82d92787085ba0cd25d171
BuybackSkeleton0x6a6791dC94DdFCDBeD68a50221CCCbCE56022dF7
ReceiveAfterBuybackSkeleton0x2dD044E21E9a9763cC53d06e2Fa812D71E65C3F6
WhitelistedCreatorsFacet0x7F04649E23C02EEC7d4d282c08b06eBB148aAbBa
WrapAssetsFacet0x00bdAB32D7891AE7FD8dd7c1146cb7FD295919cf
Erc1155NonGenesisFractionFacet0xbc87BCe45781eF5aB0DB8b2FE7314306a2a6ee8e
CreateFractionsSetterFacet0xF4744Fcb1C25101ab3dD0B71482e6147eB8D56dA
SingleStatePurchaseFacet0xb1Ca1D6d1f84E5aDE981fFc9F979F9645Bb3Ecf7
TwoBorderTimeFacet0xD491828fF16B4dBAF5eeFDb6aF0Bf8Ee289C21a9
PurchaseRoleEligibilityFacet0x1f65Aa8c72404A411e9efC009c257D959fc87585
TwoCapPurchaseAmountFacet0x4a831A387467b3521cAB289Db85B6235E8BB871c
DoTransferPacketsFacet0xa31893422e5674c3d8250371939cFcE85f7CE21F
ReturnFungAndSemiFungFractionsFacet0xb0a6E2413c69e1E43de9aB140631892E1D89Fe9B
SingleStateReceiverFacet ADD LINK0x894d5c6Cdcf7D80EAc3F036ce3370B2Dace74169
CreatorOnlyReceiveEligibilitytFacet ADD LINK0x761B2B9c9ea43C0E08329A900Be3fbB9d65fcc04
ReceiveAllGatheredFundsFacet0xB8646d62c82fb599ba66bbE1f1Fc927CA76C0bCF
SingleStateBuybackFacet0x16A6783E7D11eb62C0f284bde6B2585B366AF4cF
CreatorOnlyBuybackEligibilityFacet0x10AEEA0D9D1CC811360Ee161842be48F22db36D5
FixedInterestBuybackAmountFacet0x72f0a8D4E708fE6ab2A97ca9A88134a114aa530d
DurationBuybackTimeFacet0x5b9e7D9410cd9CA23A0e04B89fAab3c739005f0f
TransferFundsOnBuybackFacet0xB54A2353832CaC0cbFc380254FF1FB733a4B0eC3
SingleStateReceiveAfterBuybackFacet0xcc4d4683C958329eD45d3A5413c7915e3Ba287A2
ReceiveAfterBuybackRoleEligibilityFacet0x067C17309f10f0C73130083cb06dbABC015b8e1a
BurnAndRedeemFractionsFacet0x81806cd2CE10782B2E5165Aca650dD46160Abc12
FractionsAddressApprovalFacet0x26DA13B00796B4587517F2ABf83e9CD684e9b85b
PurchaseToReceiveRoleApprovalFacet0x8aea674838066222D848809638b8A0f0a108A3f0
FailedBuybackToFoldFacet0x2961E52abca6bD281d542DeC052CE74Dc2b175D2
NonFundedSubSkeletonNoDiscount0x14b911BC369CCc4E0b2C3225B37746E9D991Af5C
ForceUnlockOnNonFundedSubSkeleton0xc20BDfe886F282B86AfCc45b3c8bBED2c8b47524
MarginCallSubSkeleton0x3eb120411f65c1853F9173040AA2c76B99A1c247
TwoClickLiquidationSkeleton0x429b06366dCc9735CE2631583798549B7e052CbC
CheckAndJumpToNonFundedFacet0x23Bd606b044aC67d85b032d5F3c0861F82348427
ReceiveAfterNonFundedRoleEligibilityFacet0x71BE635EE706737c0C2a573e3d601e30a0cD09EE
SingleStateNonFundedFacet0xFD3b97B44339CDF598dead91F07c4c2DF2514B4e
ReceiveBackFundsFacet0xD8db9EDf71774d67Dea1A30880b2872394d20570
DoForceUnlockOnNonFundedFacet0x2E779cDF2D1dE603162d503a5e622daC586B03EB
CreatorOnlyOnNonFundedEligibilityFacet0xFd60ead9f81eC9F4521d8F2aaad13A23a149543F
SingleStateMarginCallFacet0xcc8d8FC9AcEaCe62ed8324709194835C8E4797B4
FixedDurationMarginCallFacet0x2205cb85653212bDd3eBD0a90fc53562C4C5F106
TransferFundsOnAddMarginFacet0x8b4C245AF302aD72AF385bf1898A1B1c24ea3f9c
PriceFeedMarginCallAmountFacet0x465E4f2CC9A491e87DD3089425176571ae757AbB
CreatorOnlyMarginCallEligibilityFacet0x1A1a035BEA129AdcF7D5Ed2A0c797469954c239D
TransferFundsAfterLiquidationFacet0x83F3624d6c065c94cFCe2E0CCD82D287Ac69AC6e
LiquidationRoleEligibilityFacet0x53A1C64F9f64640458E2f221391D5D48CcD96Cb7
LiquidationForceUnlockFacet0x59aB79DC116797CeC107A01960A1f1EC37B01195
SingleStateLiquidationFacet0xdbd8Feaae4E8a83670da8AFDd48851486Ab5cfA2
Erc721ReceiverFacet0x0BAc61d183A08A6E5185AA48E6a7C138c29Bd5Cb
Erc1155ReceiverFacet0x6A79Bb913D80032580aF3Cac6538b2E3E73cfa0D
PropWrappedAssetsFeeCollectorFacet0xc8bF27233150222F7f0D5fC137e288ab74b5C249
PropPacketsGatheredFeeCollectorFacet0x7c4079778f8C78f0875Dc64816652f1c96255662

Note

Some contracts provided in the table above are still in the development phase and are not audited. Exercise caution when deploying a new platform. Below, you can find a table that provides a list of contracts that are still in the development phase.

Contract
LendingOracleFacet
PurchaseSkeletonNoDiscount
PurchaseRoleEligibilityFacet
Erc1155NonGenesisFractionFacet
WrapAssetsFacet
ReceiveAfterBuybackSkeleton
BuybackSkeleton
DoTransferPacketsFacet
SingleStateReceiverFacet
SingleStateBuybackFacet
CreatorOnlyBuybackEligibilityFacet
FixedInterestBuybackAmountFacet
SingleStateReceiveAfterBuybackFacet
NonFundedSubSkeletonNoDiscount
FailedBuybackToFoldFacet
FractionsAddressApprovalFacet
BurnAndRedeemFractionsFacet
ReceiveAfterBuybackRoleEligibilityFacet
SingleStateReceiveAfterBuybackFacet
TransferFundsOnBuybackFacet
DurationBuybackTimeFacet
FixedInterestBuybackAmountFacet
MarginCallSubSkeleton
ReceiveBackFundsFacet
SingleStateMarginCallFacet
FixedDurationMarginCallFacet
PriceFeedMarginCallAmountFacet
CreatorOnlyMarginCallEligibilityFacet
TransferFundsAfterLiquidationFacet
LiquidationForceUnlockFacet
SingleStateLiquidationFacet

Instructions

Setting Up The Environment

Create a new directory and initialize the Node project by running:

npm init

Install the Typescript and TS execution engine:

npm install --save-dev typescript ts-node

Install the Hardhat npm package:

npm install hardhat 

Begin by initializing the Hardhat project. When prompted, confirm each option by entering Y (yes) for all configuration choices.

npx hardhat init

Then install all necessary dependencies required for smart contract deployment:

npm install @solidstate/contracts @nomicfoundation/hardhat-toolbox ethers

In your project root folder find the tsconfig.json file and add the resolveJsonModule property to your configuration and set it to true:

{
  "compilerOptions": {
    "resolveJsonModule": true
    /// ... other properties
  }
}

This property ensures that Typescript can read the data from the .json files.

Diamond Proxy Deployment

In your project, find the contracts folder and, inside it, create a new file that will contain the implementation of the diamond proxy smart contract. For demonstration purposes, the name of the file will be MyDiamond.sol.

Insert the following code into the MyDiamond.sol file:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
 
import "./proxy/diamond/SolidstateDiamondProxy.sol";
 
contract FractionDiamond is SolidStateDiamond {}

In the root folder of your project, create a new TypeScript file called deploy.ts. This file will contain the code required to:

  1. Deploy the diamond proxy smart contract.
  2. Add facets and initialize the diamond storage.

Insert the code provided below into the deploy.ts file and follow the comments.

import { ethers } from "hardhat";
// 1. Import the FractionDiamond.json file to read the ABI and the Bytecode.
import SolidStateDiamond from "./artifacts/contracts/MyDiamond.sol/FractionDiamond.json"
 
// 2. Set up provider (using a testnet like Sepolia). 
// Ensure you have set up the environment variable that contains the provider details.
const provider = new ethers.JsonRpcProvider(process.env.SEPOLIA_RPC);
 
// 3. Set up wallet (replace with your private key).
const wallet = new ethers.Wallet("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", provider);
 
// 4. Define ABI and Bytecode.
const abi = SolidStateDiamond.abi
const bytecode = SolidStateDiamond.bytecode
 
// 5. Initialize the variable for the diamond proxy address.
let diamondAddress: string;
 
// 6. Create a ContractFactory
const contractFactory = new ethers.ContractFactory(abi, bytecode, wallet);
 
// 7. Deploy the contract 
async function deployContract() {
  console.log("Deploying contract...");
 
  // 8. Deploy the contract
  const contract = await contractFactory.deploy();
 
  // 9. Wait for deployment to be mined
  await contract.waitForDeployment();
  console.log("Contract deployed at:", contract.target);
  diamondAddress = contract.target
}
 
deployContract().catch(console.error);

Deploy the diamond proxy smart contract by running it in your terminal:

npx hardhat run deploy.ts

Obtaining Selectors

At this stage, you need to obtain the function selectors corresponding to the facet signatures. This guide outlines two approaches you can use to generate these selectors.

Approach 1: Manual Conversion

For this approach, you need to use the signature-to-selector converter, which allows generating a valid selector for a given signature.

Use the data table provided in this topic to refer to the corresponding smart contract documentation for function signatures.

For each facet, you need to create the corresponding array where you will store selectors. For demonstration purposes, only two selector arrays are provided below:

const AccessControlFacetSelectors = [
    "0x91d14854"
]
 
const StateFacetSelectors = [
    "0xcc90cf94",
    "0x06a91c6a"
]

Approach 2: Automatic Conversion

This approach relies on the automatic generation of selectors based on the human-readable-abi standard.

For all function signatures, refer to the corresponding facet documentation that you can access through the data table provided in this topic.

Create arrays that will store the function signatures. For demonstration purposes, only two signature arrays are provided below:

const AccessControlFacetAbi = [
    "function hasRole(bytes32 role, address account) external view returns (bool)"
]
 
const StateFacetSelectorsAbi = [
    "function changeState(uint256 campaignId, uint256 fromState, uint256 toState) external",
    "function getStateOfId(uint256 campaignId) external view returns (uint256)"
]

Then you have to use the functionality of the ethers library to iterate through all signatures in the arrays and convert them into selectors. Ensure that you replace the value passed to the Interface instance.

import { Interface, id, FunctionFragment, hashMessage, dataLength } from "ethers";
 
//  Create an Interface
const iface = new Interface(StateFacetSelectorsAbi);
 
// Loop over all function signatures
let StateFacetSelectorsAbiResult = []
 
for (const fragment of iface.fragments) {
  if (fragment.type === "function") {
    const fnFragment = fragment as FunctionFragment;
    const signature = fnFragment.format(); // canonical signature
    const selector = id(signature).slice(0, 10); // 4-byte selector
    console.log(`${selector}`);
    StateFacetSelectorsAbiResult.push(selector);
  }
}

Adding Facets To Diamond Proxy

Next, you have to create an array of FacetCut objects containing the function selectors that will be added to the diamond proxy. In the Solidity code, the FacetCut is represented as the following struct:

struct FacetCut {
  address target;
  FacetCutAction action;
  bytes4[] selectors;
}

The FacetCutAction represents an enum that determines how to handle the facet cut process. In our case, we need to use the ADD member of the enum, which is represented by the index position 0:

enum FacetCutAction {
 ADD,
 REPLACE,
 REMOVE
}

You need to specify the address of the facet, the FacetCutAction member, which is 0, and an array of selectors for each facet that you want to add to the diamond proxy.

const AccessControlFacetAddress = "0xFd01Dd59308F9Ca6266d885f7DcED7520230074A"
const StateFacetAddress = "0x5bef016eA1e5DA7b3B8741FfcE70f7E907c174BF"
 
let facet1 = { target: AccessControlFacetAddress, action: 0, selectors: AccessControlFacetSelectors}
let facet2 = { target: StateFacetAddress, action: 0, selectors: StateFacetSelectors}

Now we have prepared all the required FacetCut objects, and we are ready to add them to the diamond proxy.

Next, you need to create an instance of the diamond proxy you just deployed:

const diamondProxyContract = new ethers.Contract(diamondAddress, abi, wallet);

We also need to define an async operation to execute the diamondCut function on the instance of the diamond proxy smart contract.

async function diamondCut() {
  const result = await diamondProxyContract.diamondCut([facet1, facet2], ethers.ZeroAddress, "0x");
  console.log(result)
}

As you can see, the first argument expects an array of facet arrays containing selectors; the second argument expects the zero address, which we pass using ethers.ZeroAddress; while the third argument expects the init data, which is empty ("0x").

Now you can call the diamondCut() function, which will return the result of the operation:

diamondCut()

After adding the facets to the diamond proxy, you can verify their presence by using the Louper web tool.
Insert the diamond proxy address into the search bar and verify the newly added functions.

Diamond Proxy Storage Initialization

After adding facets to the diamond proxy, we need to initialize them.

Note

You only need to initialize those facets that have an init function available. For example, the AccessControlFacet has the initAccessControlFacet function, which must be called.

Other facets don't require initialization.

Create instances of the facets that need initialization. These instances must be called at the address of the diamond proxy in order to interact with them through the diamond proxy:

const facet1AtDiamond = new ethers.Contract(diamondAddress, iface, wallet);

Define the async function to call initAccessControlFacet on the facet contract:

async function initializeFacet() {
  const result = await facet1AtDiamond.initAccessControlFacet(wallet.address);
  console.log(result)
}

Now, by calling the initializeFacet function, you initialize the diamond storage for the selected facet. In case you have added more facets to your diamond contract, you need to initialize them too.

initializeFacet()

If you need to pass any other data, you have to encode it properly. For instance, this guide requires initializing the TwoBorderTimeFacet by calling the initPurchaseTimeFacet() function with the initPurchaseTimeData calldata. To obtain the data, you need to refer to the corresponding FacetStorage smart contract - which in this case is: TwoBorderTimeFacetStorage.

After that, you need to find the initPurchaseTimeData parameter in the table provided in the Reference.

Finally, you have to encode the data properly by using the following function:

ethers.utils.defaultAbiCoder.encode(["bytes"], ["YOUR_DATA_IS_HERE"]);

Interacting with the Fraction Platform

Suppose you need to interact with the platform we've set up—specifically, to get the state of a specific campaign. First, obtain an instance of the facet that provides the required functionality (i.e., getStateOfId()) at the diamond proxy address:

const StateFacetFacetAbi = [
    "function getStateOfId(uint256 campaignId) external view returns (uint256)"
]
let StateFacetAtDiamond = new ethers.Contract(diamondAddress, StateFacetFacetAbi, wallet);

Then you will call the function from this instance:

let campaignId = 1;
 
async function getStateOfId() {
  const result = await StateFacetAtDiamond.getStateOfId(campaignId);
  console.log(result);
}

On this page