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.
Version | Language | Reference implementation |
---|---|---|
V1 | Vyper 0.3.7 - 0.3.10 | https://github.com/Zharta/protocol-v1 |
V2 | Vyper 0.3.10 | https://github.com/Zharta/lending-protocol-v2 |
The single major component in the protocol is the P2PLendingNfts
contract, which support 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 other 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.
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)
Below are the smart contract audits performed for the protocol so far:
Auditor | Version | Status | |
---|---|---|---|
Red4Sec | V1 | Done | Zharta - Audit Report Final.pdf |
Hacken | V1 | Done | Zharta_SCAudit_Report_Final.pdf |
Hacken | V2 | Pending |
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
Loans are created based on the borrower 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 offchain, 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
-
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 are equivalent of 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 happen 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 now 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 borrowers 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 happen regarding the collateral ownership or 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.
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.
The protocol supports the following roles:
Owner
: Can update protocol parameters, change whitelisted collections, and manage the protocolBorrower
: Defined as a individual role for each loan, can settle and replace their loansLender
: Defined as a individual role for each loan, can replace their loans and claim collateral in case of defaults
The P2P Lending NFTs contract facilitates peer-to-peer lending using NFTs as collateral. It manages loan offers and settlements.
Variable | Type | Mutable | Description |
---|---|---|---|
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 |
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.
Struct | Variable | Type | Description |
---|---|---|---|
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 |
Function | Roles Allowed | Modifier | Description |
---|---|---|---|
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 |
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.
- The contract maintains a mapping of authorized proxy addresses:
-
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.
- The contract includes an internal function
-
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.
- When an authorized proxy calls a function, the actual user is considered to be
-
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
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
Additionaly, 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:
contracts/
└── auxiliary
├── CryptoPunksMarketMock.vy
├── DelegationRegistryMock.vy
├── ERC20.vy
├── ERC721.vy
└── WETH9Mock.vy
- 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 following command to set everything up:
make install-dev
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.
make unit-tests
- Coverage
make coverage
- Branch coverage
make branch-coverage
- Gas profiling
make gas
In order 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.
For each environment a makefile rule is available to deploy the contracts, eg for DEV:
make deploy-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.
Component | DEV | INT | PROD |
---|---|---|---|
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 is deployed:
Contract | Deployment parameters | Description |
---|---|---|
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 |