multi: require Loop Out permission for Instant Out

The InstantOut RPC accepts a caller-controlled dest_addr that becomes
the output of the cooperative sweepless sweep (and of the htlc success
sweep on the fallback path), so it is a fund-moving operation equivalent
to LoopOut. Until now it required only swap:execute, while LoopOut
requires both swap:execute and loop:out. A macaroon scoped to
swap:execute -- intended for, say, an autoloop scheduler or a quote
poller -- could therefore drain reservation balances to an attacker
address. ReservationRequest is analogous on the inbound side: it
triggers an outgoing LN prepayment, so it also belongs behind loop:out.

We also harden the address handling in instantout.Manager.NewInstantOut
to match validateLoopOutRequest:

  - sweepAddr.IsForNet(m.cfg.Network) is now enforced. btcutil
    .DecodeAddress is more permissive than IsForNet for some formats
    (notably anything that happens to share a network prefix); without
    the explicit network check cross-chain copy-paste mistakes parse
    silently and then sign over an unspendable output.
  - The address must be one of the formats Loop normally accepts: P2TR /
    P2WSH / P2WPKH / P2SH / P2PKH. Anything else (e.g. a future address
    type that the user's wallet would otherwise interpret differently)
    is rejected up front rather than failing later in the signing path.

InstantOutQuote and ReservationQuote stay on swap:read since they are
read-only.
This commit is contained in:
Slyghtning 2026-05-11 15:28:26 +02:00
parent e4c4e11627
commit 508b9dfdc1
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
2 changed files with 21 additions and 0 deletions

View file

@ -156,6 +156,24 @@ func (m *Manager) NewInstantOut(ctx context.Context,
if err != nil {
return nil, err
}
if !sweepAddr.IsForNet(m.cfg.Network) {
return nil, fmt.Errorf("sweep address %s is not "+
"valid for network %s", sweepAddress,
m.cfg.Network.Name)
}
switch sweepAddr.(type) {
case *btcutil.AddressTaproot,
*btcutil.AddressWitnessScriptHash,
*btcutil.AddressWitnessPubKeyHash,
*btcutil.AddressScriptHash,
*btcutil.AddressPubKeyHash:
default:
return nil, fmt.Errorf("unsupported sweep address "+
"type %T", sweepAddr)
}
}
m.Lock()

View file

@ -184,6 +184,9 @@ var RequiredPermissions = map[string][]bakery.Op{
"/looprpc.SwapClient/ReservationRequest": {{
Entity: "swap",
Action: "execute",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/ReservationQuote": {{
Entity: "swap",