@@ -37,6 +37,11 @@ const (
3737 // defaultSweepFeeRateLimit is the default limit we place on estimated
3838 // sweep fees, (750 * 4 /1000 = 3 sat/vByte).
3939 defaultSweepFeeRateLimit = chainfee .SatPerKWeight (750 )
40+
41+ // minerMultiplier is a multiplier we use to scale our miner fee to
42+ // ensure that we will still be able to complete our swap in the case
43+ // of a severe fee spike.
44+ minerMultiplier = 100
4045)
4146
4247var (
5560 // ErrZeroPrepay is returned if a zero maximum prepay is set.
5661 ErrZeroPrepay = errors .New ("maximum prepay must be non-zero" )
5762
63+ // ErrInvalidPPM is returned is the parts per million for a fee rate
64+ // are invalid.
65+ ErrInvalidPPM = errors .New ("invalid ppm" )
66+
5867 // ErrInvalidSweepFeeRateLimit is returned if an invalid sweep fee limit
5968 // is set.
6069 ErrInvalidSweepFeeRateLimit = fmt .Errorf ("sweep fee rate limit must " +
@@ -224,3 +233,144 @@ func (f *FeeCategoryLimit) loopOutFees(amount btcutil.Amount,
224233
225234 return prepayMaxFee , routeMaxFee , f .MaximumMinerFee
226235}
236+
237+ // Compile time assertion that FeePortion implements FeeLimit interface.
238+ var _ FeeLimit = (* FeePortion )(nil )
239+
240+ // FeePortion is a fee limitation which limits fees to a set portion of
241+ // the swap amount.
242+ type FeePortion struct {
243+ // PartsPerMillion is the total portion of the swap amount that the
244+ // swap may consume.
245+ PartsPerMillion uint64
246+ }
247+
248+ // NewFeePortion creates a fee limit based on a flat percentage of swap amount.
249+ func NewFeePortion (ppm uint64 ) * FeePortion {
250+ return & FeePortion {
251+ PartsPerMillion : ppm ,
252+ }
253+ }
254+
255+ // String returns a string representation of the fee limit.
256+ func (f * FeePortion ) String () string {
257+ return fmt .Sprintf ("parts per million: %v" , f .PartsPerMillion )
258+ }
259+
260+ // validate returns an error if the values provided are invalid.
261+ func (f * FeePortion ) validate () error {
262+ if f .PartsPerMillion <= 0 {
263+ return ErrInvalidPPM
264+ }
265+
266+ return nil
267+ }
268+
269+ // mayLoopOut checks our estimated loop out sweep fee against our sweep limit.
270+ // For fee percentage, we do not check anything because we need the full quote
271+ // to determine whether we can perform a swap.
272+ func (f * FeePortion ) mayLoopOut (_ chainfee.SatPerKWeight ) error {
273+ return nil
274+ }
275+
276+ // loopOutLimits checks whether the quote provided is within our fee
277+ // limits for the swap amount.
278+ func (f * FeePortion ) loopOutLimits (swapAmt btcutil.Amount ,
279+ quote * loop.LoopOutQuote ) error {
280+
281+ // First, check whether any of the individual fee categories provided
282+ // by the server are more than our total limit. We do this so that we
283+ // can provide more specific reasons for not executing swaps.
284+ feeLimit := ppmToSat (swapAmt , f .PartsPerMillion )
285+ minerFee := scaleMinerFee (quote .MinerFee )
286+
287+ if minerFee > feeLimit {
288+ log .Debugf ("miner fee: %v greater than fee limit: %v, at " +
289+ "%v ppm" , minerFee , feeLimit , f .PartsPerMillion )
290+
291+ return newReasonError (ReasonMinerFee )
292+ }
293+
294+ if quote .SwapFee > feeLimit {
295+ log .Debugf ("swap fee: %v greater than fee limit: %v, at " +
296+ "%v ppm" , quote .SwapFee , feeLimit , f .PartsPerMillion )
297+
298+ return newReasonError (ReasonSwapFee )
299+ }
300+
301+ if quote .PrepayAmount > feeLimit {
302+ log .Debugf ("prepay amount: %v greater than fee limit: %v, at " +
303+ "%v ppm" , quote .PrepayAmount , feeLimit , f .PartsPerMillion )
304+
305+ return newReasonError (ReasonPrepay )
306+ }
307+
308+ // If our miner and swap fee equal our limit, we will have nothing left
309+ // for off-chain fees, so we fail out early.
310+ if minerFee + quote .SwapFee >= feeLimit {
311+ log .Debugf ("no budget for off-chain routing with miner fee: " +
312+ "%v, swap fee: %v and fee limit: %v, at %v ppm" ,
313+ minerFee , quote .SwapFee , feeLimit , f .PartsPerMillion )
314+
315+ return newReasonError (ReasonFeePPMInsufficient )
316+ }
317+
318+ prepay , route , miner := f .loopOutFees (swapAmt , quote )
319+
320+ // Calculate the worst case fees that we could pay for this swap,
321+ // ensuring that we are within our fee limit even if the swap fails.
322+ fees := worstCaseOutFees (
323+ prepay , route , quote .SwapFee , miner , quote .PrepayAmount ,
324+ )
325+
326+ if fees > feeLimit {
327+ log .Debugf ("total fees for swap: %v > fee limit: %v, at " +
328+ "%v ppm" , fees , feeLimit , f .PartsPerMillion )
329+
330+ return newReasonError (ReasonFeePPMInsufficient )
331+ }
332+
333+ return nil
334+ }
335+
336+ // loopOutFees return the maximum prepay and invoice routing fees for a swap
337+ // amount and quote. Note that the fee portion implementation just returns
338+ // the quote's miner fee, assuming that this value has already been validated.
339+ // We also assume that the quote's minerfee + swapfee < fee limit, so that we
340+ // have some fees left for off-chain routing.
341+ func (f * FeePortion ) loopOutFees (amount btcutil.Amount ,
342+ quote * loop.LoopOutQuote ) (btcutil.Amount , btcutil.Amount ,
343+ btcutil.Amount ) {
344+
345+ // Calculate the total amount we can spend in fees, and subtract the
346+ // amounts provided by the quote to get the total available for
347+ // off-chain fees.
348+ feeLimit := ppmToSat (amount , f .PartsPerMillion )
349+ minerFee := scaleMinerFee (quote .MinerFee )
350+
351+ available := feeLimit - minerFee - quote .SwapFee
352+
353+ prepayMaxFee , routeMaxFee := splitOffChain (
354+ available , quote .PrepayAmount , amount ,
355+ )
356+
357+ return prepayMaxFee , routeMaxFee , minerFee
358+ }
359+
360+ // splitOffChain takes an available fee budget and divides it among our prepay
361+ // and swap payments proportional to their volume.
362+ func splitOffChain (available , prepayAmt ,
363+ swapAmt btcutil.Amount ) (btcutil.Amount , btcutil.Amount ) {
364+
365+ total := swapAmt + prepayAmt
366+
367+ prepayMaxFee := available * prepayAmt / total
368+ routeMaxFee := available * swapAmt / total
369+
370+ return prepayMaxFee , routeMaxFee
371+ }
372+
373+ // scaleMinerFee scales our miner fee by our constant multiplier.
374+ func scaleMinerFee (estimate btcutil.Amount ) btcutil.Amount {
375+ return estimate * btcutil .Amount (minerMultiplier )
376+ }
0 commit comments