Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client/core,eth: Lock funds for refund #1479

Merged
merged 5 commits into from
Mar 5, 2022
Merged

Conversation

martonp
Copy link
Contributor

@martonp martonp commented Feb 19, 2022

Previously, the funds needed for refunds were locked in FundOrder together with the funds needed for initiations, but they were unlocked when Swap was called with LockChange = false. This diff applies the same logic we use to reserve funds for redemptions to refunds.

Prior to this PR, redemptions were being reserved using the server's max fee rate, but the redemption transaction was being submitted with the current fee rate estimate. This PR updates this to use the server's max fee rate when submitting the transaction as well. There is no reason to use a lower rate when submitting the transaction, as ETH uses dynamic transaction fees, and the entire fee will not be used anyways. The server's max fee rate is also used for refunds.

  • client/asset: Rename AssetRedeemer interface to AssetLocker, and add functions for refunds with the same functionality as already exist for redemptions.
  • client/eth: Refactor fund locking to keep track of funds locked for initiation, refunds, and redemptions separately. Also, make ETH a FeeRater.
  • 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.

Closes #1466

client/asset/eth/eth.go Outdated Show resolved Hide resolved
client/asset/eth/eth.go Show resolved Hide resolved
client/asset/eth/eth.go Outdated Show resolved Hide resolved
client/asset/eth/eth.go Outdated Show resolved Hide resolved
client/core/core.go Show resolved Hide resolved
@chappjc
Copy link
Member

chappjc commented Feb 22, 2022

Minor conflict by proximity to FeeRater changes in core_test.go. Not holding up my review though, that's coming shortly.

Copy link
Member

@JoeGruffins JoeGruffins left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working well.

Copy link
Member

@chappjc chappjc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some quick comments. I think we need to have a discussion in matrix about making the eth client a FeeRate with the formula we've settled on, and then perhaps capping the computed rate by the user's fee rate limit. related: #1485

client/db/types.go Outdated Show resolved Hide resolved
client/asset/interface.go Outdated Show resolved Hide resolved
client/asset/interface.go Outdated Show resolved Hide resolved
client/asset/eth/eth.go Outdated Show resolved Hide resolved
- 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.
@martonp martonp force-pushed the lockRefundFees branch 4 times, most recently from 5d55f85 to 7281814 Compare February 26, 2022 23:30
@@ -4203,17 +4206,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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me. Only additional thought about this matter is that the rate given to ReserveNRedemptions and ReserveNRefunds could be the larger of MaxFeeRate and the rate from FeeRate (if also a FeeRater). That would be pretty screwy if the server had a rate set that low, so whatever, let's not change anything now. This PR looks good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the swap cannot be initiated, then it doesn't matter if too little is reserved for refunds and redemptions.

Copy link
Member

@chappjc chappjc Feb 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The swap tx could still be mined with a gas fee cap less than what FeeRate would recommend. It would just be dicey for the refund (that happens in several hours). For the redeem, you're redeeming the other person's swap, so who knows what they used.

client/core/trade.go Outdated Show resolved Hide resolved
@@ -95,7 +96,8 @@ var (
fromVersionKey = []byte("fromVersion")
toVersionKey = []byte("toVersion")
optionsKey = []byte("options")
reservesKey = []byte("reservesKey")
Copy link
Member

@chappjc chappjc Feb 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, I'm on board with breaking compatibility with the last dev version of the DB encoding. Not worth an upgrade esp. considering the state of ETH (not used in the wild).

Copy link
Member

@chappjc chappjc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Kinda too bad that the unlock method has an error return though.

Comment on lines 256 to 257
default:
return nil, fmt.Errorf("invalid fund reserve type: %v", t)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tempted to say panic, remove the error and logging in unlock's caller.
With an enum of fundReserveTypes, it takes a special type of programmer error to pass an invalid value.

@chappjc chappjc requested a review from buck54321 March 1, 2022 15:07
@chappjc
Copy link
Member

chappjc commented Mar 1, 2022

Ready to merge unless @buck54321 is reviewing.

Comment on lines 652 to 657
initiationFunds := ord.Value + maxSwapFees
coins := asset.Coins{eth.createFundingCoin(initiationFunds)}

eth.lockedFundsMtx.Lock()
defer eth.lockedFundsMtx.Unlock()
err := eth.lockedFunds.lock(initiationFunds, initiationReserve, eth.balance)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we lock up the refund reserves here too, so the caller doesn't have to do it separately?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about that too, but I did it this way because if the refund reserves were locked here, then FundOrder would need to return the amount locked for refunds separately. I didn't want this to have to be done in non-account based coins.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. I might propose a change later, but this is good for now.

}

// lock locks funds for a use case.
func (r *fundReserves) lock(amt uint64, t fundReserveType, getBalanceFn func() (*asset.Balance, error)) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things here that suggest to me that fundReserves is better as an ExchangeWallet struct field. 1) You're passing in ExchangeWallet methods, 2) you're returning errors just for logging, and 3) the mutex is still being handled by ExchangeWallet. How about converting these to ExchangeWallet methods and doing

type ExchangeWallet struct {
    ...
    lockedFundsMtx sync.RWMutex
    lockedFunds struct {
        initiateReserves   uint64
        redemptionReserves uint64
        refundReserves     uint64
    }
    ...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. Could even embed the mutex in that struct

Comment on lines 216 to 255
newReserved := applyFraction(num, denom, t.redemptionReserves)
if t.redemptionLocked+newReserved > t.redemptionReserves {

newReserved := t.reservesToLock(num, denom, t.redemptionReserves, t.redemptionLocked)

if err := redeemer.ReReserveRedemption(newReserved); err != nil {
t.dc.log.Errorf("error re-reserving redemption %d %s for order %s: %v",
newReserved, t.wallets.toAsset.UnitInfo.AtomicUnit, t.ID(), err)
return
}
t.redemptionLocked += newReserved
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a change in behavior, even if the outcome may be identical. Before, if the requested reservers were > redemptionReserves, an error was logged and that's it. Now, an error is logged, but we still call ReReserveRedemption with an argument of 0, and add 0 to the redemptionLocked. I don't expect that this would result in any issues, but calling ReReserveRedemption with 0 is questionable behavior.

// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method would be unneeded if we lock up the refund reserves in FundOrder.

Copy link
Member

@buck54321 buck54321 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few last things.


// fundReserveOfType returns a pointer to the funds reserved for a particular
// use case.
func (eth *ExchangeWallet) fundReserveOfType(t fundReserveType) *uint64 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this down by the other related methods below the ExchangeWallet definition.

func (eth *ExchangeWallet) balance() (*asset.Balance, error) {
bal, err := eth.node.balance(eth.ctx)
if err != nil {
return nil, err
}

locked := eth.locked + eth.redemptionReserve
locked := eth.amountLocked()
fmt.Printf("in balance -- %+v - %v\n", bal, locked)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this can go

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You've got another one of these three lines down.

@@ -640,6 +702,7 @@ func (t *trackedTrade) negotiate(msgMatches []*msgjson.Match) error {
// reserves.
if remain := trade.Quantity - preCancelFilled; remain > 0 && (completedMarketSell || cancelMatch != nil) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not from this PR, but I think we need to return reserves when lo, ok := t.Order.(*order.LimitOrder); ok && lo.Force == order.StandingTiF here too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean ImmediateTiF right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Sorry.

@chappjc chappjc merged commit 20634d4 into decred:master Mar 5, 2022
@chappjc chappjc added this to the 0.5 milestone Apr 21, 2022
@chappjc chappjc added the ETH label Aug 26, 2022
@martonp martonp deleted the lockRefundFees branch December 20, 2022 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

client/eth: Funds locked to be used for refund are unlocked too early
4 participants