Zharta
Source

https://etherscan.io/address/0xb7c8c74ed765267b54f4c327f279d7e850725ef2 (opens in a new tab)

# @version 0.3.7
 
"""
@title Loans
@author [Zharta](https://zharta.io/)
@notice The loans contract exists as the main interface to create peer-to-pool NFT-backed loans
@dev Uses a `LoansCore` contract to store state
"""
 
# Interfaces
 
interface IERC721:
    def ownerOf(_tokenId: uint256) -> address: view
 
interface IERC20:
    def balanceOf(_owner: address) -> uint256: view
    def allowance(_owner: address, _operator: address) -> uint256: view
 
interface ILoansCore:
    def isLoanCreated(_borrower: address, _loanId: uint256) -> bool: view
    def addLoan(_borrower: address, _amount: uint256, _interest: uint256, _maturity: uint256, _collaterals: DynArray[Collateral, 100]) -> uint256: nonpayable
    def addCollateralToLoan(_borrower: address, _collateral: Collateral, _loanId: uint256): nonpayable
    def updateCollaterals(_collateral: Collateral, _value: bool): nonpayable
    def updateLoanStarted(_borrower: address, _loanId: uint256): nonpayable
    def updateHighestSingleCollateralLoan(_borrower: address, _loanId: uint256): nonpayable
    def updateHighestCollateralBundleLoan(_borrower: address, _loanId: uint256): nonpayable
    def getLoan(_borrower: address, _loanId: uint256) -> Loan: view
    def isLoanStarted(_borrower: address, _loanId: uint256) -> bool: view
    def updateLoanPaidAmount(_borrower: address, _loanId: uint256, _amount: uint256, _interestAmount: uint256): nonpayable
    def updatePaidLoan(_borrower: address, _loanId: uint256): nonpayable
    def updateHighestRepayment(_borrower: address, _loanId: uint256): nonpayable
    def updateDefaultedLoan(_borrower: address, _loanId: uint256): nonpayable
    def removeCollateralFromLoan(_borrower: address, _collateral: Collateral, _loanId: uint256): nonpayable
    def updateHighestDefaultedLoan(_borrower: address, _loanId: uint256): nonpayable
 
interface ICollateralVaultPeripheral:
    def isCollateralApprovedForVault(_borrower: address, _collateralAddress: address, _tokenId: uint256) -> bool: view
    def storeCollateral(_borrower: address, _collateralAddress: address, _tokenId: uint256, _erc20TokenContract: address, _delegations: bool): nonpayable
    def transferCollateralFromLoan(_borrower: address, _collateralAddress: address, _tokenId: uint256, _erc20TokenContract: address): nonpayable
    def setCollateralDelegation(_borrower: address, _collateralAddress: address, _tokenId: uint256, _erc20TokenContract: address, _delegations: bool): nonpayable
 
interface ILiquidityControls:
    def withinCollectionShareLimit(_collectionAmount: uint256, _collectionAddress: address, _loansCoreContract: address, _lendingPoolCoreContract: address) -> bool: view
    def withinLoansPoolShareLimit(_borrower: address, _amount: uint256, _loansCoreContract: address, _lendingPoolCoreContract: address) -> bool: view
 
interface IERC20Symbol:
    def symbol() -> String[100]: view
 
interface ILendingPoolPeripheral:
    def maxFundsInvestable() -> uint256: view 
    def erc20TokenContract() -> address: view
    def sendFundsEth(_to: address, _amount: uint256): nonpayable
    def sendFundsWeth(_to: address, _amount: uint256): nonpayable
    def receiveFundsEth(_borrower: address, _amount: uint256, _rewardsAmount: uint256): payable
    def receiveFundsWeth(_borrower: address, _amount: uint256, _rewardsAmount: uint256): payable
    def lendingPoolCoreContract() -> address: view
 
interface ILiquidationsPeripheral:
    def addLiquidation(_borrower: address, _loanId: uint256, _erc20TokenContract: address): nonpayable
 
# Structs
 
struct Collateral:
    contractAddress: address
    tokenId: uint256
    amount: uint256
 
struct Loan:
    id: uint256
    amount: uint256
    interest: uint256 # parts per 10000, e.g. 2.5% is represented by 250 parts per 10000
    maturity: uint256
    startTime: uint256
    collaterals: DynArray[Collateral, 100]
    paidPrincipal: uint256
    paidInterestAmount: uint256
    started: bool
    invalidated: bool
    paid: bool
    defaulted: bool
    canceled: bool
 
 
struct EIP712Domain:
    name: String[100]
    version: String[10]
    chain_id: uint256
    verifying_contract: address
 
struct ReserveMessageContent:
    amount: uint256
    interest: uint256
    maturity: uint256
    collaterals: DynArray[Collateral, 100]
    delegations: DynArray[bool, 100]
    deadline: uint256
 
 
# Events
 
event OwnershipTransferred:
    ownerIndexed: indexed(address)
    proposedOwnerIndexed: indexed(address)
    owner: address
    proposedOwner: address
    erc20TokenContract: address
 
event OwnerProposed:
    ownerIndexed: indexed(address)
    proposedOwnerIndexed: indexed(address)
    owner: address
    proposedOwner: address
    erc20TokenContract: address
 
event InterestAccrualPeriodChanged:
    erc20TokenContractIndexed: indexed(address)
    currentValue: uint256
    newValue: uint256
    erc20TokenContract: address
 
event LendingPoolPeripheralAddressSet:
    erc20TokenContractIndexed: indexed(address)
    currentValue: address
    newValue: address
    erc20TokenContract: address
 
event CollateralVaultPeripheralAddressSet:
    erc20TokenContractIndexed: indexed(address)
    currentValue: address
    newValue: address
    erc20TokenContract: address
 
event LiquidationsPeripheralAddressSet:
    erc20TokenContractIndexed: indexed(address)
    currentValue: address
    newValue: address
    erc20TokenContract: address
 
event LiquidityControlsAddressSet:
    erc20TokenContractIndexed: indexed(address)
    currentValue: address
    newValue: address
    erc20TokenContract: address
 
event ContractStatusChanged:
    erc20TokenContractIndexed: indexed(address)
    value: bool
    erc20TokenContract: address
 
event ContractDeprecated:
    erc20TokenContractIndexed: indexed(address)
    erc20TokenContract: address
 
event LoanCreated:
    walletIndexed: indexed(address)
    wallet: address
    loanId: uint256
    erc20TokenContract: address
    apr: uint256 # calculated from the interest to 365 days, in bps
    amount: uint256
    duration: uint256
    collaterals: DynArray[Collateral, 100]
    genesisToken: uint256
 
event LoanPayment:
    walletIndexed: indexed(address)
    wallet: address
    loanId: uint256
    principal: uint256
    interestAmount: uint256
    erc20TokenContract: address
 
event LoanPaid:
    walletIndexed: indexed(address)
    wallet: address
    loanId: uint256
    erc20TokenContract: address
 
event LoanDefaulted:
    walletIndexed: indexed(address)
    wallet: address
    loanId: uint256
    amount: uint256
    erc20TokenContract: address
 
event PaymentSent:
    walletIndexed: indexed(address)
    wallet: address
    amount: uint256
 
event PaymentReceived:
    walletIndexed: indexed(address)
    wallet: address
    amount: uint256
 
 
# Global variables
 
owner: public(address)
proposedOwner: public(address)
 
interestAccrualPeriod: public(uint256)
 
isAcceptingLoans: public(bool)
isDeprecated: public(bool)
 
loansCoreContract: public(address)
lendingPoolPeripheralContract: public(address)
collateralVaultPeripheralContract: public(address)
liquidationsPeripheralContract: public(address)
liquidityControlsContract: public(address)
genesisContract: public(address)
 
collectionsAmount: HashMap[address, uint256] # aux variable
 
ZHARTA_DOMAIN_NAME: constant(String[6]) = "Zharta"
ZHARTA_DOMAIN_VERSION: constant(String[1]) = "1"
 
COLLATERAL_TYPE_DEF: constant(String[66]) = "Collateral(address contractAddress,uint256 tokenId,uint256 amount)"
RESERVE_TYPE_DEF: constant(String[269]) = "ReserveMessageContent(address borrower,uint256 amount,uint256 interest,uint256 maturity,Collateral[] collaterals," \
                                          "bool delegations,uint256 deadline,uint256 nonce,uint256 genesisToken)" \
                                          "Collateral(address contractAddress,uint256 tokenId,uint256 amount)"
DOMAIN_TYPE_HASH: constant(bytes32) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")
COLLATERAL_TYPE_HASH: constant(bytes32) = keccak256(COLLATERAL_TYPE_DEF)
RESERVE_TYPE_HASH: constant(bytes32) = keccak256(RESERVE_TYPE_DEF)
 
reserve_message_typehash: bytes32
reserve_sig_domain_separator: bytes32
 
MINIMUM_INTEREST_PERIOD: constant(uint256) = 604800  # 7 days
 
 
@external
def __init__(
    _interestAccrualPeriod: uint256,
    _loansCoreContract: address,
    _lendingPoolPeripheralContract: address,
    _collateralVaultPeripheralContract: address,
    _genesisContract: address
):
    assert _loansCoreContract != empty(address), "address is the zero address"
    assert _lendingPoolPeripheralContract != empty(address), "address is the zero address"
    assert _collateralVaultPeripheralContract != empty(address), "address is the zero address"
    assert _genesisContract != empty(address), "address is the zero address"
 
    self.owner = msg.sender
    self.interestAccrualPeriod = _interestAccrualPeriod
    self.loansCoreContract = _loansCoreContract
    self.lendingPoolPeripheralContract = _lendingPoolPeripheralContract
    self.collateralVaultPeripheralContract = _collateralVaultPeripheralContract
    self.genesisContract = _genesisContract
    self.isAcceptingLoans = True
 
    self.reserve_sig_domain_separator = keccak256(
        _abi_encode(
            DOMAIN_TYPE_HASH,
            keccak256(ZHARTA_DOMAIN_NAME),
            keccak256(ZHARTA_DOMAIN_VERSION),
            chain.id,
            self
        )
    )
 
 
@internal
def _areCollateralsOwned(_borrower: address, _collaterals: DynArray[Collateral, 100]) -> bool:
    for collateral in _collaterals:
        if IERC721(collateral.contractAddress).ownerOf(collateral.tokenId) != _borrower:
            return False
    return True
 
 
@view
@internal
def _areCollateralsApproved(_borrower: address, _collaterals: DynArray[Collateral, 100]) -> bool:
    for collateral in _collaterals:
        if not ICollateralVaultPeripheral(self.collateralVaultPeripheralContract).isCollateralApprovedForVault(
            _borrower,
            collateral.contractAddress,
            collateral.tokenId
        ):
            return False
    return True
 
 
@pure
@internal
def _collateralsAmounts(_collaterals: DynArray[Collateral, 100]) -> uint256:
    sumAmount: uint256 = 0
    for collateral in _collaterals:
        sumAmount += collateral.amount
 
    return sumAmount
 
 
@internal
def _withinCollectionShareLimit(_collaterals: DynArray[Collateral, 100]) -> bool:
    collections: DynArray[address, 100] = empty(DynArray[address, 100])
 
    for collateral in _collaterals:
        if collateral.contractAddress not in collections:
            collections.append(collateral.contractAddress)
            self.collectionsAmount[collateral.contractAddress] = 0
 
        self.collectionsAmount[collateral.contractAddress] += collateral.amount
 
    for collection in collections:
        result: bool = ILiquidityControls(self.liquidityControlsContract).withinCollectionShareLimit(
            self.collectionsAmount[collection],
            collection,
            self.loansCoreContract,
            ILendingPoolPeripheral(self.lendingPoolPeripheralContract).lendingPoolCoreContract()
        )
        if not result:
            return False
 
    return True
 
@pure
@internal
def _loanPayableAmount(
    _amount: uint256,
    _paidAmount: uint256,
    _interest: uint256,
    _maxLoanDuration: uint256,
    _timePassed: uint256,
    _interestAccrualPeriod: uint256
) -> uint256:
    return (_amount - _paidAmount) * (10000 * _maxLoanDuration + _interest * (max(_timePassed + _interestAccrualPeriod, MINIMUM_INTEREST_PERIOD))) / (10000 * _maxLoanDuration)
 
 
@pure
@internal
def _computePeriodPassedInSeconds(_recentTimestamp: uint256, _olderTimestamp: uint256, _period: uint256) -> uint256:
    return (_recentTimestamp - _olderTimestamp) - ((_recentTimestamp - _olderTimestamp) % _period)
 
 
@internal
def _recoverReserveSigner(
    _borrower: address,
    _amount: uint256,
    _interest: uint256,
    _maturity: uint256,
    _collaterals: DynArray[Collateral, 100],
    _delegations: bool,
    _deadline: uint256,
    _nonce: uint256,
    _genesisToken: uint256,
    _v: uint256,
    _r: uint256,
    _s: uint256
) -> address:
    """
        @notice recovers the sender address of the signed reserve function call
    """
    collaterals_data_hash: DynArray[bytes32, 100] = []
    for c in _collaterals:
        collaterals_data_hash.append(keccak256(_abi_encode(COLLATERAL_TYPE_HASH, c.contractAddress, c.tokenId, c.amount)))
 
    data_hash: bytes32 = keccak256(_abi_encode(
                RESERVE_TYPE_HASH,
                _borrower,
                _amount,
                _interest,
                _maturity,
                keccak256(slice(_abi_encode(collaterals_data_hash), 32*2, 32*len(_collaterals))),
                _delegations,
                _deadline,
                _nonce,
                _genesisToken
                ))
 
    sig_hash: bytes32 = keccak256(concat(convert("\x19\x01", Bytes[2]), _abi_encode(self.reserve_sig_domain_separator, data_hash)))
    signer: address = ecrecover(sig_hash, _v, _r, _s)
 
    return signer
 
 
@internal
def _reserve(
    _amount: uint256,
    _interest: uint256,
    _maturity: uint256,
    _collaterals: DynArray[Collateral, 100],
    _delegations: bool,
    _deadline: uint256,
    _nonce: uint256,
    _genesisToken: uint256,
    _v: uint256,
    _r: uint256,
    _s: uint256
) -> uint256:
    assert not self.isDeprecated, "contract is deprecated"
    assert self.isAcceptingLoans, "contract is not accepting loans"
    assert block.timestamp < _maturity, "maturity is in the past"
    assert block.timestamp <= _deadline, "deadline has passed"
    assert self._areCollateralsOwned(msg.sender, _collaterals), "msg.sender does not own all NFTs"
    assert self._areCollateralsApproved(msg.sender, _collaterals) == True, "not all NFTs are approved"
    assert self._collateralsAmounts(_collaterals) == _amount, "amount in collats != than amount"
    assert ILendingPoolPeripheral(self.lendingPoolPeripheralContract).maxFundsInvestable() >= _amount, "insufficient liquidity"
 
    assert ILiquidityControls(self.liquidityControlsContract).withinLoansPoolShareLimit(
        msg.sender,
        _amount,
        self.loansCoreContract,
        self.lendingPoolPeripheralContract
    ), "max loans pool share surpassed"
    assert self._withinCollectionShareLimit(_collaterals), "max collection share surpassed"
 
    assert not ILoansCore(self.loansCoreContract).isLoanCreated(msg.sender, _nonce), "loan already created"
    if _nonce > 0:
        assert ILoansCore(self.loansCoreContract).isLoanCreated(msg.sender, _nonce - 1), "loan is not sequential"
    
    signer: address = self._recoverReserveSigner(msg.sender, _amount, _interest, _maturity, _collaterals, _delegations, _deadline, _nonce, _genesisToken, _v, _r, _s)
    assert signer == self.owner, "invalid message signature"
 
    assert _genesisToken == 0 or IERC721(self.genesisContract).ownerOf(_genesisToken) == msg.sender, "genesisToken not owned"
 
    newLoanId: uint256 = ILoansCore(self.loansCoreContract).addLoan(
        msg.sender,
        _amount,
        _interest,
        _maturity,
        _collaterals
    )
 
    for collateral in _collaterals:
        ILoansCore(self.loansCoreContract).addCollateralToLoan(msg.sender, collateral, newLoanId)
        ILoansCore(self.loansCoreContract).updateCollaterals(collateral, False)
 
        ICollateralVaultPeripheral(self.collateralVaultPeripheralContract).storeCollateral(
            msg.sender,
            collateral.contractAddress,
            collateral.tokenId,
            ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
            _delegations
        )
 
    log LoanCreated(
        msg.sender,
        msg.sender,
        newLoanId,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
        _interest * 365 * 86400 / (_maturity - block.timestamp),
        _amount,
        _maturity - block.timestamp,
        _collaterals,
        _genesisToken
    )
 
    ILoansCore(self.loansCoreContract).updateLoanStarted(msg.sender, newLoanId)
    ILoansCore(self.loansCoreContract).updateHighestSingleCollateralLoan(msg.sender, newLoanId)
    ILoansCore(self.loansCoreContract).updateHighestCollateralBundleLoan(msg.sender, newLoanId)
 
    return newLoanId
 
 
@external
def proposeOwner(_address: address):
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert _address != empty(address), "_address it the zero address"
    assert self.owner != _address, "proposed owner addr is the owner"
    assert self.proposedOwner != _address, "proposed owner addr is the same"
 
    self.proposedOwner = _address
 
    log OwnerProposed(
        self.owner,
        _address,
        self.owner,
        _address,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
 
@external
def claimOwnership():
    assert msg.sender == self.proposedOwner, "msg.sender is not the proposed"
 
    log OwnershipTransferred(
        self.owner,
        self.proposedOwner,
        self.owner,
        self.proposedOwner,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
    self.owner = self.proposedOwner
    self.proposedOwner = empty(address)
 
 
@external
def changeInterestAccrualPeriod(_value: uint256):
    """
    @notice Sets the interest accrual period, considered on loan payment calculations
    @dev Logs `InterestAccrualPeriodChanged` event
    @param _value The interest accrual period in seconds
    """
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert _value != self.interestAccrualPeriod, "_value is the same"
 
    log InterestAccrualPeriodChanged(
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
        self.interestAccrualPeriod,
        _value,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
    self.interestAccrualPeriod = _value
 
 
@external
def setLendingPoolPeripheralAddress(_address: address):
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert _address != empty(address), "_address is the zero address"
    assert _address.is_contract, "_address is not a contract"
    assert self.lendingPoolPeripheralContract != _address, "new LPPeriph addr is the same"
 
    log LendingPoolPeripheralAddressSet(
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
        self.lendingPoolPeripheralContract,
        _address,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
    self.lendingPoolPeripheralContract = _address
 
 
@external
def setCollateralVaultPeripheralAddress(_address: address):
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert _address != empty(address), "_address is the zero address"
    assert _address.is_contract, "_address is not a contract"
    assert self.collateralVaultPeripheralContract != _address, "new LPCore addr is the same"
 
    log CollateralVaultPeripheralAddressSet(
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
        self.collateralVaultPeripheralContract,
        _address,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
    self.collateralVaultPeripheralContract = _address
 
 
@external
def setLiquidationsPeripheralAddress(_address: address):
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert _address != empty(address), "_address is the zero address"
    assert _address.is_contract, "_address is not a contract"
    assert self.liquidationsPeripheralContract != _address, "new LPCore addr is the same"
 
    log LiquidationsPeripheralAddressSet(
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
        self.liquidationsPeripheralContract,
        _address,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
    self.liquidationsPeripheralContract = _address
 
 
@external
def setLiquidityControlsAddress(_address: address):
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert _address != empty(address), "_address is the zero address"
    assert _address.is_contract, "_address is not a contract"
    assert _address != self.liquidityControlsContract, "new value is the same"
 
    log LiquidityControlsAddressSet(
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
        self.liquidityControlsContract,
        _address,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
    self.liquidityControlsContract = _address
 
 
@external
def changeContractStatus(_flag: bool):
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert self.isAcceptingLoans != _flag, "new contract status is the same"
 
    self.isAcceptingLoans = _flag
 
    log ContractStatusChanged(
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
        _flag,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
 
@external
def deprecate():
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert not self.isDeprecated, "contract is already deprecated"
 
    self.isDeprecated = True
    self.isAcceptingLoans = False
 
    log ContractDeprecated(
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
 
@view
@external
def erc20TokenSymbol() -> String[100]:
    return IERC20Symbol(ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()).symbol()
 
 
@view
@external
def getLoanPayableAmount(_borrower: address, _loanId: uint256, _timestamp: uint256) -> uint256:
    loan: Loan = ILoansCore(self.loansCoreContract).getLoan(_borrower, _loanId)
 
    if loan.paid:
        return 0
 
    if loan.startTime > _timestamp:
        return max_value(uint256)
 
    if loan.started:
        timePassed: uint256 = self._computePeriodPassedInSeconds(
            _timestamp,
            loan.startTime,
            self.interestAccrualPeriod
        )
        return self._loanPayableAmount(
            loan.amount,
            loan.paidPrincipal,
            loan.interest,
            loan.maturity - loan.startTime,
            timePassed,
            self.interestAccrualPeriod
        )
 
    return max_value(uint256)
 
 
@external
def reserveWeth(
    _amount: uint256,
    _interest: uint256,
    _maturity: uint256,
    _collaterals: DynArray[Collateral, 100],
    _delegations: bool,
    _deadline: uint256,
    _nonce: uint256,
    _genesisToken: uint256,
    _v: uint256,
    _r: uint256,
    _s: uint256
) -> uint256:
    """
    @notice Creates a new loan with the defined amount, interest rate and collateral. The message must be signed by the contract owner.
    @dev Logs `LoanCreated` event. The last 3 parameters must match a signature by the contract owner of the implicit message consisting of the remaining parameters, in order for the loan to be created
    @param _amount The loan amount in wei
    @param _interest The interest rate in bps (1/1000) for the loan duration
    @param _maturity The loan maturity in unix epoch format
    @param _collaterals The list of collaterals supporting the loan
    @param _delegations Wether to set the requesting wallet as a delegate for all collaterals
    @param _deadline The deadline of validity for the signed message in unix epoch format
    @param _genesisToken The optional Genesis Pass token used to determine the loan conditions, must be > 0
    @param _v recovery id for public key recover
    @param _r r value in ECDSA signature
    @param _s s value in ECDSA signature
    @return The loan id
    """
 
    newLoanId: uint256 = self._reserve(_amount, _interest, _maturity, _collaterals, _delegations, _deadline, _nonce, _genesisToken, _v, _r, _s)
 
    ILendingPoolPeripheral(self.lendingPoolPeripheralContract).sendFundsWeth(
        msg.sender,
        _amount
    )
 
    return newLoanId
 
 
@external
def reserveEth(
    _amount: uint256,
    _interest: uint256,
    _maturity: uint256,
    _collaterals: DynArray[Collateral, 100],
    _delegations: bool,
    _deadline: uint256,
    _nonce: uint256,
    _genesisToken: uint256,
    _v: uint256,
    _r: uint256,
    _s: uint256
) -> uint256:
    """
    @notice Creates a new loan with the defined amount, interest rate and collateral. The message must be signed by the contract owner.
    @dev Logs `LoanCreated` event. The last 3 parameters must match a signature by the contract owner of the implicit message consisting of the remaining parameters, in order for the loan to be created
    @param _amount The loan amount in wei
    @param _interest The interest rate in bps (1/1000) for the loan duration
    @param _maturity The loan maturity in unix epoch format
    @param _collaterals The list of collaterals supporting the loan
    @param _delegations Wether to set the requesting wallet as a delegate for all collaterals
    @param _deadline The deadline of validity for the signed message in unix epoch format
    @param _genesisToken The optional Genesis Pass token used to determine the loan conditions, must be > 0
    @param _v recovery id for public key recover
    @param _r r value in ECDSA signature
    @param _s s value in ECDSA signature
    @return The loan id
    """
 
    newLoanId: uint256 = self._reserve(_amount, _interest, _maturity, _collaterals, _delegations, _deadline, _nonce, _genesisToken, _v, _r, _s)
 
    ILendingPoolPeripheral(self.lendingPoolPeripheralContract).sendFundsEth(
        msg.sender,
        _amount
    )
 
    return newLoanId
 
 
@payable
@external
def pay(_loanId: uint256):
 
    """
    @notice Closes an active loan by paying the full amount
    @dev Logs the `LoanPayment` and `LoanPaid` events. The associated `LendingPoolCore` contract must be approved for the payment amount
    @param _loanId The id of the loan to settle
    """
 
    receivedAmount: uint256 = msg.value
    assert ILoansCore(self.loansCoreContract).isLoanStarted(msg.sender, _loanId), "loan not found"
    
    loan: Loan = ILoansCore(self.loansCoreContract).getLoan(msg.sender, _loanId)
    assert block.timestamp <= loan.maturity, "loan maturity reached"
    assert not loan.paid, "loan already paid"
 
    # compute days passed in seconds
    timePassed: uint256 = self._computePeriodPassedInSeconds(
        block.timestamp,
        loan.startTime,
        self.interestAccrualPeriod
    )
 
    # pro-rata computation of max amount payable based on actual loan duration in days
    paymentAmount: uint256 = self._loanPayableAmount(
        loan.amount,
        loan.paidPrincipal,
        loan.interest,
        loan.maturity - loan.startTime,
        timePassed,
        self.interestAccrualPeriod
    )
 
    erc20TokenContract: address = ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    excessAmount: uint256 = 0
 
    if receivedAmount > 0:
        assert receivedAmount >= paymentAmount, "insufficient value received"
        excessAmount = receivedAmount - paymentAmount
        log PaymentReceived(msg.sender, msg.sender, receivedAmount)
    else:
        assert IERC20(erc20TokenContract).balanceOf(msg.sender) >= paymentAmount, "insufficient balance"
        assert IERC20(erc20TokenContract).allowance(
                msg.sender,
                ILendingPoolPeripheral(self.lendingPoolPeripheralContract).lendingPoolCoreContract()
        ) >= paymentAmount, "insufficient allowance"
 
    paidInterestAmount: uint256 = paymentAmount - loan.amount
 
    ILoansCore(self.loansCoreContract).updateLoanPaidAmount(msg.sender, _loanId, loan.amount, paidInterestAmount)
    ILoansCore(self.loansCoreContract).updatePaidLoan(msg.sender, _loanId)
    ILoansCore(self.loansCoreContract).updateHighestRepayment(msg.sender, _loanId)
 
    if receivedAmount > 0:
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).receiveFundsEth(msg.sender, loan.amount, paidInterestAmount, value=paymentAmount)
        log PaymentSent(self.lendingPoolPeripheralContract, self.lendingPoolPeripheralContract, paymentAmount)
    else:
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).receiveFundsWeth(msg.sender, loan.amount, paidInterestAmount)
 
    for collateral in loan.collaterals:
        ILoansCore(self.loansCoreContract).removeCollateralFromLoan(msg.sender, collateral, _loanId)
        ILoansCore(self.loansCoreContract).updateCollaterals(collateral, True)
 
        ICollateralVaultPeripheral(self.collateralVaultPeripheralContract).transferCollateralFromLoan(
            msg.sender,
            collateral.contractAddress,
            collateral.tokenId,
            erc20TokenContract
        )
 
    if excessAmount > 0:
        send(msg.sender, excessAmount)
        log PaymentSent(msg.sender, msg.sender,excessAmount)
 
    log LoanPayment(
        msg.sender,
        msg.sender,
        _loanId,
        loan.amount,
        paidInterestAmount,
        erc20TokenContract
    )
    
    log LoanPaid(
        msg.sender,
        msg.sender,
        _loanId,
        erc20TokenContract
    )
 
 
@external
def settleDefault(_borrower: address, _loanId: uint256):
    """
    @notice Settles an active loan as defaulted
    @dev Logs the `LoanDefaulted` event, removes the collaterals from the loan and creates a liquidation
    @param _borrower The wallet address of the borrower
    @param _loanId The id of the loan to settle
    """
    assert msg.sender == self.owner, "msg.sender is not the owner"
    assert ILoansCore(self.loansCoreContract).isLoanStarted(_borrower, _loanId), "loan not found"
    
    loan: Loan = ILoansCore(self.loansCoreContract).getLoan(_borrower, _loanId)
    assert not loan.paid, "loan already paid"
    assert block.timestamp > loan.maturity, "loan is within maturity period"
    assert self.liquidationsPeripheralContract != empty(address), "BNPeriph is the zero address"
 
    ILoansCore(self.loansCoreContract).updateDefaultedLoan(_borrower, _loanId)
    ILoansCore(self.loansCoreContract).updateHighestDefaultedLoan(_borrower, _loanId)
 
    for collateral in loan.collaterals:
        ILoansCore(self.loansCoreContract).removeCollateralFromLoan(_borrower, collateral, _loanId)
        ILoansCore(self.loansCoreContract).updateCollaterals(collateral, True)
 
    ILiquidationsPeripheral(self.liquidationsPeripheralContract).addLiquidation(
        _borrower,
        _loanId,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
    log LoanDefaulted(
        _borrower,
        _borrower,
        _loanId,
        loan.amount,
        ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract()
    )
 
 
@external
def setDelegation(_loanId: uint256, _collateralAddress: address, _tokenId: uint256, _value: bool):
 
    """
    @notice Sets / unsets a delegation for some collateral of a given loan. Only available to unpaid loans until maturity is reached
    @param _loanId The id of the loan to settle
    @param _collateralAddress The contract address of the collateral
    @param _tokenId The token id of the collateral
    @param _value Wether to set or unset the token delegation
    """
 
    loan: Loan = ILoansCore(self.loansCoreContract).getLoan(msg.sender, _loanId)
    assert loan.amount > 0, "invalid loan id"
    assert block.timestamp <= loan.maturity, "loan maturity reached"
    assert not loan.paid, "loan already paid"
    
    for collateral in loan.collaterals:
        if collateral.contractAddress ==_collateralAddress and collateral.tokenId == _tokenId:
            ICollateralVaultPeripheral(self.collateralVaultPeripheralContract).setCollateralDelegation(
                msg.sender,
                _collateralAddress,
                _tokenId,
                ILendingPoolPeripheral(self.lendingPoolPeripheralContract).erc20TokenContract(),
                _value
            )