From f9ffed9ffd8aadd19bca2c50eb50ceef98f43a46 Mon Sep 17 00:00:00 2001 From: sufay Date: Wed, 10 Jul 2024 12:49:35 +0800 Subject: [PATCH] handle withdrawal tx fee --- x/btcbridge/keeper/keeper_withdraw.go | 111 ++++++++++++++++++++++- x/btcbridge/types/bitcoin_transaction.go | 16 ++++ x/btcbridge/types/keys.go | 6 ++ x/btcbridge/types/params.go | 2 +- 4 files changed, 129 insertions(+), 6 deletions(-) diff --git a/x/btcbridge/keeper/keeper_withdraw.go b/x/btcbridge/keeper/keeper_withdraw.go index 615c444..b8ae250 100644 --- a/x/btcbridge/keeper/keeper_withdraw.go +++ b/x/btcbridge/keeper/keeper_withdraw.go @@ -9,6 +9,7 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -60,7 +61,12 @@ func (k Keeper) NewBtcSigningRequest(ctx sdk.Context, sender string, coin sdk.Co return nil, types.ErrInsufficientUTXOs } - psbt, selectedUTXOs, changeUTXO, err := types.BuildPsbt(utxos, sender, coin.Amount.Int64(), feeRate, vault) + psbt, selectedUTXOs, _, err := types.BuildPsbt(utxos, sender, coin.Amount.Int64(), feeRate, vault) + if err != nil { + return nil, err + } + + changeUTXO, err := k.handleBtcTxFee(psbt, vault) if err != nil { return nil, err } @@ -74,10 +80,8 @@ func (k Keeper) NewBtcSigningRequest(ctx sdk.Context, sender string, coin sdk.Co _ = k.LockUTXOs(ctx, selectedUTXOs) // save the change utxo and mark minted - if changeUTXO != nil { - k.saveUTXO(ctx, changeUTXO) - k.addToMintHistory(ctx, psbt.UnsignedTx.TxHash().String()) - } + k.saveUTXO(ctx, changeUTXO) + k.addToMintHistory(ctx, psbt.UnsignedTx.TxHash().String()) signingRequest := &types.BitcoinSigningRequest{ Address: sender, @@ -115,6 +119,10 @@ func (k Keeper) NewRunesSigningRequest(ctx sdk.Context, sender string, coin sdk. return nil, err } + if err := k.handleRunesTxFee(ctx, psbt, sender); err != nil { + return nil, err + } + psbtB64, err := psbt.B64Encode() if err != nil { return nil, types.ErrFailToSerializePsbt @@ -284,8 +292,14 @@ func (k Keeper) ProcessBitcoinWithdrawTransaction(ctx sdk.Context, msg *types.Ms return nil, err } + // spend the locked utxos k.spendUTXOs(ctx, uTx) + // burn the locked asset + if err := k.burnLockedAsset(ctx, txHash.String()); err != nil { + return nil, err + } + return &txHash, nil } @@ -300,3 +314,90 @@ func (k Keeper) spendUTXOs(ctx sdk.Context, uTx *btcutil.Tx) { } } } + +// handleTxFee performs the fee handling for the btc withdrawal tx +// Make sure that the given psbt is valid +// There are at most two outputs and the change output is the last one if any +func (k Keeper) handleBtcTxFee(p *psbt.Packet, changeAddr string) (*types.UTXO, error) { + recipientOut := p.UnsignedTx.TxOut[0] + + changeOut := new(wire.TxOut) + if len(p.UnsignedTx.TxOut) > 1 { + changeOut = p.UnsignedTx.TxOut[1] + } else { + changeOut = wire.NewTxOut(0, types.MustPkScriptFromAddress(changeAddr)) + p.UnsignedTx.TxOut = append(p.UnsignedTx.TxOut, changeOut) + } + + txFee, err := p.GetTxFee() + if err != nil { + return nil, err + } + + recipientOut.Value -= int64(txFee) + changeOut.Value += int64(txFee) + + if types.IsDustOut(recipientOut) || types.IsDustOut(changeOut) { + return nil, types.ErrDustOutput + } + + return &types.UTXO{ + Txid: p.UnsignedTx.TxHash().String(), + Vout: 1, + Address: changeAddr, + Amount: uint64(changeOut.Value), + PubKeyScript: changeOut.PkScript, + }, nil +} + +// handleRunesTxFee performs the fee handling for the runes withdrawal tx +func (k Keeper) handleRunesTxFee(ctx sdk.Context, p *psbt.Packet, recipient string) error { + txFee, err := p.GetTxFee() + if err != nil { + return err + } + + feeCoin := sdk.NewCoin(k.GetParams(ctx).BtcVoucherDenom, sdk.NewInt(int64(txFee))) + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sdk.MustAccAddressFromBech32(recipient), types.ModuleName, sdk.NewCoins(feeCoin)); err != nil { + return err + } + + k.lockAsset(ctx, p.UnsignedTx.TxHash().String(), feeCoin) + + return nil +} + +// lockAsset locks the given asset by the tx hash +func (k Keeper) lockAsset(ctx sdk.Context, txHash string, coin sdk.Coin) { + store := ctx.KVStore(k.storeKey) + + bz := k.cdc.MustMarshal(&coin) + store.Set(types.BtcLockedAssetKey(txHash), bz) +} + +// getLockedAsset gets the locked asset by the tx hash +func (k Keeper) getLockedAsset(ctx sdk.Context, txHash string) sdk.Coin { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.BtcLockedAssetKey(txHash)) + + var coin sdk.Coin + k.cdc.MustUnmarshal(bz, &coin) + + return coin +} + +// burnLockedAsset burns the locked asset +func (k Keeper) burnLockedAsset(ctx sdk.Context, txHash string) error { + store := ctx.KVStore(k.storeKey) + + if store.Has(types.BtcLockedAssetKey(txHash)) { + lockedCoin := k.getLockedAsset(ctx, txHash) + if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(lockedCoin)); err != nil { + return err + } + + store.Delete(types.BtcLockedAssetKey(txHash)) + } + + return nil +} diff --git a/x/btcbridge/types/bitcoin_transaction.go b/x/btcbridge/types/bitcoin_transaction.go index b706ee6..8033e02 100644 --- a/x/btcbridge/types/bitcoin_transaction.go +++ b/x/btcbridge/types/bitcoin_transaction.go @@ -341,3 +341,19 @@ func CheckOutputAmount(address string, amount int64) error { func IsOpReturnOutput(out *wire.TxOut) bool { return len(out.PkScript) > 0 && out.PkScript[0] == txscript.OP_RETURN } + +// MustPkScriptFromAddress returns the public script of the given address +// Panic if any error occurred +func MustPkScriptFromAddress(address string) []byte { + addr, err := btcutil.DecodeAddress(address, sdk.GetConfig().GetBtcChainCfg()) + if err != nil { + panic(err) + } + + pkScript, err := txscript.PayToAddrScript(addr) + if err != nil { + panic(err) + } + + return pkScript +} diff --git a/x/btcbridge/types/keys.go b/x/btcbridge/types/keys.go index 2731419..b58a4ca 100644 --- a/x/btcbridge/types/keys.go +++ b/x/btcbridge/types/keys.go @@ -37,6 +37,8 @@ var ( BtcOwnerRunesUtxoKeyPrefix = []byte{0x17} // prefix for each key to an owned runes utxo BtcMintedTxHashKeyPrefix = []byte{0x18} // prefix for each key to a minted tx hash + + BtcLockedAssetKeyPrefix = []byte{0x19} // prefix for each key to the locked asset ) func Int64ToBytes(number uint64) []byte { @@ -84,3 +86,7 @@ func BtcSigningRequestHashKey(txid string) []byte { func BtcMintedTxHashKey(hash string) []byte { return append(BtcMintedTxHashKeyPrefix, []byte(hash)...) } + +func BtcLockedAssetKey(txHash string) []byte { + return append(BtcLockedAssetKeyPrefix, []byte(txHash)...) +} diff --git a/x/btcbridge/types/params.go b/x/btcbridge/types/params.go index 873d5cc..d22ce6e 100644 --- a/x/btcbridge/types/params.go +++ b/x/btcbridge/types/params.go @@ -74,7 +74,7 @@ func (p Params) Validate() error { } if vault.AssetType == AssetType_ASSET_TYPE_UNSPECIFIED { - return err + return ErrInvalidParams } }