A repayment is a payment and a number

Why every Straid repayment carries a destination tag, what happens to the ones that do not, and the morning we refunded our own money on Testnet.

Somebody asks this at every demo. There is a number next to the repayment address, the app calls it a destination tag, and the obvious question follows: what is it for, and what happens if I forget it?

The short answer is that the tag says which loan the money belongs to, and if you forget it the ledger will refuse the payment before it ever reaches us.

One account, many loans

Every repayment arrives at the same Straid account. That is deliberate: one account is one thing to fund, to watch and to reconcile. But it means the payment itself has to say which loan it settles, and on the XRP Ledger the field for that is the destination tag, an unsigned 32 bit number carried by the payment.

Each loan gets its own tag when it is opened. Tags for loans start at 1000, for a reason further down.

The ledger refuses untagged payments

The account has asfRequireDest set, so the ledger itself rejects any payment that arrives without a tag. We would rather have a refusal at the protocol than a pile of money we cannot attribute to anyone. The cost of that choice is a confusing error for someone paying by hand, which is why the app builds the payment for you and fills the tag in.

What gets credited

Two rules, both narrow on purpose.

  • The amount credited comes from delivered_amount in the validated transaction metadata, never from the Amount field of the payment. With issued currencies the two can differ, and the only number that says what actually arrived is the one the ledger computed after the fact.
  • The payment has to come from the wallet that borrowed. A loan is a relationship with one address, and a repayment from somewhere else is not a repayment, whatever the tag says.

Part payments are fine. They are credited, they lower what is left, and they move the price at which the loan could be called further away.

The ones we cannot credit

Money we cannot attribute is parked rather than kept. Each parked row records why: an unknown tag, the wrong asset, a sender who is not the borrower, a loan that is not in a state to be repaid. After an hour, the ordinary cases are refunded to the address they came from, less what the refund transaction costs, and amounts too small to cover that cost are left alone rather than eaten by fees.

One case is never refunded by machine. A payment from a wallet that is not the borrower's is held and a person looks at it, because automatically sending money back to an address of a stranger's choosing is exactly the behaviour an attacker would like to find.

The morning we refunded ourselves

On Testnet, we topped up the lending float by sending RLUSD to that same account with tag 1, which our design notes had reserved for exactly that. The notes were the only place the reservation existed. In the code, loan tags simply started at 1, no loan happened to hold tag 1 in that database, and so the engine did precisely what it is built to do with money it cannot attribute. It parked the payment, waited an hour, and sent it back where it came from.

Nothing was lost, and the behaviour under test was the behaviour we wanted. It was still a bug, and a good one to find with test money rather than with somebody's rent. Tag 1 now means a float top up and is booked as one, loan tags start at 1000, and the gap between a design note and the code it describes is a thing we watch for.

If you are repaying from the app, the tag is already there. If you are typing the payment yourself, copy the tag from the loan page along with the amount, and send from the wallet that borrowed. The engine credits it within about a minute of the ledger validating it, and the page updates itself.

Back to the notes