├── tests ├── __init__.py ├── unit │ ├── __init__.py │ ├── p2p_nfts │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_p2p_nfts_revoke.py │ └── conftest.py ├── integration │ ├── __init__.py │ ├── test_aave.py │ ├── test_nftfi.py │ ├── test_benddao.py │ └── test_arcade.py └── stubs │ └── P2PNftsProxy.vy ├── scripts ├── __init__.py ├── _helpers │ ├── __init__.py │ ├── transactions.py │ ├── dependency.py │ ├── basetypes.py │ └── deployment.py ├── deployment.py ├── get_tokens.py ├── get_collections.py └── build_interfaces.py ├── .github ├── CODEOWNERS ├── workflows │ ├── tests-integration.yaml │ └── tests-unit.yaml └── pull_request_template.md ├── configs ├── dev │ ├── zapechain │ │ ├── tracking.json │ │ └── p2p.json │ └── zethereum │ │ └── tracking.json ├── int │ ├── curtis │ │ ├── tracking.json │ │ └── p2p.json │ └── sepolia │ │ └── tracking.json ├── local │ ├── foundry │ │ ├── tracking.json │ │ └── p2p.json │ └── p2p.json.template └── prod │ ├── apechain │ ├── tracking.json │ └── p2p.json │ └── ethereum │ └── tracking.json ├── .gitattributes ├── audits └── Hacken_Zharta_P2PLending_Sept2024.pdf ├── ape-config.yaml ├── .gitignore ├── tiamonds-claim.md ├── .pre-commit-config.yaml ├── 3rd-party ├── aave │ ├── PoolInstance.sol │ └── IFlashLoanReceiver.sol ├── arcade │ └── RepaymentController.sol └── gondi │ └── GondiBaseLoan.sol ├── contracts ├── EIP3156Flash.vy ├── AaveFlash.vy ├── auxiliary │ ├── BalancerMock.vy │ ├── ArcadeMock.vy │ ├── ERC20.vy │ ├── WETH9Mock.vy │ ├── WETH9_abi.json │ ├── ArcadeRepaymentController_abi.json │ └── DelegateRegistry2_abi.json ├── P2PLendingControl.vy ├── LenderClaim.vy ├── ArcadeProxy.vy ├── BendDAOProxy.vy ├── NftfiProxy.vy └── GondiProxy.vy ├── pyproject.toml ├── Makefile ├── natspec └── P2PLendingNfts.json └── requirements.txt /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/_helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/unit/p2p_nfts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @DioPires @cfcfs 2 | -------------------------------------------------------------------------------- /configs/dev/zapechain/tracking.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /configs/int/curtis/tracking.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /configs/local/foundry/tracking.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /configs/prod/apechain/tracking.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /audits/Hacken_Zharta_P2PLending_Sept2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zharta/lending-protocol-v2/HEAD/audits/Hacken_Zharta_P2PLending_Sept2024.pdf -------------------------------------------------------------------------------- /ape-config.yaml: -------------------------------------------------------------------------------- 1 | name: zharta-protocol 2 | 3 | plugins: 4 | - name: vyper 5 | - name: foundry 6 | - name: alchemy 7 | - name: arbitrum 8 | - name: base 9 | -------------------------------------------------------------------------------- /configs/dev/zethereum/tracking.json: -------------------------------------------------------------------------------- 1 | { 2 | "arcade": { 3 | "abi_file": "auxiliary/ArcadeLoanCore_abi.json", 4 | "address": "0x6c61c29A2E75B996e533428Ec7D8b4fa19dD0aA1", 5 | "contract": "Generic", 6 | "name": "Arcade" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /configs/int/sepolia/tracking.json: -------------------------------------------------------------------------------- 1 | { 2 | "arcade": { 3 | "abi_file": "auxiliary/ArcadeLoanCore_abi.json", 4 | "address": "0x7a45F7c14626e2Ef3a046143579bd4736503F290", 5 | "contract": "Generic", 6 | "name": "Arcade" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info 3 | .idea/ 4 | .env* 5 | .local* 6 | .build 7 | .history 8 | .hypothesis/ 9 | build/ 10 | abi/ 11 | bytecode/ 12 | reports/ 13 | .DS_Store 14 | .venv 15 | .vscode 16 | .cache 17 | .tool-versions 18 | *~ 19 | 20 | configs/**/collections.json 21 | configs/**/tokens.json 22 | -------------------------------------------------------------------------------- /tiamonds-claim.md: -------------------------------------------------------------------------------- 1 | 2 | 1. Deploy the `lender_claim` in prod 3 | 4 | ```bash 5 | make deploy-ethereum 6 | ``` 7 | 8 | 2. Authorize the `lender_claim` delegation for the lender 9 | 10 | 11 | ```bash 12 | export MAINNET_URL=... 13 | export LENDER_PK=... 14 | export DELEGATE_ADDRESS=0x12E2B70C57F966e190a7Eb661915dD4BEde536BD 15 | cast send --private-key $LENDER_PK --rpc-url $MAINNET_URL 0x0000000000000000000000000000000000000000 --auth $DELEGATE_ADDRESS 16 | 17 | ``` 18 | 19 | 20 | 3. Claim the collateral on ape console 21 | 22 | ```python 23 | loan_id = "0x5697ef2836a9b308ff6e8ce7a02b702bcfbfc9e39ab960a512e53ee9e8e1a87a" 24 | loan = get_loan(loan_id) 25 | p2p_usdc_nfts.claim_defaulted_loan_collateral(loan, sender=lender) 26 | ``` 27 | 28 | 4. Remove delegation 29 | 30 | ```bash 31 | cast send --private-key $LENDER_PK --rpc-url $MAINNET_URL 0x0000000000000000000000000000000000000000 --auth 0x0000000000000000000000000000000000000000 32 | ``` 33 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.5.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - id: check-toml 8 | - id: check-yaml 9 | - id: check-json 10 | - id: detect-private-key 11 | 12 | - repo: local 13 | hooks: 14 | - id: ruff-sort 15 | name: ruff-sort 16 | entry: ruff check --select I --fix 17 | require_serial: true 18 | language: system 19 | types: [python] 20 | 21 | - id: ruff-format 22 | name: ruff-format 23 | entry: ruff format 24 | require_serial: true 25 | language: system 26 | types: [python] 27 | 28 | - id: ruff-check 29 | name: ruff-check 30 | entry: ruff check scripts tests 31 | require_serial: true 32 | language: system 33 | types: [python] 34 | 35 | exclude: "^natspec/.*" 36 | -------------------------------------------------------------------------------- /scripts/deployment.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import warnings 4 | 5 | import click 6 | from ape import convert 7 | from ape.cli import ConnectedProviderCommand 8 | from rich import print 9 | 10 | from ._helpers.deployment import DeploymentManager, Environment 11 | 12 | ENV = Environment[os.environ.get("ENV", "local")] 13 | CHAIN = os.environ.get("CHAIN", "nochain") 14 | 15 | 16 | logger = logging.getLogger(__name__) 17 | logger.setLevel(logging.WARNING) 18 | warnings.filterwarnings("ignore") 19 | 20 | 21 | def gas_cost(context): # noqa: ARG001 22 | return {"gas_price": convert("10 gwei", int)} 23 | 24 | 25 | @click.command(cls=ConnectedProviderCommand) 26 | def cli(network): 27 | print(f"Connected to {network}") 28 | 29 | dm = DeploymentManager(ENV, CHAIN) 30 | dm.context.gas_func = gas_cost 31 | 32 | changes = set() 33 | # changes |= { 34 | # "configs.trait_roots", 35 | # "p2p.eth_nfts", 36 | # } 37 | 38 | dm.deploy(changes, dryrun=True) 39 | 40 | print("Done") 41 | -------------------------------------------------------------------------------- /.github/workflows/tests-integration.yaml: -------------------------------------------------------------------------------- 1 | name: Integration tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | id-token: write 12 | 13 | env: 14 | BOA_FORK_RPC_URL: "https://eth-mainnet.g.alchemy.com/v2/${{ secrets.ALCHEMY_KEY }}" 15 | BOA_FORK_NO_CACHE: "TRUE" 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Python 3.11 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: "3.11" 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 34 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi 35 | 36 | - name: Run project integration tests 37 | run: | 38 | pytest tests/integration --durations=0 --gas-profile 39 | -------------------------------------------------------------------------------- /configs/prod/ethereum/tracking.json: -------------------------------------------------------------------------------- 1 | { 2 | "arcade": { 3 | "abi_file": "auxiliary/ArcadeLoanCore_abi.json", 4 | "address": "0x89bc08BA00f135d608bc335f6B33D7a9ABCC98aF", 5 | "contract": "Generic", 6 | "name": "Arcade" 7 | }, 8 | "benddao": { 9 | "abi_file": "auxiliary/BendDAOLendPoolLoan_abi.json", 10 | "address": "0x5f6ac80CdB9E87f3Cfa6a90E5140B9a16A361d5C", 11 | "contract": "Generic", 12 | "name": "BendDAO" 13 | }, 14 | "gondi": { 15 | "abi_file": "auxiliary/GondiMultiSourceLoan_abi.json", 16 | "address": "0xf65B99CE6DC5F6c556172BCC0Ff27D3665a7d9A8", 17 | "contract": "Generic", 18 | "name": "Gondi" 19 | }, 20 | "nftfi": { 21 | "abi_file": "auxiliary/NFTfiAssetOfferLoan_abi.json", 22 | "address": "0x9F10D706D789e4c76A1a6434cd1A9841c875C0A6", 23 | "contract": "Generic", 24 | "name": "NFTfi" 25 | }, 26 | "x2y2v3": { 27 | "abi_file": "auxiliary/X2Y2_v3_abi.json", 28 | "address": "0xB81965DdFdDA3923f292a47A1be83ba3A36B5133", 29 | "contract": "Generic", 30 | "name": "X2Y2v3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /3rd-party/aave/PoolInstance.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity ^0.8.0; 3 | 4 | import {Pool} from '../protocol/pool/Pool.sol'; 5 | import {IPoolAddressesProvider} from '../interfaces/IPoolAddressesProvider.sol'; 6 | import {Errors} from '../protocol/libraries/helpers/Errors.sol'; 7 | 8 | contract PoolInstance is Pool { 9 | uint256 public constant POOL_REVISION = 6; 10 | 11 | constructor(IPoolAddressesProvider provider) Pool(provider) {} 12 | 13 | /** 14 | * @notice Initializes the Pool. 15 | * @dev Function is invoked by the proxy contract when the Pool contract is added to the 16 | * PoolAddressesProvider of the market. 17 | * @dev Caching the address of the PoolAddressesProvider in order to reduce gas consumption on subsequent operations 18 | * @param provider The address of the PoolAddressesProvider 19 | */ 20 | function initialize(IPoolAddressesProvider provider) external virtual override initializer { 21 | require(provider == ADDRESSES_PROVIDER, Errors.INVALID_ADDRESSES_PROVIDER); 22 | } 23 | 24 | function getRevision() internal pure virtual override returns (uint256) { 25 | return POOL_REVISION; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/tests-unit.yaml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | id-token: write 12 | pull-requests: write 13 | 14 | env: 15 | BOA_FORK_RPC_URL: "https://eth-mainnet.g.alchemy.com/${{ secrets.ALCHEMY_KEY }}" 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | jobs: 19 | build: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Python 3.11 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: "3.11" 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install coverage 34 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 35 | if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi 36 | 37 | - name: Run project unit tests 38 | run: | 39 | coverage run -m pytest tests/unit --durations=0 --runslow 40 | coverage report -m | tee coverage.txt 41 | sed -i '1s/^/coverage: platform marker \n/' coverage.txt 42 | coverage xml 43 | 44 | - name: Create coverage comment 45 | uses: MishaKav/pytest-coverage-comment@main 46 | with: 47 | pytest-coverage-path: ./coverage.txt 48 | if: github.event_name == 'pull_request' 49 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Purpose of this PR 🎯 2 | 3 | 4 | - [ ] Feature; 5 | - [ ] Bugfix; 6 | - [ ] Tests; 7 | - [ ] Refactoring; 8 | - [ ] Build or CI/CD; 9 | - [ ] Documentation; 10 | - [ ] Code Styling; 11 | - [ ] Other. Please describe: 12 | 13 | ## Changes 📝 14 | 15 | 16 | 17 | 18 | ## Test Coverage 🧻 19 | 20 | 21 | 22 | ## Does this PR introduce a breaking change? ⚠️ 23 | 24 | - [ ] No 25 | - [ ] Yes 26 | 27 | 28 | 29 | ## Related issues 📎 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ## Reviewers 🦺 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /3rd-party/aave/IFlashLoanReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {IPoolAddressesProvider} from '../../../interfaces/IPoolAddressesProvider.sol'; 5 | import {IPool} from '../../../interfaces/IPool.sol'; 6 | 7 | /** 8 | * @title IFlashLoanReceiver 9 | * @author Aave 10 | * @notice Defines the basic interface of a flashloan-receiver contract. 11 | * @dev Implement this interface to develop a flashloan-compatible flashLoanReceiver contract 12 | */ 13 | interface IFlashLoanReceiver { 14 | /** 15 | * @notice Executes an operation after receiving the flash-borrowed assets 16 | * @dev Ensure that the contract can return the debt + premium, e.g., has 17 | * enough funds to repay and has approved the Pool to pull the total amount 18 | * @param assets The addresses of the flash-borrowed assets 19 | * @param amounts The amounts of the flash-borrowed assets 20 | * @param premiums The fee of each flash-borrowed asset 21 | * @param initiator The address of the flashloan initiator 22 | * @param params The byte-encoded params passed when initiating the flashloan 23 | * @return True if the execution of the operation succeeds, false otherwise 24 | */ 25 | function executeOperation( 26 | address[] calldata assets, 27 | uint256[] calldata amounts, 28 | uint256[] calldata premiums, 29 | address initiator, 30 | bytes calldata params 31 | ) external returns (bool); 32 | 33 | function ADDRESSES_PROVIDER() external view returns (IPoolAddressesProvider); 34 | 35 | function POOL() external view returns (IPool); 36 | } 37 | -------------------------------------------------------------------------------- /scripts/get_tokens.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import warnings 5 | from decimal import Decimal 6 | from pathlib import Path 7 | 8 | import boto3 9 | import click 10 | 11 | from ._helpers.deployment import Environment 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(logging.WARNING) 15 | warnings.filterwarnings("ignore") 16 | 17 | 18 | ENV = Environment[os.environ.get("ENV", "local")] 19 | CHAIN = os.environ.get("CHAIN") 20 | DYNAMODB = boto3.resource("dynamodb") 21 | TOKENS = DYNAMODB.Table(f"token-symbols-{ENV.name}") 22 | 23 | 24 | def deserialize_values(item): 25 | if type(item) is dict: 26 | return {k: deserialize_values(v) for k, v in item.items()} 27 | if type(item) is list: 28 | return [deserialize_values(v) for v in item] 29 | if type(item) is Decimal: 30 | return int(item) 31 | return item 32 | 33 | 34 | def get_tokens(): 35 | items = [] 36 | response = TOKENS.scan() 37 | while "LastEvaluatedKey" in response: 38 | items.extend(deserialize_values(i) for i in response["Items"]) 39 | response = TOKENS.scan(ExclusiveStartKey=response["LastEvaluatedKey"]) 40 | items.extend(deserialize_values(i) for i in response["Items"]) 41 | return items 42 | 43 | 44 | def store_tokens_config(tokens: list[dict], env: Environment, chain: str): 45 | config_file = f"{Path.cwd()}/configs/{env.name}/{chain}/tokens.json" 46 | config = {c["symbol"].lower(): c for c in tokens if c.get("chain") == chain} 47 | 48 | with open(config_file, "w") as f: 49 | f.write(json.dumps(config, indent=4, sort_keys=True)) 50 | 51 | 52 | @click.command() 53 | def cli(): 54 | print(f"Retrieving tokens configs in {ENV.name} for {CHAIN}") 55 | 56 | tokens = get_tokens() 57 | store_tokens_config(tokens, ENV, CHAIN) 58 | 59 | print(f"Tokens configs retrieved in {ENV.name} for {CHAIN}") 60 | -------------------------------------------------------------------------------- /scripts/get_collections.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import warnings 5 | from decimal import Decimal 6 | from pathlib import Path 7 | 8 | import boto3 9 | import click 10 | 11 | from ._helpers.deployment import Environment 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.setLevel(logging.WARNING) 15 | warnings.filterwarnings("ignore") 16 | 17 | 18 | ENV = Environment[os.environ.get("ENV", "local")] 19 | CHAIN = os.environ.get("CHAIN") 20 | DYNAMODB = boto3.resource("dynamodb") 21 | COLLECTIONS = DYNAMODB.Table(f"collections-{ENV.name}") 22 | 23 | 24 | def deserialize_values(item): 25 | if type(item) is dict: 26 | return {k: deserialize_values(v) for k, v in item.items()} 27 | if type(item) is list: 28 | return [deserialize_values(v) for v in item] 29 | if type(item) is Decimal: 30 | return int(item) 31 | return item 32 | 33 | 34 | def get_collections(): 35 | collection_items = [] 36 | response = COLLECTIONS.scan() 37 | while "LastEvaluatedKey" in response: 38 | collection_items.extend(deserialize_values(i) for i in response["Items"]) 39 | response = COLLECTIONS.scan(ExclusiveStartKey=response["LastEvaluatedKey"]) 40 | collection_items.extend(deserialize_values(i) for i in response["Items"]) 41 | return collection_items 42 | 43 | 44 | def store_collections_config(collections: list[dict], env: Environment, chain: str): 45 | config_file = f"{Path.cwd()}/configs/{env.name}/{chain}/collections.json" 46 | config = {c["collection_key"]: c for c in collections if c.get("chain") == chain} 47 | 48 | with open(config_file, "w") as f: 49 | f.write(json.dumps(config, indent=4, sort_keys=True)) 50 | 51 | 52 | @click.command() 53 | def cli(): 54 | print(f"Retrieving collection configs in {ENV.name} for {CHAIN}") 55 | 56 | collections = get_collections() 57 | store_collections_config(collections, ENV, CHAIN) 58 | 59 | print(f"Collections configs retrieved in {ENV.name} for {CHAIN}") 60 | -------------------------------------------------------------------------------- /tests/integration/test_aave.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from dataclasses import dataclass 3 | from decimal import Decimal 4 | 5 | import boa 6 | import pytest 7 | from eth_abi import encode 8 | from eth_account import Account 9 | from eth_account.messages import HexBytes, SignableMessage 10 | from eth_utils import keccak 11 | from hypothesis import given, settings 12 | from hypothesis import strategies as st 13 | from web3 import Web3 14 | 15 | from ..conftest_base import ZERO_ADDRESS, CollectionContract, get_last_event 16 | 17 | 18 | @pytest.fixture 19 | def aave_proxy_contract_def(): 20 | return boa.load_partial("contracts/AaveFlash.vy") 21 | 22 | 23 | @pytest.fixture 24 | def p2p_control(p2p_lending_control_contract_def, owner, cryptopunks, bayc, bayc_key_hash, punks_key_hash): 25 | p2p_control = p2p_lending_control_contract_def.deploy() 26 | p2p_control.change_collections_contracts([CollectionContract(bayc_key_hash, bayc.address)]) 27 | return p2p_control 28 | 29 | 30 | @pytest.fixture 31 | def p2p_nfts_usdc(p2p_lending_nfts_contract_def, usdc, delegation_registry, cryptopunks, owner, p2p_control): 32 | return p2p_lending_nfts_contract_def.deploy( 33 | usdc, p2p_control, delegation_registry, cryptopunks, 0, 0, owner, 10000, 10000, 10000, 10000 34 | ) 35 | 36 | 37 | @pytest.fixture 38 | def aave_proxy(aave_proxy_contract_def, p2p_nfts_usdc): 39 | aave_pool_address_provider = "0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e" 40 | return aave_proxy_contract_def.deploy(p2p_nfts_usdc.address, aave_pool_address_provider) 41 | 42 | 43 | @pytest.fixture 44 | def aave(boa_env): 45 | return boa.load_abi("contracts/auxiliary/AavePoolv3_abi.json", name="AavePoolv3").at( 46 | "0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2" 47 | ) 48 | 49 | 50 | def test_initial_state(aave, aave_proxy, usdc, p2p_nfts_usdc, borrower): 51 | assert aave_proxy.POOL() == aave.address 52 | assert aave_proxy.ADDRESSES_PROVIDER() == aave.ADDRESSES_PROVIDER() 53 | 54 | 55 | def test_aave_flash_loan(aave, aave_proxy, usdc, p2p_nfts_usdc, borrower, owner): 56 | amount = int(1000 * 1e6) 57 | usdc.transfer(borrower, amount, sender=owner) 58 | assert usdc.balanceOf(borrower) >= amount 59 | usdc.approve(aave_proxy.address, amount, sender=borrower) # for premium, TODO: change provider 60 | aave_proxy.flash_loan(amount, sender=borrower) 61 | -------------------------------------------------------------------------------- /configs/local/p2p.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "delegation_registry": { 4 | "abi_key": "", 5 | "address": "", 6 | "contract": "DelegationRegistry", 7 | "properties_addresses": {} 8 | }, 9 | "usdc": { 10 | "abi_key": "", 11 | "address": "", 12 | "contract": "ERC20", 13 | "properties": { 14 | "decimals": 6, 15 | "name": "USDC", 16 | "supply": "1000000000000000000", 17 | "symbol": "USDC" 18 | }, 19 | "properties_addresses": {} 20 | }, 21 | "weth": { 22 | "abi_key": "", 23 | "address": "", 24 | "contract": "ERC20", 25 | "properties": { 26 | "decimals": 18, 27 | "name": "WETH", 28 | "supply": "1000000000000000000000000", 29 | "symbol": "WETH" 30 | }, 31 | "properties_addresses": {} 32 | } 33 | }, 34 | "configs": {}, 35 | "p2p": { 36 | "eth_nfts": { 37 | "abi_key": "", 38 | "address": "", 39 | "contract": "P2PLendingNfts", 40 | "properties": { 41 | "cryptopunks_key": "cryptopunks", 42 | "delegation_registry_key": "common.delegation_registry", 43 | "payment_token_key": "common.weth", 44 | "protocol_upfront_fee": 0, 45 | "protocol_settlement_fee": 0, 46 | "protocol_wallet": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 47 | }, 48 | "properties_addresses": { 49 | "cryptopunks": "", 50 | "delegation_registry": "", 51 | "payment_token": "" 52 | }, 53 | "version": "1" 54 | }, 55 | "usdc_nfts": { 56 | "abi_key": "", 57 | "address": "", 58 | "contract": "P2PLendingNfts", 59 | "properties": { 60 | "cryptopunks_key": "cryptopunks", 61 | "delegation_registry_key": "common.delegation_registry", 62 | "payment_token_key": "common.usdc", 63 | "protocol_upfront_fee": 0, 64 | "protocol_settlement_fee": 0, 65 | "protocol_wallet": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 66 | }, 67 | "properties_addresses": { 68 | "cryptopunks": "", 69 | "delegation_registry": "", 70 | "payment_token": "" 71 | }, 72 | "version": "1" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /configs/dev/zapechain/p2p.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "delegation_registry": { 4 | "abi_key": "b81a1dc53243e33e00b85ff03e7df975f34f7975", 5 | "address": "0x82c83b7f88aef2eD99d4869D547b6ED28e69C8df", 6 | "contract": "DelegationRegistry", 7 | "properties_addresses": {} 8 | }, 9 | "p2p_controller": { 10 | "abi_key": "ea96d1a2134dd14a2190e52e504bf39dd4b92ef9", 11 | "address": "0xF06D5f5BfFFCB6a52c84cfebc03AD35637728E73", 12 | "contract": "P2PLendingControl", 13 | "properties": { 14 | "trait_roots_key": "configs.trait_roots" 15 | }, 16 | "properties_addresses": {} 17 | } 18 | }, 19 | "configs": { 20 | "trait_roots": { 21 | "fuku": "4e17c4b46048fbdb0c2d3c5524410131979ea013a03996191f7acba38e19964eo", 22 | "gobsonape": "9245f5f55e85d81dea6ae13a1dece475646dfbf802e7d03a0d855b638ecdd9ed", 23 | "gsonape": "6c97bf7e96d7075261a5249f09e9e837e70ae0adc2c08f605f35c719569937c7", 24 | "monkees": "57fd37cb5315283734a918bca0035357605d277a9947bc67671d7dea431c2168", 25 | "shapes": "4f9b117b74d6d278f9ec5e322fd8c9664fe942dbf7fe912fab3efdf47d566d75", 26 | "tokengators": "227d44ce3036587028e52a7be8903ff1b3556cd0999161f97ff49faf151a0ee5" 27 | } 28 | }, 29 | "p2p": { 30 | "ape_nfts": { 31 | "abi_key": "10303d3fbb028dacb97d9fbfe3717a62bd73a126", 32 | "address": "0x724Ca58E1e6e64BFB1E15d7Eec0fe1E5f581c7bD", 33 | "contract": "P2PLendingNfts", 34 | "properties": { 35 | "cryptopunks_key": "", 36 | "delegation_registry_key": "common.delegation_registry", 37 | "max_borrower_broker_settlement_fee": 1000, 38 | "max_lender_broker_settlement_fee": 1000, 39 | "max_protocol_settlement_fee": 1000, 40 | "max_protocol_upfront_fee": 1000, 41 | "p2p_controller_key": "common.p2p_controller", 42 | "payment_token_key": "common.wape", 43 | "protocol_settlement_fee": 0, 44 | "protocol_upfront_fee": 0, 45 | "protocol_wallet": "0x66aB6D9362d4F35596279692F0251Db635165871" 46 | }, 47 | "properties_addresses": { 48 | "delegation_registry": "0x82c83b7f88aef2eD99d4869D547b6ED28e69C8df", 49 | "p2p_controller": "0xF06D5f5BfFFCB6a52c84cfebc03AD35637728E73", 50 | "payment_token": "0x9E4c14403d7d9A8A782044E86a93CAE09D7B2ac9" 51 | }, 52 | "token_symbol": "WAPE", 53 | "version": "1" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /contracts/EIP3156Flash.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | from ethereum.ercs import IERC20 4 | 5 | FLASH_LOAN_CALLBACK_SIZE: constant(uint256) = 1024 6 | 7 | interface P2PLendingNfts: 8 | def payment_token() -> address: view 9 | 10 | 11 | # https://eips.ethereum.org/EIPS/eip-3156 12 | interface IFlashLender: 13 | def maxFlashLoan(token: address) -> uint256: view 14 | def flashFee(token: address, amount: uint256) -> uint256: view 15 | def flashLoan(receiver: address, token: address, amount: uint256, data: Bytes[FLASH_LOAN_CALLBACK_SIZE]) -> bool: nonpayable 16 | 17 | 18 | interface IERC3156FlashBorrower: 19 | def onFlashLoan( 20 | initiator: address, 21 | token: address, 22 | amount: uint256, 23 | fee: uint256, 24 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 25 | ) -> bytes32: nonpayable 26 | 27 | 28 | implements: IERC3156FlashBorrower 29 | 30 | 31 | struct CallbackData: 32 | dummy: uint256 33 | 34 | ERC3156_CALLBACK_OK: constant(bytes32) = keccak256("ERC3156FlashBorrower.onFlashLoan") 35 | 36 | MAX_FEES: constant(uint256) = 4 37 | 38 | p2p_lending_nfts: public(immutable(address)) 39 | flash_lender: public(immutable(address)) 40 | 41 | @deploy 42 | def __init__(_p2p_lending_nfts: address, _flash_lender: address): 43 | p2p_lending_nfts = _p2p_lending_nfts 44 | flash_lender = _flash_lender 45 | 46 | 47 | @external 48 | def onFlashLoan( 49 | initiator: address, 50 | token: address, 51 | amount: uint256, 52 | fee: uint256, 53 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 54 | ) -> bytes32: 55 | 56 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"callback")) 57 | assert msg.sender == flash_lender, "unauthorized" 58 | assert initiator == self, "unknown initiator" 59 | assert fee == 0, "fee not supported" 60 | 61 | callback_data: CallbackData = abi_decode(data, CallbackData) 62 | 63 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 64 | assert token == payment_token, "Invalid asset" 65 | 66 | assert (staticcall IERC20(payment_token).balanceOf(self)) >= amount, "Insufficient balance" 67 | 68 | # do stuff with the flash loan 69 | 70 | extcall IERC20(payment_token).approve(flash_lender, amount + fee) 71 | return ERC3156_CALLBACK_OK 72 | 73 | 74 | @external 75 | def flash_loan(amount: uint256): 76 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"flash loan")) 77 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 78 | callback_data: CallbackData = CallbackData(dummy = 42) 79 | assert extcall IFlashLender(flash_lender).flashLoan(self, payment_token, amount, abi_encode(callback_data)), "flash loan failed" 80 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "lendging-protocol-v2" 3 | description = "Lending Protocol by Zharta" 4 | classifiers = ["Private :: Do Not Upload"] 5 | version = "0" 6 | dependencies = [ 7 | "eth-ape", 8 | "vyper==0.4.1", 9 | "ape-vyper", 10 | "ape-foundry", 11 | "ape-alchemy", 12 | "ape-arbitrum", 13 | "ape-base", 14 | "web3", 15 | ] 16 | 17 | 18 | [project.optional-dependencies] 19 | dev = [ 20 | # "titanoboa==0.2.5", 21 | "titanoboa @ git+https://github.com/vyperlang/titanoboa.git@c06e23a68df73d669738f30faa4aeb051012423b", 22 | "boto3", 23 | "click", 24 | "coverage", 25 | "hypothesis", 26 | "ipython", 27 | "mypy", 28 | "pre-commit", 29 | "pytest", 30 | "pytest-bdd", 31 | "pytest-xdist", 32 | "python-lsp-server", 33 | "rich", 34 | "rope", 35 | "ruff", 36 | # "vyper-lsp", 37 | ] 38 | 39 | [build-system] 40 | requires = ["setuptools>=63", "wheel"] 41 | build-backend = "setuptools.build_meta" 42 | 43 | [tool.setuptools.packages.find] 44 | include = ["scripts"] 45 | 46 | [tool.pytest.ini_options] 47 | log_file = "pytest-logs.txt" 48 | addopts = """ 49 | -p no:ape_test 50 | -vv 51 | --durations=10 52 | """ 53 | 54 | [tool.ruff] 55 | lint.select = ["ALL"] 56 | lint.ignore = ["ANN", "B905", "BLE", "COM812", "CPY", "D", "DTZ", "EM", "FIX", "ISC001", "PLR0913", "PLR2004", "S", "TCH", "TD", "TRY003"] 57 | line-length = 127 58 | lint.preview = true 59 | target-version = "py311" 60 | 61 | [tool.ruff.lint.per-file-ignores] 62 | "__init__.py" = ["F401"] 63 | "ape_console_extras.py" = ["T201", "B006", "PYI024", "RUF052"] 64 | "tests/*.py" = [ 65 | "ARG001", 66 | "ERA001", 67 | "F401", 68 | "FBT003", 69 | "FURB118", 70 | "FURB140", 71 | "N806", 72 | "N815", 73 | "PLC1901", 74 | "PLR0914", 75 | "PLR0915", 76 | "PLR0917", 77 | "PT004", 78 | "PT022", 79 | "PTH", 80 | "PYI024", 81 | "RUF029", 82 | "RUF052", 83 | "RUF100", 84 | "SLF001", 85 | "T201", 86 | "TID252", 87 | ] 88 | "scripts/*.py" = [ 89 | "A001", 90 | "A004", 91 | "ERA001", 92 | "FURB101", 93 | "FURB103", 94 | "ERA001", 95 | "PLW1514", 96 | "PTH123", 97 | "RUF052", 98 | "T201", 99 | "UP015" 100 | ] 101 | 102 | [tool.ruff.lint.flake8-pytest-style] 103 | fixture-parentheses = false 104 | mark-parentheses = false 105 | 106 | 107 | [tool.coverage.run] 108 | plugins = [ "boa.coverage" ] 109 | relative_files = true 110 | omit = [ 111 | "contracts/auxiliary/*", 112 | "tests/stubs/*", 113 | "None" 114 | ] 115 | 116 | [tool.coverage.paths] 117 | source = ["contracts"] 118 | 119 | [tool.coverage.report] 120 | show_missing = true 121 | -------------------------------------------------------------------------------- /contracts/AaveFlash.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | from ethereum.ercs import IERC20 4 | 5 | 6 | FLASH_LOAN_CALLBACK_SIZE: constant(uint256) = 1024 7 | 8 | interface P2PLendingNfts: 9 | def payment_token() -> address: view 10 | 11 | 12 | interface IFlashLoanSimpleReceiver: 13 | def executeOperation(asset: address, amount: uint256, premium: uint256, initiator: address, params: Bytes[FLASH_LOAN_CALLBACK_SIZE]) -> bool: nonpayable 14 | def ADDRESSES_PROVIDER() -> address: view 15 | def POOL() -> address: view 16 | 17 | 18 | interface IPoolProvider: 19 | def getPool() -> address: view 20 | 21 | 22 | interface IPool: 23 | def flashLoanSimple( 24 | receiverAddress: address, 25 | asset: address, 26 | amount: uint256, 27 | params: Bytes[FLASH_LOAN_CALLBACK_SIZE], 28 | referralCode: uint16 29 | ): nonpayable 30 | 31 | 32 | implements: IFlashLoanSimpleReceiver 33 | 34 | 35 | MAX_FEES: constant(uint256) = 4 36 | BPS: constant(uint256) = 10000 37 | 38 | p2p_lending_nfts: address 39 | aave_pool_provider: address 40 | 41 | @deploy 42 | def __init__(_p2p_lending_nfts: address, _aave_pool_provider: address): 43 | self.p2p_lending_nfts = _p2p_lending_nfts 44 | self.aave_pool_provider = _aave_pool_provider 45 | 46 | 47 | 48 | @external 49 | def executeOperation( 50 | asset: address, 51 | amount: uint256, 52 | premium: uint256, 53 | initiator: address, 54 | params: Bytes[FLASH_LOAN_CALLBACK_SIZE] 55 | ) -> bool: 56 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"callback")) 57 | assert initiator == self, "who are you?" 58 | aave_pool: address = staticcall IPoolProvider(self.aave_pool_provider).getPool() 59 | payment_token: address = staticcall P2PLendingNfts(self.p2p_lending_nfts).payment_token() 60 | 61 | # assert premium == 0, "Premium not supported" 62 | 63 | assert (staticcall IERC20(payment_token).balanceOf(self)) >= amount + premium, "Insufficient balance" 64 | assert msg.sender == aave_pool, "Unauthorized" 65 | assert asset == payment_token, "Invalid asset" 66 | 67 | # do stuff with the amount 68 | 69 | extcall IERC20(payment_token).approve(aave_pool, amount + premium) 70 | 71 | return True 72 | 73 | 74 | @external 75 | @view 76 | def ADDRESSES_PROVIDER() -> address: 77 | return self.aave_pool_provider 78 | 79 | @external 80 | @view 81 | def POOL() -> address: 82 | return staticcall IPoolProvider(self.aave_pool_provider).getPool() 83 | 84 | @external 85 | def flash_loan(amount: uint256): 86 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"flash loan")) 87 | aave_pool: address = staticcall IPoolProvider(self.aave_pool_provider).getPool() 88 | payment_token: address = staticcall P2PLendingNfts(self.p2p_lending_nfts).payment_token() 89 | extcall IERC20(payment_token).transferFrom(msg.sender, self, amount) # remove 90 | extcall IPool(aave_pool).flashLoanSimple(self, payment_token, amount, b"", 0) 91 | -------------------------------------------------------------------------------- /configs/int/curtis/p2p.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "delegation_registry": { 4 | "abi_key": "b81a1dc53243e33e00b85ff03e7df975f34f7975", 5 | "address": "0x69f18335Ab0c6E60C7cE02e501BBCb0C25025eDc", 6 | "contract": "DelegationRegistry", 7 | "properties_addresses": {} 8 | }, 9 | "p2p_controller": { 10 | "abi_key": "ea96d1a2134dd14a2190e52e504bf39dd4b92ef9", 11 | "address": "0x5c435249e08D987a77fdA86376e796C37C552136", 12 | "contract": "P2PLendingControl", 13 | "properties": { 14 | "trait_roots_key": "configs.trait_roots" 15 | }, 16 | "properties_addresses": {} 17 | } 18 | }, 19 | "configs": { 20 | "trait_roots": { 21 | "baycshadow": "7f50d10419a4e6ad3380ac409ecb62e30bc0a4fed46425dd90eb2b6bfafe9a5b", 22 | "fuku": "0000000000000000000000000000000000000000000000000000000000000001", 23 | "gobsonape": "9245f5f55e85d81dea6ae13a1dece475646dfbf802e7d03a0d855b638ecdd9ed", 24 | "gsonape": "6c97bf7e96d7075261a5249f09e9e837e70ae0adc2c08f605f35c719569937c7", 25 | "kodashadow": "0ef2c250e902d76e862db58ba1ba1173c55d8db273595937bd40a22257eaf89a", 26 | "maycshadow": "8e89000f31aab2eb0087079e6bb9418e40d5ad94cf02985f7d02de4f5d391918", 27 | "monkees": "57fd37cb5315283734a918bca0035357605d277a9947bc67671d7dea431c2168", 28 | "shapes": "4f9b117b74d6d278f9ec5e322fd8c9664fe942dbf7fe912fab3efdf47d566d75", 29 | "tokengators": "227d44ce3036587028e52a7be8903ff1b3556cd0999161f97ff49faf151a0ee5" 30 | } 31 | }, 32 | "p2p": { 33 | "ape_nfts": { 34 | "abi_key": "10303d3fbb028dacb97d9fbfe3717a62bd73a126", 35 | "address": "0x311F9f063a3e8de0fFe81beAca53EEC310432faE", 36 | "contract": "P2PLendingNfts", 37 | "properties": { 38 | "cryptopunks_key": "", 39 | "delegation_registry_key": "common.delegation_registry", 40 | "max_borrower_broker_settlement_fee": 1000, 41 | "max_lender_broker_settlement_fee": 1000, 42 | "max_protocol_settlement_fee": 1000, 43 | "max_protocol_upfront_fee": 1000, 44 | "p2p_controller_key": "common.p2p_controller", 45 | "payment_token_key": "common.wape", 46 | "protocol_settlement_fee": 0, 47 | "protocol_upfront_fee": 0, 48 | "protocol_wallet": "0x313e7bF4D508087295f092Bb9fadcE3b7b4dc89e" 49 | }, 50 | "properties_addresses": { 51 | "delegation_registry": "0x69f18335Ab0c6E60C7cE02e501BBCb0C25025eDc", 52 | "p2p_controller": "0x5c435249e08D987a77fdA86376e796C37C552136", 53 | "payment_token": "0x69B5cfEfDd30467Ea985EBac1756d81EA871798c" 54 | }, 55 | "token_symbol": "WAPE", 56 | "version": "1" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /configs/prod/apechain/p2p.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "delegation_registry": { 4 | "abi_key": "8f910c9aa38cffa69fc4a97a81cbc79dda306e2a", 5 | "address": "0x00000000000000447e69651d841bD8D104Bed493", 6 | "contract": "DelegationRegistry", 7 | "properties_addresses": {} 8 | }, 9 | "p2p_controller": { 10 | "abi_key": "ea96d1a2134dd14a2190e52e504bf39dd4b92ef9", 11 | "address": "0x323aeb15c6507f060191f6af1eCd4497506a5348", 12 | "contract": "P2PLendingControl", 13 | "properties": { 14 | "trait_roots_key": "configs.trait_roots" 15 | }, 16 | "properties_addresses": {} 17 | } 18 | }, 19 | "configs": { 20 | "trait_roots": { 21 | "baycshadow": "6411051c7e23fbb77f098572f15c352f4c14e855832f5219f96276e32db52b80", 22 | "fuku": "17138d1fd63472b95580204b9dd3a72d52413ca9955e53f06b310c8ea1f87c90", 23 | "gobsonape": "5a382a2b0a765e80c9c82fdd01c7908a522af59c7a62ef7640fc0f7a357dde3a", 24 | "gsonape": "3523f24202dc14072006142914d2630d173347b1ccf3ebc35da32be542fa2558", 25 | "hopstarz": "b78a602fd7744ba4a92f81df5e82705e8f0d162ce75ff47ec95f7c785a13d2e6", 26 | "kodashadow": "50bbd57b17826aab142c31fd4461b7529892c2b4fbc5278a2015c818d922b64d", 27 | "maycshadow": "74755f6248e271bfd0de3af14f93a53d470ee345381af7db5c1d2500ebbf8703", 28 | "mintotaurs": "68baf0e4116356a4350d090948251862baa27bcfc639418656dccd349385e29d", 29 | "monkees": "efc61ccc8a74a6e888e906629af09dfb0aa3b54b3bcd8fbe5c03fcfd85205cd0", 30 | "shapes": "422b317f3353a55fe12ac80d031fec5a420c8286d32298e1b523014b643b5a11", 31 | "tokengators": "c1bde94d371d0ec0e6e9722dad521d114134858f98d5aff99115186fb3a07531" 32 | } 33 | }, 34 | "p2p": { 35 | "ape_nfts": { 36 | "abi_key": "10303d3fbb028dacb97d9fbfe3717a62bd73a126", 37 | "address": "0x9a99Ea6AC47592F00E646be131b3075ca9AAe644", 38 | "contract": "P2PLendingNfts", 39 | "properties": { 40 | "cryptopunks_key": "", 41 | "delegation_registry_key": "common.delegation_registry", 42 | "max_borrower_broker_settlement_fee": 5000, 43 | "max_lender_broker_settlement_fee": 5000, 44 | "max_protocol_settlement_fee": 5000, 45 | "max_protocol_upfront_fee": 5000, 46 | "p2p_controller_key": "common.p2p_controller", 47 | "payment_token_key": "common.wape", 48 | "protocol_settlement_fee": 0, 49 | "protocol_upfront_fee": 0, 50 | "protocol_wallet": "0x5723759D679662cf931d686f129E3296D9545190" 51 | }, 52 | "properties_addresses": { 53 | "delegation_registry": "0x00000000000000447e69651d841bD8D104Bed493", 54 | "p2p_controller": "0x323aeb15c6507f060191f6af1eCd4497506a5348", 55 | "payment_token": "0x48b62137EdfA95a428D35C09E44256a739F6B557" 56 | }, 57 | "token_symbol": "WAPE", 58 | "version": "1" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /contracts/auxiliary/BalancerMock.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | """ 4 | Balancer Mock based implementing needed functions for testing 5 | """ 6 | 7 | from ethereum.ercs import IERC20 8 | from ethereum.ercs import IERC20Detailed 9 | 10 | FLASH_LOAN_MAX_TOKENS: constant(uint256) = 5 11 | FLASH_LOAN_CALLBACK_SIZE: constant(uint256) = 10240 12 | 13 | interface IFlashLender: 14 | def flashLoan( 15 | recepient: address, 16 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 17 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 18 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 19 | ): nonpayable 20 | 21 | 22 | interface IFlashLoanRecipient: 23 | def receiveFlashLoan( 24 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 25 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 26 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 27 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 28 | ): nonpayable 29 | 30 | 31 | implements: IFlashLender 32 | 33 | deposits: HashMap[bytes32, uint256] 34 | 35 | @deploy 36 | def __init__(): 37 | pass 38 | 39 | @external 40 | def flashLoan( 41 | recepient: address, 42 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 43 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 44 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 45 | ): 46 | assert len(tokens) == len(amounts), "Mismatched input lengths" 47 | 48 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS] = [] 49 | initial_balances: DynArray[uint256,FLASH_LOAN_MAX_TOKENS] = [] 50 | 51 | for i: uint256 in range(len(tokens), bound=FLASH_LOAN_MAX_TOKENS): 52 | initial_balance: uint256 = staticcall IERC20(tokens[i]).balanceOf(self) 53 | if initial_balance < amounts[i]: 54 | raise "Insufficient liquidity for token" 55 | initial_balances.append(initial_balance) 56 | extcall IERC20(tokens[i]).transfer(recepient, amounts[i]) 57 | fee_amounts.append(0) 58 | 59 | extcall IFlashLoanRecipient(recepient).receiveFlashLoan( 60 | tokens, 61 | amounts, 62 | fee_amounts, 63 | data 64 | ) 65 | 66 | for i: uint256 in range(len(tokens), bound=FLASH_LOAN_MAX_TOKENS): 67 | if staticcall IERC20(tokens[i]).balanceOf(self) < initial_balances[i]: 68 | raise "Flash loan not repaid" 69 | 70 | 71 | 72 | 73 | @external 74 | def deposit(token: address, amount: uint256): 75 | key: bytes32 = self._account_key(msg.sender, token) 76 | extcall IERC20(token).transferFrom(msg.sender, self, amount) 77 | self.deposits[key] += amount 78 | 79 | @external 80 | def withdraw(token: address, amount: uint256): 81 | key: bytes32 = self._account_key(msg.sender, token) 82 | assert self.deposits[key] >= amount, "Insufficient balance" 83 | self.deposits[key] -= amount 84 | extcall IERC20(token).transfer(msg.sender, amount) 85 | 86 | 87 | @external 88 | @view 89 | def balanceOf(wallet: address, token: address) -> uint256: 90 | return self.deposits[self._account_key(wallet, token)] 91 | 92 | 93 | @internal 94 | @view 95 | def _account_key(wallet: address, token: address) -> bytes32: 96 | return keccak256(concat(convert(wallet, bytes32), convert(token, bytes32))) 97 | -------------------------------------------------------------------------------- /configs/local/foundry/p2p.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "delegation_registry": { 4 | "abi_key": "b81a1dc53243e33e00b85ff03e7df975f34f7975", 5 | "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", 6 | "contract": "DelegationRegistry", 7 | "properties_addresses": {} 8 | }, 9 | "usdc": { 10 | "abi_key": "bb05a2d17d2f0265272d27c66e1c05d0b3827bfb", 11 | "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", 12 | "contract": "ERC20", 13 | "properties": { 14 | "decimals": 6, 15 | "name": "USDC", 16 | "supply": "1000000000000000000", 17 | "symbol": "USDC" 18 | }, 19 | "properties_addresses": {} 20 | }, 21 | "weth": { 22 | "abi_key": "bb05a2d17d2f0265272d27c66e1c05d0b3827bfb", 23 | "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9", 24 | "contract": "ERC20", 25 | "properties": { 26 | "decimals": 18, 27 | "name": "WETH", 28 | "supply": "1000000000000000000000000", 29 | "symbol": "WETH" 30 | }, 31 | "properties_addresses": {} 32 | } 33 | }, 34 | "configs": {}, 35 | "p2p": { 36 | "eth_nfts": { 37 | "abi_key": "4bcbf0fea553e69c251e4d3ef1bd7fdd67754760", 38 | "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", 39 | "contract": "P2PLendingNfts", 40 | "properties": { 41 | "cryptopunks_key": "cryptopunks", 42 | "delegation_registry_key": "common.delegation_registry", 43 | "payment_token_key": "common.weth", 44 | "protocol_settlement_fee": 0, 45 | "protocol_upfront_fee": 0, 46 | "protocol_wallet": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 47 | }, 48 | "properties_addresses": { 49 | "cryptopunks": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", 50 | "delegation_registry": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", 51 | "payment_token": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" 52 | }, 53 | "version": "1" 54 | }, 55 | "usdc_nfts": { 56 | "abi_key": "4bcbf0fea553e69c251e4d3ef1bd7fdd67754760", 57 | "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", 58 | "contract": "P2PLendingNfts", 59 | "properties": { 60 | "cryptopunks_key": "cryptopunks", 61 | "delegation_registry_key": "common.delegation_registry", 62 | "payment_token_key": "common.usdc", 63 | "protocol_settlement_fee": 0, 64 | "protocol_upfront_fee": 0, 65 | "protocol_wallet": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" 66 | }, 67 | "properties_addresses": { 68 | "cryptopunks": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", 69 | "delegation_registry": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", 70 | "payment_token": "0x5FbDB2315678afecb367f032d93F642f64180aa3" 71 | }, 72 | "version": "1" 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/unit/p2p_nfts/conftest.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha3_256 2 | from textwrap import dedent 3 | 4 | import boa 5 | import pytest 6 | 7 | from ...conftest_base import CollectionContract 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def max_lock_expiration(): 12 | return 2 * 86400 13 | 14 | 15 | @pytest.fixture 16 | def bayc(erc721_contract_def, owner): 17 | return erc721_contract_def.deploy() 18 | 19 | 20 | @pytest.fixture 21 | def usdc(weth9_contract_def, owner): 22 | return weth9_contract_def.deploy("USDC", "USDC", 9, 10**20) 23 | 24 | 25 | @pytest.fixture 26 | def delegation_registry(delegation_registry_contract_def, owner): 27 | return delegation_registry_contract_def.deploy() 28 | 29 | 30 | @pytest.fixture 31 | def bayc_key_hash(): 32 | return sha3_256(b"bayc").digest() 33 | 34 | 35 | @pytest.fixture 36 | def punks_key_hash(): 37 | return sha3_256(b"cryptopunks").digest() 38 | 39 | 40 | @pytest.fixture 41 | def p2p_control(p2p_lending_control_contract_def, owner, cryptopunks, bayc, bayc_key_hash, punks_key_hash): 42 | p2p_control = p2p_lending_control_contract_def.deploy() 43 | p2p_control.change_collections_contracts( 44 | [CollectionContract(punks_key_hash, cryptopunks.address), CollectionContract(bayc_key_hash, bayc.address)] 45 | ) 46 | return p2p_control 47 | 48 | 49 | @pytest.fixture 50 | def p2p_nfts_usdc(p2p_lending_nfts_contract_def, usdc, delegation_registry, cryptopunks, owner, p2p_control): 51 | return p2p_lending_nfts_contract_def.deploy( 52 | usdc, p2p_control, delegation_registry, cryptopunks, 0, 0, owner, 10000, 10000, 10000, 10000 53 | ) 54 | 55 | 56 | @pytest.fixture 57 | def now(): 58 | return boa.eval("block.timestamp") 59 | 60 | 61 | @pytest.fixture 62 | def traits(): 63 | return { 64 | "openness": [ 65 | "curious", 66 | "inventive", 67 | "artistic", 68 | "wide interests", 69 | "excitable", 70 | "unconventional", 71 | "imaginative", 72 | "traditional", 73 | "prefer routine", 74 | "practical", 75 | ], 76 | "conscientiousness": [ 77 | "organized", 78 | "efficient", 79 | "dependable", 80 | "thorough", 81 | "self-disciplined", 82 | "careful", 83 | "lazy", 84 | "impulsive", 85 | "careless", 86 | "easy-going", 87 | ], 88 | "extraversion": [ 89 | "outgoing", 90 | "energetic", 91 | "assertive", 92 | "sociable", 93 | "talkative", 94 | "enthusiastic", 95 | "reserved", 96 | "shy", 97 | "quiet", 98 | "solitary", 99 | ], 100 | "agreeableness": [ 101 | "friendly", 102 | "compassionate", 103 | "cooperative", 104 | "trusting", 105 | "helpful", 106 | "empathetic", 107 | "critical", 108 | "uncooperative", 109 | "suspicious", 110 | "competitive", 111 | ], 112 | "neuroticism": [ 113 | "sensitive", 114 | "nervous", 115 | "anxious", 116 | "moody", 117 | "easily upset", 118 | "insecure", 119 | "stable", 120 | "calm", 121 | "confident", 122 | "resilient", 123 | ], 124 | } 125 | -------------------------------------------------------------------------------- /scripts/_helpers/transactions.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import Any 3 | 4 | from rich import print 5 | from rich.markup import escape 6 | 7 | from .basetypes import ContractConfig, DeploymentContext 8 | 9 | 10 | def check_owner(f): 11 | @wraps(f) 12 | def wrapper(self, context, *args, **kwargs): 13 | if not hasattr(self, "__is_deployer_owner"): 14 | self.__is_deployer_owner = is_deployer_owner(context, self.key) 15 | return f(self, context, *args, **kwargs) 16 | 17 | return wrapper 18 | 19 | 20 | def check_different(getter: str, value_property: Any): 21 | def check_if_needed(f): 22 | @wraps(f) 23 | def wrapper(self, context, *args, **kwargs): 24 | value = getattr(self, value_property) 25 | expected_value = context[value] if value in context else value # noqa: SIM401 26 | if isinstance(expected_value, ContractConfig): 27 | expected_value = expected_value.address() 28 | if not is_config_needed(context, self.key, getter, expected_value): 29 | return lambda *_: None 30 | return f(self, context, *args, **kwargs) 31 | 32 | return wrapper 33 | 34 | return check_if_needed 35 | 36 | 37 | def is_deployer_owner(context: DeploymentContext, contract: str) -> bool: 38 | if not context[contract].address(): 39 | return True 40 | owner = execute_read(context, contract, "owner") 41 | if owner != context.owner: 42 | print(f"[dark_orange bold]WARNING[/] Contract [blue]{escape(contract)}[/] owner is {owner}, expected {context.owner}") 43 | return False 44 | return True 45 | 46 | 47 | def is_config_needed(context: DeploymentContext, contract: str, func: str, new_value: Any) -> bool: 48 | if context.dryrun: 49 | return True 50 | current_value = execute_read(context, contract, func) 51 | if current_value == new_value: 52 | print(f"Contract [blue]{escape(contract)}[/] {func} is already {new_value}, skipping update") 53 | return False 54 | return True 55 | 56 | 57 | def execute_read(context: DeploymentContext, contract: str, func: str, *args, options=None): 58 | contract_instance = context.contracts[contract].contract 59 | args_repr = [f"[blue]{escape(c)}[/blue]" if c in context else c for c in args] 60 | print(f"Calling [blue]{escape(contract)}[/blue].{func}({', '.join(args_repr)})", end=" ") 61 | 62 | args_values = [context[c] if c in context else c for c in args] # noqa: SIM401 63 | args_values = [v.address() if isinstance(v, ContractConfig) else v for v in args_values] 64 | 65 | result = contract_instance.call_view_method(func, *args_values, **(options or {})) 66 | print(f"= {result}") 67 | return result 68 | 69 | 70 | def execute(context: DeploymentContext, contract: str, func: str, *args, options=None): 71 | args_repr = [f"[blue]{escape(c)}[/blue]" if c in context else str(c) for c in args] 72 | print(f"Executing [blue]{escape(contract)}[/blue].{func}({', '.join(args_repr)})") 73 | if not context.dryrun: 74 | contract_instance = context.contracts[contract].contract 75 | function = getattr(contract_instance, func) 76 | args_values = [context[c] if c in context else c for c in args] # noqa: SIM401 77 | args_values = [v.address() if isinstance(v, ContractConfig) else v for v in args_values] 78 | try: 79 | function(*args_values, **({"sender": context.owner} | context.gas_options() | (options or {}))) 80 | except Exception as e: 81 | print(f"[bold red]Error executing {contract}.{func} with arguments {args_values}: {e}") 82 | -------------------------------------------------------------------------------- /scripts/_helpers/dependency.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from collections.abc import Callable 3 | 4 | from .basetypes import ContractConfig, DeploymentContext 5 | 6 | 7 | class DependencyManager: 8 | def __init__(self, context: DeploymentContext, changed: set[str]): 9 | self.context = context 10 | self.changed = changed 11 | self._build_dependencies() 12 | self._build_deployment_order() 13 | self._build_deployment_set() 14 | 15 | def _build_dependencies(self): 16 | internal_contracts = list(self.context.contracts.values()) 17 | dep_dependencies_set = {(dep, c.key) for c in internal_contracts for dep in c.deployment_dependencies(self.context)} 18 | config_dependencies_set1 = {(k, v) for c in internal_contracts for k, v in c.config_dependencies(self.context).items()} 19 | config_dependencies_set2 = { 20 | (c.key, v) for c in internal_contracts for k, v in c.config_dependencies(self.context).items() 21 | } 22 | self.deployment_dependencies = groupby_first(dep_dependencies_set, set(self.context.keys())) 23 | self.config_dependencies = groupby_first(config_dependencies_set1 | config_dependencies_set2, set(self.context.keys())) 24 | 25 | def _build_deployment_set(self): 26 | dependencies = self.deployment_dependencies 27 | undeployed = {k for k, c in self.context.contracts.items() if c.deployable(self.context) and c.contract is None} 28 | nodes = set(self.context.contracts.keys()) | set(self.context.config.keys()) 29 | starting_set = self.changed | undeployed 30 | vis = dict.fromkeys(nodes, False) 31 | 32 | def _dfs(n: str): 33 | vis[n] = True 34 | for d in dependencies[n]: 35 | if not vis[d]: 36 | _dfs(d) 37 | 38 | for d in starting_set: 39 | if not vis[d]: 40 | _dfs(d) 41 | 42 | self.deployment_set = {k for k in vis if vis[k] and k in self.context.contracts} 43 | self.transaction_set = { 44 | k: set(txs) for k, txs in self.config_dependencies.items() if k in (self.deployment_set | self.changed) 45 | } 46 | 47 | def _build_deployment_order(self): 48 | sorted_dependencies = topological_sort(self.deployment_dependencies) 49 | internal_deployable_sorted = list(sorted_dependencies) 50 | self.deployment_order = internal_deployable_sorted 51 | 52 | def build_transaction_set(self) -> set[Callable]: 53 | tx_set = {tx for k, txs in self.transaction_set.items() for tx in txs} 54 | # workaround to deal with partial functions 55 | tx_dict = {repr(x): x for x in tx_set} 56 | return set(tx_dict.values()) 57 | 58 | def build_contract_deploy_set(self) -> list[ContractConfig]: 59 | return [self.context.contracts[k] for k in self.deployment_order if k in self.deployment_set] 60 | 61 | 62 | def topological_sort(dependencies: dict[str, set[str]]) -> list[str]: 63 | nodes = set(dependencies.keys()) | {w for v in dependencies.values() for w in v} 64 | vis = dict.fromkeys(nodes, False) 65 | stack = [] 66 | 67 | def _dfs(n: str): 68 | vis[n] = True 69 | for d in dependencies[n]: 70 | if not vis[d]: 71 | _dfs(d) 72 | stack.append(n) 73 | 74 | for d in vis: 75 | if not vis[d]: 76 | _dfs(d) 77 | return stack[::-1] 78 | 79 | 80 | def groupby_first(tuples: set[tuple], extended_keys: set[str] | None = None) -> dict[str, set[str]]: 81 | res = defaultdict(set) 82 | for k in extended_keys or set(): 83 | res[k] = set() 84 | for k, v in tuples: 85 | res[k].add(v) 86 | return dict(res) 87 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: venv install install-dev test run clean interfaces docs 2 | 3 | VENV?=./.venv 4 | PYTHON=${VENV}/bin/python3 5 | 6 | CONTRACTS := $(shell find contracts -depth 1 -name '*.vy') 7 | NATSPEC := $(patsubst contracts/%, natspec/%, $(CONTRACTS:%.vy=%.json)) 8 | PATH := ${VENV}/bin:${PATH} 9 | 10 | vpath %.vy ./contracts 11 | 12 | $(VENV): 13 | if ! command -v uv > /dev/null; then python -m pip install -U uv; fi 14 | uv venv $(VENV) 15 | 16 | install: ${VENV} requirements.txt 17 | uv pip sync requirements.txt 18 | 19 | install-dev: $(VENV) requirements-dev.txt 20 | uv pip sync requirements-dev.txt 21 | $(VENV)/bin/pre-commit install 22 | 23 | requirements.txt: pyproject.toml 24 | uv pip compile -o requirements.txt pyproject.toml 25 | 26 | requirements-dev.txt: pyproject.toml 27 | uv pip compile -o requirements-dev.txt --extra dev pyproject.toml 28 | 29 | test: ${VENV} 30 | ${VENV}/bin/pytest tests/unit -n auto --dist loadscope 31 | 32 | coverage: 33 | ${VENV}/bin/coverage run -m pytest tests/unit --runslow 34 | ${VENV}/bin/coverage report 35 | 36 | branch-coverage: 37 | ${VENV}/bin/coverage run --branch -m pytest tests/unit --runslow 38 | ${VENV}/bin/coverage report 39 | 40 | unit-tests: 41 | ${VENV}/bin/pytest tests/unit --runslow -n auto --dist loadscope 42 | 43 | integration-tests: 44 | ${VENV}/bin/pytest tests/integration 45 | 46 | gas: 47 | ${VENV}/bin/pytest tests/unit --gas-profile 48 | 49 | interfaces: 50 | ${VENV}/bin/python scripts/build_interfaces.py contracts/*.vy 51 | 52 | docs: $(NATSPEC) 53 | 54 | natspec/%.json: %.vy 55 | ${VENV}/bin/vyper -f userdoc,devdoc $< > $@ 56 | 57 | clean: 58 | rm -rf ${VENV} .cache .build __pycache__ **/__pycache__ 59 | 60 | lint: 61 | $(VENV)/bin/ruff check --select I --fix . 62 | $(VENV)/bin/ruff format tests scripts 63 | 64 | %-local: export ENV=local 65 | %-dev: export ENV=dev 66 | %-int: export ENV=int 67 | %-prod: export ENV=prod 68 | 69 | %-zethereum %-zapechain: export ENV=dev 70 | %-sepolia %-curtis: export ENV=int 71 | %-ethereum %-apechain: export ENV=prod 72 | 73 | %-local: export CHAIN=foundry 74 | %-zethereum: export CHAIN=zethereum 75 | %-zapechain: export CHAIN=zapechain 76 | %-sepolia: export CHAIN=sepolia 77 | %-curtis: export CHAIN=curtis 78 | %-ethereum: export CHAIN=ethereum 79 | %-apechain: export CHAIN=apechain 80 | 81 | %-local: export NETWORK=ethereum:local:foundry 82 | %-zethereum: export NETWORK=ethereum:local:https://network.dev.zharta.io/dev1/ 83 | %-zapechain: export NETWORK=ethereum:local:https://network.dev.zharta.io/dev2/ 84 | %-sepolia: export NETWORK=ethereum:sepolia:alchemy 85 | %-curtis: export NETWORK=apechain:curtis:https://curtis.rpc.caldera.xyz/http 86 | %-ethereum: export NETWORK=ethereum:mainnet:alchemy 87 | %-apechain: export NETWORK=apechain:mainnet:alchemy 88 | 89 | add-account: 90 | ${VENV}/bin/ape accounts import $(alias) 91 | 92 | compile: 93 | rm -rf .build/* 94 | ${VENV}/bin/ape compile 95 | 96 | console-local console-zethereum console-zapechain console-sepolia console-curtis console-ethereum console-apechain: 97 | ${VENV}/bin/ape console --network ${NETWORK} # --verbosity DEBUG 98 | 99 | deploy-local deploy-zethereum deploy-zapechain deploy-sepolia deploy-curtis deploy-ethereum deploy-apechain: 100 | ${VENV}/bin/ape run -I deployment --network ${NETWORK} 101 | 102 | publish-zethereum publish-zapechain publish-sepolia publish-curtis publish-ethereum publish-apechain: 103 | ${VENV}/bin/ape run publish 104 | 105 | get-metadata-zethereum get-metadata-zapechain get-metadata-sepolia get-metadata-curtis get-metadata-ethereum get-metadata-apechain: 106 | ${VENV}/bin/ape run get_collections 107 | ${VENV}/bin/ape run get_tokens 108 | -------------------------------------------------------------------------------- /contracts/auxiliary/ArcadeMock.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | """ 4 | Arcade Mock based implementing needed functions for testing 5 | """ 6 | 7 | from ethereum.ercs import IERC20 8 | from ethereum.ercs import IERC721 9 | from ethereum.ercs import IERC20Detailed 10 | 11 | 12 | interface Arcade: 13 | def repay(loan_id: uint256): nonpayable 14 | def getLoan(loan_id: uint256) -> LoanData: view 15 | def getInterestAmount(principal: uint256, proratedInterestRate: uint256) -> uint256: view 16 | 17 | implements: Arcade 18 | 19 | struct LoanData: 20 | state: LoanState 21 | startDate: uint160 22 | terms: LoanTerms 23 | feeSnapshot: FeeSnapshot 24 | 25 | struct FeeSnapshot: 26 | lenderDefaultFee: uint16 27 | lenderInterestFee: uint16 28 | lenderPrincipalFee: uint16 29 | 30 | struct LoanTerms: 31 | proratedInterestRate: uint256 32 | principal: uint256 33 | collateralAddress: address 34 | durationSecs: uint96 35 | collateralId: uint256 36 | payableCurrency: address 37 | deadline: uint96 38 | affiliateCode: bytes32 39 | 40 | flag LoanState: 41 | DUMMY_DO_NOT_USE 42 | ACTIVE 43 | REPAID 44 | DEFAULTED 45 | 46 | 47 | event LoanStarted: 48 | loanId: uint256 49 | lender: address 50 | borrower: address 51 | 52 | event LoanRepaid: 53 | loanId: uint256 54 | 55 | 56 | BPS : constant(uint256) = 10000 57 | 58 | loans: public(HashMap[uint256, LoanData]) 59 | lenders: public(HashMap[uint256, address]) 60 | borrowers: public(HashMap[uint256, address]) 61 | loan_counter: public(uint256) 62 | 63 | 64 | @deploy 65 | def __init__(): 66 | self.loan_counter = 0 67 | 68 | @external 69 | def startLoan(lender: address, loan_terms: LoanTerms) -> uint256: 70 | loan_id: uint256 = self.loan_counter 71 | self.loans[loan_id] = LoanData( 72 | state=LoanState.ACTIVE, 73 | startDate=convert(block.timestamp, uint160), 74 | terms=loan_terms, 75 | feeSnapshot=FeeSnapshot( 76 | lenderDefaultFee=0, 77 | lenderInterestFee=0, 78 | lenderPrincipalFee=0 79 | ) 80 | ) 81 | self.lenders[loan_id] = lender 82 | self.borrowers[loan_id] = msg.sender 83 | self.loan_counter += 1 84 | extcall IERC20(loan_terms.payableCurrency).transferFrom(lender, msg.sender, loan_terms.principal) 85 | extcall IERC721(loan_terms.collateralAddress).transferFrom(msg.sender, self, loan_terms.collateralId) 86 | log LoanStarted(loanId=loan_id, lender=lender, borrower=msg.sender) 87 | return loan_id 88 | 89 | @external 90 | def repay(loan_id: uint256): 91 | assert self.loans[loan_id].state == LoanState.ACTIVE, "Loan not active" 92 | loan: LoanData = self.loans[loan_id] 93 | interest: uint256 = self._get_interest_amount(loan.terms.principal, loan.terms.proratedInterestRate) 94 | total_repayment: uint256 = loan.terms.principal + interest 95 | extcall IERC20(loan.terms.payableCurrency).transferFrom(msg.sender, self.lenders[loan_id], total_repayment) 96 | self.loans[loan_id].state = LoanState.REPAID 97 | extcall IERC721(loan.terms.collateralAddress).transferFrom(self, self.borrowers[loan_id], loan.terms.collateralId) 98 | log LoanRepaid(loanId=loan_id) 99 | 100 | @external 101 | @view 102 | def getLoan(loan_id: uint256) -> LoanData: 103 | return self.loans[loan_id] 104 | 105 | @external 106 | @view 107 | def getInterestAmount(principal: uint256, proratedInterestRate: uint256) -> uint256: 108 | return self._get_interest_amount(principal, proratedInterestRate) 109 | 110 | @view 111 | @external 112 | def onERC721Received(_operator: address, _from: address, _tokenId: uint256, _data: Bytes[1024]) -> bytes4: 113 | return method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes4) 114 | 115 | @internal 116 | @pure 117 | def _get_interest_amount(principal: uint256, proratedInterestRate: uint256) -> uint256: 118 | return (principal * proratedInterestRate) // 10**18 // BPS 119 | -------------------------------------------------------------------------------- /contracts/P2PLendingControl.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | """ 4 | @title P2PLendingControl 5 | @author [Zharta](https://zharta.io/) 6 | @notice This contract keeps some lending parameters for P2P lending contracts, namely the contracts for the collections and the trait roots. 7 | """ 8 | 9 | # Interfaces 10 | 11 | 12 | # Structs 13 | 14 | struct CollectionStatus: 15 | contract: address 16 | trait_root: bytes32 17 | 18 | struct CollectionContract: 19 | collection_key_hash: bytes32 20 | contract: address 21 | 22 | struct TraitRoot: 23 | collection_key_hash: bytes32 24 | root_hash: bytes32 25 | 26 | # Events 27 | 28 | event ContractsChanged: 29 | changed: DynArray[CollectionContract, CHANGE_BATCH] 30 | 31 | event TraitRootChanged: 32 | changed: DynArray[TraitRoot, CHANGE_BATCH] 33 | 34 | event OwnerProposed: 35 | owner: address 36 | proposed_owner: address 37 | 38 | event OwnershipTransferred: 39 | old_owner: address 40 | new_owner: address 41 | 42 | # Global variables 43 | 44 | CHANGE_BATCH: constant(uint256) = 128 45 | 46 | VERSION: public(constant(String[30])) = "P2PLendingControl.20241002" 47 | 48 | owner: public(address) 49 | proposed_owner: public(address) 50 | 51 | contracts: public(HashMap[bytes32, address]) 52 | 53 | # leafs are calculated as keccak256(_abi_encode(collection_address, trait_hash, token_id)) 54 | # all valid (contract, trait, token_id) tuples are stored in the tree and the root 55 | # is stored in the contract for each collection. 56 | # The collection key is hashed and must match the collection key hash in the offer. 57 | trait_roots: public(HashMap[bytes32, bytes32]) 58 | 59 | 60 | @deploy 61 | def __init__(): 62 | self.owner = msg.sender 63 | 64 | 65 | @external 66 | def propose_owner(_address: address): 67 | 68 | """ 69 | @notice Propose a new owner 70 | @dev Proposes a new owner and logs the event. Admin function. 71 | @param _address The address of the proposed owner. 72 | """ 73 | 74 | assert msg.sender == self.owner, "not owner" 75 | assert _address != empty(address), "address is zero" 76 | 77 | log OwnerProposed(self.owner, _address) 78 | self.proposed_owner = _address 79 | 80 | 81 | @external 82 | def claim_ownership(): 83 | 84 | """ 85 | @notice Claim the ownership of the contract 86 | @dev Claims the ownership of the contract and logs the event. Requires the caller to be the proposed owner. 87 | """ 88 | 89 | assert msg.sender == self.proposed_owner, "not the proposed owner" 90 | 91 | log OwnershipTransferred(self.owner, self.proposed_owner) 92 | self.owner = msg.sender 93 | self.proposed_owner = empty(address) 94 | 95 | 96 | @external 97 | def change_collections_contracts(collections: DynArray[CollectionContract, CHANGE_BATCH]): 98 | """ 99 | @notice Set the contracts for the collections 100 | @param collections array of CollectionContract 101 | """ 102 | assert msg.sender == self.owner, "sender not owner" 103 | for c: CollectionContract in collections: 104 | self.contracts[c.collection_key_hash] = c.contract 105 | 106 | log ContractsChanged(collections) 107 | 108 | 109 | @external 110 | def change_collections_trait_roots(roots: DynArray[TraitRoot, CHANGE_BATCH]): 111 | """ 112 | @notice Set trait roots 113 | @param roots array of bytes32 114 | """ 115 | assert msg.sender == self.owner, "sender not owner" 116 | for r: TraitRoot in roots: 117 | self.trait_roots[r.collection_key_hash] = r.root_hash 118 | 119 | log TraitRootChanged(roots) 120 | 121 | 122 | @external 123 | @view 124 | def get_collection_status(collection_key_hash: bytes32) -> CollectionStatus: 125 | """ 126 | @notice Get the collection status 127 | @param collection_key_hash hash of the collection key 128 | @return the contract address and traits root 129 | """ 130 | return CollectionStatus( 131 | contract=self.contracts[collection_key_hash], 132 | trait_root=self.trait_roots[collection_key_hash] 133 | ) 134 | -------------------------------------------------------------------------------- /tests/integration/test_nftfi.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from dataclasses import dataclass 3 | from decimal import Decimal 4 | 5 | import boa 6 | import pytest 7 | from eth_abi import encode 8 | from eth_account import Account 9 | from eth_account.messages import HexBytes, SignableMessage 10 | from eth_utils import keccak 11 | from hypothesis import given, settings 12 | from hypothesis import strategies as st 13 | from web3 import Web3 14 | 15 | from ..conftest_base import ZERO_ADDRESS, CollectionContract, Offer, get_last_event, sign_offer 16 | 17 | BPS = 10000 18 | 19 | 20 | @pytest.fixture 21 | def p2p_control(p2p_lending_control_contract_def, owner, cryptopunks, bayc, bayc_key_hash, punks_key_hash): 22 | p2p_control = p2p_lending_control_contract_def.deploy() 23 | p2p_control.change_collections_contracts([CollectionContract(bayc_key_hash, bayc.address)]) 24 | return p2p_control 25 | 26 | 27 | @pytest.fixture 28 | def p2p_nfts_weth(p2p_lending_nfts_contract_def, weth, delegation_registry, cryptopunks, owner, p2p_control): 29 | return p2p_lending_nfts_contract_def.deploy( 30 | weth, p2p_control, delegation_registry, cryptopunks, 0, 0, owner, BPS, BPS, BPS, BPS 31 | ) 32 | 33 | 34 | @pytest.fixture 35 | def nftfi_proxy(nftfi_proxy_contract_def, p2p_nfts_weth, balancer): 36 | proxy = nftfi_proxy_contract_def.deploy(p2p_nfts_weth.address, balancer.address) 37 | p2p_nfts_weth.set_proxy_authorization(proxy, True, sender=p2p_nfts_weth.owner()) 38 | return proxy 39 | 40 | 41 | def test_initial_state(balancer, nftfi_proxy, weth, p2p_nfts_weth, borrower): 42 | assert nftfi_proxy.p2p_lending_nfts() == p2p_nfts_weth.address 43 | assert nftfi_proxy.flash_lender() == balancer.address 44 | assert p2p_nfts_weth.authorized_proxies(nftfi_proxy.address) is True 45 | 46 | 47 | def _test_pay_loan(nftfi_proxy, weth, borrower, owner, mayc): 48 | nftfi_contract = "0x9F10D706D789e4c76A1a6434cd1A9841c875C0A6" 49 | borrower = "0x47cf584925b637B1023f63b6141f795cBaA1AE79" 50 | # lender = "0xa317566d1eb36cee30cb923f7575bfb7c168032e" 51 | loan_id = 915 52 | token_id = 3350 53 | amount = 3094932000000000000 54 | approved = "0x6730697f33d6D2490029b32899E7865c0d902Ca0" 55 | 56 | weth.transfer(nftfi_proxy.address, amount, sender=owner) 57 | # weth.approve(nftfi_contract, amount, sender=borrower) 58 | nftfi_proxy.pay_nftfi_loan(nftfi_contract, approved, weth.address, loan_id, amount, sender=borrower) 59 | 60 | assert mayc.ownerOf(token_id) == borrower 61 | 62 | 63 | def test_refinance( 64 | balancer, 65 | borrower, 66 | lender, 67 | lender_key, 68 | mayc, 69 | mayc_key_hash, 70 | nftfi_proxy, 71 | now, 72 | owner, 73 | p2p_control, 74 | p2p_nfts_weth, 75 | weth, 76 | ): 77 | nftfi_contract = "0x9F10D706D789e4c76A1a6434cd1A9841c875C0A6" 78 | borrower = "0x47cf584925b637B1023f63b6141f795cBaA1AE79" 79 | loan_id = 915 80 | token_id = 3350 81 | amount = 3094932000000000000 82 | approved = "0x6730697f33d6D2490029b32899E7865c0d902Ca0" 83 | 84 | offer = Offer( 85 | principal=amount, 86 | interest=amount // 100, 87 | payment_token=weth.address, 88 | duration=30 * 86400, 89 | collection_key_hash=mayc_key_hash, 90 | token_id=token_id, 91 | expiration=now + 100, 92 | lender=lender, 93 | pro_rata=True, 94 | ) 95 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_weth.address) 96 | 97 | weth.transfer(lender, offer.principal, sender=owner) 98 | 99 | assert weth.balanceOf(lender) >= offer.principal 100 | 101 | weth.approve(nftfi_proxy.address, amount, sender=borrower) 102 | weth.approve(p2p_nfts_weth.address, offer.principal, sender=lender) 103 | 104 | # mayc.approve(nftfi_proxy.address, token_id, sender=borrower) 105 | mayc.setApprovalForAll(p2p_nfts_weth.address, True, sender=borrower) 106 | 107 | p2p_control.change_collections_contracts([CollectionContract(mayc_key_hash, mayc.address)]) 108 | 109 | nftfi_proxy.refinance_loan_balancer( 110 | nftfi_contract, 111 | approved, 112 | loan_id, 113 | amount, 114 | signed_offer, 115 | token_id, 116 | [], 117 | ZERO_ADDRESS, 118 | 0, 119 | 0, 120 | ZERO_ADDRESS, 121 | sender=borrower, 122 | ) 123 | 124 | assert mayc.ownerOf(token_id) == p2p_nfts_weth.address 125 | assert weth.balanceOf(borrower) == 0 126 | -------------------------------------------------------------------------------- /tests/integration/test_benddao.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from dataclasses import dataclass 3 | from decimal import Decimal 4 | 5 | import boa 6 | import pytest 7 | from eth_abi import encode 8 | from eth_account import Account 9 | from eth_account.messages import HexBytes, SignableMessage 10 | from eth_utils import keccak 11 | from hypothesis import given, settings 12 | from hypothesis import strategies as st 13 | from web3 import Web3 14 | 15 | from ..conftest_base import ZERO_ADDRESS, CollectionContract, Offer, get_last_event, sign_offer 16 | 17 | BPS = 10000 18 | 19 | 20 | @pytest.fixture 21 | def p2p_control(p2p_lending_control_contract_def, owner, cryptopunks, bayc, bayc_key_hash, punks_key_hash): 22 | p2p_control = p2p_lending_control_contract_def.deploy() 23 | p2p_control.change_collections_contracts([CollectionContract(bayc_key_hash, bayc.address)]) 24 | return p2p_control 25 | 26 | 27 | @pytest.fixture 28 | def p2p_nfts_weth(p2p_lending_nfts_contract_def, weth, delegation_registry, cryptopunks, owner, p2p_control): 29 | return p2p_lending_nfts_contract_def.deploy( 30 | weth, p2p_control, delegation_registry, cryptopunks, 0, 0, owner, BPS, BPS, BPS, BPS 31 | ) 32 | 33 | 34 | @pytest.fixture 35 | def benddao_proxy(benddao_proxy_contract_def, p2p_nfts_weth, balancer): 36 | proxy = benddao_proxy_contract_def.deploy(p2p_nfts_weth.address, balancer.address) 37 | p2p_nfts_weth.set_proxy_authorization(proxy, True, sender=p2p_nfts_weth.owner()) 38 | return proxy 39 | 40 | 41 | def test_initial_state(balancer, benddao_proxy, weth, p2p_nfts_weth, borrower): 42 | assert benddao_proxy.p2p_lending_nfts() == p2p_nfts_weth.address 43 | assert benddao_proxy.flash_lender() == balancer.address 44 | assert p2p_nfts_weth.authorized_proxies(benddao_proxy.address) is True 45 | 46 | 47 | def _test_pay_loan(benddao_proxy, weth, borrower, owner, koda): 48 | benddao_contract = "0x70b97A0da65C15dfb0FFA02aEE6FA36e507C2762" 49 | borrower = "0xFb71960563af69888eb10182711cD69dDD01dbF7" 50 | token_id = 9851 51 | amount = 670300000000000000 + 5436224997180424 52 | approved = benddao_contract 53 | 54 | weth.transfer(borrower, amount, sender=owner) 55 | weth.approve(benddao_proxy.address, amount, sender=borrower) 56 | # weth.approve(benddao_contract, amount, sender=borrower) 57 | benddao_proxy.pay_benddao_loan(benddao_contract, approved, weth.address, koda.address, token_id, amount, sender=borrower) 58 | 59 | assert koda.ownerOf(token_id) == borrower 60 | 61 | 62 | def test_refinance( 63 | balancer, 64 | borrower, 65 | lender, 66 | lender_key, 67 | koda, 68 | koda_key_hash, 69 | benddao_proxy, 70 | now, 71 | owner, 72 | p2p_control, 73 | p2p_nfts_weth, 74 | weth, 75 | ): 76 | benddao_contract = "0x70b97A0da65C15dfb0FFA02aEE6FA36e507C2762" 77 | borrower = "0xFb71960563af69888eb10182711cD69dDD01dbF7" 78 | token_id = 9851 79 | amount = 670300000000000000 + 5436224997180424 80 | approved = benddao_contract 81 | 82 | offer = Offer( 83 | principal=amount, 84 | interest=amount // 100, 85 | payment_token=weth.address, 86 | duration=30 * 86400, 87 | collection_key_hash=koda_key_hash, 88 | token_id=token_id, 89 | expiration=now + 100, 90 | lender=lender, 91 | pro_rata=True, 92 | ) 93 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_weth.address) 94 | 95 | weth.transfer(owner, weth.balanceOf(borrower), sender=borrower) # borrower wallet reset 96 | 97 | weth.transfer(lender, offer.principal, sender=owner) 98 | 99 | assert weth.balanceOf(lender) >= offer.principal 100 | 101 | weth.approve(benddao_proxy.address, amount, sender=borrower) 102 | weth.approve(p2p_nfts_weth.address, offer.principal, sender=lender) 103 | 104 | koda.setApprovalForAll(p2p_nfts_weth.address, True, sender=borrower) 105 | 106 | p2p_control.change_collections_contracts([CollectionContract(koda_key_hash, koda.address)]) 107 | 108 | benddao_proxy.refinance_loan( 109 | benddao_contract, 110 | approved, 111 | koda.address, 112 | amount, 113 | signed_offer, 114 | token_id, 115 | [], 116 | ZERO_ADDRESS, 117 | 0, 118 | 0, 119 | ZERO_ADDRESS, 120 | sender=borrower, 121 | ) 122 | 123 | assert koda.ownerOf(token_id) == p2p_nfts_weth.address 124 | assert weth.balanceOf(borrower) == 0 125 | -------------------------------------------------------------------------------- /tests/integration/test_arcade.py: -------------------------------------------------------------------------------- 1 | import datetime as dt 2 | from dataclasses import dataclass 3 | from decimal import Decimal 4 | 5 | import boa 6 | import pytest 7 | from eth_abi import encode 8 | from eth_account import Account 9 | from eth_account.messages import HexBytes, SignableMessage 10 | from eth_utils import keccak 11 | from hypothesis import given, settings 12 | from hypothesis import strategies as st 13 | from web3 import Web3 14 | 15 | from ..conftest_base import ZERO_ADDRESS, CollectionContract, Offer, get_last_event, sign_offer 16 | 17 | BPS = 10000 18 | 19 | 20 | @pytest.fixture 21 | def p2p_control(p2p_lending_control_contract_def, owner, cryptopunks, bayc, bayc_key_hash, punks_key_hash): 22 | p2p_control = p2p_lending_control_contract_def.deploy() 23 | p2p_control.change_collections_contracts([CollectionContract(bayc_key_hash, bayc.address)]) 24 | return p2p_control 25 | 26 | 27 | @pytest.fixture 28 | def p2p_nfts_weth(p2p_lending_nfts_contract_def, weth, delegation_registry, cryptopunks, owner, p2p_control): 29 | return p2p_lending_nfts_contract_def.deploy( 30 | weth, p2p_control, delegation_registry, cryptopunks, 0, 0, owner, BPS, BPS, BPS, BPS 31 | ) 32 | 33 | 34 | @pytest.fixture 35 | def arcade_proxy(arcade_proxy_contract_def, p2p_nfts_weth, balancer): 36 | proxy = arcade_proxy_contract_def.deploy(p2p_nfts_weth.address, balancer.address) 37 | p2p_nfts_weth.set_proxy_authorization(proxy, True, sender=p2p_nfts_weth.owner()) 38 | return proxy 39 | 40 | 41 | def test_initial_state(balancer, arcade_proxy, weth, p2p_nfts_weth, borrower): 42 | assert arcade_proxy.p2p_lending_nfts() == p2p_nfts_weth.address 43 | assert arcade_proxy.flash_lender() == balancer.address 44 | assert p2p_nfts_weth.authorized_proxies(arcade_proxy.address) is True 45 | 46 | 47 | def _test_pay_loan(arcade_proxy, weth, borrower, owner, wpunk): 48 | arcade_contract = "0x74241e1A9c021643289476426B9B70229Ab40D53" 49 | borrower = "0xCffC336E6D019C1aF58257A0b10bf2146a3f42A4" 50 | loan_id = 6541 51 | token_id = 7994 52 | amount = 31356712328767124000 53 | borrower = "0xCffC336E6D019C1aF58257A0b10bf2146a3f42A4" 54 | approved = "0x89bc08BA00f135d608bc335f6B33D7a9ABCC98aF" 55 | 56 | weth.transfer(borrower, amount, sender=owner) 57 | weth.approve(arcade_proxy.address, amount, sender=borrower) 58 | # weth.approve(arcade_contract, amount, sender=borrower) 59 | arcade_proxy.pay_arcade_loan(arcade_contract, approved, weth.address, loan_id, amount, sender=borrower) 60 | 61 | assert wpunk.ownerOf(token_id) == borrower 62 | 63 | 64 | def test_refinance( 65 | balancer, 66 | borrower, 67 | lender, 68 | lender_key, 69 | wpunk, 70 | wpunk_key_hash, 71 | arcade_proxy, 72 | now, 73 | owner, 74 | p2p_control, 75 | p2p_nfts_weth, 76 | weth, 77 | ): 78 | arcade_contract = "0x74241e1A9c021643289476426B9B70229Ab40D53" 79 | borrower = "0xCffC336E6D019C1aF58257A0b10bf2146a3f42A4" 80 | loan_id = 6541 81 | token_id = 7994 82 | amount = 31356712328767124000 83 | approved = "0x89bc08BA00f135d608bc335f6B33D7a9ABCC98aF" 84 | 85 | offer = Offer( 86 | principal=amount, 87 | interest=amount // 100, 88 | payment_token=weth.address, 89 | duration=30 * 86400, 90 | collection_key_hash=wpunk_key_hash, 91 | token_id=token_id, 92 | expiration=now + 100, 93 | lender=lender, 94 | pro_rata=True, 95 | ) 96 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_weth.address) 97 | 98 | weth.transfer(owner, weth.balanceOf(borrower), sender=borrower) # borrower wallet reset 99 | 100 | weth.transfer(lender, offer.principal, sender=owner) 101 | 102 | assert weth.balanceOf(lender) >= offer.principal 103 | 104 | weth.approve(arcade_proxy.address, amount, sender=borrower) 105 | weth.approve(p2p_nfts_weth.address, offer.principal, sender=lender) 106 | 107 | wpunk.setApprovalForAll(p2p_nfts_weth.address, True, sender=borrower) 108 | 109 | p2p_control.change_collections_contracts([CollectionContract(wpunk_key_hash, wpunk.address)]) 110 | 111 | arcade_proxy.refinance_loan( 112 | arcade_contract, 113 | approved, 114 | loan_id, 115 | amount, 116 | signed_offer, 117 | token_id, 118 | [], 119 | ZERO_ADDRESS, 120 | 0, 121 | 0, 122 | ZERO_ADDRESS, 123 | sender=borrower, 124 | ) 125 | 126 | assert wpunk.ownerOf(token_id) == p2p_nfts_weth.address 127 | assert weth.balanceOf(borrower) == 0 128 | -------------------------------------------------------------------------------- /tests/stubs/P2PNftsProxy.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | from ethereum.ercs import IERC165 4 | from ethereum.ercs import IERC721 5 | from ethereum.ercs import IERC20 6 | 7 | 8 | interface P2PLendingNfts: 9 | def create_loan( 10 | offer: SignedOffer, 11 | collateral_token_id: uint256, 12 | collateral_proof: DynArray[bytes32, 32], 13 | delegate: address, 14 | borrower_broker_upfront_fee_amount: uint256, 15 | borrower_broker_settlement_fee_bps: uint256, 16 | borrower_broker: address 17 | ) -> bytes32: nonpayable 18 | def settle_loan(loan: Loan): nonpayable 19 | def claim_defaulted_loan_collateral(loan: Loan): nonpayable 20 | def replace_loan( 21 | loan: Loan, 22 | offer: SignedOffer, 23 | collateral_proof: DynArray[bytes32, 32], 24 | borrower_broker_upfront_fee_amount: uint256, 25 | borrower_broker_settlement_fee_bps: uint256, 26 | borrower_broker: address 27 | ) -> bytes32: nonpayable 28 | def replace_loan_lender(loan: Loan, offer: SignedOffer, collateral_proof: DynArray[bytes32, 32]) -> bytes32: nonpayable 29 | def revoke_offer(offer: SignedOffer): nonpayable 30 | def onERC721Received(_operator: address, _from: address, _tokenId: uint256, _data: Bytes[1024]) -> bytes4: view 31 | 32 | 33 | flag FeeType: 34 | PROTOCOL_FEE 35 | ORIGINATION_FEE 36 | LENDER_BROKER_FEE 37 | BORROWER_BROKER_FEE 38 | 39 | 40 | struct Fee: 41 | type: FeeType 42 | upfront_amount: uint256 43 | interest_bps: uint256 44 | wallet: address 45 | 46 | struct FeeAmount: 47 | type: FeeType 48 | amount: uint256 49 | wallet: address 50 | 51 | flag OfferType: 52 | TOKEN 53 | COLLECTION 54 | TRAIT 55 | 56 | struct Offer: 57 | principal: uint256 58 | interest: uint256 59 | payment_token: address 60 | duration: uint256 61 | origination_fee_amount: uint256 62 | broker_upfront_fee_amount: uint256 63 | broker_settlement_fee_bps: uint256 64 | broker_address: address 65 | offer_type: OfferType 66 | token_id: uint256 67 | token_range_min: uint256 68 | token_range_max: uint256 69 | collection_key_hash: bytes32 70 | trait_hash: bytes32 71 | expiration: uint256 72 | lender: address 73 | pro_rata: bool 74 | size: uint256 75 | tracing_id: bytes32 76 | 77 | 78 | struct Signature: 79 | v: uint256 80 | r: uint256 81 | s: uint256 82 | 83 | struct SignedOffer: 84 | offer: Offer 85 | signature: Signature 86 | 87 | struct Loan: 88 | id: bytes32 89 | offer_id: bytes32 90 | offer_tracing_id: bytes32 91 | amount: uint256 # principal - origination_fee_amount 92 | interest: uint256 93 | payment_token: address 94 | maturity: uint256 95 | start_time: uint256 96 | borrower: address 97 | lender: address 98 | collateral_contract: address 99 | collateral_token_id: uint256 100 | fees: DynArray[Fee, MAX_FEES] 101 | pro_rata: bool 102 | delegate: address 103 | 104 | 105 | TOKEN_IDS_BATCH: constant(uint256) = 1 << 14 106 | MAX_FEES: constant(uint256) = 4 107 | BPS: constant(uint256) = 10000 108 | 109 | p2p_lending_nfts: address 110 | 111 | @external 112 | def __init__(_p2p_lending_nfts: address): 113 | self.p2p_lending_nfts = _p2p_lending_nfts 114 | 115 | @external 116 | def create_loan( 117 | offer: SignedOffer, 118 | collateral_token_id: uint256, 119 | proof: DynArray[bytes32, 32], 120 | delegate: address, 121 | borrower_broker_upfront_fee_amount: uint256, 122 | borrower_broker_settlement_fee_bps: uint256, 123 | borrower_broker: address 124 | ) -> bytes32: 125 | return P2PLendingNfts(self.p2p_lending_nfts).create_loan( 126 | offer, 127 | collateral_token_id, 128 | proof, 129 | delegate, 130 | borrower_broker_upfront_fee_amount, 131 | borrower_broker_settlement_fee_bps, 132 | borrower_broker 133 | ) 134 | 135 | @external 136 | def settle_loan(loan: Loan): 137 | P2PLendingNfts(self.p2p_lending_nfts).settle_loan(loan) 138 | 139 | @external 140 | def claim_defaulted_loan_collateral(loan: Loan): 141 | P2PLendingNfts(self.p2p_lending_nfts).claim_defaulted_loan_collateral(loan) 142 | 143 | @external 144 | def replace_loan( 145 | loan: Loan, 146 | offer: SignedOffer, 147 | proof: DynArray[bytes32, 32], 148 | borrower_broker_upfront_fee_amount: uint256, 149 | borrower_broker_settlement_fee_bps: uint256, 150 | borrower_broker: address 151 | ) -> bytes32: 152 | return P2PLendingNfts(self.p2p_lending_nfts).replace_loan( 153 | loan, 154 | offer, 155 | proof, 156 | borrower_broker_upfront_fee_amount, 157 | borrower_broker_settlement_fee_bps, 158 | borrower_broker 159 | ) 160 | 161 | @external 162 | def replace_loan_lender(loan: Loan, offer: SignedOffer, proof: DynArray[bytes32, 32]) -> bytes32: 163 | return P2PLendingNfts(self.p2p_lending_nfts).replace_loan_lender(loan, offer, proof) 164 | 165 | 166 | @external 167 | def revoke_offer(offer: SignedOffer): 168 | P2PLendingNfts(self.p2p_lending_nfts).revoke_offer(offer) 169 | -------------------------------------------------------------------------------- /tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import boa 4 | import pytest 5 | from boa.vm.py_evm import register_raw_precompile 6 | from eth_account import Account 7 | 8 | 9 | def pytest_addoption(parser): 10 | parser.addoption("--runslow", action="store_true", default=False, help="run slow tests") 11 | 12 | 13 | def pytest_configure(config): 14 | config.addinivalue_line("markers", "slow: mark test as slow to run") 15 | 16 | 17 | def pytest_collection_modifyitems(config, items): 18 | if config.getoption("--runslow"): 19 | # --runslow given in cli: do not skip slow tests 20 | return 21 | skip_slow = pytest.mark.skip(reason="need --runslow option to run") 22 | for item in items: 23 | if "slow" in item.keywords: 24 | item.add_marker(skip_slow) 25 | 26 | 27 | @pytest.fixture(scope="session", autouse=True) 28 | def boa_env(): 29 | boa.interpret.set_cache_dir(cache_dir=".cache/titanoboa") 30 | return boa 31 | 32 | 33 | @pytest.fixture(scope="session") 34 | def accounts(boa_env): 35 | _accounts = [boa.env.generate_address() for _ in range(10)] 36 | for account in _accounts: 37 | boa.env.set_balance(account, 10**21) 38 | return _accounts 39 | 40 | 41 | @pytest.fixture(scope="session") 42 | def owner_account(): 43 | return Account.create() 44 | 45 | 46 | @pytest.fixture(scope="session") 47 | def owner(owner_account, boa_env): 48 | boa.env.eoa = owner_account.address 49 | boa.env.set_balance(owner_account.address, 10**21) 50 | return owner_account.address 51 | 52 | 53 | @pytest.fixture(scope="session") 54 | def owner_key(owner_account): 55 | return owner_account.key 56 | 57 | 58 | @pytest.fixture(scope="session") 59 | def borrower_account(): 60 | return Account.create() 61 | 62 | 63 | @pytest.fixture(scope="session") 64 | def borrower(borrower_account, boa_env): 65 | boa.env.set_balance(borrower_account.address, 10**21) 66 | return borrower_account.address 67 | 68 | 69 | @pytest.fixture(scope="session") 70 | def borrower_key(borrower_account): 71 | return borrower_account.key 72 | 73 | 74 | @pytest.fixture(scope="session") 75 | def lender_account(): 76 | return Account.create() 77 | 78 | 79 | @pytest.fixture(scope="session") 80 | def lender(lender_account, boa_env): 81 | boa.env.set_balance(lender_account.address, 10**21) 82 | return lender_account.address 83 | 84 | 85 | @pytest.fixture(scope="session") 86 | def lender_key(lender_account): 87 | return lender_account.key 88 | 89 | 90 | @pytest.fixture(scope="session") 91 | def lender2_account(): 92 | return Account.create() 93 | 94 | 95 | @pytest.fixture(scope="session") 96 | def lender2(lender2_account, boa_env): 97 | boa.env.set_balance(lender2_account.address, 10**21) 98 | return lender2_account.address 99 | 100 | 101 | @pytest.fixture(scope="session") 102 | def lender2_key(lender2_account): 103 | return lender2_account.key 104 | 105 | 106 | @pytest.fixture(scope="session") 107 | def protocol_wallet(accounts): 108 | yield accounts[3] 109 | 110 | 111 | @pytest.fixture(scope="session") 112 | def erc721_contract_def(boa_env): 113 | return boa.load_partial("contracts/auxiliary/ERC721.vy") 114 | 115 | 116 | @pytest.fixture(scope="session") 117 | def weth9_contract_def(boa_env): 118 | return boa.load_partial("contracts/auxiliary/WETH9Mock.vy") 119 | 120 | 121 | @pytest.fixture(scope="session") 122 | def weth(weth9_contract_def, owner): 123 | return weth9_contract_def.deploy("Wrapped Ether", "WETH", 18, 10**20) 124 | 125 | 126 | @pytest.fixture(scope="session") 127 | def cryptopunks_contract_def(boa_env): 128 | return boa.load_partial("contracts/auxiliary/CryptoPunksMarketMock.vy") 129 | 130 | 131 | @pytest.fixture(scope="session") 132 | def cryptopunks(cryptopunks_contract_def, owner): 133 | return cryptopunks_contract_def.deploy() 134 | 135 | 136 | @pytest.fixture(scope="session") 137 | def delegation_registry_contract_def(boa_env): 138 | return boa.load_partial("contracts/auxiliary/DelegationRegistryMock.vy") 139 | 140 | 141 | @pytest.fixture(scope="session") 142 | def p2p_lending_nfts_contract_def(boa_env): 143 | return boa.load_partial("contracts/P2PLendingNfts.vy") 144 | 145 | 146 | @pytest.fixture(scope="session") 147 | def p2p_lending_control_contract_def(boa_env): 148 | return boa.load_partial("contracts/P2PLendingControl.vy") 149 | 150 | 151 | @pytest.fixture(scope="session") 152 | def p2p_lending_nfts_proxy_contract_def(boa_env): 153 | return boa.load_partial("tests/stubs/P2PNftsProxy.vy") 154 | 155 | 156 | @pytest.fixture(scope="module") 157 | def empty_contract_def(boa_env): 158 | return boa.loads_partial( 159 | dedent( 160 | """ 161 | dummy: uint256 162 | """ 163 | ) 164 | ) 165 | 166 | 167 | # @boa.precompile("def debug_bytes32(data: bytes32)") 168 | # def debug_bytes32(data: bytes): 169 | # print(f"DEBUG: {data.hex()}") 170 | 171 | 172 | # @pytest.fixture(scope="session") 173 | # def debug_precompile(boa_env): 174 | # register_raw_precompile("0x0000000000000000000000000000000000011111", debug_bytes32) 175 | # yield 176 | -------------------------------------------------------------------------------- /contracts/auxiliary/ERC20.vy: -------------------------------------------------------------------------------- 1 | # @dev Implementation of ERC-20 token standard. 2 | # @author Takayuki Jimba (@yudetamago) 3 | # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md 4 | 5 | # @version 0.4.1 6 | 7 | from ethereum.ercs import IERC20 8 | from ethereum.ercs import IERC20Detailed 9 | 10 | implements: IERC20 11 | implements: IERC20Detailed 12 | 13 | event Transfer: 14 | sender: indexed(address) 15 | receiver: indexed(address) 16 | value: uint256 17 | 18 | event Approval: 19 | owner: indexed(address) 20 | spender: indexed(address) 21 | value: uint256 22 | 23 | name: public(String[32]) 24 | symbol: public(String[32]) 25 | decimals: public(uint8) 26 | 27 | # NOTE: By declaring `balanceOf` as public, vyper automatically generates a 'balanceOf()' getter 28 | # method to allow access to account balances. 29 | # The _KeyType will become a required parameter for the getter and it will return _ValueType. 30 | # See: https://vyper.readthedocs.io/en/v0.1.0-beta.8/types.html?highlight=getter#mappings 31 | balanceOf: public(HashMap[address, uint256]) 32 | # By declaring `allowance` as public, vyper automatically generates the `allowance()` getter 33 | allowance: public(HashMap[address, HashMap[address, uint256]]) 34 | # By declaring `totalSupply` as public, we automatically create the `totalSupply()` getter 35 | totalSupply: public(uint256) 36 | minter: address 37 | 38 | 39 | @deploy 40 | def __init__(_name: String[32], _symbol: String[32], _decimals: uint8, _supply: uint256): 41 | init_supply: uint256 = _supply * 10 ** convert(_decimals, uint256) 42 | self.name = _name 43 | self.symbol = _symbol 44 | self.decimals = _decimals 45 | self.balanceOf[msg.sender] = init_supply 46 | self.totalSupply = init_supply 47 | self.minter = msg.sender 48 | log Transfer(empty(address), msg.sender, init_supply) 49 | 50 | 51 | 52 | @external 53 | def transfer(_to : address, _value : uint256) -> bool: 54 | """ 55 | @dev Transfer token for a specified address 56 | @param _to The address to transfer to. 57 | @param _value The amount to be transferred. 58 | """ 59 | # NOTE: vyper does not allow underflows 60 | # so the following subtraction would revert on insufficient balance 61 | self.balanceOf[msg.sender] -= _value 62 | self.balanceOf[_to] += _value 63 | log Transfer(msg.sender, _to, _value) 64 | return True 65 | 66 | 67 | @external 68 | def transferFrom(_from : address, _to : address, _value : uint256) -> bool: 69 | """ 70 | @dev Transfer tokens from one address to another. 71 | @param _from address The address which you want to send tokens from 72 | @param _to address The address which you want to transfer to 73 | @param _value uint256 the amount of tokens to be transferred 74 | """ 75 | # NOTE: vyper does not allow underflows 76 | # so the following subtraction would revert on insufficient balance 77 | self.balanceOf[_from] -= _value 78 | self.balanceOf[_to] += _value 79 | # NOTE: vyper does not allow underflows 80 | # so the following subtraction would revert on insufficient allowance 81 | self.allowance[_from][msg.sender] -= _value 82 | log Transfer(_from, _to, _value) 83 | return True 84 | 85 | 86 | @external 87 | def approve(_spender : address, _value : uint256) -> bool: 88 | """ 89 | @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 90 | Beware that changing an allowance with this method brings the risk that someone may use both the old 91 | and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this 92 | race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: 93 | https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 94 | @param _spender The address which will spend the funds. 95 | @param _value The amount of tokens to be spent. 96 | """ 97 | self.allowance[msg.sender][_spender] = _value 98 | log Approval(msg.sender, _spender, _value) 99 | return True 100 | 101 | 102 | @external 103 | def mint(_to: address, _value: uint256): 104 | """ 105 | @dev Mint an amount of the token and assigns it to an account. 106 | This encapsulates the modification of balances such that the 107 | proper events are emitted. 108 | @param _to The account that will receive the created tokens. 109 | @param _value The amount that will be created. 110 | """ 111 | assert msg.sender == self.minter 112 | assert _to != empty(address) 113 | self.totalSupply += _value 114 | self.balanceOf[_to] += _value 115 | log Transfer(empty(address), _to, _value) 116 | 117 | 118 | @internal 119 | def _burn(_to: address, _value: uint256): 120 | """ 121 | @dev Internal function that burns an amount of the token of a given 122 | account. 123 | @param _to The account whose tokens will be burned. 124 | @param _value The amount that will be burned. 125 | """ 126 | assert _to != empty(address) 127 | self.totalSupply -= _value 128 | self.balanceOf[_to] -= _value 129 | log Transfer(_to, empty(address), _value) 130 | 131 | 132 | @external 133 | def burn(_value: uint256): 134 | """ 135 | @dev Burn an amount of the token of msg.sender. 136 | @param _value The amount that will be burned. 137 | """ 138 | self._burn(msg.sender, _value) 139 | 140 | 141 | @external 142 | def burnFrom(_to: address, _value: uint256): 143 | """ 144 | @dev Burn an amount of the token from a given account. 145 | @param _to The account whose tokens will be burned. 146 | @param _value The amount that will be burned. 147 | """ 148 | self.allowance[_to][msg.sender] -= _value 149 | self._burn(_to, _value) 150 | -------------------------------------------------------------------------------- /contracts/auxiliary/WETH9Mock.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | """ 4 | WETH9 Mock based on 5 | https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC20.vy 6 | https://github.com/gnosis/canonical-weth/blob/master/contracts/WETH9.sol 7 | """ 8 | 9 | from ethereum.ercs import IERC20 10 | from ethereum.ercs import IERC20Detailed 11 | 12 | implements: IERC20 13 | implements: IERC20Detailed 14 | 15 | event Transfer: 16 | sender: indexed(address) 17 | receiver: indexed(address) 18 | value: uint256 19 | 20 | event Approval: 21 | owner: indexed(address) 22 | spender: indexed(address) 23 | value: uint256 24 | 25 | event Deposit: 26 | wallet: indexed(address) 27 | value: uint256 28 | 29 | event Withdrawal: 30 | wallet: indexed(address) 31 | value: uint256 32 | 33 | name: public(String[32]) 34 | symbol: public(String[32]) 35 | decimals: public(uint8) 36 | 37 | balanceOf: public(HashMap[address, uint256]) 38 | allowance: public(HashMap[address, HashMap[address, uint256]]) 39 | totalSupply: public(uint256) 40 | minter: address 41 | 42 | blacklisted: public(HashMap[address, bool]) 43 | 44 | @deploy 45 | def __init__(_name: String[32], _symbol: String[32], _decimals: uint8, _supply: uint256): 46 | init_supply: uint256 = _supply * 10 ** convert(_decimals, uint256) 47 | self.name = _name 48 | self.symbol = _symbol 49 | self.decimals = _decimals 50 | self.balanceOf[msg.sender] = init_supply 51 | self.totalSupply = init_supply 52 | self.minter = msg.sender 53 | log Transfer(empty(address), msg.sender, init_supply) 54 | 55 | 56 | 57 | @external 58 | def transfer(_to : address, _value : uint256) -> bool: 59 | """ 60 | @dev Transfer token for a specified address 61 | @param _to The address to transfer to. 62 | @param _value The amount to be transferred. 63 | """ 64 | assert not self.blacklisted[msg.sender] 65 | assert not self.blacklisted[_to] 66 | 67 | self.balanceOf[msg.sender] -= _value 68 | self.balanceOf[_to] += _value 69 | log Transfer(msg.sender, _to, _value) 70 | return True 71 | 72 | 73 | @external 74 | def transferFrom(_from : address, _to : address, _value : uint256) -> bool: 75 | """ 76 | @dev Transfer tokens from one address to another. 77 | @param _from address The address which you want to send tokens from 78 | @param _to address The address which you want to transfer to 79 | @param _value uint256 the amount of tokens to be transferred 80 | """ 81 | assert not self.blacklisted[_from] 82 | assert not self.blacklisted[_to] 83 | 84 | self.balanceOf[_from] -= _value 85 | self.balanceOf[_to] += _value 86 | self.allowance[_from][msg.sender] -= _value 87 | log Transfer(_from, _to, _value) 88 | return True 89 | 90 | 91 | @external 92 | def approve(_spender : address, _value : uint256) -> bool: 93 | """ 94 | @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 95 | Beware that changing an allowance with this method brings the risk that someone may use both the old 96 | and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this 97 | race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: 98 | https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 99 | @param _spender The address which will spend the funds. 100 | @param _value The amount of tokens to be spent. 101 | """ 102 | self.allowance[msg.sender][_spender] = _value 103 | log Approval(msg.sender, _spender, _value) 104 | return True 105 | 106 | 107 | @external 108 | def mint(_to: address, _value: uint256): 109 | """ 110 | @dev Mint an amount of the token and assigns it to an account. 111 | This encapsulates the modification of balances such that the 112 | proper events are emitted. 113 | @param _to The account that will receive the created tokens. 114 | @param _value The amount that will be created. 115 | """ 116 | assert msg.sender == self.minter 117 | assert _to != empty(address) 118 | self.totalSupply += _value 119 | self.balanceOf[_to] += _value 120 | log Transfer(empty(address), _to, _value) 121 | 122 | 123 | @internal 124 | def _burn(_to: address, _value: uint256): 125 | """ 126 | @dev Internal function that burns an amount of the token of a given 127 | account. 128 | @param _to The account whose tokens will be burned. 129 | @param _value The amount that will be burned. 130 | """ 131 | assert _to != empty(address) 132 | self.totalSupply -= _value 133 | self.balanceOf[_to] -= _value 134 | log Transfer(_to, empty(address), _value) 135 | 136 | 137 | @external 138 | def burn(_value: uint256): 139 | """ 140 | @dev Burn an amount of the token of msg.sender. 141 | @param _value The amount that will be burned. 142 | """ 143 | self._burn(msg.sender, _value) 144 | 145 | 146 | @external 147 | def burnFrom(_to: address, _value: uint256): 148 | """ 149 | @dev Burn an amount of the token from a given account. 150 | @param _to The account whose tokens will be burned. 151 | @param _value The amount that will be burned. 152 | """ 153 | self.allowance[_to][msg.sender] -= _value 154 | self._burn(_to, _value) 155 | 156 | 157 | @external 158 | @payable 159 | def deposit(): 160 | self.balanceOf[msg.sender] += msg.value 161 | log Deposit(msg.sender, msg.value) 162 | 163 | @external 164 | def withdraw(amount: uint256): 165 | assert self.balanceOf[msg.sender] >= amount 166 | self.balanceOf[msg.sender] -= amount 167 | send(msg.sender, amount) 168 | log Withdrawal(msg.sender, amount) 169 | 170 | @external 171 | def blacklist(_address: address, _value: bool): 172 | assert msg.sender == self.minter 173 | self.blacklisted[_address] = _value 174 | -------------------------------------------------------------------------------- /contracts/LenderClaim.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | 4 | # Interfaces 5 | 6 | from ethereum.ercs import IERC721 7 | from ethereum.ercs import IERC20 8 | 9 | 10 | interface P2PLendingNfts: 11 | def claim_defaulted_loan_collateral(loan: Loan): nonpayable 12 | 13 | # Structs 14 | 15 | PROOF_MAX_SIZE: constant(uint256) = 32 16 | MAX_FEES: constant(uint256) = 4 17 | BPS: constant(uint256) = 10000 18 | 19 | flag FeeType: 20 | PROTOCOL_FEE 21 | ORIGINATION_FEE 22 | LENDER_BROKER_FEE 23 | BORROWER_BROKER_FEE 24 | 25 | flag OfferType: 26 | TOKEN 27 | COLLECTION 28 | TRAIT 29 | 30 | struct Fee: 31 | type: FeeType 32 | upfront_amount: uint256 33 | interest_bps: uint256 34 | wallet: address 35 | 36 | struct FeeAmount: 37 | type: FeeType 38 | amount: uint256 39 | wallet: address 40 | 41 | struct Offer: 42 | principal: uint256 43 | interest: uint256 44 | payment_token: address 45 | duration: uint256 46 | origination_fee_amount: uint256 47 | broker_upfront_fee_amount: uint256 48 | broker_settlement_fee_bps: uint256 49 | broker_address: address 50 | offer_type: OfferType 51 | token_id: uint256 52 | token_range_min: uint256 53 | token_range_max: uint256 54 | collection_key_hash: bytes32 55 | trait_hash: bytes32 56 | expiration: uint256 57 | lender: address 58 | pro_rata: bool 59 | size: uint256 60 | tracing_id: bytes32 61 | 62 | 63 | struct Signature: 64 | v: uint256 65 | r: uint256 66 | s: uint256 67 | 68 | struct SignedOffer: 69 | offer: Offer 70 | signature: Signature 71 | 72 | struct Loan: 73 | id: bytes32 74 | offer_id: bytes32 75 | offer_tracing_id: bytes32 76 | amount: uint256 # principal - origination_fee_amount 77 | interest: uint256 78 | payment_token: address 79 | maturity: uint256 80 | start_time: uint256 81 | borrower: address 82 | lender: address 83 | collateral_contract: address 84 | collateral_token_id: uint256 85 | fees: DynArray[Fee, MAX_FEES] 86 | pro_rata: bool 87 | delegate: address 88 | 89 | 90 | struct CollectionStatus: 91 | contract: address 92 | trait_root: bytes32 93 | 94 | struct PunkOffer: 95 | isForSale: bool 96 | punkIndex: uint256 97 | seller: address 98 | minValue: uint256 99 | onlySellTo: address 100 | 101 | event LoanCreated: 102 | id: bytes32 103 | amount: uint256 104 | interest: uint256 105 | payment_token: address 106 | maturity: uint256 107 | start_time: uint256 108 | borrower: address 109 | lender: address 110 | collateral_contract: address 111 | collateral_token_id: uint256 112 | fees: DynArray[Fee, MAX_FEES] 113 | pro_rata: bool 114 | offer_id: bytes32 115 | offer_tracing_id: bytes32 116 | delegate: address 117 | 118 | event LoanReplaced: 119 | id: bytes32 120 | amount: uint256 121 | interest: uint256 122 | payment_token: address 123 | maturity: uint256 124 | start_time: uint256 125 | collateral_contract: address 126 | collateral_token_id: uint256 127 | borrower: address 128 | lender: address 129 | fees: DynArray[Fee, MAX_FEES] 130 | pro_rata: bool 131 | original_loan_id: bytes32 132 | paid_principal: uint256 133 | paid_interest: uint256 134 | paid_settlement_fees: DynArray[FeeAmount, MAX_FEES] 135 | offer_id: bytes32 136 | offer_tracing_id: bytes32 137 | 138 | event LoanReplacedByLender: 139 | id: bytes32 140 | amount: uint256 141 | interest: uint256 142 | payment_token: address 143 | maturity: uint256 144 | start_time: uint256 145 | collateral_contract: address 146 | collateral_token_id: uint256 147 | borrower: address 148 | lender: address 149 | fees: DynArray[Fee, MAX_FEES] 150 | pro_rata: bool 151 | original_loan_id: bytes32 152 | paid_principal: uint256 153 | paid_interest: uint256 154 | paid_settlement_fees: DynArray[FeeAmount, MAX_FEES] 155 | borrower_compensation: uint256 156 | offer_id: bytes32 157 | offer_tracing_id: bytes32 158 | 159 | event LoanPaid: 160 | id: bytes32 161 | borrower: address 162 | lender: address 163 | payment_token: address 164 | paid_principal: uint256 165 | paid_interest: uint256 166 | paid_settlement_fees: DynArray[FeeAmount, MAX_FEES] 167 | 168 | event LoanCollateralClaimed: 169 | id: bytes32 170 | borrower: address 171 | lender: address 172 | collateral_contract: address 173 | collateral_token_id: uint256 174 | 175 | event OfferRevoked: 176 | offer_id: bytes32 177 | lender: address 178 | collection_key_hash: bytes32 179 | offer_type: OfferType 180 | 181 | event OwnerProposed: 182 | owner: address 183 | proposed_owner: address 184 | 185 | event OwnershipTransferred: 186 | old_owner: address 187 | new_owner: address 188 | 189 | event ProtocolFeeSet: 190 | old_upfront_fee: uint256 191 | old_settlement_fee: uint256 192 | new_upfront_fee: uint256 193 | new_settlement_fee: uint256 194 | 195 | event ProtocolWalletChanged: 196 | old_wallet: address 197 | new_wallet: address 198 | 199 | event ProxyAuthorizationChanged: 200 | proxy: address 201 | value: bool 202 | 203 | event TransferFailed: 204 | _to: address 205 | amount: uint256 206 | 207 | event PendingTransfersClaimed: 208 | _to: address 209 | amount: uint256 210 | 211 | 212 | 213 | 214 | @deploy 215 | def __init__(): 216 | pass 217 | 218 | @external 219 | def claim_defaulted_loan_collateral(p2p_contract_address: address, loan: Loan): 220 | extcall P2PLendingNfts(p2p_contract_address).claim_defaulted_loan_collateral(loan) 221 | 222 | 223 | @view 224 | @external 225 | def onERC721Received(_operator: address, _from: address, _tokenId: uint256, _data: Bytes[1024]) -> bytes4: 226 | return method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes4) 227 | -------------------------------------------------------------------------------- /contracts/auxiliary/WETH9_abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "guy", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "wad", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "src", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "dst", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "wad", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": false, 82 | "inputs": [ 83 | { 84 | "name": "wad", 85 | "type": "uint256" 86 | } 87 | ], 88 | "name": "withdraw", 89 | "outputs": [], 90 | "payable": false, 91 | "stateMutability": "nonpayable", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [], 97 | "name": "decimals", 98 | "outputs": [ 99 | { 100 | "name": "", 101 | "type": "uint8" 102 | } 103 | ], 104 | "payable": false, 105 | "stateMutability": "view", 106 | "type": "function" 107 | }, 108 | { 109 | "constant": true, 110 | "inputs": [ 111 | { 112 | "name": "", 113 | "type": "address" 114 | } 115 | ], 116 | "name": "balanceOf", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "uint256" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": true, 129 | "inputs": [], 130 | "name": "symbol", 131 | "outputs": [ 132 | { 133 | "name": "", 134 | "type": "string" 135 | } 136 | ], 137 | "payable": false, 138 | "stateMutability": "view", 139 | "type": "function" 140 | }, 141 | { 142 | "constant": false, 143 | "inputs": [ 144 | { 145 | "name": "dst", 146 | "type": "address" 147 | }, 148 | { 149 | "name": "wad", 150 | "type": "uint256" 151 | } 152 | ], 153 | "name": "transfer", 154 | "outputs": [ 155 | { 156 | "name": "", 157 | "type": "bool" 158 | } 159 | ], 160 | "payable": false, 161 | "stateMutability": "nonpayable", 162 | "type": "function" 163 | }, 164 | { 165 | "constant": false, 166 | "inputs": [], 167 | "name": "deposit", 168 | "outputs": [], 169 | "payable": true, 170 | "stateMutability": "payable", 171 | "type": "function" 172 | }, 173 | { 174 | "constant": true, 175 | "inputs": [ 176 | { 177 | "name": "", 178 | "type": "address" 179 | }, 180 | { 181 | "name": "", 182 | "type": "address" 183 | } 184 | ], 185 | "name": "allowance", 186 | "outputs": [ 187 | { 188 | "name": "", 189 | "type": "uint256" 190 | } 191 | ], 192 | "payable": false, 193 | "stateMutability": "view", 194 | "type": "function" 195 | }, 196 | { 197 | "payable": true, 198 | "stateMutability": "payable", 199 | "type": "fallback" 200 | }, 201 | { 202 | "anonymous": false, 203 | "inputs": [ 204 | { 205 | "indexed": true, 206 | "name": "src", 207 | "type": "address" 208 | }, 209 | { 210 | "indexed": true, 211 | "name": "guy", 212 | "type": "address" 213 | }, 214 | { 215 | "indexed": false, 216 | "name": "wad", 217 | "type": "uint256" 218 | } 219 | ], 220 | "name": "Approval", 221 | "type": "event" 222 | }, 223 | { 224 | "anonymous": false, 225 | "inputs": [ 226 | { 227 | "indexed": true, 228 | "name": "src", 229 | "type": "address" 230 | }, 231 | { 232 | "indexed": true, 233 | "name": "dst", 234 | "type": "address" 235 | }, 236 | { 237 | "indexed": false, 238 | "name": "wad", 239 | "type": "uint256" 240 | } 241 | ], 242 | "name": "Transfer", 243 | "type": "event" 244 | }, 245 | { 246 | "anonymous": false, 247 | "inputs": [ 248 | { 249 | "indexed": true, 250 | "name": "dst", 251 | "type": "address" 252 | }, 253 | { 254 | "indexed": false, 255 | "name": "wad", 256 | "type": "uint256" 257 | } 258 | ], 259 | "name": "Deposit", 260 | "type": "event" 261 | }, 262 | { 263 | "anonymous": false, 264 | "inputs": [ 265 | { 266 | "indexed": true, 267 | "name": "src", 268 | "type": "address" 269 | }, 270 | { 271 | "indexed": false, 272 | "name": "wad", 273 | "type": "uint256" 274 | } 275 | ], 276 | "name": "Withdrawal", 277 | "type": "event" 278 | } 279 | ] 280 | -------------------------------------------------------------------------------- /scripts/_helpers/basetypes.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: PLR6301, ARG002 2 | 3 | import hashlib 4 | import json 5 | from collections.abc import Callable 6 | from dataclasses import dataclass, field 7 | from enum import Enum 8 | from typing import Any 9 | 10 | from ape.contracts.base import ContractContainer, ContractInstance 11 | from ape_accounts.accounts import KeyfileAccount 12 | from rich import print as rprint 13 | from rich.markup import escape 14 | 15 | Environment = Enum("Environment", ["local", "dev", "int", "prod"]) 16 | 17 | 18 | def abi_key(abi: list) -> str: 19 | json_dump = json.dumps(abi, sort_keys=True) 20 | _hash = hashlib.sha1(json_dump.encode("utf8")) 21 | return _hash.hexdigest() 22 | 23 | 24 | @dataclass 25 | class DeploymentContext: 26 | contracts: dict[str, Any] 27 | env: Environment 28 | chain: str 29 | owner: KeyfileAccount 30 | config: dict[str, Any] = field(default_factory=dict) 31 | gas_func: Callable | None = None 32 | dryrun: bool = False 33 | 34 | def __getitem__(self, key): 35 | if key in self.contracts: 36 | return self.contracts[key] 37 | return self.config[key] 38 | 39 | def __contains__(self, key): 40 | try: 41 | return key in self.contracts or key in self.config 42 | except TypeError: 43 | return False 44 | 45 | def keys(self): 46 | return self.contracts.keys() | self.config.keys() 47 | 48 | def gas_options(self): 49 | return self.gas_func(self) if self.gas_func is not None else {} 50 | 51 | 52 | @dataclass 53 | class ContractConfig: 54 | key: str 55 | contract: ContractInstance | None 56 | container: ContractContainer | None 57 | deployment_deps: set[str] = field(default_factory=set) 58 | config_deps: dict[str, Callable] = field(default_factory=dict) 59 | deployment_args: list[Any] = field(default_factory=list) 60 | abi_key: str | None = None 61 | version: str | None = None 62 | 63 | nft: bool = False 64 | token: bool = False 65 | 66 | def deployable(self, context: DeploymentContext) -> bool: 67 | return True 68 | 69 | def deployment_dependencies(self, context: DeploymentContext) -> set[str]: 70 | return self.deployment_deps 71 | 72 | def deployment_args_values(self, context: DeploymentContext) -> list[Any]: 73 | values = [context[c] if c in context else c for c in self.deployment_args] # noqa: SIM401 74 | return [v.contract if isinstance(v, ContractConfig) else v for v in values] 75 | 76 | def deployment_args_repr(self, context: DeploymentContext) -> list[Any]: 77 | return [f"[blue]{escape(c)}[/blue]" if c in context else c for c in self.deployment_args] 78 | 79 | def deployment_options(self, context: DeploymentContext) -> dict[str, Any]: 80 | return {"sender": context.owner} | context.gas_options() 81 | 82 | def config_dependencies(self, context: DeploymentContext) -> dict[str, Callable]: 83 | return self.config_deps 84 | 85 | def address(self): 86 | return self.contract.address if self.contract else None 87 | 88 | def container_name(self): 89 | return self.container.contract_type.name if self.container else None 90 | 91 | def __str__(self): 92 | return self.key 93 | 94 | def __repr__(self): 95 | return f"Contract[key={self.key}, contract={self.contract}, container_name={self.container_name()}]" 96 | 97 | def load_contract(self, address: str): 98 | self.contract = self.container.at(address) 99 | 100 | def deploy(self, context: DeploymentContext): 101 | if self.contract is not None: 102 | rprint( 103 | f"[dark_orange bold]WARNING[/]: Deployment will override contract [blue bold]{self.key}[/] at {self.contract}" 104 | ) 105 | if not self.deployable(context): 106 | raise Exception(f"Cant deploy contract {self} in current context") # noqa: TRY002 107 | print_args = self.deployment_args_repr(context) 108 | kwargs = self.deployment_options(context) 109 | kwargs_str = ", ".join(f"{k}={v}" for k, v in kwargs.items()) 110 | rprint( 111 | f"Deploying [blue]{self.key}[/blue] <- {self.container_name()}.deploy({', '.join(str(a) for a in print_args)}, {kwargs_str})" # noqa: E501 112 | ) 113 | 114 | if not context.dryrun: 115 | deploy_args = self.container.constructor.encode_input(*self.deployment_args_values(context)) 116 | rprint(f"Deployment args for [blue]{self.key}[/]: [bright_black]{deploy_args.hex()}[/]") 117 | 118 | self.contract = self.container.deploy(*self.deployment_args_values(context), **kwargs) 119 | self.abi_key = abi_key(self.contract.contract_type.dict()["abi"]) 120 | 121 | 122 | @dataclass 123 | class MinimalProxy(ContractConfig): 124 | impl: str = "" 125 | factory_func: str = "create_proxy" 126 | 127 | def deploy(self, context: DeploymentContext): 128 | if self.contract is not None: 129 | rprint( 130 | f"[dark_orange bold]WARNING[/dark_orange bold]: Deployment will override contract [blue bold]{self.key}[/blue bold] at {self.contract}" # noqa: E501 131 | ) 132 | if not self.deployable(context): 133 | raise Exception(f"Cant deploy contract {self} in current context") # noqa: TRY002 134 | impl_contract = context[self.impl].contract 135 | print_args = self.deployment_args_repr(context) 136 | kwargs = self.deployment_options(context) 137 | kwargs_str = ",".join(f"{k}={v}" for k, v in kwargs.items()) 138 | rprint( 139 | f"Deploying Proxy [blue]{self.key}[/blue] <- {self.impl}.{self.factory_func}({', '.join(str(a) for a in print_args)}, {kwargs_str})" # noqa: E501 140 | ) 141 | 142 | if not context.dryrun: 143 | tx = impl_contract.invoke_transaction(self.factory_func, *self.deployment_args_values(context), **kwargs) 144 | self.contract = self.container.at(tx.return_value) 145 | self.abi_key = abi_key(self.contract.contract_type.dict()["abi"]) 146 | -------------------------------------------------------------------------------- /contracts/auxiliary/ArcadeRepaymentController_abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "_loanCore", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "_feeController", 12 | "type": "address" 13 | } 14 | ], 15 | "stateMutability": "nonpayable", 16 | "type": "constructor" 17 | }, 18 | { 19 | "inputs": [ 20 | { 21 | "internalType": "uint256", 22 | "name": "target", 23 | "type": "uint256" 24 | } 25 | ], 26 | "name": "RC_CannotDereference", 27 | "type": "error" 28 | }, 29 | { 30 | "inputs": [ 31 | { 32 | "internalType": "enum LoanLibrary.LoanState", 33 | "name": "state", 34 | "type": "uint8" 35 | } 36 | ], 37 | "name": "RC_InvalidState", 38 | "type": "error" 39 | }, 40 | { 41 | "inputs": [ 42 | { 43 | "internalType": "address", 44 | "name": "lender", 45 | "type": "address" 46 | }, 47 | { 48 | "internalType": "address", 49 | "name": "caller", 50 | "type": "address" 51 | } 52 | ], 53 | "name": "RC_OnlyLender", 54 | "type": "error" 55 | }, 56 | { 57 | "inputs": [ 58 | { 59 | "internalType": "string", 60 | "name": "addressType", 61 | "type": "string" 62 | } 63 | ], 64 | "name": "RC_ZeroAddress", 65 | "type": "error" 66 | }, 67 | { 68 | "inputs": [], 69 | "name": "BASIS_POINTS_DENOMINATOR", 70 | "outputs": [ 71 | { 72 | "internalType": "uint256", 73 | "name": "", 74 | "type": "uint256" 75 | } 76 | ], 77 | "stateMutability": "view", 78 | "type": "function" 79 | }, 80 | { 81 | "inputs": [], 82 | "name": "FL_01", 83 | "outputs": [ 84 | { 85 | "internalType": "bytes32", 86 | "name": "", 87 | "type": "bytes32" 88 | } 89 | ], 90 | "stateMutability": "view", 91 | "type": "function" 92 | }, 93 | { 94 | "inputs": [], 95 | "name": "FL_02", 96 | "outputs": [ 97 | { 98 | "internalType": "bytes32", 99 | "name": "", 100 | "type": "bytes32" 101 | } 102 | ], 103 | "stateMutability": "view", 104 | "type": "function" 105 | }, 106 | { 107 | "inputs": [], 108 | "name": "FL_03", 109 | "outputs": [ 110 | { 111 | "internalType": "bytes32", 112 | "name": "", 113 | "type": "bytes32" 114 | } 115 | ], 116 | "stateMutability": "view", 117 | "type": "function" 118 | }, 119 | { 120 | "inputs": [], 121 | "name": "FL_04", 122 | "outputs": [ 123 | { 124 | "internalType": "bytes32", 125 | "name": "", 126 | "type": "bytes32" 127 | } 128 | ], 129 | "stateMutability": "view", 130 | "type": "function" 131 | }, 132 | { 133 | "inputs": [], 134 | "name": "FL_05", 135 | "outputs": [ 136 | { 137 | "internalType": "bytes32", 138 | "name": "", 139 | "type": "bytes32" 140 | } 141 | ], 142 | "stateMutability": "view", 143 | "type": "function" 144 | }, 145 | { 146 | "inputs": [], 147 | "name": "FL_06", 148 | "outputs": [ 149 | { 150 | "internalType": "bytes32", 151 | "name": "", 152 | "type": "bytes32" 153 | } 154 | ], 155 | "stateMutability": "view", 156 | "type": "function" 157 | }, 158 | { 159 | "inputs": [], 160 | "name": "FL_07", 161 | "outputs": [ 162 | { 163 | "internalType": "bytes32", 164 | "name": "", 165 | "type": "bytes32" 166 | } 167 | ], 168 | "stateMutability": "view", 169 | "type": "function" 170 | }, 171 | { 172 | "inputs": [], 173 | "name": "FL_08", 174 | "outputs": [ 175 | { 176 | "internalType": "bytes32", 177 | "name": "", 178 | "type": "bytes32" 179 | } 180 | ], 181 | "stateMutability": "view", 182 | "type": "function" 183 | }, 184 | { 185 | "inputs": [], 186 | "name": "INTEREST_RATE_DENOMINATOR", 187 | "outputs": [ 188 | { 189 | "internalType": "uint256", 190 | "name": "", 191 | "type": "uint256" 192 | } 193 | ], 194 | "stateMutability": "view", 195 | "type": "function" 196 | }, 197 | { 198 | "inputs": [ 199 | { 200 | "internalType": "uint256", 201 | "name": "loanId", 202 | "type": "uint256" 203 | } 204 | ], 205 | "name": "claim", 206 | "outputs": [], 207 | "stateMutability": "nonpayable", 208 | "type": "function" 209 | }, 210 | { 211 | "inputs": [ 212 | { 213 | "internalType": "uint256", 214 | "name": "loanId", 215 | "type": "uint256" 216 | } 217 | ], 218 | "name": "forceRepay", 219 | "outputs": [], 220 | "stateMutability": "nonpayable", 221 | "type": "function" 222 | }, 223 | { 224 | "inputs": [ 225 | { 226 | "internalType": "uint256", 227 | "name": "principal", 228 | "type": "uint256" 229 | }, 230 | { 231 | "internalType": "uint256", 232 | "name": "proratedInterestRate", 233 | "type": "uint256" 234 | } 235 | ], 236 | "name": "getInterestAmount", 237 | "outputs": [ 238 | { 239 | "internalType": "uint256", 240 | "name": "", 241 | "type": "uint256" 242 | } 243 | ], 244 | "stateMutability": "pure", 245 | "type": "function" 246 | }, 247 | { 248 | "inputs": [ 249 | { 250 | "internalType": "uint256", 251 | "name": "loanId", 252 | "type": "uint256" 253 | }, 254 | { 255 | "internalType": "address", 256 | "name": "to", 257 | "type": "address" 258 | } 259 | ], 260 | "name": "redeemNote", 261 | "outputs": [], 262 | "stateMutability": "nonpayable", 263 | "type": "function" 264 | }, 265 | { 266 | "inputs": [ 267 | { 268 | "internalType": "uint256", 269 | "name": "loanId", 270 | "type": "uint256" 271 | } 272 | ], 273 | "name": "repay", 274 | "outputs": [], 275 | "stateMutability": "nonpayable", 276 | "type": "function" 277 | } 278 | ] 279 | -------------------------------------------------------------------------------- /scripts/_helpers/deployment.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import warnings 5 | from enum import Enum 6 | from pathlib import Path 7 | from typing import Any 8 | 9 | from ape import accounts 10 | 11 | from . import contracts as contracts_module 12 | from .basetypes import ( 13 | ContractConfig, 14 | DeploymentContext, 15 | Environment, 16 | ) 17 | from .dependency import DependencyManager 18 | 19 | ENV = Environment[os.environ.get("ENV", "local")] 20 | 21 | logger = logging.getLogger(__name__) 22 | logger.setLevel(logging.WARNING) 23 | warnings.filterwarnings("ignore") 24 | 25 | 26 | class Context(Enum): 27 | DEPLOYMENT = "deployment" 28 | CONSOLE = "console" 29 | 30 | 31 | def load_contracts(env: Environment, chain: str) -> list[ContractConfig]: 32 | config_file = Path.cwd() / "configs" / env.name / chain / "p2p.json" 33 | with config_file.open(encoding="utf8") as f: 34 | config = json.load(f) 35 | 36 | return [ 37 | contracts_module.__dict__[c["contract"]]( 38 | key=f"{scope}.{name}", address=c.get("address"), abi_key=c.get("abi_key"), **c.get("properties", {}) 39 | ) 40 | for scope in ["common", "p2p", "proxies"] 41 | for name, c in config[scope].items() 42 | ] 43 | 44 | 45 | def store_contracts(env: Environment, chain: str, contracts: list[ContractConfig]): 46 | config_file = Path.cwd() / "configs" / env.name / chain / "p2p.json" 47 | with config_file.open(encoding="utf8") as f: 48 | config = json.load(f) 49 | 50 | contracts_dict = {c.key: c for c in contracts} 51 | for scope in ["common", "p2p", "proxies"]: 52 | for name, c in config[scope].items(): 53 | key = f"{scope}.{name}" 54 | if key in contracts_dict: 55 | c["address"] = contracts_dict[key].address() 56 | if contracts_dict[key].abi_key: 57 | c["abi_key"] = contracts_dict[key].abi_key 58 | if contracts_dict[key].version: 59 | c["version"] = contracts_dict[key].version 60 | properties = c.get("properties", {}) 61 | addresses = c.get("properties_addresses", {}) 62 | for prop_key, prop_val in properties.items(): 63 | if prop_key.endswith("_key") and prop_val in contracts_dict: 64 | addresses[prop_key[:-4]] = contracts_dict[prop_val].address() 65 | c["properties_addresses"] = addresses 66 | 67 | with open(config_file, "w") as f: 68 | f.write(json.dumps(config, indent=4, sort_keys=True)) 69 | 70 | 71 | def load_nft_contracts(env: Environment, chain: str) -> list[ContractConfig]: 72 | config_file = Path.cwd() / "configs" / env.name / chain / "collections.json" 73 | with config_file.open(encoding="utf8") as f: 74 | config = json.load(f) 75 | 76 | return [ 77 | contracts_module.__dict__[c.get("contract_def", "ERC721")]( 78 | key=key, 79 | address=c.get("contract_address"), 80 | abi_key=c.get("abi_key"), 81 | ) 82 | for key, c in config.items() 83 | ] 84 | 85 | 86 | def load_tokens(env: Environment, chain: str) -> list[ContractConfig]: 87 | config_file = Path.cwd() / "configs" / env.name / chain / "tokens.json" 88 | with config_file.open(encoding="utf8") as f: 89 | config = json.load(f) 90 | 91 | return [ 92 | contracts_module.__dict__[c.get("contract_def", "ERC20External")]( 93 | key=f"common.{name}", address=c.get("address"), abi_key=c.get("abi_key") 94 | ) 95 | for name, c in config.items() 96 | ] 97 | 98 | 99 | def load_configs(env: Environment, chain: str) -> dict: 100 | config_file = Path.cwd() / "configs" / env.name / chain / "p2p.json" 101 | with config_file.open(encoding="utf8") as f: 102 | config = json.load(f) 103 | 104 | _configs = config.get("configs", {}) 105 | return {f"configs.{k}": v for k, v in _configs.items()} 106 | 107 | 108 | def load_tracking(env: Environment, chain: str) -> dict: 109 | config_file = Path.cwd() / "configs" / env.name / chain / "tracking.json" 110 | with config_file.open(encoding="utf8") as f: 111 | config = json.load(f) 112 | 113 | return dict(config.items()) 114 | 115 | 116 | class DeploymentManager: 117 | def __init__(self, env: Environment, chain: str, context: Context = Context.DEPLOYMENT): 118 | self.env = env 119 | self.chain = chain 120 | match env: 121 | case Environment.local: 122 | self.owner = accounts.test_accounts[0] 123 | case Environment.dev: 124 | self.owner = accounts.load("devacc") 125 | case Environment.int: 126 | self.owner = accounts.load("intacc") 127 | case Environment.prod: 128 | self.owner = accounts.load("prodacc") 129 | self.context = DeploymentContext(self._get_contracts(context), self.env, self.chain, self.owner, self._get_configs()) 130 | self.tracking = self._get_tracking() 131 | 132 | def _get_contracts(self, context: Context) -> dict[str, ContractConfig]: 133 | contracts = load_contracts(self.env, self.chain) 134 | nfts = load_nft_contracts(self.env, self.chain) 135 | tokens = load_tokens(self.env, self.chain) 136 | all_contracts = contracts + nfts + tokens 137 | 138 | # always deploy everything in local 139 | if self.env == Environment.local and context == Context.DEPLOYMENT: 140 | for contract in all_contracts: 141 | contract.contract = None 142 | 143 | return {c.key: c for c in all_contracts} 144 | 145 | def _get_configs(self) -> dict[str, Any]: 146 | return load_configs(self.env, self.chain) 147 | 148 | def _get_tracking(self) -> dict[str, Any]: 149 | return load_tracking(self.env, self.chain) 150 | 151 | def _save_state(self): 152 | store_contracts(self.env, self.chain, list(self.context.contracts.values())) 153 | 154 | def deploy(self, changes: set[str], *, dryrun=False, save_state=True): 155 | self.owner.set_autosign(True) if self.env != Environment.local else None 156 | self.context.dryrun = dryrun 157 | dependency_manager = DependencyManager(self.context, changes) 158 | contracts_to_deploy = dependency_manager.build_contract_deploy_set() 159 | dependencies_tx = dependency_manager.build_transaction_set() 160 | 161 | for contract in contracts_to_deploy: 162 | if contract.deployable(self.context): 163 | contract.deploy(self.context) 164 | 165 | if save_state and not dryrun: 166 | self._save_state() 167 | 168 | for dependency_tx in dependencies_tx: 169 | dependency_tx(self.context) 170 | 171 | if save_state and not dryrun: 172 | self._save_state() 173 | 174 | def deploy_all(self, *, dryrun=False, save_state=True): 175 | self.deploy(self.context.contract.keys(), dryrun=dryrun, save_state=save_state) 176 | -------------------------------------------------------------------------------- /scripts/build_interfaces.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa: T201, RUF013 2 | 3 | from pathlib import Path 4 | 5 | import click 6 | from vyper import compile_code 7 | 8 | FUNCTIONS_BLACKLIST = ["__init__", "__default__"] 9 | 10 | 11 | def nested_get(d: dict, *args, default=None): 12 | if not args: 13 | return default 14 | for a in args[:-1]: 15 | d = d.get(a) or {} 16 | return d.get(args[-1], default) 17 | 18 | 19 | def traverse_filtering(node: dict, handler: callable = None, **kwargs): 20 | print(kwargs, node.keys()) 21 | 22 | def filter_keys(_node): 23 | return all(_node.get(k, None) == v for k, v in kwargs.items()) 24 | 25 | yield from traverse(node, handler=handler, _filter=filter_keys) 26 | 27 | 28 | def traverse(node: dict, handler: callable = None, _filter: callable = None): 29 | _handler = handler or (lambda x: x) 30 | if _filter is None or _filter(node): 31 | yield _handler(node) 32 | for child in node.get("body", []): 33 | yield from traverse(child, handler=handler, _filter=_filter) 34 | 35 | 36 | def node_summary(node: dict): 37 | attrs = ["node_id", "name", "ast_type", "level", "alias", "module"] 38 | return ",".join(f"{attr}={node.get(attr, '')}" for attr in attrs) 39 | 40 | 41 | def is_external_function(node): 42 | is_func = node["ast_type"] = "FunctionDef" 43 | decorators = node.get("decorator_list", []) 44 | return is_func and any(d for d in decorators if d["id"] == "external") 45 | 46 | 47 | def is_public_variable(node): 48 | is_var = node["ast_type"] = "VariableDec" 49 | is_public = node.get("is_public", False) 50 | return is_var and is_public 51 | 52 | 53 | def is_event(node): 54 | return node["ast_type"] == "EventDef" 55 | 56 | 57 | def is_struct(node): 58 | return node["ast_type"] == "StructDef" 59 | 60 | 61 | def get_arg_type(node: dict): # noqa: PLR0911 62 | match node["ast_type"]: 63 | case "Name": 64 | return node["id"] 65 | case "Int": 66 | return str(node["value"]) 67 | case "Index": 68 | return f"[{get_arg_type(node['value'])}]" 69 | case "Tuple": 70 | return ", ".join(get_arg_type(e) for e in node["elements"]) 71 | case "Subscript": 72 | return get_arg_type(node["value"]) + get_arg_type(node["slice"]) 73 | case "BinOp": 74 | return get_arg_type(node["op"])(get_arg_type(node["left"]), get_arg_type(node["right"])) 75 | case "Pow": 76 | return lambda x, y: str(int(x) ** int(y)) 77 | case _: 78 | return None 79 | 80 | 81 | def get_struct(node: dict): 82 | name = node.get("name") 83 | struct_code = [f"struct {name}:"] 84 | attr_list = traverse(node, _filter=lambda n: n["ast_type"] == "AnnAssign") 85 | attrs = [(nested_get(a, "target", "id"), get_arg_type(a["annotation"])) for a in attr_list] 86 | attr_code = [f" {name}: {target}" for name, target in attrs] 87 | return "\n".join(struct_code + attr_code) 88 | 89 | 90 | def get_structs(ast): 91 | structs = traverse(ast, _filter=is_struct) 92 | header = ["# Structs"] 93 | structs_code = [get_struct(e) for e in structs] 94 | return "\n\n".join(header + structs_code) 95 | 96 | 97 | def get_event(node: dict): 98 | name = node.get("name") 99 | event_code = [f"event {name}:"] 100 | attr_list = traverse(node, _filter=lambda n: n["ast_type"] == "AnnAssign") 101 | attrs = [ 102 | ( 103 | nested_get(a, "target", "id"), 104 | get_arg_type(a["annotation"]) or nested_get(a, "annotation", "args", default=[])[0]["id"], 105 | nested_get(a, "annotation", "func", "id"), 106 | ) 107 | for a in attr_list 108 | ] 109 | attr_code = [f" {name}: " + (f"{func}({target})" if func else f"{target}") for name, target, func in attrs] 110 | return "\n".join(event_code + attr_code) 111 | 112 | 113 | def get_events(ast): 114 | events = traverse(ast, _filter=is_event) 115 | header = ["# Events"] 116 | events_code = [get_event(e) for e in events] 117 | return "\n\n".join(header + events_code) 118 | 119 | 120 | def get_function(node: dict): 121 | name = node.get("name") 122 | decorators = [d["id"] for d in node.get("decorator_list", [])] 123 | decorators_code = [f"@{d}" for d in decorators] 124 | arg_list = (node.get("args") or {}).get("args") 125 | args = [(a["arg"], get_arg_type(a.get("annotation"))) for a in arg_list] 126 | args_code = [f"{name}: {typ}" if typ else name for (name, typ) in args] 127 | return_node = node.get("returns") 128 | return_type = get_arg_type(return_node) if return_node else None 129 | return_code = f" -> {return_type}" if return_type else "" 130 | function_code = [f"def {name}({', '.join(args_code)}){return_code}:", " pass"] 131 | return "\n".join(decorators_code + function_code) 132 | 133 | 134 | def get_public_var(node: dict): 135 | name = nested_get(node, "target", "id") 136 | return_type = get_arg_type(node.get("annotation")) 137 | args = [] 138 | if nested_get(node, "annotation", "ast_type") == "Subscript": 139 | annotation = node["annotation"] 140 | while nested_get(annotation, "value", "id") == "HashMap": 141 | elements = nested_get(annotation, "slice", "value", "elements") 142 | _args = [get_arg_type(e) for e in elements] 143 | return_type = _args[1] 144 | args.append((f"arg{len(args)}", _args[0])) 145 | annotation = elements[-1] 146 | 147 | decorators_code = ["@view", "@external"] 148 | args_code = [f"{name}: {typ}" for (name, typ) in args] 149 | return_code = f" -> {return_type}" 150 | 151 | function_code = [f"def {name}({', '.join(args_code)}){return_code}:", " pass"] 152 | 153 | return "\n".join(decorators_code + function_code) 154 | 155 | 156 | def get_functions(ast: dict): 157 | external_functions = traverse(ast, _filter=is_external_function) 158 | public_variables = traverse(ast, _filter=is_public_variable) 159 | header = ["# Functions"] 160 | public_vars_code = [get_public_var(v) for v in public_variables] 161 | functions_code = [get_function(f) for f in external_functions if f["name"] not in FUNCTIONS_BLACKLIST] 162 | return "\n\n".join(header + public_vars_code + functions_code) 163 | 164 | 165 | def generate_interface(input_file: Path, output_file: Path): 166 | with input_file.open("r") as f: 167 | code = f.read() 168 | compiler_output = compile_code(code, ["ast_dict"]) 169 | ast = compiler_output["ast_dict"]["ast"] 170 | 171 | structs = get_structs(ast) 172 | events = get_events(ast) 173 | functions = get_functions(ast) 174 | gen_code = "\n\n".join([structs, events, functions]) # noqa: FLY002 175 | 176 | with output_file.open("w") as f: 177 | f.write(gen_code) 178 | 179 | 180 | @click.command() 181 | @click.argument("filenames", nargs=-1, type=click.Path(path_type=Path, exists=True)) 182 | @click.option("-o", "--output-dir", type=click.Path(path_type=Path, exists=True), default="interfaces") 183 | def main(filenames: list, output_dir: str): 184 | for f in filenames: 185 | opath = output_dir / f"I{f.name}" 186 | print(f"Generating {f} -> {opath}") 187 | generate_interface(f, opath) 188 | 189 | 190 | if __name__ == "__main__": 191 | main() 192 | -------------------------------------------------------------------------------- /tests/unit/p2p_nfts/test_p2p_nfts_revoke.py: -------------------------------------------------------------------------------- 1 | import boa 2 | import pytest 3 | 4 | from ...conftest_base import ZERO_ADDRESS, ZERO_BYTES32, Offer, OfferType, compute_signed_offer_id, get_last_event, sign_offer 5 | 6 | 7 | @pytest.fixture 8 | def p2p_nfts_proxy(p2p_nfts_usdc, p2p_lending_nfts_proxy_contract_def): 9 | return p2p_lending_nfts_proxy_contract_def.deploy(p2p_nfts_usdc.address) 10 | 11 | 12 | def test_revoke_offer_reverts_if_sender_is_not_lender(p2p_nfts_usdc, borrower, now, lender, lender_key, p2p_nfts_proxy): 13 | offer = Offer( 14 | principal=1000, 15 | interest=100, 16 | payment_token=ZERO_ADDRESS, 17 | duration=100, 18 | origination_fee_amount=0, 19 | broker_upfront_fee_amount=0, 20 | broker_settlement_fee_bps=0, 21 | broker_address=ZERO_ADDRESS, 22 | token_id=1, 23 | expiration=now + 100, 24 | lender=lender, 25 | pro_rata=False, 26 | tracing_id=b"random".zfill(32), 27 | ) 28 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_usdc.address) 29 | 30 | with boa.reverts("not lender"): 31 | p2p_nfts_usdc.revoke_offer(signed_offer, sender=borrower) 32 | 33 | p2p_nfts_usdc.set_proxy_authorization(p2p_nfts_proxy, True, sender=p2p_nfts_usdc.owner()) 34 | with boa.reverts("not lender"): 35 | p2p_nfts_proxy.revoke_offer(signed_offer, sender=borrower) 36 | 37 | 38 | def test_revoke_offer_reverts_if_proxy_not_auth(p2p_nfts_usdc, borrower, now, lender, lender_key, p2p_nfts_proxy): 39 | offer = Offer( 40 | principal=1000, 41 | interest=100, 42 | payment_token=ZERO_ADDRESS, 43 | duration=100, 44 | origination_fee_amount=0, 45 | broker_upfront_fee_amount=0, 46 | broker_settlement_fee_bps=0, 47 | broker_address=ZERO_ADDRESS, 48 | token_id=1, 49 | expiration=now + 100, 50 | lender=lender, 51 | pro_rata=False, 52 | tracing_id=b"random".zfill(32), 53 | ) 54 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_usdc.address) 55 | 56 | p2p_nfts_usdc.set_proxy_authorization(p2p_nfts_proxy, False, sender=p2p_nfts_usdc.owner()) 57 | with boa.reverts("not lender"): 58 | p2p_nfts_proxy.revoke_offer(signed_offer, sender=lender) 59 | 60 | 61 | def test_revoke_offer_reverts_if_offer_expired(p2p_nfts_usdc, borrower, now, lender, lender_key): 62 | offer = Offer( 63 | principal=1000, 64 | interest=100, 65 | payment_token=ZERO_ADDRESS, 66 | duration=100, 67 | origination_fee_amount=0, 68 | broker_upfront_fee_amount=0, 69 | broker_settlement_fee_bps=0, 70 | broker_address=ZERO_ADDRESS, 71 | token_id=1, 72 | expiration=now, 73 | lender=lender, 74 | pro_rata=False, 75 | tracing_id=b"random".zfill(32), 76 | ) 77 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_usdc.address) 78 | 79 | with boa.reverts("offer expired"): 80 | p2p_nfts_usdc.revoke_offer(signed_offer, sender=lender) 81 | 82 | 83 | def test_revoke_offer_reverts_if_offer_not_signed_by_lender(p2p_nfts_usdc, borrower, now, lender, borrower_key): 84 | offer = Offer( 85 | principal=1000, 86 | interest=100, 87 | payment_token=ZERO_ADDRESS, 88 | duration=100, 89 | origination_fee_amount=0, 90 | broker_upfront_fee_amount=0, 91 | broker_settlement_fee_bps=0, 92 | broker_address=ZERO_ADDRESS, 93 | token_id=1, 94 | expiration=now + 100, 95 | lender=lender, 96 | pro_rata=False, 97 | tracing_id=b"random".zfill(32), 98 | ) 99 | signed_offer = sign_offer(offer, borrower_key, p2p_nfts_usdc.address) 100 | 101 | with boa.reverts("offer not signed by lender"): 102 | p2p_nfts_usdc.revoke_offer(signed_offer, sender=lender) 103 | 104 | 105 | def test_revoke_offer_reverts_if_offer_already_revoked(p2p_nfts_usdc, borrower, now, lender, lender_key): 106 | offer = Offer( 107 | principal=1000, 108 | interest=100, 109 | payment_token=ZERO_ADDRESS, 110 | duration=100, 111 | origination_fee_amount=0, 112 | broker_upfront_fee_amount=0, 113 | broker_settlement_fee_bps=0, 114 | broker_address=ZERO_ADDRESS, 115 | token_id=1, 116 | expiration=now + 100, 117 | lender=lender, 118 | pro_rata=False, 119 | tracing_id=b"random".zfill(32), 120 | ) 121 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_usdc.address) 122 | 123 | p2p_nfts_usdc.revoke_offer(signed_offer, sender=lender) 124 | 125 | with boa.reverts("offer already revoked"): 126 | p2p_nfts_usdc.revoke_offer(signed_offer, sender=lender) 127 | 128 | 129 | def test_revoke_offer(p2p_nfts_usdc, borrower, now, lender, lender_key): 130 | offer = Offer( 131 | principal=1000, 132 | interest=100, 133 | payment_token=ZERO_ADDRESS, 134 | duration=100, 135 | origination_fee_amount=0, 136 | broker_upfront_fee_amount=0, 137 | broker_settlement_fee_bps=0, 138 | broker_address=ZERO_ADDRESS, 139 | token_id=1, 140 | expiration=now + 100, 141 | lender=lender, 142 | pro_rata=False, 143 | tracing_id=b"random".zfill(32), 144 | ) 145 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_usdc.address) 146 | 147 | p2p_nfts_usdc.revoke_offer(signed_offer, sender=lender) 148 | 149 | assert p2p_nfts_usdc.revoked_offers(compute_signed_offer_id(signed_offer)) 150 | 151 | 152 | def test_revoke_offer_logs_event(p2p_nfts_usdc, borrower, now, lender, lender_key): 153 | offer = Offer( 154 | principal=1000, 155 | interest=100, 156 | payment_token=ZERO_ADDRESS, 157 | duration=100, 158 | origination_fee_amount=0, 159 | broker_upfront_fee_amount=0, 160 | broker_settlement_fee_bps=0, 161 | broker_address=ZERO_ADDRESS, 162 | token_id=1, 163 | expiration=now + 100, 164 | lender=lender, 165 | pro_rata=False, 166 | tracing_id=b"random".zfill(32), 167 | ) 168 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_usdc.address) 169 | 170 | p2p_nfts_usdc.revoke_offer(signed_offer, sender=lender) 171 | 172 | event = get_last_event(p2p_nfts_usdc, "OfferRevoked") 173 | assert event.offer_id == compute_signed_offer_id(signed_offer) 174 | assert event.lender == lender 175 | assert event.collection_key_hash == ZERO_BYTES32 176 | assert event.offer_type == OfferType.TOKEN 177 | 178 | 179 | def test_revoke_offer_works_with_proxy(p2p_nfts_usdc, borrower, now, lender, lender_key, p2p_nfts_proxy): 180 | offer = Offer( 181 | principal=1000, 182 | interest=100, 183 | payment_token=ZERO_ADDRESS, 184 | duration=100, 185 | origination_fee_amount=0, 186 | broker_upfront_fee_amount=0, 187 | broker_settlement_fee_bps=0, 188 | broker_address=ZERO_ADDRESS, 189 | token_id=1, 190 | expiration=now + 100, 191 | lender=lender, 192 | pro_rata=False, 193 | tracing_id=b"random".zfill(32), 194 | ) 195 | signed_offer = sign_offer(offer, lender_key, p2p_nfts_usdc.address) 196 | 197 | p2p_nfts_usdc.set_proxy_authorization(p2p_nfts_proxy, True, sender=p2p_nfts_usdc.owner()) 198 | p2p_nfts_proxy.revoke_offer(signed_offer, sender=lender) 199 | 200 | assert p2p_nfts_usdc.revoked_offers(compute_signed_offer_id(signed_offer)) 201 | -------------------------------------------------------------------------------- /contracts/ArcadeProxy.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | from ethereum.ercs import IERC20 4 | 5 | FLASH_LOAN_CALLBACK_SIZE: constant(uint256) = 10240 6 | FLASH_LOAN_MAX_TOKENS: constant(uint256) = 1 7 | 8 | interface P2PLendingNfts: 9 | def create_loan( 10 | offer: SignedOffer, 11 | collateral_token_id: uint256, 12 | collateral_proof: DynArray[bytes32, 32], 13 | delegate: address, 14 | borrower_broker_upfront_fee_amount: uint256, 15 | borrower_broker_settlement_fee_bps: uint256, 16 | borrower_broker: address 17 | ) -> bytes32: nonpayable 18 | def payment_token() -> address: view 19 | 20 | 21 | interface Arcade: 22 | def repay(loan_id: uint256): nonpayable 23 | 24 | 25 | interface IFlashLender: 26 | def flashLoan( 27 | recepient: address, 28 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 29 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 30 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 31 | ): nonpayable 32 | 33 | 34 | interface IFlashLoanRecipient: 35 | def receiveFlashLoan( 36 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 37 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 38 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 39 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 40 | ): nonpayable 41 | 42 | 43 | implements: IFlashLoanRecipient 44 | 45 | 46 | flag FeeType: 47 | PROTOCOL_FEE 48 | ORIGINATION_FEE 49 | LENDER_BROKER_FEE 50 | BORROWER_BROKER_FEE 51 | 52 | 53 | struct Fee: 54 | type: FeeType 55 | upfront_amount: uint256 56 | interest_bps: uint256 57 | wallet: address 58 | 59 | flag OfferType: 60 | TOKEN 61 | COLLECTION 62 | TRAIT 63 | 64 | struct Offer: 65 | principal: uint256 66 | interest: uint256 67 | payment_token: address 68 | duration: uint256 69 | origination_fee_amount: uint256 70 | broker_upfront_fee_amount: uint256 71 | broker_settlement_fee_bps: uint256 72 | broker_address: address 73 | offer_type: OfferType 74 | token_id: uint256 75 | token_range_min: uint256 76 | token_range_max: uint256 77 | collection_key_hash: bytes32 78 | trait_hash: bytes32 79 | expiration: uint256 80 | lender: address 81 | pro_rata: bool 82 | size: uint256 83 | tracing_id: bytes32 84 | 85 | 86 | struct Signature: 87 | v: uint256 88 | r: uint256 89 | s: uint256 90 | 91 | struct SignedOffer: 92 | offer: Offer 93 | signature: Signature 94 | 95 | struct Loan: 96 | id: bytes32 97 | offer_id: bytes32 98 | offer_tracing_id: bytes32 99 | amount: uint256 100 | interest: uint256 101 | payment_token: address 102 | maturity: uint256 103 | start_time: uint256 104 | borrower: address 105 | lender: address 106 | collateral_contract: address 107 | collateral_token_id: uint256 108 | fees: DynArray[Fee, MAX_FEES] 109 | pro_rata: bool 110 | delegate: address 111 | 112 | 113 | PROOF_MAX_SIZE: constant(uint256) = 32 114 | 115 | struct CallbackData: 116 | arcade_repayment_contract: address 117 | arcade_loan_core: address 118 | payment_token: address 119 | loan_id: uint256 120 | amount: uint256 121 | signed_offer: SignedOffer 122 | borrower: address 123 | token_id: uint256 124 | collateral_proof: DynArray[bytes32, PROOF_MAX_SIZE] 125 | delegate: address 126 | borrower_broker_upfront_fee_amount: uint256 127 | borrower_broker_settlement_fee_bps: uint256 128 | borrower_broker: address 129 | 130 | 131 | MAX_FEES: constant(uint256) = 4 132 | BPS: constant(uint256) = 10000 133 | 134 | p2p_lending_nfts: public(immutable(address)) 135 | flash_lender: public(immutable(address)) 136 | 137 | 138 | @deploy 139 | def __init__(_p2p_lending_nfts: address, _flash_lender: address): 140 | p2p_lending_nfts = _p2p_lending_nfts 141 | flash_lender = _flash_lender 142 | 143 | 144 | @external 145 | def receiveFlashLoan( 146 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 147 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 148 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 149 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 150 | ) : 151 | 152 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"callback")) 153 | assert msg.sender == flash_lender, "unauthorized" 154 | assert fee_amounts[0] == 0, "fee not supported" 155 | 156 | callback_data: CallbackData = abi_decode(data, CallbackData) 157 | 158 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 159 | assert tokens[0] == payment_token, "Invalid asset" 160 | 161 | assert (staticcall IERC20(payment_token).balanceOf(self)) >= amounts[0], "Insufficient balance" 162 | 163 | extcall IERC20(payment_token).approve(callback_data.arcade_loan_core, amounts[0]) 164 | extcall Arcade(callback_data.arcade_repayment_contract).repay(callback_data.loan_id) 165 | 166 | self._create_loan( 167 | callback_data.signed_offer, 168 | callback_data.token_id, 169 | callback_data.collateral_proof, 170 | callback_data.delegate, 171 | callback_data.borrower_broker_upfront_fee_amount, 172 | callback_data.borrower_broker_settlement_fee_bps, 173 | callback_data.borrower_broker 174 | ) 175 | 176 | assert (staticcall IERC20(payment_token).balanceOf(callback_data.borrower)) >= amounts[0], "Insufficient balance" 177 | extcall IERC20(payment_token).transferFrom(callback_data.borrower, flash_lender, amounts[0]) 178 | 179 | 180 | 181 | @internal 182 | def _create_loan( 183 | offer: SignedOffer, 184 | collateral_token_id: uint256, 185 | proof: DynArray[bytes32, 32], 186 | delegate: address, 187 | borrower_broker_upfront_fee_amount: uint256, 188 | borrower_broker_settlement_fee_bps: uint256, 189 | borrower_broker: address 190 | ) -> bytes32: 191 | return extcall P2PLendingNfts(p2p_lending_nfts).create_loan( 192 | offer, 193 | collateral_token_id, 194 | proof, 195 | delegate, 196 | borrower_broker_upfront_fee_amount, 197 | borrower_broker_settlement_fee_bps, 198 | borrower_broker 199 | ) 200 | 201 | 202 | @external 203 | def refinance_loan( 204 | arcade_repayment_contract: address, 205 | arcade_loan_core: address, 206 | loan_id: uint256, 207 | amount: uint256, 208 | signed_offer: SignedOffer, 209 | token_id: uint256, 210 | collateral_proof: DynArray[bytes32, 32], 211 | delegate: address, 212 | borrower_broker_upfront_fee_amount: uint256, 213 | borrower_broker_settlement_fee_bps: uint256, 214 | borrower_broker: address 215 | ): 216 | 217 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"refinance")) 218 | 219 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 220 | callback_data: CallbackData = CallbackData( 221 | arcade_repayment_contract = arcade_repayment_contract, 222 | arcade_loan_core = arcade_loan_core, 223 | payment_token = payment_token, 224 | loan_id = loan_id, 225 | amount = amount, 226 | signed_offer = signed_offer, 227 | borrower = msg.sender, 228 | token_id = token_id, 229 | collateral_proof = collateral_proof, 230 | delegate = delegate, 231 | borrower_broker_upfront_fee_amount = borrower_broker_upfront_fee_amount, 232 | borrower_broker_settlement_fee_bps = borrower_broker_settlement_fee_bps, 233 | borrower_broker = borrower_broker 234 | ) 235 | 236 | extcall IFlashLender(flash_lender).flashLoan( 237 | self, 238 | [payment_token], 239 | [amount], 240 | abi_encode(callback_data) 241 | ) 242 | -------------------------------------------------------------------------------- /contracts/BendDAOProxy.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | from ethereum.ercs import IERC20 4 | 5 | 6 | FLASH_LOAN_CALLBACK_SIZE: constant(uint256) = 10240 7 | FLASH_LOAN_MAX_TOKENS: constant(uint256) = 1 8 | 9 | 10 | interface P2PLendingNfts: 11 | def create_loan( 12 | offer: SignedOffer, 13 | collateral_token_id: uint256, 14 | collateral_proof: DynArray[bytes32, 32], 15 | delegate: address, 16 | borrower_broker_upfront_fee_amount: uint256, 17 | borrower_broker_settlement_fee_bps: uint256, 18 | borrower_broker: address 19 | ) -> bytes32: nonpayable 20 | def payment_token() -> address: view 21 | 22 | 23 | interface BendDAO: 24 | def repay(nft_asset: address, nft_token_id: uint256, amount: uint256): nonpayable 25 | 26 | 27 | interface IFlashLender: 28 | def flashLoan( 29 | recepient: address, 30 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 31 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 32 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 33 | ): nonpayable 34 | 35 | 36 | interface IFlashLoanRecipient: 37 | def receiveFlashLoan( 38 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 39 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 40 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 41 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 42 | ): nonpayable 43 | 44 | 45 | implements: IFlashLoanRecipient 46 | 47 | 48 | flag FeeType: 49 | PROTOCOL_FEE 50 | ORIGINATION_FEE 51 | LENDER_BROKER_FEE 52 | BORROWER_BROKER_FEE 53 | 54 | 55 | struct Fee: 56 | type: FeeType 57 | upfront_amount: uint256 58 | interest_bps: uint256 59 | wallet: address 60 | 61 | flag OfferType: 62 | TOKEN 63 | COLLECTION 64 | TRAIT 65 | 66 | struct Offer: 67 | principal: uint256 68 | interest: uint256 69 | payment_token: address 70 | duration: uint256 71 | origination_fee_amount: uint256 72 | broker_upfront_fee_amount: uint256 73 | broker_settlement_fee_bps: uint256 74 | broker_address: address 75 | offer_type: OfferType 76 | token_id: uint256 77 | token_range_min: uint256 78 | token_range_max: uint256 79 | collection_key_hash: bytes32 80 | trait_hash: bytes32 81 | expiration: uint256 82 | lender: address 83 | pro_rata: bool 84 | size: uint256 85 | tracing_id: bytes32 86 | 87 | 88 | struct Signature: 89 | v: uint256 90 | r: uint256 91 | s: uint256 92 | 93 | struct SignedOffer: 94 | offer: Offer 95 | signature: Signature 96 | 97 | struct Loan: 98 | id: bytes32 99 | offer_id: bytes32 100 | offer_tracing_id: bytes32 101 | amount: uint256 # principal - origination_fee_amount 102 | interest: uint256 103 | payment_token: address 104 | maturity: uint256 105 | start_time: uint256 106 | borrower: address 107 | lender: address 108 | collateral_contract: address 109 | collateral_token_id: uint256 110 | fees: DynArray[Fee, MAX_FEES] 111 | pro_rata: bool 112 | delegate: address 113 | 114 | 115 | PROOF_MAX_SIZE: constant(uint256) = 32 116 | 117 | struct CallbackData: 118 | benddao_contract: address 119 | approved: address 120 | payment_token: address 121 | collateral_address: address 122 | amount: uint256 123 | signed_offer: SignedOffer 124 | borrower: address 125 | token_id: uint256 126 | collateral_proof: DynArray[bytes32, PROOF_MAX_SIZE] 127 | delegate: address 128 | borrower_broker_upfront_fee_amount: uint256 129 | borrower_broker_settlement_fee_bps: uint256 130 | borrower_broker: address 131 | 132 | 133 | ERC3156_CALLBACK_OK: constant(bytes32) = keccak256("ERC3156FlashBorrower.onFlashLoan") 134 | 135 | MAX_FEES: constant(uint256) = 4 136 | BPS: constant(uint256) = 10000 137 | 138 | p2p_lending_nfts: public(immutable(address)) 139 | flash_lender: public(immutable(address)) 140 | 141 | 142 | @deploy 143 | def __init__(_p2p_lending_nfts: address, _flash_lender: address): 144 | p2p_lending_nfts = _p2p_lending_nfts 145 | flash_lender = _flash_lender 146 | 147 | 148 | @external 149 | def receiveFlashLoan( 150 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 151 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 152 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 153 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 154 | ) : 155 | 156 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"callback")) 157 | assert msg.sender == flash_lender, "unauthorized" 158 | assert fee_amounts[0] == 0, "fee not supported" 159 | 160 | callback_data: CallbackData = abi_decode(data, CallbackData) 161 | 162 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 163 | assert tokens[0] == payment_token, "Invalid asset" 164 | 165 | assert (staticcall IERC20(payment_token).balanceOf(self)) >= amounts[0], "Insufficient balance" 166 | 167 | extcall IERC20(payment_token).approve(callback_data.approved, amounts[0]) 168 | extcall BendDAO(callback_data.benddao_contract).repay(callback_data.collateral_address, callback_data.token_id, amounts[0]) 169 | 170 | self._create_loan( 171 | callback_data.signed_offer, 172 | callback_data.token_id, 173 | callback_data.collateral_proof, 174 | callback_data.delegate, 175 | callback_data.borrower_broker_upfront_fee_amount, 176 | callback_data.borrower_broker_settlement_fee_bps, 177 | callback_data.borrower_broker 178 | ) 179 | 180 | assert (staticcall IERC20(payment_token).balanceOf(callback_data.borrower)) >= amounts[0], "Insufficient balance" 181 | extcall IERC20(payment_token).transferFrom(callback_data.borrower, flash_lender, amounts[0]) 182 | 183 | 184 | 185 | @internal 186 | def _create_loan( 187 | offer: SignedOffer, 188 | collateral_token_id: uint256, 189 | proof: DynArray[bytes32, 32], 190 | delegate: address, 191 | borrower_broker_upfront_fee_amount: uint256, 192 | borrower_broker_settlement_fee_bps: uint256, 193 | borrower_broker: address 194 | ) -> bytes32: 195 | return extcall P2PLendingNfts(p2p_lending_nfts).create_loan( 196 | offer, 197 | collateral_token_id, 198 | proof, 199 | delegate, 200 | borrower_broker_upfront_fee_amount, 201 | borrower_broker_settlement_fee_bps, 202 | borrower_broker 203 | ) 204 | 205 | 206 | @external 207 | def refinance_loan( 208 | benddao_contract: address, 209 | approved: address, 210 | collateral_address: address, 211 | amount: uint256, 212 | signed_offer: SignedOffer, 213 | token_id: uint256, 214 | collateral_proof: DynArray[bytes32, 32], 215 | delegate: address, 216 | borrower_broker_upfront_fee_amount: uint256, 217 | borrower_broker_settlement_fee_bps: uint256, 218 | borrower_broker: address 219 | ): 220 | 221 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"refinance")) 222 | 223 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 224 | callback_data: CallbackData = CallbackData( 225 | benddao_contract = benddao_contract, 226 | approved = approved, 227 | payment_token = payment_token, 228 | collateral_address = collateral_address, 229 | amount = amount, 230 | signed_offer = signed_offer, 231 | borrower = msg.sender, 232 | token_id = token_id, 233 | collateral_proof = collateral_proof, 234 | delegate = delegate, 235 | borrower_broker_upfront_fee_amount = borrower_broker_upfront_fee_amount, 236 | borrower_broker_settlement_fee_bps = borrower_broker_settlement_fee_bps, 237 | borrower_broker = borrower_broker 238 | ) 239 | 240 | extcall IFlashLender(flash_lender).flashLoan( 241 | self, 242 | [payment_token], 243 | [amount], 244 | abi_encode(callback_data) 245 | ) 246 | -------------------------------------------------------------------------------- /contracts/NftfiProxy.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | from ethereum.ercs import IERC20 4 | 5 | 6 | FLASH_LOAN_CALLBACK_SIZE: constant(uint256) = 10240 7 | FLASH_LOAN_MAX_TOKENS: constant(uint256) = 1 8 | 9 | 10 | interface P2PLendingNfts: 11 | def create_loan( 12 | offer: SignedOffer, 13 | collateral_token_id: uint256, 14 | collateral_proof: DynArray[bytes32, 32], 15 | delegate: address, 16 | borrower_broker_upfront_fee_amount: uint256, 17 | borrower_broker_settlement_fee_bps: uint256, 18 | borrower_broker: address 19 | ) -> bytes32: nonpayable 20 | def payment_token() -> address: view 21 | 22 | 23 | interface NFTfi: 24 | def payBackLoan(loan_id: uint32): nonpayable 25 | 26 | 27 | interface IFlashLender: 28 | def flashLoan( 29 | recepient: address, 30 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 31 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 32 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 33 | ): nonpayable 34 | 35 | 36 | interface IFlashLoanRecipient: 37 | def receiveFlashLoan( 38 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 39 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 40 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 41 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 42 | ): nonpayable 43 | 44 | 45 | implements: IFlashLoanRecipient 46 | 47 | 48 | flag FeeType: 49 | PROTOCOL_FEE 50 | ORIGINATION_FEE 51 | LENDER_BROKER_FEE 52 | BORROWER_BROKER_FEE 53 | 54 | 55 | struct Fee: 56 | type: FeeType 57 | upfront_amount: uint256 58 | interest_bps: uint256 59 | wallet: address 60 | 61 | struct FeeAmount: 62 | type: FeeType 63 | amount: uint256 64 | wallet: address 65 | 66 | flag OfferType: 67 | TOKEN 68 | COLLECTION 69 | TRAIT 70 | 71 | struct Offer: 72 | principal: uint256 73 | interest: uint256 74 | payment_token: address 75 | duration: uint256 76 | origination_fee_amount: uint256 77 | broker_upfront_fee_amount: uint256 78 | broker_settlement_fee_bps: uint256 79 | broker_address: address 80 | offer_type: OfferType 81 | token_id: uint256 82 | token_range_min: uint256 83 | token_range_max: uint256 84 | collection_key_hash: bytes32 85 | trait_hash: bytes32 86 | expiration: uint256 87 | lender: address 88 | pro_rata: bool 89 | size: uint256 90 | tracing_id: bytes32 91 | 92 | 93 | struct Signature: 94 | v: uint256 95 | r: uint256 96 | s: uint256 97 | 98 | struct SignedOffer: 99 | offer: Offer 100 | signature: Signature 101 | 102 | struct Loan: 103 | id: bytes32 104 | offer_id: bytes32 105 | offer_tracing_id: bytes32 106 | amount: uint256 # principal - origination_fee_amount 107 | interest: uint256 108 | payment_token: address 109 | maturity: uint256 110 | start_time: uint256 111 | borrower: address 112 | lender: address 113 | collateral_contract: address 114 | collateral_token_id: uint256 115 | fees: DynArray[Fee, MAX_FEES] 116 | pro_rata: bool 117 | delegate: address 118 | 119 | 120 | PROOF_MAX_SIZE: constant(uint256) = 32 121 | 122 | struct CallbackData: 123 | nftfi_contract: address 124 | approved: address 125 | payment_token: address 126 | loan_id: uint256 127 | amount: uint256 128 | signed_offer: SignedOffer 129 | borrower: address 130 | token_id: uint256 131 | collateral_proof: DynArray[bytes32, PROOF_MAX_SIZE] 132 | delegate: address 133 | borrower_broker_upfront_fee_amount: uint256 134 | borrower_broker_settlement_fee_bps: uint256 135 | borrower_broker: address 136 | 137 | 138 | 139 | ERC3156_CALLBACK_OK: constant(bytes32) = keccak256("ERC3156FlashBorrower.onFlashLoan") 140 | 141 | MAX_FEES: constant(uint256) = 4 142 | BPS: constant(uint256) = 10000 143 | 144 | p2p_lending_nfts: public(immutable(address)) 145 | flash_lender: public(immutable(address)) 146 | 147 | @deploy 148 | def __init__(_p2p_lending_nfts: address, _flash_lender: address): 149 | p2p_lending_nfts = _p2p_lending_nfts 150 | flash_lender = _flash_lender 151 | 152 | 153 | 154 | @external 155 | def receiveFlashLoan( 156 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 157 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 158 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 159 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 160 | ) : 161 | 162 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"callback")) 163 | assert msg.sender == flash_lender, "unauthorized" 164 | assert fee_amounts[0] == 0, "fee not supported" 165 | 166 | callback_data: CallbackData = abi_decode(data, CallbackData) 167 | 168 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 169 | assert tokens[0] == payment_token, "Invalid asset" 170 | 171 | assert (staticcall IERC20(payment_token).balanceOf(self)) >= amounts[0], "Insufficient balance" 172 | 173 | extcall IERC20(payment_token).approve(callback_data.approved, amounts[0]) 174 | extcall NFTfi(callback_data.nftfi_contract).payBackLoan(convert(callback_data.loan_id, uint32)) 175 | 176 | self._create_loan( 177 | callback_data.signed_offer, 178 | callback_data.token_id, 179 | callback_data.collateral_proof, 180 | callback_data.delegate, 181 | callback_data.borrower_broker_upfront_fee_amount, 182 | callback_data.borrower_broker_settlement_fee_bps, 183 | callback_data.borrower_broker 184 | ) 185 | 186 | assert (staticcall IERC20(payment_token).balanceOf(callback_data.borrower)) >= amounts[0], "Insufficient balance" 187 | extcall IERC20(payment_token).transferFrom(callback_data.borrower, flash_lender, amounts[0]) 188 | 189 | 190 | @internal 191 | def _create_loan( 192 | offer: SignedOffer, 193 | collateral_token_id: uint256, 194 | proof: DynArray[bytes32, 32], 195 | delegate: address, 196 | borrower_broker_upfront_fee_amount: uint256, 197 | borrower_broker_settlement_fee_bps: uint256, 198 | borrower_broker: address 199 | ) -> bytes32: 200 | return extcall P2PLendingNfts(p2p_lending_nfts).create_loan( 201 | offer, 202 | collateral_token_id, 203 | proof, 204 | delegate, 205 | borrower_broker_upfront_fee_amount, 206 | borrower_broker_settlement_fee_bps, 207 | borrower_broker 208 | ) 209 | 210 | 211 | @external 212 | def refinance_loan_balancer( 213 | nftfi_contract: address, 214 | approved: address, 215 | loan_id: uint256, 216 | amount: uint256, 217 | signed_offer: SignedOffer, 218 | token_id: uint256, 219 | collateral_proof: DynArray[bytes32, 32], 220 | delegate: address, 221 | borrower_broker_upfront_fee_amount: uint256, 222 | borrower_broker_settlement_fee_bps: uint256, 223 | borrower_broker: address 224 | ): 225 | 226 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"refinance")) 227 | # TODO add checklist for nftfi contracts 228 | 229 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 230 | callback_data: CallbackData = CallbackData( 231 | nftfi_contract = nftfi_contract, 232 | approved = approved, 233 | payment_token = payment_token, 234 | loan_id = loan_id, 235 | amount = amount, 236 | signed_offer = signed_offer, 237 | borrower = msg.sender, 238 | token_id = token_id, 239 | collateral_proof = collateral_proof, 240 | delegate = delegate, 241 | borrower_broker_upfront_fee_amount = borrower_broker_upfront_fee_amount, 242 | borrower_broker_settlement_fee_bps = borrower_broker_settlement_fee_bps, 243 | borrower_broker = borrower_broker 244 | ) 245 | 246 | extcall IFlashLender(flash_lender).flashLoan( 247 | self, 248 | [payment_token], 249 | [amount], 250 | abi_encode(callback_data) 251 | ) 252 | -------------------------------------------------------------------------------- /3rd-party/arcade/RepaymentController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | 7 | import "./interfaces/IRepaymentController.sol"; 8 | import "./interfaces/IPromissoryNote.sol"; 9 | import "./interfaces/ILoanCore.sol"; 10 | import "./interfaces/IFeeController.sol"; 11 | 12 | import "./libraries/InterestCalculator.sol"; 13 | import "./libraries/FeeLookups.sol"; 14 | import "./libraries/LoanLibrary.sol"; 15 | 16 | import { 17 | RC_ZeroAddress, 18 | RC_CannotDereference, 19 | RC_InvalidState, 20 | RC_OnlyLender 21 | } from "./errors/Lending.sol"; 22 | 23 | /** 24 | * @title RepaymentController 25 | * @author Non-Fungible Technologies, Inc. 26 | * 27 | * The Repayment Controller is the entry point for all loan lifecycle 28 | * operations in the Arcade.xyz lending protocol once a loan has begun. 29 | * This contract allows a caller to calculate an amount due on a loan, 30 | * repay an open loan, and claim collateral on a defaulted loan. It 31 | * is this contract's responsibility to verify loan conditions before 32 | * calling LoanCore. 33 | */ 34 | contract RepaymentController is IRepaymentController, InterestCalculator, FeeLookups { 35 | using SafeERC20 for IERC20; 36 | 37 | // ============================================ STATE =============================================== 38 | 39 | ILoanCore private immutable loanCore; 40 | IPromissoryNote private immutable lenderNote; 41 | IFeeController private immutable feeController; 42 | 43 | // ========================================= CONSTRUCTOR ============================================ 44 | 45 | /** 46 | * @notice Creates a new repayment controller contract. 47 | * 48 | * @dev For this controller to work, it needs to be granted the REPAYER_ROLE 49 | * in loan core after deployment. 50 | * 51 | * @param _loanCore The address of the loan core logic of the protocol. 52 | * @param _feeController The address of the fee logic of the protocol. 53 | */ 54 | constructor(address _loanCore, address _feeController) { 55 | if (_loanCore == address(0)) revert RC_ZeroAddress("loanCore"); 56 | if (_feeController == address(0)) revert RC_ZeroAddress("feeController"); 57 | 58 | loanCore = ILoanCore(_loanCore); 59 | lenderNote = loanCore.lenderNote(); 60 | feeController = IFeeController(_feeController); 61 | } 62 | 63 | // ==================================== LIFECYCLE OPERATIONS ======================================== 64 | 65 | /** 66 | * @notice Repay an active loan, referenced by borrower note ID (equivalent to loan ID). The interest for a loan 67 | * is calculated, and the principal plus interest is withdrawn from the caller. 68 | * Anyone can repay a loan. Control is passed to LoanCore to complete repayment. 69 | * 70 | * @param loanId The ID of the loan. 71 | */ 72 | function repay(uint256 loanId) external override { 73 | (uint256 amountFromBorrower, uint256 amountToLender) = _prepareRepay(loanId); 74 | 75 | // call repay function in loan core - msg.sender will pay the amountFromBorrower 76 | loanCore.repay(loanId, msg.sender, amountFromBorrower, amountToLender); 77 | } 78 | 79 | /** 80 | * @notice Repay an active loan, referenced by borrower note ID (equivalent to loan ID). The interest for a loan 81 | * is calculated, and the principal plus interest is withdrawn from the caller. Anyone can repay a loan. 82 | * Using forceRepay will not send funds to the lender: instead, those funds will be made 83 | * available for withdrawal in LoanCore. Can be used in cases where a borrower has funds to repay 84 | * but the lender is not able to receive those tokens (e.g. token blacklist). 85 | * 86 | * @param loanId The ID of the loan. 87 | */ 88 | function forceRepay(uint256 loanId) external override { 89 | (uint256 amountFromBorrower, uint256 amountToLender) = _prepareRepay(loanId); 90 | 91 | // call repay function in loan core - msg.sender will pay the amountFromBorrower 92 | loanCore.forceRepay(loanId, msg.sender, amountFromBorrower, amountToLender); 93 | } 94 | 95 | /** 96 | * @notice Claim collateral on an active loan, referenced by lender note ID (equivalent to loan ID). 97 | * The loan must be past the due date. No funds are collected 98 | * from the borrower. 99 | * 100 | * @param loanId The ID of the loan. 101 | */ 102 | function claim(uint256 loanId) external override { 103 | LoanLibrary.LoanData memory data = loanCore.getLoan(loanId); 104 | if (data.state == LoanLibrary.LoanState.DUMMY_DO_NOT_USE) revert RC_CannotDereference(loanId); 105 | 106 | // make sure that caller owns lender note 107 | // Implicitly checks if loan is active - if inactive, note will not exist 108 | address lender = lenderNote.ownerOf(loanId); 109 | if (lender != msg.sender) revert RC_OnlyLender(lender, msg.sender); 110 | 111 | LoanLibrary.LoanTerms memory terms = data.terms; 112 | uint256 interest = getInterestAmount(terms.principal, terms.proratedInterestRate); 113 | uint256 totalOwed = terms.principal + interest; 114 | 115 | uint256 claimFee = (totalOwed * data.feeSnapshot.lenderDefaultFee) / BASIS_POINTS_DENOMINATOR; 116 | 117 | loanCore.claim(loanId, claimFee); 118 | } 119 | 120 | /** 121 | * @notice Redeem a lender note for a completed return in return for funds repaid in an earlier 122 | * transaction via forceRepay. The lender note must be owned by the caller. 123 | * 124 | * @param loanId The ID of the lender note to redeem. 125 | */ 126 | function redeemNote(uint256 loanId, address to) external override { 127 | if (to == address(0)) revert RC_ZeroAddress("to"); 128 | 129 | LoanLibrary.LoanData memory data = loanCore.getLoan(loanId); 130 | (, uint256 amountOwed) = loanCore.getNoteReceipt(loanId); 131 | 132 | if (data.state != LoanLibrary.LoanState.Repaid) revert RC_InvalidState(data.state); 133 | address lender = lenderNote.ownerOf(loanId); 134 | if (lender != msg.sender) revert RC_OnlyLender(lender, msg.sender); 135 | 136 | uint256 redeemFee = (amountOwed * feeController.getLendingFee(FL_08)) / BASIS_POINTS_DENOMINATOR; 137 | 138 | loanCore.redeemNote(loanId, redeemFee, to); 139 | } 140 | 141 | // =========================================== HELPERS ============================================== 142 | 143 | /** 144 | * @dev Shared logic to perform validation and calculations for repay and forceRepay. 145 | * 146 | * @param loanId The ID of the loan. 147 | * 148 | * @return amountFromBorrower The amount to collect from the borrower. 149 | * @return amountToLender The amount owed to the lender. 150 | */ 151 | function _prepareRepay(uint256 loanId) internal view returns (uint256 amountFromBorrower, uint256 amountToLender) { 152 | LoanLibrary.LoanData memory data = loanCore.getLoan(loanId); 153 | if (data.state == LoanLibrary.LoanState.DUMMY_DO_NOT_USE) revert RC_CannotDereference(loanId); 154 | if (data.state != LoanLibrary.LoanState.Active) revert RC_InvalidState(data.state); 155 | 156 | LoanLibrary.LoanTerms memory terms = data.terms; 157 | 158 | uint256 interest = getInterestAmount(terms.principal, terms.proratedInterestRate); 159 | 160 | uint256 interestFee = (interest * data.feeSnapshot.lenderInterestFee) / BASIS_POINTS_DENOMINATOR; 161 | uint256 principalFee = (terms.principal * data.feeSnapshot.lenderPrincipalFee) / BASIS_POINTS_DENOMINATOR; 162 | 163 | amountFromBorrower = terms.principal + interest; 164 | amountToLender = amountFromBorrower - interestFee - principalFee; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /natspec/P2PLendingNfts.json: -------------------------------------------------------------------------------- 1 | {"notice": "This contract facilitates peer-to-peer lending using NFTs as collateral.", "methods": {"__init__(address,address,address,uint256,uint256,address)": {"notice": "Initialize the contract with the given parameters."}, "set_protocol_fee(uint256,uint256)": {"notice": "Set the protocol fee"}, "change_protocol_wallet(address)": {"notice": "Change the protocol wallet"}, "set_proxy_authorization(address,bool)": {"notice": "Set authorization"}, "propose_owner(address)": {"notice": "Propose a new owner"}, "claim_ownership()": {"notice": "Claim the ownership of the contract"}, "change_whitelisted_collections((address,bool)[])": {"notice": "Set whitelisted collections"}, "create_loan(((uint256,uint256,address,uint256,uint256,uint256,uint256,address,address,uint256,uint256[],uint256,address,bool,uint256),(uint256,uint256,uint256)),uint256,address,uint256,uint256,address)": {"notice": "Create a loan."}, "settle_loan((bytes32,uint256,uint256,address,uint256,uint256,address,address,address,uint256,(uint256,uint256,uint256,address)[],bool))": {"notice": "Settle a loan."}, "claim_defaulted_loan_collateral((bytes32,uint256,uint256,address,uint256,uint256,address,address,address,uint256,(uint256,uint256,uint256,address)[],bool))": {"notice": "Claim defaulted loan collateral."}, "replace_loan((bytes32,uint256,uint256,address,uint256,uint256,address,address,address,uint256,(uint256,uint256,uint256,address)[],bool),((uint256,uint256,address,uint256,uint256,uint256,uint256,address,address,uint256,uint256[],uint256,address,bool,uint256),(uint256,uint256,uint256)),uint256,uint256,address)": {"notice": "Replace an existing loan by accepting a new offer over the same collateral. The current loan is settled and the new loan is created. Must be called by the borrower."}, "replace_loan_lender((bytes32,uint256,uint256,address,uint256,uint256,address,address,address,uint256,(uint256,uint256,uint256,address)[],bool),((uint256,uint256,address,uint256,uint256,uint256,uint256,address,address,uint256,uint256[],uint256,address,bool,uint256),(uint256,uint256,uint256)))": {"notice": "Replace a loan by the lender. The current loan is settled and the new loan is created. Must be called by the lender."}, "revoke_offer(((uint256,uint256,address,uint256,uint256,uint256,uint256,address,address,uint256,uint256[],uint256,address,bool,uint256),(uint256,uint256,uint256)))": {"notice": "Revoke an offer."}, "onERC721Received(address,address,uint256,bytes)": {"notice": "ERC721 token receiver callback."}}} 2 | {"title": "P2PLendingNfts", "author": "[Zharta](https://zharta.io/)", "details": "It facilitates peer-to-peer lending using NFTs as collateral. The contract allows lenders to offer loans and borrowers to accept them by providing NFTs as collateral. Key functionalities include: - Creating and managing loan offers - Accepting loan offers and locking NFTs as collateral - Accepts ERC721 and CryptoPunks NFTs as collateral - Delegating the collateral using [Delegate](https://delegate.xyz/) DelegateRegistry v2 - Settling loans by repaying the principal and interest - Claiming collateral in case of loan default - Replacing existing loans with new terms - Four types of fees are supported: protocol fee, origination fee, lender broker fee, and borrower broker fee - Managing protocol fees and authorized proxies - Handling ownership transfer of the contract - Loan state is kept hashed in the contract to save gas The contract ensures secure and transparent lending operations within the Zharta ecosystem.", "methods": {"__init__(address,address,address,uint256,uint256,address)": {"params": {"_payment_token": "The address of the payment token.", "_delegation_registry": "The address of the delegation registry.", "_cryptopunks": "The address of the CryptoPunksMarket contract.", "_protocol_upfront_fee": "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": "The address where the protocol fees are accrued."}}, "set_protocol_fee(uint256,uint256)": {"details": "Sets the protocol fee to the given value and logs the event. Admin function.", "params": {"protocol_upfront_fee": "The new protocol upfront fee.", "protocol_settlement_fee": "The new protocol settlement fee."}}, "change_protocol_wallet(address)": {"details": "Changes the protocol wallet to the given address and logs the event. Admin function.", "params": {"new_protocol_wallet": "The new protocol wallet."}}, "set_proxy_authorization(address,bool)": {"details": "Sets the authorization for the given proxy and logs the event. Admin function.", "params": {"_proxy": "The address of the proxy.", "_value": "The value of the authorization."}}, "propose_owner(address)": {"details": "Proposes a new owner and logs the event. Admin function.", "params": {"_address": "The address of the proposed owner."}}, "claim_ownership()": {"details": "Claims the ownership of the contract and logs the event. Requires the caller to be the proposed owner."}, "change_whitelisted_collections((address,bool)[])": {"params": {"collections": "array of WhitelistRecord"}}, "create_loan(((uint256,uint256,address,uint256,uint256,uint256,uint256,address,address,uint256,uint256[],uint256,address,bool,uint256),(uint256,uint256,uint256)),uint256,address,uint256,uint256,address)": {"params": {"offer": "The signed offer.", "collateral_token_id": "The ID of the collateral token.", "delegate": "The address of the delegate. If empty, no delegation is set.", "borrower_broker_upfront_fee_amount": "The upfront fee amount for the borrower broker.", "borrower_broker_settlement_fee_bps": "The settlement fee basis points relative to the interest for the borrower broker.", "borrower_broker": "The address of the borrower broker."}, "returns": {"_0": "The ID of the created loan."}}, "settle_loan((bytes32,uint256,uint256,address,uint256,uint256,address,address,address,uint256,(uint256,uint256,uint256,address)[],bool))": {"params": {"loan": "The loan to be settled."}}, "claim_defaulted_loan_collateral((bytes32,uint256,uint256,address,uint256,uint256,address,address,address,uint256,(uint256,uint256,uint256,address)[],bool))": {"params": {"loan": "The loan whose collateral is to be claimed. The loan maturity must have been passed."}}, "replace_loan((bytes32,uint256,uint256,address,uint256,uint256,address,address,address,uint256,(uint256,uint256,uint256,address)[],bool),((uint256,uint256,address,uint256,uint256,uint256,uint256,address,address,uint256,uint256[],uint256,address,bool,uint256),(uint256,uint256,uint256)),uint256,uint256,address)": {"details": "No collateral transfer is required and the delegation is not changed. The borrower must be the same as the borrower of the current loan.", "params": {"loan": "The loan to be replaced.", "offer": "The new signed offer.", "borrower_broker_upfront_fee_amount": "The upfront fee amount for the borrower broker.", "borrower_broker_settlement_fee_bps": "The settlement fee basis points relative to the interest for the borrower broker.", "borrower_broker": "The address of the borrower broker, if any."}, "returns": {"_0": "The ID of the new loan."}}, "replace_loan_lender((bytes32,uint256,uint256,address,uint256,uint256,address,address,address,uint256,(uint256,uint256,uint256,address)[],bool),((uint256,uint256,address,uint256,uint256,uint256,uint256,address,address,uint256,uint256[],uint256,address,bool,uint256),(uint256,uint256,uint256)))": {"details": "No collateral transfer is required and the delegation is not changed. The borrower must be the same as the borrower of the current loan. No funds are required from the borrower. Also no funds are required from the lender, except when the current and new lender are the same.", "params": {"loan": "The loan to be replaced.", "offer": "The new signed offer."}, "returns": {"_0": "The ID of the new loan."}}, "revoke_offer(((uint256,uint256,address,uint256,uint256,uint256,uint256,address,address,uint256,uint256[],uint256,address,bool,uint256),(uint256,uint256,uint256)))": {"params": {"offer": "The signed offer to be revoked."}}, "onERC721Received(address,address,uint256,bytes)": {"details": "Returns the ERC721 receiver callback selector.", "params": {"_operator": "The address which called `safeTransferFrom` function.", "_from": "The address which previously owned the token.", "_tokenId": "The NFT identifier which is being transferred.", "_data": "Additional data with no specified format."}, "returns": {"_0": "The ERC721 receiver callback selector."}}}} 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile -o requirements.txt pyproject.toml 3 | aiohappyeyeballs==2.5.0 4 | # via aiohttp 5 | aiohttp==3.11.13 6 | # via web3 7 | aiosignal==1.3.2 8 | # via aiohttp 9 | annotated-types==0.7.0 10 | # via pydantic 11 | ape-alchemy==0.8.8 12 | ape-arbitrum==0.8.3 13 | ape-base==0.8.1 14 | ape-foundry==0.8.7 15 | ape-optimism==0.8.3 16 | # via ape-base 17 | ape-vyper==0.8.9 18 | asttokens==2.4.1 19 | # via 20 | # eth-ape 21 | # stack-data 22 | # vyper 23 | attrs==25.1.0 24 | # via aiohttp 25 | base58==1.0.3 26 | # via 27 | # py-cid 28 | # py-multihash 29 | bitarray==3.1.1 30 | # via eth-account 31 | cached-property==2.0.1 32 | # via 33 | # py-ecc 34 | # py-evm 35 | cbor2==5.6.5 36 | # via vyper 37 | cchecksum==0.0.4 38 | # via eth-ape 39 | certifi==2025.1.31 40 | # via requests 41 | charset-normalizer==3.4.1 42 | # via requests 43 | ckzg==2.0.1 44 | # via 45 | # eth-account 46 | # py-evm 47 | click==8.1.8 48 | # via eth-ape 49 | cytoolz==1.0.1 50 | # via eth-utils 51 | dataclassy==0.11.1 52 | # via eip712 53 | decorator==5.2.1 54 | # via ipython 55 | eip712==0.2.11 56 | # via eth-ape 57 | eth-abi==5.2.0 58 | # via 59 | # eip712 60 | # eth-account 61 | # eth-ape 62 | # eth-tester 63 | # ethpm-types 64 | # web3 65 | eth-account==0.13.5 66 | # via 67 | # eip712 68 | # eth-ape 69 | # eth-tester 70 | # web3 71 | eth-ape==0.8.29 72 | # via 73 | # ape-alchemy 74 | # ape-arbitrum 75 | # ape-base 76 | # ape-foundry 77 | # ape-optimism 78 | # ape-vyper 79 | eth-bloom==3.1.0 80 | # via py-evm 81 | eth-hash==0.7.1 82 | # via 83 | # eth-bloom 84 | # eth-tester 85 | # eth-utils 86 | # trie 87 | # web3 88 | eth-keyfile==0.8.1 89 | # via eth-account 90 | eth-keys==0.6.1 91 | # via 92 | # eth-account 93 | # eth-keyfile 94 | # eth-tester 95 | # py-evm 96 | eth-pydantic-types==0.1.3 97 | # via 98 | # ape-alchemy 99 | # ape-arbitrum 100 | # ape-foundry 101 | # eth-ape 102 | # ethpm-types 103 | # evm-trace 104 | eth-rlp==2.2.0 105 | # via eth-account 106 | eth-tester==0.12.1b1 107 | # via web3 108 | eth-typing==5.2.0 109 | # via 110 | # cchecksum 111 | # eip712 112 | # eth-abi 113 | # eth-ape 114 | # eth-keys 115 | # eth-pydantic-types 116 | # eth-utils 117 | # py-ecc 118 | # py-evm 119 | # web3 120 | eth-utils==5.2.0 121 | # via 122 | # cchecksum 123 | # eip712 124 | # eth-abi 125 | # eth-account 126 | # eth-ape 127 | # eth-keyfile 128 | # eth-keys 129 | # eth-pydantic-types 130 | # eth-rlp 131 | # eth-tester 132 | # ethpm-types 133 | # evm-trace 134 | # py-ecc 135 | # py-evm 136 | # rlp 137 | # trie 138 | # web3 139 | ethpm-types==0.6.24 140 | # via 141 | # ape-alchemy 142 | # ape-arbitrum 143 | # ape-base 144 | # ape-foundry 145 | # ape-optimism 146 | # ape-vyper 147 | # eth-ape 148 | evm-trace==0.2.4 149 | # via 150 | # ape-alchemy 151 | # ape-foundry 152 | # eth-ape 153 | evmchains==0.1.4 154 | # via 155 | # ape-alchemy 156 | # eth-ape 157 | executing==2.2.0 158 | # via stack-data 159 | frozenlist==1.5.0 160 | # via 161 | # aiohttp 162 | # aiosignal 163 | hexbytes==1.3.0 164 | # via 165 | # ape-foundry 166 | # eip712 167 | # eth-account 168 | # eth-ape 169 | # eth-pydantic-types 170 | # eth-rlp 171 | # trie 172 | # web3 173 | idna==3.10 174 | # via 175 | # requests 176 | # yarl 177 | ijson==3.3.0 178 | # via eth-ape 179 | importlib-metadata==8.6.1 180 | # via vyper 181 | iniconfig==2.0.0 182 | # via pytest 183 | ipython==8.34.0 184 | # via eth-ape 185 | jedi==0.19.2 186 | # via ipython 187 | lark==1.2.2 188 | # via vyper 189 | lazyasd==0.1.4 190 | # via eth-ape 191 | lru-dict==1.3.0 192 | # via py-evm 193 | markdown-it-py==3.0.0 194 | # via rich 195 | matplotlib-inline==0.1.7 196 | # via ipython 197 | mdurl==0.1.2 198 | # via markdown-it-py 199 | morphys==1.0 200 | # via 201 | # py-cid 202 | # py-multibase 203 | # py-multicodec 204 | # py-multihash 205 | multidict==6.1.0 206 | # via 207 | # aiohttp 208 | # yarl 209 | numpy==1.26.4 210 | # via 211 | # eth-ape 212 | # pandas 213 | packaging==23.2 214 | # via 215 | # eth-ape 216 | # pytest 217 | # vvm 218 | # vyper 219 | pandas==2.2.3 220 | # via eth-ape 221 | parsimonious==0.10.0 222 | # via eth-abi 223 | parso==0.8.4 224 | # via jedi 225 | pexpect==4.9.0 226 | # via ipython 227 | pluggy==1.5.0 228 | # via 229 | # eth-ape 230 | # pytest 231 | prompt-toolkit==3.0.50 232 | # via ipython 233 | propcache==0.3.0 234 | # via 235 | # aiohttp 236 | # yarl 237 | ptyprocess==0.7.0 238 | # via pexpect 239 | pure-eval==0.2.3 240 | # via stack-data 241 | py-cid==0.3.0 242 | # via ethpm-types 243 | py-ecc==7.0.1 244 | # via py-evm 245 | py-evm==0.10.1b2 246 | # via 247 | # eth-tester 248 | # evm-trace 249 | py-geth==5.2.1 250 | # via 251 | # eth-ape 252 | # web3 253 | py-multibase==1.0.3 254 | # via py-cid 255 | py-multicodec==0.2.1 256 | # via py-cid 257 | py-multihash==0.2.3 258 | # via py-cid 259 | pycryptodome==3.21.0 260 | # via 261 | # eth-hash 262 | # eth-keyfile 263 | # vyper 264 | pydantic==2.10.6 265 | # via 266 | # eth-account 267 | # eth-ape 268 | # eth-pydantic-types 269 | # ethpm-types 270 | # evm-trace 271 | # evmchains 272 | # py-geth 273 | # pydantic-settings 274 | # web3 275 | pydantic-core==2.27.2 276 | # via pydantic 277 | pydantic-settings==2.8.1 278 | # via eth-ape 279 | pygments==2.19.1 280 | # via 281 | # ipython 282 | # rich 283 | pytest==8.3.5 284 | # via eth-ape 285 | python-baseconv==1.2.2 286 | # via py-multibase 287 | python-dateutil==2.9.0.post0 288 | # via 289 | # eth-ape 290 | # pandas 291 | python-dotenv==1.0.1 292 | # via pydantic-settings 293 | pytz==2025.1 294 | # via pandas 295 | pyunormalize==16.0.0 296 | # via web3 297 | pyyaml==6.0.2 298 | # via eth-ape 299 | regex==2024.11.6 300 | # via parsimonious 301 | requests==2.32.3 302 | # via 303 | # ape-alchemy 304 | # eth-ape 305 | # ethpm-types 306 | # py-geth 307 | # vvm 308 | # web3 309 | rich==13.9.4 310 | # via eth-ape 311 | rlp==4.1.0 312 | # via 313 | # eth-account 314 | # eth-rlp 315 | # eth-tester 316 | # py-evm 317 | # trie 318 | safe-pysha3==1.0.4 319 | # via eth-hash 320 | semantic-version==2.10.0 321 | # via 322 | # eth-tester 323 | # py-geth 324 | six==1.17.0 325 | # via 326 | # asttokens 327 | # py-multibase 328 | # py-multicodec 329 | # py-multihash 330 | # python-dateutil 331 | sortedcontainers==2.4.0 332 | # via trie 333 | sqlalchemy==2.0.38 334 | # via eth-ape 335 | stack-data==0.6.3 336 | # via ipython 337 | toolz==1.0.0 338 | # via cytoolz 339 | tqdm==4.67.1 340 | # via 341 | # ape-vyper 342 | # eth-ape 343 | traitlets==5.14.3 344 | # via 345 | # eth-ape 346 | # ipython 347 | # matplotlib-inline 348 | trie==3.1.0 349 | # via 350 | # eth-ape 351 | # py-evm 352 | types-requests==2.32.0.20250306 353 | # via 354 | # py-geth 355 | # web3 356 | typing-extensions==4.12.2 357 | # via 358 | # eth-pydantic-types 359 | # eth-typing 360 | # py-geth 361 | # pydantic 362 | # pydantic-core 363 | # sqlalchemy 364 | # web3 365 | tzdata==2025.1 366 | # via pandas 367 | urllib3==2.3.0 368 | # via 369 | # eth-ape 370 | # requests 371 | # types-requests 372 | varint==1.0.2 373 | # via 374 | # py-multicodec 375 | # py-multihash 376 | vvm==0.3.2 377 | # via ape-vyper 378 | vyper==0.4.1 379 | # via ape-vyper 380 | watchdog==3.0.0 381 | # via eth-ape 382 | wcwidth==0.2.13 383 | # via prompt-toolkit 384 | web3==7.8.0 385 | # via 386 | # ape-alchemy 387 | # ape-foundry 388 | # eth-ape 389 | websockets==13.1 390 | # via web3 391 | wheel==0.45.1 392 | # via vyper 393 | yarl==1.18.3 394 | # via 395 | # aiohttp 396 | # ape-foundry 397 | zipp==3.21.0 398 | # via importlib-metadata 399 | -------------------------------------------------------------------------------- /3rd-party/gondi/GondiBaseLoan.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0 2 | pragma solidity ^0.8.21; 3 | 4 | import "@openzeppelin/utils/cryptography/MessageHashUtils.sol"; 5 | import "@openzeppelin/interfaces/IERC1271.sol"; 6 | 7 | import "@solmate/auth/Owned.sol"; 8 | import "@solmate/tokens/ERC721.sol"; 9 | import "@solmate/utils/FixedPointMathLib.sol"; 10 | 11 | import "../../interfaces/loans/IBaseLoan.sol"; 12 | import "../utils/Hash.sol"; 13 | import "../AddressManager.sol"; 14 | import "../LiquidationHandler.sol"; 15 | 16 | /// @title BaseLoan 17 | /// @author Florida St 18 | /// @notice Base implementation that we expect all loans to share. Offers can either be 19 | /// for new loans or renegotiating existing ones. 20 | /// Offers are signed off-chain. 21 | /// Offers have a nonce associated that is used for cancelling and 22 | /// marking as executed. 23 | abstract contract BaseLoan is ERC721TokenReceiver, IBaseLoan, LiquidationHandler { 24 | using FixedPointMathLib for uint256; 25 | using InputChecker for address; 26 | using MessageHashUtils for bytes32; 27 | 28 | /// @notice Used in compliance with EIP712 29 | uint256 internal immutable INITIAL_CHAIN_ID; 30 | bytes32 public immutable INITIAL_DOMAIN_SEPARATOR; 31 | 32 | bytes4 internal constant MAGICVALUE_1271 = 0x1626ba7e; 33 | 34 | /// @notice Precision used for calculating interests. 35 | uint256 internal constant _PRECISION = 10000; 36 | 37 | bytes public constant VERSION = "3"; 38 | 39 | /// @notice Minimum improvement (in BPS) required for a strict improvement. 40 | uint256 internal _minImprovementApr = 1000; 41 | 42 | string public name; 43 | 44 | /// @notice Total number of loans issued. Given it's a serial value, we use it 45 | /// as loan id. 46 | uint256 public override getTotalLoansIssued; 47 | 48 | /// @notice Offer capacity 49 | mapping(address user => mapping(uint256 offerId => uint256 used)) internal _used; 50 | 51 | /// @notice Used for validate off chain maker offers / canceling one 52 | mapping(address user => mapping(uint256 offerId => bool notActive)) public isOfferCancelled; 53 | /// @notice Used for validating off chain maker offers / canceling all 54 | mapping(address user => uint256 minOfferId) public minOfferId; 55 | 56 | /// @notice Used in a similar way as `isOfferCancelled` to handle renegotiations. 57 | mapping(address user => mapping(uint256 renegotiationIf => bool notActive)) public isRenegotiationOfferCancelled; 58 | 59 | /// @notice Loans are only denominated in whitelisted addresses. Within each struct, 60 | /// we save those as their `uint` representation. 61 | AddressManager internal immutable _currencyManager; 62 | 63 | /// @notice Only whilteslited collections are accepted as collateral. Within each struct, 64 | /// we save those as their `uint` representation. 65 | AddressManager internal immutable _collectionManager; 66 | 67 | event OfferCancelled(address lender, uint256 offerId); 68 | 69 | event AllOffersCancelled(address lender, uint256 minOfferId); 70 | 71 | event RenegotiationOfferCancelled(address lender, uint256 renegotiationId); 72 | 73 | event MinAprImprovementUpdated(uint256 _minimum); 74 | 75 | error CancelledOrExecutedOfferError(address _lender, uint256 _offerId); 76 | 77 | error ExpiredOfferError(uint256 _expirationTime); 78 | 79 | error LowOfferIdError(address _lender, uint256 _newMinOfferId, uint256 _minOfferId); 80 | 81 | error LowRenegotiationOfferIdError(address _lender, uint256 _newMinRenegotiationOfferId, uint256 _minOfferId); 82 | 83 | error ZeroInterestError(); 84 | 85 | error InvalidSignatureError(); 86 | 87 | error CurrencyNotWhitelistedError(); 88 | 89 | error CollectionNotWhitelistedError(); 90 | 91 | error MaxCapacityExceededError(); 92 | 93 | error InvalidLoanError(uint256 _loanId); 94 | 95 | error NotStrictlyImprovedError(); 96 | 97 | error InvalidAmountError(uint256 _amount, uint256 _principalAmount); 98 | 99 | /// @notice Constructor 100 | /// @param _name The name of the loan contract 101 | /// @param currencyManager The address of the currency manager 102 | /// @param collectionManager The address of the collection manager 103 | /// @param protocolFee The protocol fee 104 | /// @param loanLiquidator The liquidator contract 105 | /// @param owner The owner of the contract 106 | /// @param minWaitTime The time to wait before a new owner can be set 107 | constructor( 108 | string memory _name, 109 | address currencyManager, 110 | address collectionManager, 111 | ProtocolFee memory protocolFee, 112 | address loanLiquidator, 113 | address owner, 114 | uint256 minWaitTime 115 | ) LiquidationHandler(owner, minWaitTime, loanLiquidator, protocolFee) { 116 | name = _name; 117 | currencyManager.checkNotZero(); 118 | collectionManager.checkNotZero(); 119 | 120 | _currencyManager = AddressManager(currencyManager); 121 | _collectionManager = AddressManager(collectionManager); 122 | 123 | INITIAL_CHAIN_ID = block.chainid; 124 | INITIAL_DOMAIN_SEPARATOR = _computeDomainSeparator(); 125 | } 126 | 127 | /// @return The minimum improvement for a loan to be considered strictly better. 128 | function getMinImprovementApr() external view returns (uint256) { 129 | return _minImprovementApr; 130 | } 131 | 132 | /// @notice Updates the minimum improvement for a loan to be considered strictly better. 133 | /// Only the owner can call this function. 134 | /// @param _newMinimum The new minimum improvement. 135 | function updateMinImprovementApr(uint256 _newMinimum) external onlyOwner { 136 | _minImprovementApr = _newMinimum; 137 | 138 | emit MinAprImprovementUpdated(_minImprovementApr); 139 | } 140 | 141 | /// @return Address of the currency manager. 142 | function getCurrencyManager() external view returns (address) { 143 | return address(_currencyManager); 144 | } 145 | 146 | /// @return Address of the collection manager. 147 | function getCollectionManager() external view returns (address) { 148 | return address(_collectionManager); 149 | } 150 | 151 | /// @inheritdoc IBaseLoan 152 | function cancelOffer(uint256 _offerId) external { 153 | address user = msg.sender; 154 | isOfferCancelled[user][_offerId] = true; 155 | 156 | emit OfferCancelled(user, _offerId); 157 | } 158 | 159 | /// @inheritdoc IBaseLoan 160 | function cancelAllOffers(uint256 _minOfferId) external virtual { 161 | address user = msg.sender; 162 | uint256 currentMinOfferId = minOfferId[user]; 163 | if (currentMinOfferId >= _minOfferId) { 164 | revert LowOfferIdError(user, _minOfferId, currentMinOfferId); 165 | } 166 | minOfferId[user] = _minOfferId; 167 | 168 | emit AllOffersCancelled(user, _minOfferId); 169 | } 170 | 171 | /// @inheritdoc IBaseLoan 172 | function cancelRenegotiationOffer(uint256 _renegotiationId) external virtual { 173 | address lender = msg.sender; 174 | isRenegotiationOfferCancelled[lender][_renegotiationId] = true; 175 | 176 | emit RenegotiationOfferCancelled(lender, _renegotiationId); 177 | } 178 | 179 | /// @notice Returns the remaining capacity for a given loan offer. 180 | /// @param _lender The address of the lender. 181 | /// @param _offerId The id of the offer. 182 | /// @return The amount lent out. 183 | function getUsedCapacity(address _lender, uint256 _offerId) external view returns (uint256) { 184 | return _used[_lender][_offerId]; 185 | } 186 | 187 | /// @notice Get the domain separator requried to comply with EIP-712. 188 | function DOMAIN_SEPARATOR() public view returns (bytes32) { 189 | return block.chainid == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : _computeDomainSeparator(); 190 | } 191 | 192 | /// @notice Call when issuing a new loan to get/set a unique serial id. 193 | /// @dev This id should never be 0. 194 | /// @return The new loan id. 195 | function _getAndSetNewLoanId() internal returns (uint256) { 196 | unchecked { 197 | return ++getTotalLoansIssued; 198 | } 199 | } 200 | 201 | /// @notice Compute domain separator for EIP-712. 202 | /// @return The domain separator. 203 | function _computeDomainSeparator() private view returns (bytes32) { 204 | return keccak256( 205 | abi.encode( 206 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), 207 | keccak256(bytes(name)), 208 | keccak256(VERSION), 209 | block.chainid, 210 | address(this) 211 | ) 212 | ); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /contracts/GondiProxy.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | from ethereum.ercs import IERC721 4 | from ethereum.ercs import IERC20 5 | 6 | 7 | FLASH_LOAN_CALLBACK_SIZE: constant(uint256) = 32*1024 8 | FLASH_LOAN_MAX_TOKENS: constant(uint256) = 1 9 | 10 | interface P2PLendingNfts: 11 | def create_loan( 12 | offer: SignedOffer, 13 | collateral_token_id: uint256, 14 | collateral_proof: DynArray[bytes32, 32], 15 | delegate: address, 16 | borrower_broker_upfront_fee_amount: uint256, 17 | borrower_broker_settlement_fee_bps: uint256, 18 | borrower_broker: address 19 | ) -> bytes32: nonpayable 20 | def payment_token() -> address: view 21 | 22 | 23 | interface IGondiMultiSourceLoan: 24 | def repayLoan(repaymentData: LoanRepaymentData): nonpayable 25 | 26 | 27 | interface IFlashLender: 28 | def flashLoan( 29 | recepient: address, 30 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 31 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 32 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 33 | ): nonpayable 34 | 35 | 36 | interface IFlashLoanRecipient: 37 | def receiveFlashLoan( 38 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 39 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 40 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 41 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 42 | ): nonpayable 43 | 44 | 45 | implements: IFlashLoanRecipient 46 | 47 | 48 | struct Tranche: 49 | loanId: uint256 50 | floor: uint256 51 | principalAmount: uint256 52 | lender: address 53 | accruedInterest: uint256 54 | startTime: uint256 55 | aprBps: uint256 56 | 57 | struct GondiLoan: 58 | borrower: address 59 | nftCollateralTokenId: uint256 60 | nftCollateralAddress: address 61 | principalAddress: address 62 | principalAmount: uint256 63 | startTime: uint256 64 | duration: uint256 65 | tranche: DynArray[Tranche, 32] 66 | protocolFee: uint256 67 | 68 | 69 | struct SignableRepaymentData: 70 | loanId: uint256 71 | callbackData: Bytes[1024] 72 | shouldDelegate: bool 73 | 74 | struct LoanRepaymentData: 75 | data: SignableRepaymentData 76 | loan: GondiLoan 77 | borrowerSignature: Bytes[1024] 78 | 79 | 80 | 81 | flag FeeType: 82 | PROTOCOL_FEE 83 | ORIGINATION_FEE 84 | LENDER_BROKER_FEE 85 | BORROWER_BROKER_FEE 86 | 87 | 88 | struct Fee: 89 | type: FeeType 90 | upfront_amount: uint256 91 | interest_bps: uint256 92 | wallet: address 93 | 94 | flag OfferType: 95 | TOKEN 96 | COLLECTION 97 | TRAIT 98 | 99 | struct Offer: 100 | principal: uint256 101 | interest: uint256 102 | payment_token: address 103 | duration: uint256 104 | origination_fee_amount: uint256 105 | broker_upfront_fee_amount: uint256 106 | broker_settlement_fee_bps: uint256 107 | broker_address: address 108 | offer_type: OfferType 109 | token_id: uint256 110 | token_range_min: uint256 111 | token_range_max: uint256 112 | collection_key_hash: bytes32 113 | trait_hash: bytes32 114 | expiration: uint256 115 | lender: address 116 | pro_rata: bool 117 | size: uint256 118 | tracing_id: bytes32 119 | 120 | 121 | struct Signature: 122 | v: uint256 123 | r: uint256 124 | s: uint256 125 | 126 | struct SignedOffer: 127 | offer: Offer 128 | signature: Signature 129 | 130 | struct Loan: 131 | id: bytes32 132 | offer_id: bytes32 133 | offer_tracing_id: bytes32 134 | amount: uint256 # principal - origination_fee_amount 135 | interest: uint256 136 | payment_token: address 137 | maturity: uint256 138 | start_time: uint256 139 | borrower: address 140 | lender: address 141 | collateral_contract: address 142 | collateral_token_id: uint256 143 | fees: DynArray[Fee, MAX_FEES] 144 | pro_rata: bool 145 | delegate: address 146 | 147 | PROOF_MAX_SIZE: constant(uint256) = 32 148 | 149 | struct CallbackData: 150 | gondi_contract: address 151 | # approved: address 152 | payment_token: address 153 | # loan_id: uint256 154 | amount: uint256 155 | loan_repayment_data: LoanRepaymentData 156 | signed_offer: SignedOffer 157 | borrower: address 158 | token_id: uint256 159 | collateral_proof: DynArray[bytes32, PROOF_MAX_SIZE] 160 | delegate: address 161 | borrower_broker_upfront_fee_amount: uint256 162 | borrower_broker_settlement_fee_bps: uint256 163 | borrower_broker: address 164 | 165 | 166 | ERC3156_CALLBACK_OK: constant(bytes32) = keccak256("ERC3156FlashBorrower.onFlashLoan") 167 | MAX_FEES: constant(uint256) = 4 168 | BPS: constant(uint256) = 10000 169 | 170 | p2p_lending_nfts: public(immutable(address)) 171 | flash_lender: public(immutable(address)) 172 | 173 | 174 | @deploy 175 | def __init__(_p2p_lending_nfts: address, _flash_lender: address): 176 | p2p_lending_nfts = _p2p_lending_nfts 177 | flash_lender = _flash_lender 178 | 179 | 180 | @external 181 | def receiveFlashLoan( 182 | tokens: DynArray[address,FLASH_LOAN_MAX_TOKENS], 183 | amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 184 | fee_amounts: DynArray[uint256,FLASH_LOAN_MAX_TOKENS], 185 | data: Bytes[FLASH_LOAN_CALLBACK_SIZE] 186 | ) : 187 | 188 | # raw_call(0x0000000000000000000000000000000000011111, abi_encode(b"callback")) 189 | assert msg.sender == flash_lender, "unauthorized" 190 | assert fee_amounts[0] == 0, "fee not supported" 191 | 192 | callback_data: CallbackData = abi_decode(data, CallbackData) 193 | 194 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 195 | assert tokens[0] == payment_token, "Invalid asset" 196 | 197 | assert (staticcall IERC20(payment_token).balanceOf(self)) >= amounts[0], "Insufficient balance" 198 | 199 | extcall IERC20(payment_token).transfer(callback_data.borrower, amounts[0]) 200 | assert (staticcall IERC20(payment_token).balanceOf(callback_data.borrower)) >= amounts[0], "Insufficient balance" 201 | 202 | extcall IGondiMultiSourceLoan(callback_data.gondi_contract).repayLoan(callback_data.loan_repayment_data) 203 | 204 | assert (staticcall IERC721(callback_data.loan_repayment_data.loan.nftCollateralAddress).ownerOf(callback_data.token_id)) == callback_data.borrower, "NFT not returned to user" 205 | 206 | self._create_loan( 207 | callback_data.signed_offer, 208 | callback_data.token_id, 209 | callback_data.collateral_proof, 210 | callback_data.delegate, 211 | callback_data.borrower_broker_upfront_fee_amount, 212 | callback_data.borrower_broker_settlement_fee_bps, 213 | callback_data.borrower_broker 214 | ) 215 | 216 | assert (staticcall IERC20(payment_token).balanceOf(callback_data.borrower)) >= amounts[0], "Insufficient balance" 217 | extcall IERC20(payment_token).transferFrom(callback_data.borrower, flash_lender, amounts[0]) 218 | 219 | 220 | @internal 221 | def _create_loan( 222 | offer: SignedOffer, 223 | collateral_token_id: uint256, 224 | proof: DynArray[bytes32, 32], 225 | delegate: address, 226 | borrower_broker_upfront_fee_amount: uint256, 227 | borrower_broker_settlement_fee_bps: uint256, 228 | borrower_broker: address 229 | ) -> bytes32: 230 | return extcall P2PLendingNfts(p2p_lending_nfts).create_loan( 231 | offer, 232 | collateral_token_id, 233 | proof, 234 | delegate, 235 | borrower_broker_upfront_fee_amount, 236 | borrower_broker_settlement_fee_bps, 237 | borrower_broker 238 | ) 239 | 240 | 241 | @external 242 | def refinance_loan( 243 | gondi_contract: address, 244 | approved: address, 245 | loan_repayment_data: LoanRepaymentData, 246 | amount: uint256, 247 | signed_offer: SignedOffer, 248 | token_id: uint256, 249 | collateral_proof: DynArray[bytes32, PROOF_MAX_SIZE], 250 | delegate: address, 251 | borrower_broker_upfront_fee_amount: uint256, 252 | borrower_broker_settlement_fee_bps: uint256, 253 | borrower_broker: address 254 | ): 255 | 256 | payment_token: address = staticcall P2PLendingNfts(p2p_lending_nfts).payment_token() 257 | callback_data: CallbackData = CallbackData( 258 | gondi_contract = gondi_contract, 259 | payment_token = payment_token, 260 | amount = amount, 261 | loan_repayment_data = loan_repayment_data, 262 | signed_offer = signed_offer, 263 | borrower = msg.sender, 264 | token_id = token_id, 265 | collateral_proof = collateral_proof, 266 | delegate = delegate, 267 | borrower_broker_upfront_fee_amount = borrower_broker_upfront_fee_amount, 268 | borrower_broker_settlement_fee_bps = borrower_broker_settlement_fee_bps, 269 | borrower_broker = borrower_broker 270 | ) 271 | 272 | extcall IFlashLender(flash_lender).flashLoan( 273 | self, 274 | [payment_token], 275 | [amount], 276 | abi_encode(callback_data) 277 | ) 278 | -------------------------------------------------------------------------------- /contracts/auxiliary/DelegateRegistry2_abi.json: -------------------------------------------------------------------------------- 1 | [{"inputs":[],"name":"MulticallFailed","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"bytes32","name":"rights","type":"bytes32"},{"indexed":false,"internalType":"bool","name":"enable","type":"bool"}],"name":"DelegateAll","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"contract_","type":"address"},{"indexed":false,"internalType":"bytes32","name":"rights","type":"bytes32"},{"indexed":false,"internalType":"bool","name":"enable","type":"bool"}],"name":"DelegateContract","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"contract_","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"rights","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"DelegateERC1155","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"contract_","type":"address"},{"indexed":false,"internalType":"bytes32","name":"rights","type":"bytes32"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"DelegateERC20","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":true,"internalType":"address","name":"contract_","type":"address"},{"indexed":false,"internalType":"uint256","name":"tokenId","type":"uint256"},{"indexed":false,"internalType":"bytes32","name":"rights","type":"bytes32"},{"indexed":false,"internalType":"bool","name":"enable","type":"bool"}],"name":"DelegateERC721","type":"event"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"}],"name":"checkDelegateForAll","outputs":[{"internalType":"bool","name":"valid","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"}],"name":"checkDelegateForContract","outputs":[{"internalType":"bool","name":"valid","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes32","name":"rights","type":"bytes32"}],"name":"checkDelegateForERC1155","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"}],"name":"checkDelegateForERC20","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes32","name":"rights","type":"bytes32"}],"name":"checkDelegateForERC721","outputs":[{"internalType":"bool","name":"valid","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"},{"internalType":"bool","name":"enable","type":"bool"}],"name":"delegateAll","outputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"},{"internalType":"bool","name":"enable","type":"bool"}],"name":"delegateContract","outputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes32","name":"rights","type":"bytes32"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"delegateERC1155","outputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"delegateERC20","outputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"bytes32","name":"rights","type":"bytes32"},{"internalType":"bool","name":"enable","type":"bool"}],"name":"delegateERC721","outputs":[{"internalType":"bytes32","name":"hash","type":"bytes32"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"hashes","type":"bytes32[]"}],"name":"getDelegationsFromHashes","outputs":[{"components":[{"internalType":"enum IDelegateRegistry.DelegationType","name":"type_","type":"uint8"},{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"}],"internalType":"struct IDelegateRegistry.Delegation[]","name":"delegations_","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"getIncomingDelegationHashes","outputs":[{"internalType":"bytes32[]","name":"delegationHashes","type":"bytes32[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"}],"name":"getIncomingDelegations","outputs":[{"components":[{"internalType":"enum IDelegateRegistry.DelegationType","name":"type_","type":"uint8"},{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"}],"internalType":"struct IDelegateRegistry.Delegation[]","name":"delegations_","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"}],"name":"getOutgoingDelegationHashes","outputs":[{"internalType":"bytes32[]","name":"delegationHashes","type":"bytes32[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"}],"name":"getOutgoingDelegations","outputs":[{"components":[{"internalType":"enum IDelegateRegistry.DelegationType","name":"type_","type":"uint8"},{"internalType":"address","name":"to","type":"address"},{"internalType":"address","name":"from","type":"address"},{"internalType":"bytes32","name":"rights","type":"bytes32"},{"internalType":"address","name":"contract_","type":"address"},{"internalType":"uint256","name":"tokenId","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"}],"internalType":"struct IDelegateRegistry.Delegation[]","name":"delegations_","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes[]","name":"data","type":"bytes[]"}],"name":"multicall","outputs":[{"internalType":"bytes[]","name":"results","type":"bytes[]"}],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"bytes32","name":"location","type":"bytes32"}],"name":"readSlot","outputs":[{"internalType":"bytes32","name":"contents","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32[]","name":"locations","type":"bytes32[]"}],"name":"readSlots","outputs":[{"internalType":"bytes32[]","name":"contents","type":"bytes32[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"sweep","outputs":[],"stateMutability":"nonpayable","type":"function"}] --------------------------------------------------------------------------------