Abstract
This proposal defines an interface for a contract that lends an ERC-20 token to identities held in an
ERC-721 identity registry, such as the Identity Registry of ERC-8004. An identity's
controller draws loans against a line, each with a principal, a fee and a due time. Anyone may repay a loan in full, and
anyone may mark it defaulted once a grace period after its due time has passed. Three events, Borrowed, Repaid and
Defaulted, record every loan's life, so wallets, payment clients and credit readers can work with any conforming lender
through one ABI.
Motivation
Autonomous agents pay for services before they are paid for their own work, so they need working capital. Lenders that extend unsecured or partly secured credit to agent identities already exist, and each exposes its own functions and events. A wallet that wants to borrow a shortfall at payment time, a job market that wants to see whether a provider is in default, or a reader that rebuilds an agent's credit history from logs, each has to integrate every lender separately.
The calls involved are few and the same everywhere: how much can this identity borrow now, what would it cost, draw it, how much closes it, repay it, mark it defaulted. Fixing them, and fixing the events that record them, lets one client borrow from any lender and lets one reader rebuild any lender's loan history without a lender-specific adapter.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Interface
A conforming contract MUST implement IAgentCreditLine and MUST implement ERC-165, returning true for
the interface identifier of IAgentCreditLine and for 0x01ffc9a7.
interface IAgentCreditLine {
enum LoanStatus { None, Active, Repaid, Defaulted }
struct LoanInfo {
uint256 agentId;
address owner;
uint256 principal;
uint256 fee;
uint64 issuedAt;
uint64 dueAt;
uint64 defaultableAt;
LoanStatus status;
}
event Borrowed(uint256 indexed loanId, uint256 indexed agentId, address indexed to, uint256 principal, uint256 fee, uint64 dueAt);
event Repaid(uint256 indexed loanId, uint256 indexed agentId, address indexed payer, uint256 principal, uint256 fee);
event Defaulted(uint256 indexed loanId, uint256 indexed agentId, address indexed owner, uint256 principal);
function identityRegistry() external view returns (address);
function asset() external view returns (address);
function isController(uint256 agentId, address account) external view returns (bool);
function availableCredit(uint256 agentId) external view returns (uint256);
function quote(uint256 agentId, uint256 amount, uint64 term) external view returns (uint256 fee);
function borrow(uint256 agentId, uint256 amount, uint64 term, address to, uint256 maxFee) external returns (uint256 loanId);
function amountDue(uint256 loanId) external view returns (uint256);
function repay(uint256 loanId, uint256 maxDue) external returns (uint256 paid);
function markDefault(uint256 loanId) external;
function loanInfo(uint256 loanId) external view returns (LoanInfo memory);
}Identities and controllers
identityRegistry()MUST return the ERC-721 contract whose token identifiers are theagentIdvalues the lender serves. It MUST NOT change.asset()MUST return the ERC-20 token lent and repaid. It MUST NOT change. Every amount in this interface is in the token's base units.isController(agentId, account)MUST returntruewhenaccountis the registry'sownerOf(agentId). It MAY returntruefor other accounts the lender accepts as acting for the identity (delegates). It MUST returnfalsefor the zero address and MUST NOT revert whenagentIddoes not exist.- A delegate MUST stop being a controller when the identity is held by an owner other than the one that named it. When
the identity registry exposes a transfer nonce (
nonce(uint256)of ERC-5008), the lender SHOULD also bind each delegate to the nonce at the time it was named, so that a delegate never acts again after the identity leaves and returns to the same owner.
Credit and quotes
availableCredit(agentId)MUST return the largestamountfor whichborrowwould not revert for lack of credit or liquidity now, and0when there is none. It MUST NOT revert.quote(agentId, amount, term)MUST return the fee thatborrow(agentId, amount, term, to, maxFee)would record if called in the same state.
Borrowing
borrow(agentId, amount, term, to, maxFee):
- MUST revert unless
isController(agentId, msg.sender)istrue. - MUST revert if
amountis zero or greater thanavailableCredit(agentId), or if the fee it would record is greater thanmaxFee. - MAY revert for lender-specific reasons, such as a term outside the lender's bounds or an identity in default.
- MUST record a loan with a
loanIdthat is not zero and is never reused by the contract, withownerset toownerOf(agentId)at that moment,issuedAtset to the block timestamp,dueAtset toissuedAt + term, adefaultableAtat or afterdueAt, andstatusActive. - MUST transfer
amountofasset()totoand emitBorrowed.
Repaying
amountDue(loanId)MUST return the amountrepaywould take to close the loan now, and0unless its status isActive.repay(loanId, maxDue)MUST be callable by any account. It MUST revert unless the loan isActiveor if the amount due is greater thanmaxDue. It MUST take the amount due frommsg.senderwithtransferFrom, set the status toRepaid, emitRepaidwithpayerset tomsg.sender, and return the amount taken.- The
principalandfeeinRepaidMUST be those recorded atborrow. A lender that charges more afterdueAtMUST include it inamountDueand MAY emit its own event for it.
Defaults
markDefault(loanId)MUST be callable by any account. It MUST revert unless the loan isActiveand the block timestamp is at or after itsdefaultableAt. It MUST set the status toDefaultedand emitDefaultedwith theownerrecorded atborrow.- What else a default changes (the lender's own records, a backer's stake, collateral) is outside this interface.
Loan records
loanInfo(loanId) MUST return the loan as recorded, with the status as of now, and a zero-valued record with status
None for a loanId never issued.
Rationale
- The identity borrows, the owner answers. The line belongs to the identity, so an agent's credit can follow its identifier across lenders. Each loan records the owner at the time of the draw, because the owner is the party that chose to borrow; a default names that owner even if the identity has changed hands since.
- Full repayment only. Partial repayment, rollover and refinancing differ widely between lenders and would make the three events ambiguous. A loan opens once and closes once.
- Anyone may repay and anyone may mark a default. Repayment by a third party is how revenue routers, automated repayment services and sponsors pay on an agent's behalf. Marking a default after a published time needs no trusted keeper.
maxFeeandmaxDue. Both protect the caller against a change in the lender's state between signing and inclusion.- Events carry what a reader needs.
Borrowedcarries the principal and the due time,RepaidandDefaultedthe principal, each indexed by loan and identity, so a reader can rebuild every loan of an identity from logs alone. - Not a collateral loan. ERC-7565 already has term loans with a due date and a
Defaultedevent, but each loan locks an NFT as collateral and a default hands that NFT to the lender. Here the identity is not collateral: seizing an agent identity would transfer its reputation to the lender, and most lines to agents are unsecured. A default names the owner instead. Both proposals call an eventDefaulted; the signatures differ, so the topics do not collide, but indexers should match events by topic, not by name. - Delegates that cannot outlive a sale. A key named by one owner that still borrows after the identity is sold, or that acts again when the identity comes back, lets a former owner borrow against the current owner's line. Binding delegates to the naming owner, and to a transfer nonce where the registry has one, closes both cases.
Backwards Compatibility
No backward compatibility issues found. Existing lenders whose events differ can expose this interface through a new contract version; their past logs stay readable with their own event signatures.
Test Cases
A conforming implementation passes these cases, each starting from an identity with a line of 50 units at a fee of 1% per 30 days and a grace period of 3 days:
- A draw of 10 units for 7 days records a fee of 23,334 base units (rounded up), sends 10 units to
to, and emitsBorrowedwithdueAtequal to the draw time plus 7 days. quotereturns the feeborrowrecords, for any amount and term the lender accepts.- A draw by an account that is neither the owner nor an accepted delegate reverts.
- A draw above
availableCredit, or withmaxFeebelow the fee, reverts. - A third party repays with
maxDueequal toamountDue; the loan isRepaid,Repaidnames the third party, andavailableCreditis restored. A second repayment reverts. markDefaultreverts one second beforedefaultableAtand succeeds at it; after the identity was transferred, theDefaultedevent names the owner at the draw.- A delegate named by the owner can draw; after the identity is transferred it is no longer a controller.
Reference Implementation
A minimal lender, SimpleCreditLine, accompanies this proposal with its tests: an administrator sets each identity's
line and fee rate, the contract's own token balance is its liquidity, fees are pro rata per 30 days and rounded up, and a
default closes the identity's line.
Security Considerations
- Registry trust. Every authorization reads
ownerOffrom the identity registry. A registry that can be upgraded or paused can change who controls an identity; lenders should know the registry's governance. - Delegates. A delegate is a key that can draw the whole line. Lenders MUST end it when the identity changes owner and SHOULD bind it to a transfer nonce where one exists. Owners should clear delegates before transferring an identity.
- Cheap identities. Identities cost little to create. A line to a new identity is a bet on whoever stands behind it; this interface does not say who that is, and lenders need their own admission rules.
- Defaults follow addresses, not people. The owner recorded by a loan is an address. A defaulting owner can create a new address; readers should treat an identity with no history as unknown, not as clean.
- Repayment by
transferFrom. The payer approves the lender. Tokens that take a fee on transfer would leave the lender short; lenders SHOULD use tokens without transfer fees or check the balance received. - Rounding. Fees SHOULD round in the lender's favour, so that splitting a draw never lowers the total fee.
- Timestamps. Due and default times follow block timestamps, which block producers can shift by seconds. Lenders should leave a grace period long enough that this does not matter.
Copyright
Copyright and related rights waived via CC0.
