Perpetual Contract NFTs as Collateral
Abstract
This ERC proposes a mechanism where a person (referred to as the “Asset Owner”) can collateralize NFTs that represent locked deposits or assets, to borrow funds against them. These NFTs represent the right to claim the underlying assets, along with any accrued benefits, after a predefined maturity period. 1
Motivation
The rapidly evolving landscape of DeFi has introduced various mechanisms for asset locking, offering benefits like interest and voting rights. However, one of the significant challenges in this space is maintaining liquidity while these assets are locked. This ERC addresses this challenge by proposing a method to generate profit from locked assets using ERC-721 and ERC-4907.
In DeFi services, running Automated Market Maker (AMM), liquidity providers contribute assets to pools and receive NFTs representing their stake. These NFTs denote the rights to the assets and the associated benefits, but they also lock the assets in the pool, often causing liquidity challenges for the providers. The current practice requires providers to withdraw their assets for urgent liquidity needs, adversely affecting the pool’s liquidity and potentially increasing slippage during asset swaps.
Our proposal allows these NFTs, representing locked assets in liquidity pools, to be used as collateral. This approach enables liquidity providers to gain temporary liquidity without withdrawing their assets, maintaining the pool’s liquidity levels. Furthermore, it extends to a broader range of DeFi services, including lending and trading, where asset locking is prevalent. By allowing the collateralization of locked asset representations through NFTs, our approach aims to provide versatile liquidity solutions across DeFi services, benefitting a diverse user base within the ecosystem.
The concept of perpetual contract NFTs, which we introduce, exploits the idea of perpetual futures contracts in the cryptocurrency derivatives market. These NFTs represent the rights to the perpetual contract and its collateral, enabling them to be used effectively as collateral for DeFi composability. The perpetual contract NFT offers a new form of NFT that enhances the utility of locked assets, providing a significant advantage in DeFi applications by offering liquidity while retaining the benefits of asset locking.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Contract Interface
Solidity interface.
interface IPerpetualContractNFT {
// Emitted when an NFT is collateralized for obtaining a loan
event Collateralized(uint256 indexed tokenId, address indexed owner, uint256 loanAmount, uint256 interestRate, uint256 loanDuration);
// Emitted when a loan secured by an NFT is fully repaid, releasing the NFT from collateral
event LoanRepaid(uint256 indexed tokenId, address indexed owner);
// Emitted when a loan defaults, resulting in the transfer of the NFT to the lender
event Defaulted(uint256 indexed tokenId, address indexed lender);
// Enables an NFT owner to collateralize their NFT in exchange for a loan
// @param tokenId The NFT to be used as collateral
// @param loanAmount The amount of funds to be borrowed
// @param interestRate The interest rate for the loan
// @param loanDuration The duration of the loan
function collateralize(uint256 tokenId, uint256 loanAmount, uint256 interestRate, uint64 loanDuration) external;
// Enables a borrower to repay their loan and regain ownership of the collateralized NFT
// @param tokenId The NFT that was used as collateral
// @param repayAmount The amount of funds to be repaid
function repayLoan(uint256 tokenId, uint256 repayAmount) external;
// Allows querying the loan terms for a given NFT
// @param tokenId The NFT used as collateral
// @return loanAmount The amount of funds borrowed
// @return interestRate The interest rate for the loan
// @return loanDuration The duration of the loan
// @return loanDueDate The due date for the loan repayment
function getLoanTerms(uint256 tokenId) external view returns (uint256 loanAmount, uint256 interestRate, uint256 loanDuration, uint256 loanDueDate);
// Allows querying the current owner of the NFT
// @param tokenId The NFT in question
// @return The address of the current owner
function currentOwner(uint256 tokenId) external view returns (address);
// View the total amount required to repay the loan for a given NFT
// @param tokenId The NFT used as collateral
// @return The total amount required to repay the loan, including interest
function viewRepayAmount(uint256 tokenId) external view returns (uint256);
}
Event Collateralized
- The
Collateralized
event MUST be emitted when the collateralize function is successfully executed. - Usage: Logs the event of an NFT being used as collateral for a loan, capturing essential details like the loan amount, interest rate, and loan duration.
Event LoanRepaid
- The
LoanRepaid
event MUST be emitted when the repayLoan function is successfully executed. - Usage: Logs the event of a loan being repaid and the corresponding NFT being released from collateral.
Event Defaulted
- The
Defaulted
event MUST be emitted in scenarios where the loan defaults and the NFT is transferred to the lender. - Usage: Used to log the event of a loan default and the transfer of the NFT to the lender.
Function collateralize
- The
collateralize
event SHOULD be implemented asexternal
. - Usage: Allows an NFT owner to collateralize their NFT to receive a loan.
Function repayLoan
- The
repayLoan
function SHOULD be implemented asexternal
. - Usage: Enables an NFT owner to repay their loan and reclaim their NFT.
Function getLoanTerms
- The
getLoanTerms
function MAY be implemented asexternal
view
. - Usage: Allows querying the loan terms for a given NFT.
Function currentOwner
- The
currentOwner
function MAY be implemented asexternal
view
. - Usage: Enables querying the current owner of a specific NFT.
Function viewRepayAmount
- The
viewRepayAmount
function MAY be implemented asexternal
view
. - Usage: Enables querying the current repay amount of a specific NFT.
Rationale
Design Motivation
The design of this standard is driven by the need to address specific challenges in the DeFi sector, particularly concerning the liquidity and management of assets locked as collateral. Traditional mechanisms in DeFi often require asset holders to lock up their assets for participation in activities such as lending, staking, or yield farming, which results in a loss of liquidity. This standard aims to introduce a more flexible approach, allowing asset holders to retain some liquidity while their assets are locked, thereby enhancing the utility and appeal of DeFi products.
Design Decision
-
Dual-Role System (Asset Owner and DeFi Platform/Contract): A clear division is established between the NFT owner (asset holder) and the DeFi platform or contract utilizing the NFT as collateral. This distinction simplifies the management of rights and responsibilities, enhancing clarity and reducing potential conflicts.
-
Enhancing Liquidity without Compromising Asset Locking Benefits: A key feature of this standard is enabling asset owners to use their NFTs, which represent locked assets, as collateral to secure loans. This approach allows asset owners to access liquidity without needing to withdraw their assets from pools or staking programs, thus preserving the associated benefits like interest accrual or voting rights.
-
Automated Loan and Collateral Management: The integration of automated features for managing the terms and conditions of the collateralized NFT is a deliberate choice to minimize transaction costs and complexity.
-
DeFi Composability: The strategic emphasis on DeFi composability, particularly the integration between asset-locking and collateralizing services, is pivotal for this standard. This approach aims to streamline the adoption of the standard across diverse DeFi platforms and services, fostering seamless connections within the DeFi ecosystem.
Alternate Designs and Related Work
-
Comparison with ERC-4907: While ERC-4907 also introduces a dual-role model for NFTs (owner and user), our standard focuses specifically on the use of NFTs for collateralization in financial transactions, diverging from ERC-4907’s rental-oriented approach.
-
Improvement Over Traditional Collateralization Methods: Compared to traditional DeFi collateralization, which often requires complete asset lock-up, this standard proposes a more dynamic and flexible model that allows for continued liquidity access.
Backwards Compatibility
Fully compatible with ERC-721 and integrates with ERC-4907 for renting NFTs.
Test Cases
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
import "./PerpetualContractNFT.sol";
contract PerpetualContractNFTDemo is PerpetualContractNFT {
constructor(string memory name, string memory symbol)
PerpetualContractNFT(name, symbol)
{
}
function mint(uint256 tokenId, address to) public {
_mint(to, tokenId);
}
}
import { expect } from "chai";
import { ethers } from "hardhat";
describe("PerpetualContractNFTDemo", function () {
it("should allow an owner to collateralize an NFT, rent it to a contract, and then have the owner repay the loan", async function () {
const [owner] = await ethers.getSigners();
const PerpetualContractNFTDemo = await ethers.getContractFactory("PerpetualContractNFTDemo");
const demo = await PerpetualContractNFTDemo.deploy("DemoNFT", "DNFT");
await demo.waitForDeployment();
expect(demo.target).to.be.properAddress;
// Mint an NFT to the owner
await demo.mint(1, owner.address);
// Owner collateralizes the NFT for a loan
const loanAmount = ethers.parseUnits("1", "ether"); // 1 Ether in Wei. Use Wei to avoid precision error.
const interest = 5; // 5% interest
const expiration = Math.floor(new Date().getTime() / 1000) + 3600; // Expire after 60 minutes (3600 seconds), convert it to seconds because `hours` in solidity converted to seconds
await demo.connect(owner).collateralize(1, loanAmount, interest, expiration); // tokenId, loanAmount, interestRate, loanDuration
// Check current user of the NFT (should be the contract address)
expect(await demo.userOf(1)).to.equal(demo.target);
// Borrower repays the loan to release the NFT
const repayAmountWei = await demo.connect(owner).viewRepayAmount(1);
await demo.connect(owner).repayLoan(1, repayAmountWei);
// Check if the NFT is returned to the original owner after the loan is repaid
expect(await demo.userOf(1)).to.equal("0x0000000000000000000000000000000000000000");
});
});
Run in Terminal:
npx hardhat test
Reference Implementation
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.0;
//import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IPerpetualContractNFT.sol";
import "./ERC4907/ERC4907.sol";
contract PerpetualContractNFT is ERC4907, IPerpetualContractNFT {
struct LoanInfo {
address borrower; // Address that borrowed against the NFT
uint256 loanAmount; // Amount of funds borrowed
uint256 interestRate; // Interest rate for the loan
uint64 loanDuration; // Duration of the loan
uint256 loanStartTime; // Timestamp when the loan starts
}
mapping(uint256 => LoanInfo) internal _loans;
//Constructor to initialize the Perpetual Contract NFT contract with the given name and symbo
constructor(string memory name_, string memory symbol_)
ERC4907(name_, symbol_)
{}
function collateralize(uint256 tokenId, uint256 loanAmount, uint256 interestRate, uint64 loanDuration) public override {
require(ownerOf(tokenId) == msg.sender || isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId) == msg.sender, "Not owner nor approved");
LoanInfo storage info = _loans[tokenId];
info.borrower = msg.sender;
// The loan amount should reflect the asset's value as represented by the NFT, considering an appropriate loan-to-value (LTV) ratio.
info.loanAmount = loanAmount;
info.interestRate = interestRate;
info.loanDuration = loanDuration;
info.loanStartTime = block.timestamp;
setUser(tokenId, address(this), loanDuration);
emit Collateralized(tokenId, msg.sender, loanAmount, interestRate, loanDuration);
// Further logic can be implemented here to manage the lending of assets
}
function repayLoan(uint256 tokenId, uint256 repayAmount) public override {
require(_loans[tokenId].borrower == msg.sender, "Not the borrower.");
// Calculate the total amount due for repayment
uint256 totalDue = viewRepayAmount(tokenId);
// Check if the repayAmount is sufficient to cover at least a part of the total due amount
require(repayAmount <= totalDue, "Repay amount exceeds total due.");
// Calculate the remaining loan amount after repayment
_loans[tokenId].loanAmount = totalDue - repayAmount;
// Resets the user of the NFT to the default state if the entire loan amount is fully repaid
if(_loans[tokenId].loanAmount == 0) {
setUser(tokenId, address(0), 0);
}
emit LoanRepaid(tokenId, msg.sender);
}
function getLoanTerms(uint256 tokenId) public view override returns (uint256, uint256, uint256, uint256) {
LoanInfo storage info = _loans[tokenId];
return (info.loanAmount, info.interestRate, info.loanDuration, info.loanStartTime);
}
function currentOwner(uint256 tokenId) public view override returns (address) {
return ownerOf(tokenId);
}
function viewRepayAmount(uint256 tokenId) public view returns (uint256) {
if (_loans[tokenId].loanAmount == 0) {
// If the loan amount is zero, there is nothing to repay
return 0;
}
// The interest is calculated on an hourly basis, prorated based on the actual duration for which the loan was held.
// If the borrower repays before the loan duration ends, they are charged interest only for the time the loan was held.
// For example, if the annual interest rate is 5% and the borrower repays in half the loan term, they pay only 2.5% interest.
uint256 elapsed = block.timestamp > (_loans[tokenId].loanStartTime + _loans[tokenId].loanDuration)
? _loans[tokenId].loanDuration / 1 hours
: (block.timestamp - _loans[tokenId].loanStartTime) / 1 hours;
// Round up
// Example: 15/4 = 3.75
// round((15 + 4 - 1)/4) = 4, round((15/4) = 3)
uint256 interest = ((_loans[tokenId].loanAmount * _loans[tokenId].interestRate / 100) * elapsed + (_loans[tokenId].loanDuration / 1 hours) - 1) /
(_loans[tokenId].loanDuration / 1 hours);
// Calculate the total amount due
uint256 totalDue = _loans[tokenId].loanAmount + interest;
return totalDue;
}
// Additional functions and logic to handle loan defaults, transfers, and other aspects of the NFT lifecycle
}
Security Considerations
Copyright
Copyright and related rights waived via CC0.
-
{ "container-title": "IEEE Access", "author": [ { "given": "Hyoungsung", "family": "Kim" }, { "given": "Hyun-Sik", "family": "Kim" }, { "given": "Yong-Suk", "family": "Park" } ], "DOI": "10.1109/ACCESS.2022.3225884", "URL": "https://ieeexplore.ieee.org/document/9967987", "type": "article-journal", "id": 9967987, "citation-label": "9967987", "issued": { "date-parts": [ [ 2022 ] ] }, "keyword": "Contracts;Nonfungible tokens;Cryptocurrency;Finance;Smart contracts;Blockchains;Financial services;Automated market maker (AMM);blockchain;decentralized exchange (DEX);decentralized finance (DeFi);Ethereum;liquidity pool (LP);non-fungible token (NFT);uniswap", "page": "126802-126814", "title": "Perpetual Contract NFT as Collateral for DeFi Composability", "volume": 10 }