EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8467: Agent Credit Backers

Third parties that stand behind a lender's credit line to an agent identity and absorb its defaults

Authors
Created2026-10-20
Discussion Linkhttps://ethereum-magicians.org/t/erc-8467-agent-credit-backers/30062
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#2111PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationInterfacesWhat the Agent Credit Lines draft providesTermsOwner consentSetting a backerRemoving a backerRevoking consentsDrawingRepayingDefaultsAfter a defaultReleasesNotificationsThe backerRationaleBackwards CompatibilityTest CasesReference ImplementationSecurity ConsiderationsCopyright

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 setBacker names a backer for an identity that has none or another, and ends when removeBacker is called or setBacker names 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 Active loans 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.

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.sender is backer.
  • MUST revert unless backer reports through ERC-165 that it implements ICreditBacker.
  • MUST revert if line is zero, if the block timestamp is greater than deadline, or unless ownerConsent is a valid consent for these arguments and the current nonce. On success it MUST increase consentNonce(agentId) by one, so that a consent can be used once.
  • When backer is 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 by backer with a line of line begins.
  • When backer is already the identity's backer, MUST revert if line is less than the outstanding principal. Otherwise the line becomes line.
  • MUST emit BackerSet(agentId, backer, line), and then MUST call onRelease as 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.sender is 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 emit BackerSet(agentId, address(0), 0), and then MUST call onRelease as set out under Releases.

Revoking consents

revokeConsents(agentId):

  • MUST revert unless msg.sender is the identity's owner.
  • MUST increase consentNonce(agentId) by one, and emit ConsentsRevoked(agentId, owner, nonce) with nonce the new consentNonce(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 amount is 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, with lender its own address, fee the fee the loan would record, caller the msg.sender of borrow and owner the identity's owner, and MUST revert unless the call returns true. A call that reverts, or returns anything but true, 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:

  1. 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.
  2. MUST then take from the backer, with the asset's transferFrom(backer, lender, amount), an amount equal to the least of the principal, the backer's balanceOf and its allowance to the lender. A read that reverts, or does not return exactly 32 bytes, counts as zero. paid is amount when the transfer does not revert and returns either no data or exactly 32 bytes holding true, and zero otherwise, including when amount is zero.
  3. 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.
  4. MUST emit BackerCharged(loanId, agentId, backer, principal, paid).
  5. 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 if taken, the least of amount and unpaid, is zero.
  • MUST take taken from msg.sender with the asset's transferFrom, add it to paid, subtract it from unpaid, emit LossPaid(loanId, backer, msg.sender, taken), and return taken. 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 if taken, the least of amount and the loan's principal and fee less recovered, is zero.
  • MUST take taken from msg.sender with the asset's transferFrom and apply it in this order: to unpaid, which it lowers, the lender keeping that part; then to the backer, up to paid less returned, which it MUST add to returned and to dueToBacker(backer), and MUST NOT transfer to the backer; then to the fee, the lender keeping that part.
  • MUST add taken to recovered, emit Recovered(loanId, backer, msg.sender, taken, toBacker), with toBacker the part credited to the backer, and return taken.

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 to backer, and to no other address, with the asset's transfer, emit BackerPaid(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:

reasonNameWhenamount
1LoweredsetBacker by the identity's backer set a lower lineThe old line less the new one
2RemovedremoveBacker ended the backingThe whole line
3ReplacedsetBacker named another backerThe 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

  • canBorrow MUST NOT depend on msg.sender, so that anyone can ask a backer whether it would allow a draw at a given lender.
  • onBorrow, onDefault and onRelease MUST revert unless msg.sender is lender.
  • 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 call markDefault once defaultableAt has 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 reads dueAt from loanInfo and 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's transferFrom. 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 from transferFrom are 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, not repay, and the loan stays Defaulted. 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. payLoss is 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. lossInfo keeps all of it per loan, where a contract can read it; a contract cannot read BackerCharged. recover credits the backer's part instead of transferring it, and payBacker pays 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. canBorrow and onBorrow can refuse a draw: the backer carries the risk, and it has to know of every loan it carries. repay does 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 markDefault call. 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 revokeConsents lets 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; backerOfLoan keeps 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 against msg.sender. In canBorrow, 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 BACKING claims about credit-backed instruments has a GUARANTEE backing 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: backerOf returns the backer, backedLine returns 50 units with nothing outstanding, consentNonce goes from 0 to 1, and BackerSet is emitted.
  • setBacker reverts 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-s twin of a valid signature; when called by anyone but the backer, the owner included; for a backer that does not report ICreditBacker, 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 then onBorrow(lender, agentId, 1). A draw the backer refuses, or whose canBorrow or onBorrow reverts, reverts and sends nothing.
  • With 30 units drawn on a line of 50, availableCredit is 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 calls onDefault(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 onDefault reverts, the line still goes down to 40 units, BackerHookFailed is emitted, and lossInfo returns 4 units paid, 6 unpaid, nothing recovered and nothing returned. lossInfo returns zeros for an active loan, a repaid one and a loanId never issued.
  • After a default that left 6 units unpaid, payLoss of 100 units takes 6, emits LossPaid and leaves 10 paid and nothing unpaid, and a second payLoss reverts. payLoss and recover revert for an active loan, a repaid one and a loanId never 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 payBacker pays it, the lender keeps 6 units and the fee, and another recover reverts. A backer that paid all 10 units at the default gets the first 10 recovered; one that paid 4 and then 6 through payLoss gets back 10 from a recovery of the whole amount due. None of payLoss, recover and payBacker makes 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. payBacker reverts while the address is frozen; once it is not, payBacker called by anyone pays the backer its 10 units and emits BackerPaid, and a second payBacker reverts. With 15 units of cash, 10 of them due to the backer, availableCredit is 5 units and a draw of 5 units and one base unit reverts.
  • After any sequence of payLoss and recover calls, 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 transferFrom returns false, uses all its gas, or reverts or returns with as much data as its gas can pay for, or its balanceOf uses all its gas, a default sent with 1,000,000 gas still completes, with paid zero, 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 returns false.
  • A backer whose onDefault or onRelease reverts, uses all its gas, or returns about 160 KB, cannot stop a default, a removal by the owner or a replacement; the lender emits BackerHookFailed, and the 160 KB costs the lender no more than the backer's own gas limit.
  • A markDefault sent 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. A removeBacker without 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. removeBacker by the backer or the owner calls onRelease(lender, agentId, 50 units, 2). Another backer's setBacker calls onRelease(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, and consentNonce has moved by one. revokeConsents by the owner moves the nonce by one, emits ConsentsRevoked, 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 reason is 1, 2 or 3.
  • The reference backer gives the same canBorrow answer to every caller, and reverts onBorrow, onDefault and onRelease called by anyone but the lender.
  • In the reference lender, a backer whose onBorrow calls back into borrow, repay, markDefault, setBacker or removeBacker gets the lender's re-entry error and the draw reverts. Called back from onDefault or onRelease, 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 payBacker pays 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 removeBacker and the owner's revokeConsents. An owner that no longer wants a consent it signed to be used should call revokeConsents rather 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 owner is not the one that consented, and a buyer should read backerOf before 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, setBacker and removeBacker against re-entry.
  • Missed notifications. A notification that fails is logged, not retried. A backer should be able to rebuild its records from backerOf, backedLine, loanInfo and lossInfo, which gives, for each defaulted loan, what the backer paid and what came back to it.
  • Which call pays what. What payLoss takes counts as the backer's, and a later recovery returns it to the backer. A borrower or a collector paying toward a defaulted loan uses recover. A recovery credits the backer's part at the lender, and payBacker pays it, so a token that refuses transfers to the backer's address, as one with a freeze list does, stops only payBacker, and the backer's part waits at the lender until the backer can receive it. payBacker pays 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 paid overstate 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 and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth