Lightning is getting easier to use every day, leading to a lot of exciting services being built on top of it as they should be. But while better documentation and API bindings make it easy to use for even inexperienced developers there are still a few rough edges to be aware of or else one may become the target of attackers. One of these will be the topic of this article: fee handling for custodial lightning accounts.
Even though I agree on a philosophical basis that this shouldn't be an article I have to write because people are supposed not to trust custodial solutions, this is not the reality we live in. For a variety of reasons including UX, maintenance effort and available know how custodial solutions integrating lightning are often preferable, especially for small amounts of money.
All of them have to solve the same problem: letting users send and receive money using one or a small number of underlying, real lightning nodes. Not maintaining one Lightning node per user and the associated channels is such an obvious optimization that most probably didn't even think of the possibility of doing so. But with this optimization comes the problem of accounting. A user is only supposed to be able to withdraw as much money as they are owed. Any satoshi more more would open the doors for theft. But there lies the problem: when paying lightning invoices the amount written in the invoice is not actually what the node sends, it has to add fees for the intermediate hops.
Business making money from transactions e.g. by automatically deducting a fee of a certain fraction on withdrawal don't experience this problem as much. Fees in lightning are generally small and all major node implementations allow to limit (and do so by default) the maximum relative amount of fees to be paid. So if this amount is lower than the profit margin it would merely decrease profits, but not create any real incentives to withdraw more often to gain any advantage.
Non-profit small service operators on the other hand might want to avoid any profits from running a custodial service to not be forced to think about paying taxes on the 2ct of profits made that year. That's where it begins to get tricky. Fees are not known beforehand, but depend on which route is chosen by the lightning node for a payment. This may lead developers to ignore fees for an MVP, which is very dangerous as it creates non-obvious incentives.
Suppose Malory has an account with a custodial service that doesn't care about fees for now because it's just an MVP and fees are typically low anyway. Malory runs two nodes: one directly connected to the Service and one connected to that one. Both have standard low fees, e.g. 1 sat per routing operation + a negligible percentage.
To steal from the service Malory simply runs the following protocol repeatedly:
- Deposit 1000 sat into the account from the directly connected node (paying no fee)
- withdrawing 100 sat 10 times to the indirectly connected node
- Sending the money from the indirectly to the directly connected node, repeat
Step 2 is where the magic happens: the service provider always pays the low fee out of pocket. Either they didn't think about it or judged it to be so small that it won't be worth the hassle to implement it correctly. But in this case the attacker stands to directly profit from it since she controls all the nodes on the path and thus receives all the fees. There suddenly being an incentive to make withdrawals totally breaks the assumptions of being able to pay a few low fees out of pocket. The attacker could steal all the money on the node using this technique, leaving only a few msat behind.
The easiest way to avoid this problem is always requiring a reserve of a certain percentage of the funds to be sent or
an absolute amount that can't ever be sent. Which of these to implement mainly depends on the API of the underlying node.
C-Lightning only offers to limit the
relative amount of fees paid while LND also allows to set a fixed
maximum. The latter allows for a tweak to build a more optimal solution: it allows to set the maximum fee to
min(max_fee, account_balance - send_amount) more easily and by doing so allowing the all transactions that
stay inside the account's balance instead of reserving a fixed worst-case percentage/absolute amount for fees and denying transactions
that would be possible because they don't use the theoretical maximum fee but have to be denied under the static rules.
But there is still one problem: as a service provider the goal was not to make any profit. Yet this construction
basically guarantees profits in form of abandoned capital. To get all their money out users had to correctly guess the
fee paid for a route to them and request a payment of
account_balance - fees. But this is often impossible or at least
very annoying and error prone. As mentioned earlier the fee depends on the chosen route. But this is neither under the
control of the user nor can the service tell for certain which route will work/they will use eventually.
Such a "send all" feature does not exist yet in any lightning implementation I'm aware of, probably for that reason. One way to implement it would be
using an invoice without an amount or an amount less than
account_value - fee_buffer. Lightning allows to overpay
invoices, so that's not a problem.
The trickier part is to send the payment. As no implementation seems to support a pay feature that lets one specify how much to send out instead of how much the recipient shall receive, the route for the payment - including how much to pay send/pay in fees - has to be constructed by the application. One possibility would be using a feature like C-Lightning's getroute to first get a route for the full amount and then decrease the amounts to be sent so that the service node only sends out the account balance1. This route could then be used to manually send the payment. It's far from elegant but appears to be the only sane solution so far if one doesn't want to end up with user's money.
I hope the discussed solutions will help some of you build more awesome, hacky, but still secure Lightning service. This article was inspired by working on LNbits, a free and open-source lightning-network wallet/accounts system. It's still a very young project but with a lot of interesting features. If you are looking for a nice project to contribute to, give it a try :)
If I missed any implementation already providing described features please let me know and I'll include it in the article. Let's build the future together!
It might be necessary to adjust the amount multiple times if the changed amount leads to a change in paid fees, leading to a change in the amount (does this even terminate all the time?).