Skip to content

Commit

Permalink
client/core,eth: Lock funds for refund
Browse files Browse the repository at this point in the history
- client/asset: Rename AssetRedeemer interface to AssetLocker,
  and add identical functions used for refunds in addition to
  redemptions.
- client/eth: Refactor fund locking to keep track of funds locked
  for initiation, refunds, and redemptions separately.
- client/core: Keep track of refund reserves the same way as they are
  tracked for redemptions. In addition to unlocking when the
  deal is revoked or cancelled, funds reserved for refunds
  are also unlocked when either the counterparty or the current
  user does a redemption.

* Make eth fee rater and use server max fee rate for redemptions and refunds.
  • Loading branch information
martonp authored Mar 5, 2022
1 parent a823416 commit 20634d4
Show file tree
Hide file tree
Showing 8 changed files with 1,135 additions and 369 deletions.
284 changes: 184 additions & 100 deletions client/asset/eth/eth.go

Large diffs are not rendered by default.

289 changes: 215 additions & 74 deletions client/asset/eth/eth_test.go

Large diffs are not rendered by default.

40 changes: 27 additions & 13 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,20 +372,34 @@ type TokenMaster interface {
OpenTokenWallet(assetID uint32, settings map[string]string, tipChange func(error)) (Wallet, error)
}

// AccountRedeemer is a wallet in which redemptions require a wallet to have
// available balance to pay fees.
type AccountRedeemer interface {
// ReserveN is used when preparing funding for an order that redeems to an
// account-based asset. The wallet will set aside the appropriate amount of
// funds so that we can redeem. It is an error to request funds > spendable
// balance.
ReserveN(n, feeRate uint64, assetVer uint32) (uint64, error)
// ReReserve is used when reconstructing existing orders on startup. It is
// AccountLocker is a wallet in which redemptions and refunds require a wallet
// to have available balance to pay fees.
type AccountLocker interface {
// ReserveNRedemption is used when preparing funding for an order that
// redeems to an account-based asset. The wallet will set aside the
// appropriate amount of funds so that we can redeem N swaps on the
// specified version of the asset, at the specified fee rate. It is an
// error to request funds > spendable balance.
ReserveNRedemptions(n, feeRate uint64, assetVer uint32) (uint64, error)
// ReReserveRedemption is used when reconstructing existing orders on
// startup. It is an error to request funds > spendable balance.
ReReserveRedemption(amt uint64) error
// UnlockRedemptionReserves is used to return funds reserved for redemption
// when an order is canceled or otherwise completed unfilled.
UnlockRedemptionReserves(uint64)
// ReserveNRefunds is used when preparing funding for an order that
// refunds to an account-based asset. The wallet will set aside the
// appropriate amount of funds so that we can refund N swaps on the
// specified version of the asset, at the specified fee rate. It is
// an error to request funds > spendable balance.
ReReserve(amt uint64) error
// UnlockReserves is used to return funds when an order is canceled or
// otherwise completed unfilled.
UnlockReserves(uint64)
ReserveNRefunds(n, feeRate uint64, assetVer uint32) (uint64, error)
// ReReserveRefund is used when reconstructing existing orders on
// startup. It is an error to request funds > spendable balance.
ReReserveRefund(uint64) error
// UnlockRefundReserves is used to return funds reserved for refunds
// when an order was cancelled or revoked before a swap was initiated,
// completed successully, or after a refund was done.
UnlockRefundReserves(uint64)
}

// Balance is categorized information about a wallet's balance.
Expand Down
56 changes: 44 additions & 12 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -812,8 +812,10 @@ func (dc *dexConnection) reconcileTrades(srvOrderStatuses []*msgjson.OrderStatus
(!trade.isMarketBuy() || len(trade.matches) == 0) {
if trade.isMarketBuy() {
trade.unlockRedemptionFraction(1, 1)
trade.unlockRefundFraction(1, 1)
} else {
trade.unlockRedemptionFraction(trade.Trade().Remaining(), trade.Trade().Quantity)
trade.unlockRefundFraction(trade.Trade().Remaining(), trade.Trade().Quantity)
}
}
// Now update the trade.
Expand Down Expand Up @@ -4067,7 +4069,8 @@ func (c *Core) prepareTrackedTrade(dc *dexConnection, form *TradeForm, crypter e
}
fromWallet, toWallet := wallets.fromWallet, wallets.toWallet

accountRedeemer, isAccountRedemption := toWallet.Wallet.(asset.AccountRedeemer)
accountRedeemer, isAccountRedemption := toWallet.Wallet.(asset.AccountLocker)
accountRefunder, isAccountRefund := fromWallet.Wallet.(asset.AccountLocker)

prepareWallet := func(w *xcWallet) error {
// NOTE: If the wallet is already internally unlocked (the decrypted
Expand Down Expand Up @@ -4114,7 +4117,7 @@ func (c *Core) prepareTrackedTrade(dc *dexConnection, form *TradeForm, crypter e
if form.IsLimit && !form.Sell {
fundQty = calc.BaseToQuote(rate, fundQty)
}
redemptionLots := lots
redemptionRefundLots := lots

isImmediate := (!form.IsLimit || form.TifNow)

Expand All @@ -4137,7 +4140,7 @@ func (c *Core) prepareTrackedTrade(dc *dexConnection, form *TradeForm, crypter e
if err == nil {
baseQty := calc.QuoteToBase(midGap, fundQty)
lots = baseQty / lotSize
redemptionLots = lots * marketBuyRedemptionSlippageBuffer
redemptionRefundLots = lots * marketBuyRedemptionSlippageBuffer
if lots == 0 {
err = newError(orderParamsErr,
"order quantity is too low for current market rates. "+
Expand Down Expand Up @@ -4252,7 +4255,7 @@ func (c *Core) prepareTrackedTrade(dc *dexConnection, form *TradeForm, crypter e
// Everything is ready. Send the order.
route, msgOrder, msgTrade := messageOrder(ord, msgCoins)

// If the to asset is an AccountRedeemer, we need to lock up redemption
// If the to asset is an AccountLocker, we need to lock up redemption
// funds.
var redemptionReserves uint64
if isAccountRedemption {
Expand All @@ -4263,17 +4266,31 @@ func (c *Core) prepareTrackedTrade(dc *dexConnection, form *TradeForm, crypter e
if len(pubKeys) == 0 || len(sigs) == 0 {
return nil, 0, newError(signatureErr, "wrong number of pubkeys or signatures, %d & %d", len(pubKeys), len(sigs))
}
redemptionReserves, err = accountRedeemer.ReserveN(redemptionLots, wallets.toAsset.MaxFeeRate, wallets.toAsset.Version)
redemptionReserves, err = accountRedeemer.ReserveNRedemptions(redemptionRefundLots, wallets.toAsset.MaxFeeRate, wallets.toAsset.Version)
if err != nil {
return nil, 0, codedError(walletErr, fmt.Errorf("ReserveN error: %w", err))
return nil, 0, codedError(walletErr, fmt.Errorf("ReserveNRedemptions error: %w", err))
}
msgTrade.RedeemSig = &msgjson.RedeemSig{
PubKey: pubKeys[0],
Sig: sigs[0],
}
defer func() {
if !success {
accountRedeemer.UnlockReserves(redemptionReserves)
accountRedeemer.UnlockRedemptionReserves(redemptionReserves)
}
}()
}

// If the from asset is an AccountLocker, we need to lock up refund funds.
var refundReserves uint64
if isAccountRefund {
refundReserves, err = accountRefunder.ReserveNRefunds(redemptionRefundLots, wallets.fromAsset.MaxFeeRate, wallets.fromAsset.Version)
if err != nil {
return nil, 0, codedError(walletErr, fmt.Errorf("ReserveNRefunds error: %w", err))
}
defer func() {
if !success {
accountRefunder.UnlockRefundReserves(refundReserves)
}
}()
}
Expand Down Expand Up @@ -4319,9 +4336,10 @@ func (c *Core) prepareTrackedTrade(dc *dexConnection, form *TradeForm, crypter e
// Store the order.
dbOrder := &db.MetaOrder{
MetaData: &db.OrderMetaData{
Status: order.OrderStatusEpoch,
Host: dc.acct.host,
MaxFeeRate: wallets.fromAsset.MaxFeeRate,
Status: order.OrderStatusEpoch,
Host: dc.acct.host,
MaxFeeRate: wallets.fromAsset.MaxFeeRate,
RedeemMaxFeeRate: wallets.toAsset.MaxFeeRate,
Proof: db.OrderProof{
DEXSig: result.Sig,
Preimage: preImg[:],
Expand All @@ -4342,9 +4360,10 @@ func (c *Core) prepareTrackedTrade(dc *dexConnection, form *TradeForm, crypter e

// Prepare and store the tracker and get the core.Order to return.
tracker := newTrackedTrade(dbOrder, preImg, dc, dc.marketEpochDuration(mktID), c.lockTimeTaker, c.lockTimeMaker,
c.db, c.latencyQ, wallets, coins, c.notify, c.formatDetails, form.Options, redemptionReserves)
c.db, c.latencyQ, wallets, coins, c.notify, c.formatDetails, form.Options, redemptionReserves, refundReserves)

tracker.redemptionLocked = tracker.redemptionReserves
tracker.refundLocked = tracker.refundReserves

if recoveryCoin != nil {
tracker.change = recoveryCoin
Expand Down Expand Up @@ -4957,7 +4976,7 @@ func (c *Core) dbTrackers(dc *dexConnection) (map[order.OrderID]*trackedTrade, e
copy(preImg[:], dbOrder.MetaData.Proof.Preimage)
tracker := newTrackedTrade(dbOrder, preImg, dc, dc.marketEpochDuration(mktID), c.lockTimeTaker,
c.lockTimeMaker, c.db, c.latencyQ, nil, nil, c.notify, c.formatDetails,
dbOrder.MetaData.Options, dbOrder.MetaData.RedemptionReserves)
dbOrder.MetaData.Options, dbOrder.MetaData.RedemptionReserves, dbOrder.MetaData.RefundReserves)
trackers[dbOrder.Order.ID()] = tracker

// Get matches.
Expand Down Expand Up @@ -5166,6 +5185,14 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa
}
}

lockMatchRefund := func(match *matchTracker) {
if tracker.isMarketBuy() {
tracker.lockRefundFraction(1, uint64(len(tracker.matches)))
} else {
tracker.lockRefundFraction(match.Quantity, trade.Quantity)
}
}

// If matches haven't redeemed, but the counter-swap has been received,
// reload the audit info.
isActive := tracker.metaData.Status == order.OrderStatusBooked || tracker.metaData.Status == order.OrderStatusEpoch
Expand All @@ -5183,6 +5210,7 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa
}
if match.Status < order.MakerRedeemed {
lockMatchRedemption(match)
lockMatchRefund(match)
}
} else { // Taker
if match.Status < order.TakerSwapCast {
Expand All @@ -5192,6 +5220,9 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa
needsAuditInfo = true // taker needs AuditInfo for maker's contract
counterSwap = match.MetaData.Proof.MakerSwap
}
if match.Status < order.MakerRedeemed {
lockMatchRefund(match)
}
if match.Status < order.MatchComplete {
lockMatchRedemption(match)
}
Expand Down Expand Up @@ -5310,6 +5341,7 @@ func (c *Core) resumeTrades(dc *dexConnection, trackers []*trackedTrade) assetMa

if isActive {
tracker.lockRedemptionFraction(trade.Remaining(), trade.Quantity)
tracker.lockRefundFraction(trade.Remaining(), trade.Quantity)
}

// Balances should be updated for any orders with locked wallet coins,
Expand Down
Loading

0 comments on commit 20634d4

Please sign in to comment.