Protocol - Lending
Introduction
This protocol implements a peer-to-peer lending system for NFTs. It allows NFT owners to use their assets as collateral to borrow cryptocurrency, while lenders can provide loans and earn interest. The protocol is designed to be trustless, efficient, and flexible, with support for various NFT standards including ERC721 and CryptoPunks.
Overview
V2
Vyper 0.3.10
The single major component in the protocol is the P2PLendingNfts
contract, which supports NFTs backed peer to peer lending
The lending of an NFT in the context of this protocol means that:
A lender provides a loan offer with specific terms
A borrower creates a loan using their NFT as collateral
The loan is created when the borrower accepts an offer
The borrower repays the loan within the specified term
If the borrower defaults, the lender can claim the NFT collateral
A loan may be replaced by the borrower while still ongoing, by accepting another offer
A loan may be replaced by the lender while still ongoing, within some defined conditions
In addition, the protocol supports a broker system to facilitate loans and integrates with a delegation registry for potential NFT rentals during the loan period.
General considerations
The current status of the protocol follows certain assumptions:
Support for ERC721 NFTs and CryptoPunks as collateral
The set of accepted NFT contracts is whitelisted
Integration with a delegation registry for potential NFT utility during loans
Use of some ERC20 (eg USDC) as a payment token, defined at deployment time for each instance of
P2PLendingNfts
The loan terms are part of the lender offers, which are signed and kept off-chain
Offers have an expiration timestamp and can also be revoked onchain
Brokers can be part of the loan negotiation and the protocol supports fees for both lender and borrower brokers
Additional fees are supported both for the protocol and for the lender (origination)
Security
Below are the smart contract audits performed for the protocol so far:
Hacken
V2
Done
Architecture
As previously stated, the P2PLendingNfts.vy
contract is the main component of the protocol, implementing the core lending functionality as well as the required configurations.
Users and other protocols should primarily interact with the P2PLendingNfts.vy
contract. This contract is responsible for:
Creating loans based on signed offers and collateral
Settling loans
Handling defaulted loans
Replacing existing loans, either by the borrower's initiative or the lender's initiative
Managing protocol fees
Revoking unused offers
Managing authorized proxies
Setting and managing the delegation of collateral during loan creation
Whitelisted NFT collections
Offers
Loans are created based on the borrower's acceptance of offers from lenders, which specify the loan terms. The general features of an offer are:
Offer Structure: An offer is defined by the
Offer
structure, which includes:Principal amount
Interest amount
Payment token address
Loan duration
Origination fee amount
Broker fees (upfront and settlement)
Broker address
Offer type, either for a collection or for specific tokens
Collateral contract address and token ID range/list
Expiration timestamp
Lender address
Pro-rata flag (for interest calculation)
Signed Offers: Lenders create and sign offers off-chain. These signed offers (
SignedOffer
) combine theOffer
structure with a signature.Offer Validation: When a borrower wants to create a loan using an offer, the protocol verifies the offer's signature and checks if it's still valid (not expired or fully utilized).
Offer Utilization: Each time an offer is used to create a loan, its utilization count is increased. An offer can be used multiple times up to its specified size.
Offer Revocation: Lenders can revoke their offers before they expire or are fully utilized.
Collateral Range / List: Offers specify a list of token IDs for the collateral (or a range for collection offers), allowing flexibility in which specific NFT can be used as collateral within the same collection.
Pro-rata Interest: Offers can specify whether interest should be calculated on a pro-rata basis or for the full duration regardless of early repayment.
As offers are kept off-chain, to prevent abusive usage of an offer some validations are in place:
Each offer has an expiration timestamp, after which it can't be used
Offers can be revoked before expiration by calling
revoke_offer
inP2PLendingNfts
Each offer has a
size
determining how many loans it can originate
Loans
Loan Creation (
create_loan
): The loan creation process begins with the verification of the offer's signature and its validity. Once verified, the NFT collateral is transferred to the contract, which can be either an ERC721 token or a CryptoPunk. If delegation is required, it is set up using delegate.xyz. The principal amount, minus any applicable fees, is then transferred from the lender to the borrower. Upfront fees are distributed to the relevant parties, and a loan record is created and stored within the contract.Loan Settlement (
settle_loan
): To settle a loan, the contract calculates the total repayment amount, which includes the principal, interest, and any fees. The borrower transfers this repayment amount to the contract. The contract then distributes the funds to the lender and any fee recipients. The collateral is transferred back to the borrower.Defaulted Loan Collateral Claim (
claim_defaulted_loan_collateral
): When a loan has defaulted, the lender can claim the collateral. The collateral is then transferred from the contract to the lender and no funds are transferred in this process.Loan Replacement by Borrower (
replace_loan
): A borrower can replace an existing loan with a new one by accepting a new offer. The contract calculates the settlement amounts for the current loan, which is equivalent to settling the current loan and accepting an offer for the same collateral. The repayment and fees for the current loan are distributed, and new loan terms are set up. No changes happened regarding the collateral ownership or delegation.Loan Replacement by Lender (
replace_loan_lender
): A lender can replace an existing loan with a new one by accepting a new offer on behalf of the borrower. The contract calculates the settlement amounts and any borrower compensation needed to ensure that:no additional liquidity is needed from the borrower;
the borrower's repayment under the new conditions is not higher than the original loan's conditions (up until the original loan's maturity)
Funds are transferred to cover the new loan terms if needed. The repayment and fees for the current loan are distributed, and new loan terms are set up. No changes happened regarding the collateral ownership or delegation.
Delegation
The protocol integrates with delegation.xyz delegation registry V2, potentially allowing for NFT utility during the loan period. Delegation is set when a new loan is created and remains in place until the loan is settled, regardless of whether the collateral is claimed or returned to the borrower. Delegation is set in full, not using the registry's subdelegation feature.
Fees
The protocol supports several types of fees:
Protocol Fee: This is a fee that goes to the protocol. It can have both an upfront component, paid when the loan is created, and a settlement component, paid as a percentage of the interest during loan settlement. The protocol fee is defined in the
P2PLendingNfts
contract.Origination Fee: This is a fee paid to the lender when a loan is created. It can be defined as an upfront amount. It is part of the loan terms defined in the
Offer
structure.Lender Broker Fee: This is a fee paid to the broker facilitating the loan on the lender's side. It can have both an upfront component and a settlement component, defined as a percentage of the interest. It is part of the loan terms defined in the
Offer
structure.Borrower Broker Fee: This is a fee paid to the broker facilitating the loan on the borrower's side. It can have both an upfront component and a settlement component, defined as a percentage of the interest. It is specified by the borrower when the loan is created.
The upfront fees are paid during loan creation, while the settlement fees are paid as a fraction of the interest amount during loan settlement.
Roles
The protocol supports the following roles:
Owner
: Can update protocol parameters, change whitelisted collections, and manage the protocolBorrower
: Defined as an individual role for each loan, can settle and replace their loansLender
: Defined as an individual role for each loan, can replace their loans and claim collateral in case of defaults
Development
Implementation
P2P Lending NFTs Contract (P2PLendingNfts.vy
)
P2PLendingNfts.vy
)The P2P Lending NFTs contract facilitates peer-to-peer lending using NFTs as collateral. It manages loan offers and settlements.
State variables
owner
address
Yes
Address of the contract owner
proposed_owner
address
Yes
Address of the proposed new owner
payment_token
address
No
Address of the payment token (ERC20) contract
protocol_wallet
address
Yes
Address of the protocol fee wallet
protocol_upfront_fee
uint256
Yes
Upfront fee amount for the protocol
protocol_settlement_fee
uint256
Yes
Settlement fee amount for the protocol
loans
HashMap[bytes32, bytes32]
Yes
Mapping of loan IDs to loan state hashes
offer_count
HashMap[bytes32, uint256]
Yes
Mapping of offer IDs to their usage count
revoked_offers
HashMap[bytes32, bool]
Yes
Mapping of offer IDs to their revocation status
authorized_proxies
HashMap[address, bool]
Yes
Mapping of authorized proxy addresses
Externalized State
In the P2PLendingNfts contract, certain state information is externalized to reduce gas costs while using the protocol. This approach primarily involves the Loan and Offer structures.
For Loans:
The contract stores hashes of loan states in the
loans
mapping instead of storing the full loan data on-chain.When interacting with a loan (e.g., in functions like
settle_loan
andreplace_loan
), the full loan state is passed as an argument and validated by matching its hash against the stored hash.Changes to the loan state are hashed and stored, and the resulting state variables are published as events.
For Offers:
The contract doesn't store full offer data on-chain. Instead, it tracks offer usage in the
offer_count
mapping and revocation status in therevoked_offers
mapping.When creating a loan or interacting with an offer, the full offer data is passed as an argument and validated using the stored counters and the offer signature.
Structs
Offer
principal
uint256
Principal amount of the loan
interest
uint256
Interest amount of the loan
payment_token
address
Address of the payment token
duration
uint256
Duration of the loan
expiration
uint256
Expiration timestamp of the offer
lender
address
Address of the lender
collateral_contract
address
Address of the collateral NFT contract
offer_type
OfferType
Either COLLECTION
or TOKEN
token_ids
uint256[]
List of token ids accepted for a TOKEN
offer, or a range of token ids for COLLECTION
offers
Loan
id
bytes32
Unique identifier of the loan
amount
uint256
Loan amount
interest
uint256
Interest amount
payment_token
address
Address of the payment token
maturity
uint256
Maturity timestamp of the loan
start_time
uint256
Start timestamp of the loan
borrower
address
Address of the borrower
lender
address
Address of the lender
collateral_contract
address
Address of the collateral NFT contract
collateral_token_id
uint256
Token ID of the collateral
Relevant external functions
create_loan
Any
Nonpayable
Creates a new loan based on a signed offer
settle_loan
Borrower
Payable
Settles an existing loan
claim_defaulted_loan_collateral
Lender
Nonpayable
Claims collateral for a defaulted loan
replace_loan
Borrower
Payable
Replaces an existing loan with a new one
replace_loan_lender
Lender
Payable
Replaces a loan by the lender
revoke_offer
Lender
Nonpayable
Revokes a signed offer
set_protocol_fee
Owner
Nonpayable
Sets the protocol fee
change_protocol_wallet
Owner
Nonpayable
Changes the protocol wallet address
change_whitelisted_collections
Owner
Nonpayable
Updates the whitelisted status of collections
set_proxy_authorization
Owner
Nonpayable
Sets authorization for a proxy address
Proxy support
The P2PLendingNfts
contract includes support for authorized proxies, allowing for more flexible interaction with the protocol. This feature is particularly useful for integrations with other protocols or for implementing advanced user interfaces.
Key aspects of proxy support include:
Authorized Proxies:
The contract maintains a mapping of authorized proxy addresses:
authorized_proxies: public(HashMap[address, bool])
The contract owner can set or revoke proxy authorization using the
set_proxy_authorization
function.
User Checks:
The contract includes an internal function
_check_user
that verifies if the caller is either the user themselves or an authorized proxy acting on behalf of the user.This check is used in various functions where user-specific actions are performed, such as settling loans or revoking offers.
Proxy Usage:
When an authorized proxy calls a function, the actual user is considered to be
tx.origin
rather thanmsg.sender
.This allows proxies to perform actions on behalf of users while still maintaining proper access control and attribution.
Security Considerations:
Only the contract owner can authorize or deauthorize proxies, providing centralized control over which addresses can act as proxies.
The use of
tx.origin
is only performed if the caller is an authorized proxy, otherwise, the fallback authorization procedure usedmsg.sender
Testing
There are two types of tests implemented, running on py-evm using titanoboa:
Unit tests focus on individual functions for each contract, mocking external dependencies (eg WETH and delegation contracts)
Integration tests run on a forked chain, testing the integration between the contracts in the protocol and real implementations of the external dependencies
Additionally, under contracts/auxiliary
there are mock implementations of external dependencies which are NOT part of the protocol and are only used to support deployments in private and test networks:
The
ERC20.vy
andERC721.vy
contracts are used to deploy mock ERC20 and ERC721 tokens, respectively.The
CryptoPunksMarketMock.vy
contract mocks the CryptoPunksMarket contract.The
DelegationRegistryMock.vy
contract is used to deploy a mock implementation of the delegate.xyz delegation contract V2.The
WETH9Mock.vy
contract mocks the Wrapped Ether contract.
Run the project
Run the following command to set everything up:
To run the tests:
Unit tests
Important note: when running the unit tests for the first time, run the command twice! The first time will throw a lot of errors because the titanoboa cache isn't initialized yet.
Coverage
Branch coverage
Gas profiling
Deployment
Deploy the protocol locally
To run the protocol locally:
Install Foundry
In a terminal, run
make install-dev
In another terminal, run
anvil
In the line 39 of
scripts/deployment.py
, change the statement todm.deploy(changes, dryrun=False)
. You can leavedryrun=True
if you want to see the deployment steps without actually deploying the contracts.`In the first terminal, run
make deploy-local
To run transactions manually, run
make console-local
and use the console to interact with the contracts
After step 5, you should see that the config file configs/local/p2p.json
contains all the contract addresses and other relevant information from the local deployment. If you want to redeploy the contracts again, you can copy the contents of configs/local/p2p.json.template
to configs/local/p2p.json
and run make deploy-local
again.
Deploy the protocol to existing networks
For each environment a makefile rule is available to deploy the contracts, eg for DEV:
Because the protocol depends on external contracts that may not be available in all environments, mocks are also deployed to replace them if needed.
Network
Private network
Sepolia
Mainnet
Payment Contract
Mock (ERC20.vy
)
Mock (ERC20.vy
)
USDC 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
WETH Contract
Mock (WETH9Mock.vy
)
Mock (WETH9Mock.vy
)
WETH9 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
NFT Contract
Mock (ERC721.vy
)
Mock (ERC721.vy
)
Several, eg Koda 0xE012Baf811CF9c05c408e879C399960D1f305903
Delegation Contract
Mock (HotWalletMock.vy
)
delegate.xyz DelegateRegistry 0x00000000000000447e69651d841bD8D104Bed493
delegate.xyz DelegateRegistry 0x00000000000000447e69651d841bD8D104Bed493
Additionally, for each P2P Lending Market in each environment (e.g., NFTs backed USDC Loans PROD), the P2PLendingNfts
contracts are deployed:
P2PLendingNfts
_payment_token: address
Address of the payment token (ERC20) contract
_delegation_registry: address
Address of the delegation registry
_cryptopunks: address
Address of the CryptoPunksMarket contract
_protocol_upfront_fee: uint256
The percentage (bps) of the principal paid to the protocol at origination
_protocol_settlement_fee
The percentage (bps) of the interest paid to the protocol at settlement
_protocol_wallet
Address where the protocol fees are accrued
Last updated