diff --git a/x/btcbridge/client/cli/query.go b/x/btcbridge/client/cli/query.go index fd680f0..b58f9bf 100644 --- a/x/btcbridge/client/cli/query.go +++ b/x/btcbridge/client/cli/query.go @@ -31,7 +31,6 @@ func GetQueryCmd(_ string) *cobra.Command { cmd.AddCommand(CmdQueryBlock()) cmd.AddCommand(CmdQueryUTXOs()) cmd.AddCommand(CmdQuerySigningRequest()) - // this line is used by starport scaffolding # 1 return cmd diff --git a/x/btcbridge/keeper/keeper_deposit.go b/x/btcbridge/keeper/keeper_deposit.go index 553558f..7935c5a 100644 --- a/x/btcbridge/keeper/keeper_deposit.go +++ b/x/btcbridge/keeper/keeper_deposit.go @@ -61,7 +61,7 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg return err } - // extract senders from the previous transaction + // Decode the previous transaction prevTxBytes, err := base64.StdEncoding.DecodeString(msg.PrevTxBytes) if err != nil { fmt.Println("Error decoding transaction from base64:", err) @@ -90,19 +90,10 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg return types.ErrInvalidBtcTransaction } - // check if the output is a valid address - // if there are multiple inputs, then the first input is considered as the sender - // assumpe all inputs are from the same sender - out := prevTx.MsgTx().TxOut[tx.TxIn[0].PreviousOutPoint.Index] - // check if the output is a valid address - pk, err := txscript.ParsePkScript(out.PkScript) - if err != nil { - return err - } - chainCfg := sdk.GetConfig().GetBtcChainCfg() - sender, err := pk.Address(chainCfg) + // Extract the recipient address + recipient, err := types.ExtractRecipientAddr(&tx, &prevMsgTx, param.Vaults, chainCfg) if err != nil { return err } @@ -145,12 +136,12 @@ func (k Keeper) ProcessBitcoinDepositTransaction(ctx sdk.Context, msg *types.Msg // skip if the asset type of the sender address is unspecified switch vault.AssetType { case types.AssetType_ASSET_TYPE_BTC: - err := k.mintBTC(ctx, uTx, header.Height, sender.EncodeAddress(), vault, out, i, param.BtcVoucherDenom) + err := k.mintBTC(ctx, uTx, header.Height, recipient.EncodeAddress(), vault, out, i, param.BtcVoucherDenom) if err != nil { return err } case types.AssetType_ASSET_TYPE_RUNE: - k.mintRUNE(ctx, uTx, header.Height, sender.EncodeAddress(), vault, out, i, "rune") + k.mintRUNE(ctx, uTx, header.Height, recipient.EncodeAddress(), vault, out, i, "rune") } } @@ -196,8 +187,7 @@ func (k Keeper) mintBTC(ctx sdk.Context, uTx *btcutil.Tx, height uint64, sender IsLocked: false, } - k.SetUTXO(ctx, &utxo) - k.SetOwnerUTXO(ctx, &utxo) + k.saveUTXO(ctx, &utxo) return nil } diff --git a/x/btcbridge/keeper/keeper_withdraw.go b/x/btcbridge/keeper/keeper_withdraw.go index 8884bd3..009267d 100644 --- a/x/btcbridge/keeper/keeper_withdraw.go +++ b/x/btcbridge/keeper/keeper_withdraw.go @@ -9,7 +9,9 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/sideprotocol/side/x/btcbridge/types" ) @@ -54,9 +56,9 @@ func (k Keeper) NewSigningRequest(ctx sdk.Context, sender string, coin sdk.Coin, return nil, types.ErrInsufficientUTXOs } - psbt, selectedUTXOs, err := types.BuildPsbt(utxos, sender, coin.Amount.Int64(), feeRate, vault) + psbt, selectedUTXOs, changeUTXO, err := types.BuildPsbt(utxos, sender, coin.Amount.Int64(), feeRate, vault) if err != nil { - return nil, types.ErrFailToBuildTransaction + return nil, err } psbtB64, err := psbt.B64Encode() @@ -67,6 +69,10 @@ func (k Keeper) NewSigningRequest(ctx sdk.Context, sender string, coin sdk.Coin, // lock the selected utxos k.LockUTXOs(ctx, selectedUTXOs) + // save the change utxo and mark minted + k.saveUTXO(ctx, changeUTXO) + k.addToMintHistory(ctx, psbt.UnsignedTx.TxHash().String()) + signingRequest := &types.BitcoinSigningRequest{ Address: sender, Txid: psbt.UnsignedTx.TxHash().String(), diff --git a/x/btcbridge/keeper/utxo.go b/x/btcbridge/keeper/utxo.go index 1e246b8..9ea0c54 100644 --- a/x/btcbridge/keeper/utxo.go +++ b/x/btcbridge/keeper/utxo.go @@ -272,6 +272,12 @@ func (bk *BaseUTXOKeeper) SpendUTXOs(ctx sdk.Context, utxos []*types.UTXO) error return nil } +// saveUTXO saves the given utxo +func (bk *BaseUTXOKeeper) saveUTXO(ctx sdk.Context, utxo *types.UTXO) { + bk.SetUTXO(ctx, utxo) + bk.SetOwnerUTXO(ctx, utxo) +} + // removeUTXO deletes the given utxo which is assumed to exist. func (bk *BaseUTXOKeeper) removeUTXO(ctx sdk.Context, hash string, vout uint64) { store := ctx.KVStore(bk.storeKey) diff --git a/x/btcbridge/types/bitcoin_transaction.go b/x/btcbridge/types/bitcoin_transaction.go index b94e14a..5d40386 100644 --- a/x/btcbridge/types/bitcoin_transaction.go +++ b/x/btcbridge/types/bitcoin_transaction.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/mempool" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" + sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -19,35 +20,35 @@ const ( ) // BuildPsbt builds a bitcoin psbt from the given params. -// Assume that the utxo script type is witness. -func BuildPsbt(utxos []*UTXO, recipient string, amount int64, feeRate int64, change string) (*psbt.Packet, []*UTXO, error) { +// Assume that the utxo script type is native segwit. +func BuildPsbt(utxos []*UTXO, recipient string, amount int64, feeRate int64, change string) (*psbt.Packet, []*UTXO, *UTXO, error) { chaincfg := sdk.GetConfig().GetBtcChainCfg() recipientAddr, err := btcutil.DecodeAddress(recipient, chaincfg) if err != nil { - return nil, nil, err + return nil, nil, nil, err } recipientPkScript, err := txscript.PayToAddrScript(recipientAddr) if err != nil { - return nil, nil, err + return nil, nil, nil, err } changeAddr, err := btcutil.DecodeAddress(change, chaincfg) if err != nil { - return nil, nil, err + return nil, nil, nil, err } txOuts := make([]*wire.TxOut, 0) txOuts = append(txOuts, wire.NewTxOut(amount, recipientPkScript)) - unsignedTx, selectedUTXOs, err := BuildUnsignedTransaction(utxos, txOuts, feeRate, changeAddr) + unsignedTx, selectedUTXOs, changeUTXO, err := BuildUnsignedTransaction(utxos, txOuts, feeRate, changeAddr) if err != nil { - return nil, nil, err + return nil, nil, nil, err } p, err := psbt.NewFromUnsignedTx(unsignedTx) if err != nil { - return nil, nil, err + return nil, nil, nil, err } for i, utxo := range selectedUTXOs { @@ -55,17 +56,17 @@ func BuildPsbt(utxos []*UTXO, recipient string, amount int64, feeRate int64, cha p.Inputs[i].WitnessUtxo = wire.NewTxOut(int64(utxo.Amount), utxo.PubKeyScript) } - return p, selectedUTXOs, nil + return p, selectedUTXOs, changeUTXO, nil } // BuildUnsignedTransaction builds an unsigned tx from the given params. -func BuildUnsignedTransaction(utxos []*UTXO, txOuts []*wire.TxOut, feeRate int64, change btcutil.Address) (*wire.MsgTx, []*UTXO, error) { +func BuildUnsignedTransaction(utxos []*UTXO, txOuts []*wire.TxOut, feeRate int64, change btcutil.Address) (*wire.MsgTx, []*UTXO, *UTXO, error) { tx := wire.NewMsgTx(TxVersion) outAmount := int64(0) for _, txOut := range txOuts { if mempool.IsDust(txOut, MinRelayFee) { - return nil, nil, ErrDustOutput + return nil, nil, nil, ErrDustOutput } tx.AddTxOut(txOut) @@ -74,17 +75,29 @@ func BuildUnsignedTransaction(utxos []*UTXO, txOuts []*wire.TxOut, feeRate int64 changePkScript, err := txscript.PayToAddrScript(change) if err != nil { - return nil, nil, err + return nil, nil, nil, err } changeOut := wire.NewTxOut(0, changePkScript) selectedUTXOs, err := AddUTXOsToTx(tx, utxos, outAmount, changeOut, feeRate) if err != nil { - return nil, nil, err + return nil, nil, nil, err + } + + var changeUTXO *UTXO + if len(tx.TxOut) > len(txOuts) { + changeOut := tx.TxOut[len(tx.TxOut)-1] + changeUTXO = &UTXO{ + Txid: tx.TxHash().String(), + Vout: uint64(len(tx.TxOut) - 1), + Address: change.EncodeAddress(), + Amount: uint64(changeOut.Value), + PubKeyScript: changeOut.PkScript, + } } - return tx, selectedUTXOs, nil + return tx, selectedUTXOs, changeUTXO, nil } // AddUTXOsToTx adds the given utxos to the tx. @@ -169,3 +182,22 @@ func GetTxVirtualSize(tx *wire.MsgTx, utxos []*UTXO) int64 { return mempool.GetTxVirtualSize(btcutil.NewTx(newTx)) } + +// CheckOutput checks the given output +func CheckOutput(address string, amount int64) error { + addr, err := btcutil.DecodeAddress(address, sdk.GetConfig().GetBtcChainCfg()) + if err != nil { + return err + } + + pkScript, err := txscript.PayToAddrScript(addr) + if err != nil { + return err + } + + if mempool.IsDust(&wire.TxOut{Value: amount, PkScript: pkScript}, MinRelayFee) { + return ErrDustOutput + } + + return nil +} diff --git a/x/btcbridge/types/deposit_policy.go b/x/btcbridge/types/deposit_policy.go new file mode 100644 index 0000000..ad2d1e3 --- /dev/null +++ b/x/btcbridge/types/deposit_policy.go @@ -0,0 +1,58 @@ +package types + +import ( + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" +) + +const ( + // maximum allowed number of the non-vault outputs for the deposit transaction + MaxNonVaultOutNum = 1 +) + +// ExtractRecipientAddr extracts the recipient address for minting voucher token. +// First, extract the recipient from the tx out which is a non-vault address; +// Then fallback to the first input +func ExtractRecipientAddr(tx *wire.MsgTx, prevTx *wire.MsgTx, vaults []*Vault, chainCfg *chaincfg.Params) (btcutil.Address, error) { + var recipient btcutil.Address + + nonVaultOutCount := 0 + + // extract from the tx out which is a non-vault address + for _, out := range tx.TxOut { + pkScript, err := txscript.ParsePkScript(out.PkScript) + if err != nil { + return nil, err + } + + addr, err := pkScript.Address(chainCfg) + if err != nil { + return nil, err + } + + vault := SelectVaultByBitcoinAddress(vaults, addr.EncodeAddress()) + if vault == nil { + recipient = addr + nonVaultOutCount++ + } + } + + // exceed allowed non vault out number + if nonVaultOutCount > MaxNonVaultOutNum { + return nil, ErrInvalidDepositTransaction + } + + if recipient != nil { + return recipient, nil + } + + // fallback to extract from the first input + pkScript, err := txscript.ParsePkScript(prevTx.TxOut[tx.TxIn[0].PreviousOutPoint.Index].PkScript) + if err != nil { + return nil, err + } + + return pkScript.Address(chainCfg) +} diff --git a/x/btcbridge/types/errors.go b/x/btcbridge/types/errors.go index cd19051..80a67f9 100644 --- a/x/btcbridge/types/errors.go +++ b/x/btcbridge/types/errors.go @@ -15,13 +15,14 @@ var ( ErrInvalidSenders = errorsmod.Register(ModuleName, 2100, "invalid allowed senders") - ErrInvalidBtcTransaction = errorsmod.Register(ModuleName, 3100, "invalid bitcoin transaction") - ErrBlockNotFound = errorsmod.Register(ModuleName, 3101, "block not found") - ErrTransactionNotIncluded = errorsmod.Register(ModuleName, 3102, "transaction not included in block") - ErrNotConfirmed = errorsmod.Register(ModuleName, 3200, "transaction not confirmed") - ErrExceedMaxAcceptanceDepth = errorsmod.Register(ModuleName, 3201, "exceed max acceptance block depth") - ErrUnsupportedScriptType = errorsmod.Register(ModuleName, 3202, "unsupported script type") - ErrTransactionAlreadyMinted = errorsmod.Register(ModuleName, 3203, "transaction already minted") + ErrInvalidBtcTransaction = errorsmod.Register(ModuleName, 3100, "invalid bitcoin transaction") + ErrBlockNotFound = errorsmod.Register(ModuleName, 3101, "block not found") + ErrTransactionNotIncluded = errorsmod.Register(ModuleName, 3102, "transaction not included in block") + ErrNotConfirmed = errorsmod.Register(ModuleName, 3200, "transaction not confirmed") + ErrExceedMaxAcceptanceDepth = errorsmod.Register(ModuleName, 3201, "exceed max acceptance block depth") + ErrUnsupportedScriptType = errorsmod.Register(ModuleName, 3202, "unsupported script type") + ErrTransactionAlreadyMinted = errorsmod.Register(ModuleName, 3203, "transaction already minted") + ErrInvalidDepositTransaction = errorsmod.Register(ModuleName, 3204, "invalid deposit transaction") ErrInvalidSignatures = errorsmod.Register(ModuleName, 4200, "invalid signatures") ErrInsufficientBalance = errorsmod.Register(ModuleName, 4201, "insufficient balance") @@ -31,9 +32,9 @@ var ( ErrUTXOLocked = errorsmod.Register(ModuleName, 5101, "utxo locked") ErrUTXOUnlocked = errorsmod.Register(ModuleName, 5102, "utxo unlocked") - ErrInvalidFeeRate = errorsmod.Register(ModuleName, 6100, "invalid fee rate") - ErrDustOutput = errorsmod.Register(ModuleName, 6101, "dust output value") - ErrInsufficientUTXOs = errorsmod.Register(ModuleName, 6102, "insufficient utxos") - ErrFailToBuildTransaction = errorsmod.Register(ModuleName, 6103, "failed to build transaction") - ErrFailToSerializePsbt = errorsmod.Register(ModuleName, 6104, "failed to serialize psbt") + ErrInvalidAmount = errorsmod.Register(ModuleName, 6100, "invalid amount") + ErrInvalidFeeRate = errorsmod.Register(ModuleName, 6101, "invalid fee rate") + ErrDustOutput = errorsmod.Register(ModuleName, 6102, "dust output value") + ErrInsufficientUTXOs = errorsmod.Register(ModuleName, 6103, "insufficient utxos") + ErrFailToSerializePsbt = errorsmod.Register(ModuleName, 6104, "failed to serialize psbt") ) diff --git a/x/btcbridge/types/message_withdraw_bitcoin.go b/x/btcbridge/types/message_withdraw_bitcoin.go index 49a0161..287a412 100644 --- a/x/btcbridge/types/message_withdraw_bitcoin.go +++ b/x/btcbridge/types/message_withdraw_bitcoin.go @@ -46,8 +46,14 @@ func (msg *MsgWithdrawBitcoinRequest) ValidateBasic() error { return sdkerrors.Wrapf(err, "invalid Sender address (%s)", err) } - if len(msg.Amount) == 0 { - return sdkerrors.Wrap(sdk.ErrInvalidLengthCoin, "amount cannot be empty") + coin, err := sdk.ParseCoinNormalized(msg.Amount) + if err != nil { + return sdkerrors.Wrapf(ErrInvalidAmount, "invalid amount %s", msg.Amount) + } + + err = CheckOutput(msg.Sender, coin.Amount.Int64()) + if err != nil { + return err } if msg.FeeRate <= 0 {