Abstract
This proposal lets a third party, a backer, stand behind a lender's credit line to an identity of an
ERC-721 identity registry, such as the Identity Registry of ERC-8004, and absorb the
line's losses. It extends the Agent Credit Lines draft, in which a lender lends an ERC-20 token to such
identities. With the identity owner's signed consent, a backer puts itself behind a line of a given size. The lender
then asks the backer before each draw (canBorrow), tells it of each draw (onBorrow), takes the principal of each
defaulted loan from the backer's balance and tells it (onDefault), and tells it when part of the line is no longer
its concern (onRelease). A backer can refuse a draw. It cannot stop a repayment, a default or the end of its backing.
After a default, the backer, or anyone for it, can pay in a part of its charge it left unpaid (payLoss), and money
paid toward the defaulted loan (recover) goes first to that part, then back to the backer, credited to it until anyone
pays it out (payBacker), then to the lender's fee.
Motivation
A line of credit to an agent is a bet on whoever stands behind the agent. Lenders to agent identities already let third parties take that place: stakers who put stake behind a line, vaults that hold collateral for it, a lender's own treasury that backs first lines. Each does it through a hook written for one lender. A staker or an insurer that backs an agent at one lender cannot back the same agent at another, and a lender cannot accept a backer written for someone else's contract.
What a lender and a backer tell each other is short and the same everywhere: may this draw go ahead, a draw happened, a loan defaulted and this is your loss, this part of the line is no longer yours. Fixing those four calls, how the loss is taken, and how the identity's owner consents, lets one backer stand behind agents at any conforming lender, and lets anyone see on chain who carries the risk of a line.
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.
Interfaces
A lender MUST implement IBackedCreditLine and IAgentCreditLine, and MUST implement ERC-165,
returning true for the interface identifiers of IBackedCreditLine and IAgentCreditLine and for 0x01ffc9a7. A
backer MUST implement ICreditBacker and ERC-165, returning true for the interface identifier of ICreditBacker and
for 0x01ffc9a7.
IAgentCreditLine is the interface of the Agent Credit Lines draft, restated here unchanged:
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);
}This proposal adds:
interface ICreditBacker {
function canBorrow(address lender, uint256 agentId, uint256 amount, uint64 term, uint256 fee, address caller, address owner, address to) external view returns (bool);
function onBorrow(address lender, uint256 agentId, uint256 loanId) external;
function onDefault(address lender, uint256 agentId, uint256 loanId, uint256 principal, uint256 paid) external;
function onRelease(address lender, uint256 agentId, uint256 amount, uint8 reason) external;
}
interface IBackedCreditLine is IAgentCreditLine {
event BackerSet(uint256 indexed agentId, address indexed backer, uint256 line);
event BackerCharged(uint256 indexed loanId, uint256 indexed agentId, address indexed backer, uint256 principal, uint256 paid);
event BackerHookFailed(uint256 indexed agentId, address indexed backer, bytes4 selector);
event LossPaid(uint256 indexed loanId, address indexed backer, address indexed payer, uint256 amount);
event Recovered(uint256 indexed loanId, address indexed backer, address indexed payer, uint256 amount, uint256 toBacker);
event BackerPaid(address indexed backer, uint256 amount);
event ConsentsRevoked(uint256 indexed agentId, address indexed owner, uint256 nonce);
function backerOf(uint256 agentId) external view returns (address);
function backedLine(uint256 agentId) external view returns (uint256 line, uint256 outstanding);
function backerOfLoan(uint256 loanId) external view returns (address);
function consentNonce(uint256 agentId) external view returns (uint256);
function lossInfo(uint256 loanId) external view returns (uint256 paid, uint256 unpaid, uint256 recovered, uint256 returned);
function dueToBacker(address backer) external view returns (uint256);
function payLoss(uint256 loanId, uint256 amount) external returns (uint256 taken);
function recover(uint256 loanId, uint256 amount) external returns (uint256 taken);
function payBacker(address backer) external returns (uint256 amount);
function setBacker(uint256 agentId, address backer, uint256 line, uint256 deadline, bytes calldata ownerConsent) external;
function removeBacker(uint256 agentId) external;
function revokeConsents(uint256 agentId) external;
}What the Agent Credit Lines draft provides
This proposal relies on these parts of the Agent Credit Lines draft. identityRegistry() is the ERC-721 contract
whose token identifiers are the agentId values, and asset() is the ERC-20 token lent and repaid; neither changes,
and every amount is in the token's base units. borrow can only be called by a controller of the identity (its owner,
or a delegate the lender accepts); it records a loan whose loanId is never zero and never reused, whose owner is the
identity's owner at the draw and whose fee is what quote returns, and sends amount to to. availableCredit is
the largest amount borrow would lend now. Anyone can repay a loan in full, and anyone can markDefault a loan once
the block timestamp reaches its defaultableAt. A loan's status is Active until it is repaid or defaulted.
Terms
- The owner of an identity is the identity registry's
ownerOf(agentId)at the time of the call. - A backing is a backer standing behind one identity's line at one lender. It begins when
setBackernames a backer for an identity that has none or another, and ends whenremoveBackeris called orsetBackernames another backer. An identity has at most one backer at a lender at a time. - The backing's line is the most principal the identity can have outstanding under it. Its outstanding
principal is the sum of the principals of the
Activeloans drawn under it.backedLine(agentId)returns both, and zeros when the identity has no backer. - A loan is drawn under the backer that
backerOf(agentId)returned when it was drawn.
Owner consent
ownerConsent is a signature by the owner of this EIP-712 typed structure:
BackerConsent(address lender,uint256 agentId,address backer,uint256 line,uint256 nonce,uint256 deadline)where lender is the lender's address, nonce is consentNonce(agentId), and the other fields are the arguments of
setBacker. The domain has the fields name, version, chainId and verifyingContract, set to "Agent Credit
Backers", "1", the chain's identifier and the lender's address.
A consent is valid when it is 65 bytes holding r, s and v in that order, with v equal to 27 or 28 and s at
most half the secp256k1 order, from which ecrecover returns the owner; or when the owner is a contract whose
ERC-1271 isValidSignature returns 0x1626ba7e for the typed data hash and ownerConsent.
Setting a backer
setBacker(agentId, backer, line, deadline, ownerConsent):
- MUST revert unless
msg.senderisbacker. - MUST revert unless
backerreports through ERC-165 that it implementsICreditBacker. - MUST revert if
lineis zero, if the block timestamp is greater thandeadline, or unlessownerConsentis a valid consent for these arguments and the current nonce. On success it MUST increaseconsentNonce(agentId)by one, so that a consent can be used once. - When
backeris not the identity's backer, MUST revert while the outstanding principal is not zero. Otherwise the current backing, if there is one, ends, and a backing bybackerwith a line oflinebegins. - When
backeris already the identity's backer, MUST revert iflineis less than the outstanding principal. Otherwise the line becomesline. - MUST emit
BackerSet(agentId, backer, line), and then MUST callonReleaseas set out under Releases. - MAY revert for lender-specific reasons, such as a backer the lender does not accept or a line above its limits.
Removing a backer
removeBacker(agentId):
- MUST revert if the identity has no backer, if
msg.senderis neither its backer nor its owner, or if the outstanding principal is not zero. - MUST increase
consentNonce(agentId)by one, so that no consent signed before the removal can be used after it. - MUST end the backing, so that
backerOf(agentId)returns the zero address, MUST emitBackerSet(agentId, address(0), 0), and then MUST callonReleaseas set out under Releases.
Revoking consents
revokeConsents(agentId):
- MUST revert unless
msg.senderis the identity's owner. - MUST increase
consentNonce(agentId)by one, and emitConsentsRevoked(agentId, owner, nonce)withnoncethe newconsentNonce(agentId). - MUST NOT change the backing, if there is one, or call the backer.
Drawing
When the identity has a backer, borrow(agentId, amount, term, to, maxFee):
- MUST revert if
amountis greater than the line less the outstanding principal.availableCredit(agentId)MUST NOT return more than that difference. - MUST call
canBorrow(lender, agentId, amount, term, fee, caller, owner, to)on the backer before it sends any of the asset, withlenderits own address,feethe fee the loan would record,callerthemsg.senderofborrowandownerthe identity's owner, and MUST revert unless the call returnstrue. A call that reverts, or returns anything buttrue, is a refusal. - MUST record the loan as drawn under the backer and add its principal to the outstanding principal.
- After it has recorded the loan and sent the asset, MUST call
onBorrow(lender, agentId, loanId)on the backer, and MUST revert if that call reverts.
backerOfLoan(loanId) MUST return the backer the loan was drawn under, and the zero address for a loan drawn under no
backer and for a loanId never issued.
availableCredit does not ask the backer. A client that wants to know whether a draw would go through calls the
backer's canBorrow with the same arguments. This proposal does not say whether a lender lends to an identity that has
no backer; a loan drawn without one has no backer, and the rest of this proposal does not apply to it.
Repaying
repay MUST NOT call the backer. When the loan was drawn under a backer, repay MUST subtract its principal from the
outstanding principal.
Defaults
When markDefault(loanId) marks a loan drawn under a backer defaulted, the lender:
- MUST charge the backer the loan's principal, subtracting it from both the line and the outstanding principal. The fee is not part of the charge.
- MUST then take from the backer, with the asset's
transferFrom(backer, lender, amount), anamountequal to the least of the principal, the backer'sbalanceOfand itsallowanceto the lender. A read that reverts, or does not return exactly 32 bytes, counts as zero.paidisamountwhen the transfer does not revert and returns either no data or exactly 32 bytes holdingtrue, and zero otherwise, including whenamountis zero. - MUST make each of these three calls to the asset with a gas limit it fixes, and MUST revert, rather than make one with less, when it cannot give the call that limit. It MUST NOT copy more than the first 32 bytes of what a call returns, and MUST NOT revert because of anything the asset does in these calls.
- MUST emit
BackerCharged(loanId, agentId, backer, principal, paid). - MUST then call
onDefault(lender, agentId, loanId, principal, paid)on the backer, as a notification.
The backer, or anyone for it, can pay in later, through payLoss, a part of the principal the backer did not pay. What
else the lender does about that part, such as lending no more under that backer until it is paid, is outside this
proposal.
After a default
For each loan drawn under a backer and marked defaulted, the lender keeps four amounts, and lossInfo(loanId) MUST
return them: paid, what the backer has paid of its charge; unpaid, the part of the charge still unpaid; recovered,
all that recover has taken for the loan; and returned, the part of recovered credited to the backer. The default
sets paid to the paid of BackerCharged and unpaid to the principal less paid. For any other loanId,
lossInfo MUST return zeros.
payLoss(loanId, amount) pays a part of the charge the backer left unpaid. It:
- MUST revert unless the loan was drawn under a backer and is
Defaulted, or iftaken, the least ofamountandunpaid, is zero. - MUST take
takenfrommsg.senderwith the asset'stransferFrom, add it topaid, subtract it fromunpaid, emitLossPaid(loanId, backer, msg.sender, taken), and returntaken. What it takes counts as paid by the backer, whoever sends it.
recover(loanId, amount) pays toward the loan after its default. It:
- MUST revert unless the loan was drawn under a backer and is
Defaulted, or iftaken, the least ofamountand the loan's principal and fee lessrecovered, is zero. - MUST take
takenfrommsg.senderwith the asset'stransferFromand apply it in this order: tounpaid, which it lowers, the lender keeping that part; then to the backer, up topaidlessreturned, which it MUST add toreturnedand todueToBacker(backer), and MUST NOT transfer to the backer; then to the fee, the lender keeping that part. - MUST add
takentorecovered, emitRecovered(loanId, backer, msg.sender, taken, toBacker), withtoBackerthe part credited to the backer, and returntaken.
dueToBacker(backer) MUST return what recoveries have credited to backer and payBacker has not yet paid it. The
lender MUST keep that amount for the backer: it MUST NOT lend it, and availableCredit MUST NOT count it.
payBacker(backer) pays a backer what recoveries credited to it. Anyone may call it. It:
- MUST revert if
dueToBacker(backer)is zero. - MUST set
dueToBacker(backer)to zero, transfer that amount tobacker, and to no other address, with the asset'stransfer, emitBackerPaid(backer, amount), and return the amount.
None of payLoss, recover and payBacker calls the backer. amountDue and repay stay as the Agent Credit Lines
draft defines them: a defaulted loan is not repaid, and its status stays Defaulted.
Releases
onRelease(lender, agentId, amount, reason) tells a backer that amount of the line it stood behind for the identity
is no longer backed by it. The lender MUST call it, as a notification, in exactly these cases, with these values, and
MUST NOT use any other value of reason:
reason | Name | When | amount |
|---|---|---|---|
1 | Lowered | setBacker by the identity's backer set a lower line | The old line less the new one |
2 | Removed | removeBacker ended the backing | The whole line |
3 | Replaced | setBacker named another backer | The whole line of the backer replaced |
A line grows only through setBacker, which the backer calls itself. Every decrease of a line MUST be told to its
backer exactly once: through onDefault for the principal of a defaulted loan, or through onRelease for amount.
Over a backing, the principals passed to onDefault, the amounts passed to onRelease and the line left therefore add
up to the line the backer was first given plus every raise, so a backer can keep its own account of what it stands
behind.
Notifications
onDefault and onRelease are notifications. For each, the lender:
- MUST call the backer with a gas limit it fixes, of at least 100,000, and MUST revert, rather than make the call with less, when it cannot give the call that limit.
- MUST NOT revert because of the call, whatever the backer does in it: revert, use all its gas, or return data of any
size. When the call fails, the lender MUST emit
BackerHookFailed(agentId, backer, selector), with the selector of the function called, and MUST complete the action.
canBorrow and onBorrow are not notifications: a backer refuses a draw by refusing either.
The backer
canBorrowMUST NOT depend onmsg.sender, so that anyone can ask a backer whether it would allow a draw at a given lender.onBorrow,onDefaultandonReleaseMUST revert unlessmsg.senderislender.- A backer SHOULD keep a balance of the asset, and an allowance to each lender, at least equal to the lines it stands behind there.
Rationale
- Hooks, not deposits. A backer could deposit its stake with each lender, but it could then back only as many lenders as it splits its stake between, and every lender would hold capital it does not lend. With hooks, the backer keeps its stake and stands behind lines at any number of lenders, and a lender reaches the stake only when a loan the backer agreed to defaults.
- The loss is taken before the backer is told. The lender takes the principal from the backer's balance, up to its
allowance, and only then calls
onDefault. A default is paid without calling the backer (unless the token itself calls it, as the next point covers), and the backer cannot act on a default before paying for it. It also keeps a conversion of collateral out of a transaction someone else times: anyone may callmarkDefaultoncedefaultableAthas passed, so a swap made inside the hook would run where its caller can build a sandwich around it. A backer whose collateral is another asset keeps enough of the lent asset at hand, has a contract that holds it give the allowance, or readsdueAtfromloanInfoand converts during the grace period, in its own transaction. - Calls to the asset are bounded too. With a token that calls the holder during a transfer, as
ERC-777 tokens call
tokensToSend, the backer's own code runs inside the lender'stransferFrom. If that call could use all the gas left, or return data the lender had to copy, a backer could make every default of its loans revert. A fixed gas limit and reading one word back keep the default going whatever the asset does. The rule against calling with less stops a caller from making the transfer fail on purpose, so that the backer pays nothing while the default goes through. Tokens that return no data fromtransferFromare accepted, as they are by most lenders today. - The principal, not the fee. The principal is what left the lender; the fee is income it expected. A lender that wants its fees covered too can price that into the line.
- After a default: the unpaid part, then the backer, then the fee. A loan opens once and closes once in the Agent
Credit Lines draft, so money that arrives after a default goes through
recover, notrepay, and the loan staysDefaulted. The backer owed the whole principal, so a recovery first pays what it left unpaid, which also settles that part of its charge. The backer then gets back what it paid, since it bore that loss, and the fee comes last, since the backer never covered it.payLossis the backer's side: what it pays late counts as paid at the default, so a later recovery returns it, and a backer that converts its collateral slowly can still settle its charge.lossInfokeeps all of it per loan, where a contract can read it; a contract cannot readBackerCharged.recovercredits the backer's part instead of transferring it, andpayBackerpays it out: a backer's address the asset will not pay, one on the token's freeze list or one whose transfer hook reverts, then holds up only its own payment, never a recovery or the fee behind its part. It is the next point's rule applied after a default. - A backer can stop money leaving, never money coming back.
canBorrowandonBorrowcan refuse a draw: the backer carries the risk, and it has to know of every loan it carries.repaydoes not call the backer, and a default, a removal or a replacement goes ahead whatever the backer does. A repayment a backer could block would turn a repaid loan into a default; a default it could block would stop the lender from recovering; a removal it could block would hold the identity to a backer its owner no longer wants. ERC-8183 makes the same choice for its refund, which no hook can block. - A fixed gas limit for notifications. Without a limit, a backer could use all the gas of a
markDefaultcall. Without the rule against calling with less, a caller could send just enough gas for the notification to fail while the default goes through, and the backer would never hear of it. With both, a backer always gets its limit, and a notification that fails is the backer's own doing and is logged. - Both parties consent, and the lender may refuse. The owner's signature stops anyone from putting a backer behind
an identity: a backer decides which draws go through and may hold the owner to terms of its own. The backer submits
the call itself, so no one can put a backer behind a line it did not choose. The signature names the lender, the
identity, the backer and the line, and carries a nonce and a deadline, so it serves one backing at one lender, once.
A removal moves the nonce too, so a consent signed before it, such as a raise not yet used, cannot bring the backer
back, and
revokeConsentslets an owner whose key cannot take back a signature void the consents it has not seen used, also while the identity has no backer. - A line, not a loan. The backer stands behind a revolving line, so it is not called on repayment, and what it can lose is bounded by the line. Every decrease of the line is told to it exactly once, so it can keep its account from the calls it receives. A raise needs a new consent, which the backer submits itself.
- One backer per identity, changed only when nothing is drawn. The loss of a loan belongs to the backer that agreed
to it. Changing backers only when nothing is outstanding keeps that rule without moving risk from one backer to
another;
backerOfLoankeeps the record per loan for readers. - The lender's address in every call. One backer can serve many lenders, and tells them apart by
lender, which it checks againstmsg.sender. IncanBorrow, a view, it lets anyone ask a backer about a draw at a given lender before signing it. - Related work. ERC-7246 pledges a token balance to a counterparty without saying when it is
taken; here the backer's allowance is taken only for the principal of a defaulted loan it agreed to.
ERC-8320 registries hold signed claims about assets, and its example schema for
BACKINGclaims about credit-backed instruments has aGUARANTEEbacking type. Such a claim can state that a backer guarantees an agent's loans; it does not make the backer pay. The hooks here are what make that guarantee hold: the backer agrees to each draw and the lender takes the principal of each default, so a claim and a backing can describe the same commitment, one as an attestation and one as its enforcement.
Backwards Compatibility
No backward compatibility issues found. A lender of the Agent Credit Lines draft that does not implement this proposal
stays conforming to that draft. A backer hook keyed by a lender's internal identifier rather than by the lender's
address can be adapted by a contract that implements ICreditBacker and forwards to it.
Test Cases
A conforming lender and backer pass these cases, each for an identity whose owner signs consents, a lender that charges 1% per 30 days with a grace period of 3 days, and a backer that holds 100 units and approved the lender for 50 units:
- The owner's consent lets the backer set a line of 50 units:
backerOfreturns the backer,backedLinereturns 50 units with nothing outstanding,consentNoncegoes from 0 to 1, andBackerSetis emitted. setBackerreverts for a consent signed by another key; for a consent naming another line, backer, lender or deadline; for a consent signed for the same lender address on another chain, where a consent signed for that chain is accepted; for a consent already used; one second after the deadline (and succeeds at it); for the high-stwin of a valid signature; when called by anyone but the backer, the owner included; for a backer that does not reportICreditBacker, or an account with no code; and for a line of zero.- An owner that is a contract consents through ERC-1271, and a signature its key did not make is refused.
- A draw of 10 units for 7 days by a delegate calls
canBorrow(lender, agentId, 10 units, 7 days, 23,334 base units, delegate, owner, to)and thenonBorrow(lender, agentId, 1). A draw the backer refuses, or whosecanBorroworonBorrowreverts, reverts and sends nothing. - With 30 units drawn on a line of 50,
availableCreditis 20 units although the lender holds 970, and a draw of 20 units and one base unit reverts. - A repayment succeeds while every call to the backer would revert, and makes no call to it.
- The default of a 10-unit loan takes 10 units, not the fee, from the backer, lowers the line to 40 units, emits
BackerCharged(loanId, agentId, backer, 10 units, 10 units)and callsonDefault(lender, agentId, loanId, 10 units, 10 units). - A backer holding 4 units pays 4 of a 10-unit default; with an allowance of 3 units it pays 3; with no allowance, or when the token refuses the transfer, it pays nothing, and each loan is still marked defaulted.
- When a backer holding 4 units pays 4 of a 10-unit default and its
onDefaultreverts, the line still goes down to 40 units,BackerHookFailedis emitted, andlossInforeturns 4 units paid, 6 unpaid, nothing recovered and nothing returned.lossInforeturns zeros for an active loan, a repaid one and aloanIdnever issued. - After a default that left 6 units unpaid,
payLossof 100 units takes 6, emitsLossPaidand leaves 10 paid and nothing unpaid, and a secondpayLossreverts.payLossandrecoverrevert for an active loan, a repaid one and aloanIdnever issued. - After a default where the backer paid 4 of 10 units, recoveries of 5, 3 and 100 units take 5 units (to the unpaid
part), 3 (1 to the unpaid part, 2 to the backer) and 2 units and 23,334 base units (2 to the backer, then the fee).
The backer has 4 units due, which
payBackerpays it, the lender keeps 6 units and the fee, and anotherrecoverreverts. A backer that paid all 10 units at the default gets the first 10 recovered; one that paid 4 and then 6 throughpayLossgets back 10 from a recovery of the whole amount due. None ofpayLoss,recoverandpayBackermakes any call to the backer. - When the token freezes the backer's address after it paid all 10 units at the default, a recovery of the whole
amount due takes 10 units and 23,334 base units, credits 10 units to the backer and reaches the fee.
payBackerreverts while the address is frozen; once it is not,payBackercalled by anyone pays the backer its 10 units and emitsBackerPaid, and a secondpayBackerreverts. With 15 units of cash, 10 of them due to the backer,availableCreditis 5 units and a draw of 5 units and one base unit reverts. - After any sequence of
payLossandrecovercalls, the backer never has back more than it paid, nothing is taken beyond the principal and the fee, and the fee is reached only once nothing is unpaid and the backer has all it paid back. - When the token's
transferFromreturnsfalse, uses all its gas, or reverts or returns with as much data as its gas can pay for, or itsbalanceOfuses all its gas, a default sent with 1,000,000 gas still completes, withpaidzero, the whole principal unpaid and the backer told. Such data costs the default less than one and a half times the asset's gas limit more than a transfer that just returnsfalse. - A backer whose
onDefaultoronReleasereverts, uses all its gas, or returns about 160 KB, cannot stop a default, a removal by the owner or a replacement; the lender emitsBackerHookFailed, and the 160 KB costs the lender no more than the backer's own gas limit. - A
markDefaultsent with any amount of gas, from too little upwards in steps of 1,000, either reverts and changes nothing or charges the backer in full and tells it: some amounts are refused because a call to the asset could not be given its gas limit, others because the backer's call could not. AremoveBackerwithout gas for the backer's 100,000 reverts and changes nothing. With plenty of gas, the backer is given 100,000 gas and no more. - Lowering a line from 50 to 30 units calls
onRelease(lender, agentId, 20 units, 1), and a raise calls nothing.removeBackerby the backer or the owner callsonRelease(lender, agentId, 50 units, 2). Another backer'ssetBackercallsonRelease(lender, agentId, 50 units, 3)on the one it replaces. Replacing or removing a backer, or lowering its line below what is drawn, reverts while a loan is active. - After
removeBacker, by the owner or by the backer, a consent signed before it for a raise of the line to 80 units reverts, andconsentNoncehas moved by one.revokeConsentsby the owner moves the nonce by one, emitsConsentsRevoked, and voids a consent signed while the identity had no backer; called by anyone else, the backer and a delegate included, it reverts. - After any sequence of line changes, draws, repayments, defaults and removals, the principals charged, the amounts
released and the line left add up to every line and raise the backer was given, and every
reasonis 1, 2 or 3. - The reference backer gives the same
canBorrowanswer to every caller, and revertsonBorrow,onDefaultandonReleasecalled by anyone but the lender. - In the reference lender, a backer whose
onBorrowcalls back intoborrow,repay,markDefault,setBackerorremoveBackergets the lender's re-entry error and the draw reverts. Called back fromonDefaultoronRelease, the lender refuses the same way, and the default or the removal completes.
Reference Implementation
Two contracts accompany this proposal with their tests. BackedCreditLine is a lender derived from the reference
lender of the Agent Credit Lines draft: it lends only behind a backer, gives each notification and each call to the
asset during a default 100,000 gas, refuses re-entry from any hook, and gives no new credit under a backer while part
of a loss it was charged is unpaid. StakeBacker is a backer that pays a defaulted principal
from its stake: its manager can withdraw only stake that no line holds, it approves each lender for exactly its lines
there, it refuses draws once the identity has changed hands or defaulted, and anyone can bring its records back in line
with a lender's after a notification that failed.
Security Considerations
- A backer's cover is its balance and its allowance. The lender takes a loss from what the backer holds when the loan defaults, and a backer can move its funds or lower its allowance before then. Lenders should read the backer's balance and allowance before each draw, prefer backers whose stake cannot leave while their lines are open, and stop lending under a backer until it pays a loss it left unpaid.
- A lender is trusted with the allowance. A lender can take up to the allowance at any time, and holds what
recoveries credit to a backer until
payBackerpays it. Backers should approve only lenders they have reviewed, and only for the lines they stand behind there. - Consents. A consent lets the backer it names act until its deadline. Owners should sign short deadlines. Using a
consent moves the identity's nonce at that lender, which voids every other consent signed with the same nonce, and so
do
removeBackerand the owner'srevokeConsents. An owner that no longer wants a consent it signed to be used should callrevokeConsentsrather than wait for the deadline. A consent stops working when its signer no longer owns the identity, and works again if the identity returns to it before the deadline. - Changes of owner. A backing does not end when the identity changes hands. A backer that backs an owner rather
than an identity should refuse draws whose
owneris not the one that consented, and a buyer should readbackerOfbefore buying. - Re-entry. Every hook is a call into a contract the lender did not write. Lenders should make the calls after
their own state changes and guard
borrow,repay,markDefault,setBackerandremoveBackeragainst re-entry. - Missed notifications. A notification that fails is logged, not retried. A backer should be able to rebuild its
records from
backerOf,backedLine,loanInfoandlossInfo, which gives, for each defaulted loan, what the backer paid and what came back to it. - Which call pays what. What
payLosstakes counts as the backer's, and a later recovery returns it to the backer. A borrower or a collector paying toward a defaulted loan usesrecover. A recovery credits the backer's part at the lender, andpayBackerpays it, so a token that refuses transfers to the backer's address, as one with a freeze list does, stops onlypayBacker, and the backer's part waits at the lender until the backer can receive it.payBackerpays the backer's own address and no other, so a lender is never the way around a freeze. - Self-backing. An owner can back its own identity through a contract of its own. A backer is worth what it holds and what it has approved; its existence tells a lender nothing by itself.
- Tokens. A token that takes a fee on transfer makes
paidoverstate what reached the lender. A token that calls the holder during a transfer lets the backer decide whether it pays, as a backer that lowers its allowance does. Lenders should lend tokens without transfer fees or holder hooks.
Copyright
Copyright and related rights waived via CC0.
