forked from cerc-io/laconicd-deprecated
ante: add stacktrace (#123)
This commit is contained in:
parent
3a74d2402b
commit
7f72891c09
@ -3,6 +3,7 @@ package ante
|
|||||||
import (
|
import (
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/palantir/stacktrace"
|
||||||
log "github.com/xlab/suplog"
|
log "github.com/xlab/suplog"
|
||||||
|
|
||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
@ -73,8 +74,10 @@ func NewAnteHandler(
|
|||||||
)
|
)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
log.WithField("type_url", typeURL).Errorln("rejecting tx with unsupported extension option")
|
return ctx, stacktrace.Propagate(
|
||||||
return ctx, sdkerrors.Wrap(sdkerrors.ErrUnknownExtensionOptions, typeURL)
|
sdkerrors.Wrap(sdkerrors.ErrUnknownExtensionOptions, typeURL),
|
||||||
|
"rejecting tx with unsupported extension option",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return anteHandler(ctx, tx, sim)
|
return anteHandler(ctx, tx, sim)
|
||||||
@ -102,7 +105,10 @@ func NewAnteHandler(
|
|||||||
authante.NewIncrementSequenceDecorator(ak), // innermost AnteDecorator
|
authante.NewIncrementSequenceDecorator(ak), // innermost AnteDecorator
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", tx)
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", tx),
|
||||||
|
"transaction is not an SDK tx",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return anteHandler(ctx, tx, sim)
|
return anteHandler(ctx, tx, sim)
|
||||||
|
118
app/ante/eth.go
118
app/ante/eth.go
@ -6,6 +6,7 @@ import (
|
|||||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
|
authante "github.com/cosmos/cosmos-sdk/x/auth/ante"
|
||||||
|
"github.com/palantir/stacktrace"
|
||||||
|
|
||||||
ethermint "github.com/cosmos/ethermint/types"
|
ethermint "github.com/cosmos/ethermint/types"
|
||||||
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
evmtypes "github.com/cosmos/ethermint/x/evm/types"
|
||||||
@ -50,7 +51,10 @@ func (esvd EthSigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(tx.GetMsgs()) != 1 {
|
if len(tx.GetMsgs()) != 1 {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "only 1 ethereum msg supported per tx, got %d", len(tx.GetMsgs()))
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "only 1 ethereum msg supported per tx, got %d", len(tx.GetMsgs())),
|
||||||
|
"",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get and set account must be called with an infinite gas meter in order to prevent
|
// get and set account must be called with an infinite gas meter in order to prevent
|
||||||
@ -71,12 +75,19 @@ func (esvd EthSigVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, s
|
|||||||
msg := tx.GetMsgs()[0]
|
msg := tx.GetMsgs()[0]
|
||||||
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{})
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}),
|
||||||
|
"failed to cast transaction",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sender, err := signer.Sender(msgEthTx.AsTransaction())
|
sender, err := signer.Sender(msgEthTx.AsTransaction())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, err.Error())
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrap(sdkerrors.ErrorInvalidSigner, err.Error()),
|
||||||
|
"couldn't retrieve sender address ('%s') from the ethereum transaction",
|
||||||
|
msgEthTx.From,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set up the sender to the transaction field if not already
|
// set up the sender to the transaction field if not already
|
||||||
@ -118,16 +129,22 @@ func (avd EthAccountVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx
|
|||||||
|
|
||||||
evmDenom := avd.evmKeeper.GetParams(infCtx).EvmDenom
|
evmDenom := avd.evmKeeper.GetParams(infCtx).EvmDenom
|
||||||
|
|
||||||
for _, msg := range tx.GetMsgs() {
|
for i, msg := range tx.GetMsgs() {
|
||||||
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{})
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}),
|
||||||
|
"failed to cast transaction %d", i,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sender address should be in the tx cache from the previous AnteHandle call
|
// sender address should be in the tx cache from the previous AnteHandle call
|
||||||
from := msgEthTx.GetFrom()
|
from := msgEthTx.GetFrom()
|
||||||
if from.Empty() {
|
if from.Empty() {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "from address cannot be empty")
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "from address cannot be empty"),
|
||||||
|
"sender address should have been in the tx field from the previous AnteHandle call",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
acc := avd.ak.GetAccount(infCtx, from)
|
acc := avd.ak.GetAccount(infCtx, from)
|
||||||
@ -136,12 +153,15 @@ func (avd EthAccountVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx
|
|||||||
avd.ak.SetAccount(infCtx, acc)
|
avd.ak.SetAccount(infCtx, acc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate sender has enough funds to pay for gas cost
|
// validate sender has enough funds to pay for tx cost
|
||||||
balance := avd.bankKeeper.GetBalance(infCtx, from, evmDenom)
|
balance := avd.bankKeeper.GetBalance(infCtx, from, evmDenom)
|
||||||
if balance.Amount.BigInt().Cmp(msgEthTx.Cost()) < 0 {
|
if balance.Amount.BigInt().Cmp(msgEthTx.Cost()) < 0 {
|
||||||
return ctx, sdkerrors.Wrapf(
|
return ctx, stacktrace.Propagate(
|
||||||
sdkerrors.ErrInsufficientFunds,
|
sdkerrors.Wrapf(
|
||||||
"sender balance < tx gas cost (%s < %s%s)", balance.String(), msgEthTx.Cost().String(), evmDenom,
|
sdkerrors.ErrInsufficientFunds,
|
||||||
|
"sender balance < tx cost (%s < %s%s)", balance, msgEthTx.Cost(), evmDenom,
|
||||||
|
),
|
||||||
|
"sender should have had enough funds to pay for tx cost = fee + amount (%s = %s + amount)", msgEthTx.Cost(), msgEthTx.Fee(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -174,25 +194,31 @@ func (nvd EthNonceVerificationDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx,
|
|||||||
// additional gas from being deducted.
|
// additional gas from being deducted.
|
||||||
infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
|
infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
|
||||||
|
|
||||||
for _, msg := range tx.GetMsgs() {
|
for i, msg := range tx.GetMsgs() {
|
||||||
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{})
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}),
|
||||||
|
"failed to cast transaction %d", i,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sender address should be in the tx cache from the previous AnteHandle call
|
// sender address should be in the tx cache from the previous AnteHandle call
|
||||||
seq, err := nvd.ak.GetSequence(infCtx, msgEthTx.GetFrom())
|
seq, err := nvd.ak.GetSequence(infCtx, msgEthTx.GetFrom())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, err
|
return ctx, stacktrace.Propagate(err, "sequence not found for address %s", msgEthTx.From)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if multiple transactions are submitted in succession with increasing nonces,
|
// if multiple transactions are submitted in succession with increasing nonces,
|
||||||
// all will be rejected except the first, since the first needs to be included in a block
|
// all will be rejected except the first, since the first needs to be included in a block
|
||||||
// before the sequence increments
|
// before the sequence increments
|
||||||
if msgEthTx.Data.Nonce != seq {
|
if msgEthTx.Data.Nonce != seq {
|
||||||
return ctx, sdkerrors.Wrapf(
|
return ctx, stacktrace.Propagate(
|
||||||
sdkerrors.ErrInvalidSequence,
|
sdkerrors.Wrapf(
|
||||||
"invalid nonce; got %d, expected %d", msgEthTx.Data.Nonce, seq,
|
sdkerrors.ErrInvalidSequence,
|
||||||
|
"invalid nonce; got %d, expected %d", msgEthTx.Data.Nonce, seq,
|
||||||
|
),
|
||||||
|
"",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,10 +276,13 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula
|
|||||||
homestead := ethCfg.IsHomestead(blockHeight)
|
homestead := ethCfg.IsHomestead(blockHeight)
|
||||||
istanbul := ethCfg.IsIstanbul(blockHeight)
|
istanbul := ethCfg.IsIstanbul(blockHeight)
|
||||||
|
|
||||||
for _, msg := range tx.GetMsgs() {
|
for i, msg := range tx.GetMsgs() {
|
||||||
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{})
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}),
|
||||||
|
"failed to cast transaction %d", i,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
isContractCreation := msgEthTx.To() == nil
|
isContractCreation := msgEthTx.To() == nil
|
||||||
@ -261,7 +290,7 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula
|
|||||||
// fetch sender account from signature
|
// fetch sender account from signature
|
||||||
signerAcc, err := authante.GetSignerAcc(infCtx, egcd.ak, msgEthTx.GetFrom())
|
signerAcc, err := authante.GetSignerAcc(infCtx, egcd.ak, msgEthTx.GetFrom())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, err
|
return ctx, stacktrace.Propagate(err, "account not found for sender %s", msgEthTx.From)
|
||||||
}
|
}
|
||||||
|
|
||||||
gasLimit := msgEthTx.GetGas()
|
gasLimit := msgEthTx.GetGas()
|
||||||
@ -273,7 +302,9 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula
|
|||||||
|
|
||||||
intrinsicGas, err := core.IntrinsicGas(msgEthTx.Data.Input, accessList, isContractCreation, homestead, istanbul)
|
intrinsicGas, err := core.IntrinsicGas(msgEthTx.Data.Input, accessList, isContractCreation, homestead, istanbul)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, sdkerrors.Wrap(err, "failed to compute intrinsic gas cost")
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrap(err, "failed to compute intrinsic gas cost"),
|
||||||
|
"failed to retrieve intrinsic gas, contract creation = %t; homestead = %t, istanbul = %t", isContractCreation, homestead, istanbul)
|
||||||
}
|
}
|
||||||
|
|
||||||
// intrinsic gas verification during CheckTx
|
// intrinsic gas verification during CheckTx
|
||||||
@ -289,7 +320,10 @@ func (egcd EthGasConsumeDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simula
|
|||||||
|
|
||||||
// deduct the full gas cost from the user balance
|
// deduct the full gas cost from the user balance
|
||||||
if err := authante.DeductFees(egcd.bankKeeper, infCtx, signerAcc, fees); err != nil {
|
if err := authante.DeductFees(egcd.bankKeeper, infCtx, signerAcc, fees); err != nil {
|
||||||
return ctx, err
|
return ctx, stacktrace.Propagate(
|
||||||
|
err,
|
||||||
|
"failed to deduct full gas cost %s from the user %s balance", fees, msgEthTx.From,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// consume intrinsic gas for the current transaction. After runTx is executed on Baseapp, the
|
// consume intrinsic gas for the current transaction. After runTx is executed on Baseapp, the
|
||||||
@ -343,15 +377,21 @@ func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate
|
|||||||
ethCfg := config.EthereumConfig(ctd.evmKeeper.ChainID())
|
ethCfg := config.EthereumConfig(ctd.evmKeeper.ChainID())
|
||||||
signer := ethtypes.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight()))
|
signer := ethtypes.MakeSigner(ethCfg, big.NewInt(ctx.BlockHeight()))
|
||||||
|
|
||||||
for _, msg := range tx.GetMsgs() {
|
for i, msg := range tx.GetMsgs() {
|
||||||
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", msg)
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}),
|
||||||
|
"failed to cast transaction %d", i,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
coreMsg, err := msgEthTx.AsMessage(signer)
|
coreMsg, err := msgEthTx.AsMessage(signer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx, err
|
return ctx, stacktrace.Propagate(
|
||||||
|
err,
|
||||||
|
"failed to create an ethereum core.Message from signer %T", signer,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
evm := ctd.evmKeeper.NewEVM(coreMsg, ethCfg)
|
evm := ctd.evmKeeper.NewEVM(coreMsg, ethCfg)
|
||||||
@ -359,7 +399,10 @@ func (ctd CanTransferDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate
|
|||||||
// check that caller has enough balance to cover asset transfer for **topmost** call
|
// check that caller has enough balance to cover asset transfer for **topmost** call
|
||||||
// NOTE: here the gas consumed is from the context with the infinite gas meter
|
// NOTE: here the gas consumed is from the context with the infinite gas meter
|
||||||
if coreMsg.Value().Sign() > 0 && !evm.Context.CanTransfer(ctd.evmKeeper, coreMsg.From(), coreMsg.Value()) {
|
if coreMsg.Value().Sign() > 0 && !evm.Context.CanTransfer(ctd.evmKeeper, coreMsg.From(), coreMsg.Value()) {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "address %s", coreMsg.From().Hex())
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrInsufficientFunds, "address %s", coreMsg.From()),
|
||||||
|
"failed to transfer %s using the EVM block context transfer function", coreMsg.Value(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,10 +457,13 @@ func (ald AccessListDecorator) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate b
|
|||||||
// setup the keeper context before setting the access list
|
// setup the keeper context before setting the access list
|
||||||
ald.evmKeeper.WithContext(infCtx)
|
ald.evmKeeper.WithContext(infCtx)
|
||||||
|
|
||||||
for _, msg := range tx.GetMsgs() {
|
for i, msg := range tx.GetMsgs() {
|
||||||
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", msg)
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}),
|
||||||
|
"failed to cast transaction %d", i,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
sender := common.BytesToAddress(msgEthTx.GetFrom())
|
sender := common.BytesToAddress(msgEthTx.GetFrom())
|
||||||
@ -450,10 +496,13 @@ func (issd EthIncrementSenderSequenceDecorator) AnteHandle(ctx sdk.Context, tx s
|
|||||||
// additional gas from being deducted.
|
// additional gas from being deducted.
|
||||||
infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
|
infCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter())
|
||||||
|
|
||||||
for _, msg := range tx.GetMsgs() {
|
for i, msg := range tx.GetMsgs() {
|
||||||
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
msgEthTx, ok := msg.(*evmtypes.MsgEthereumTx)
|
||||||
if !ok {
|
if !ok {
|
||||||
return ctx, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type: %T", msg)
|
return ctx, stacktrace.Propagate(
|
||||||
|
sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "invalid transaction type %T, expected %T", tx, &evmtypes.MsgEthereumTx{}),
|
||||||
|
"failed to cast transaction %d", i,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: on contract creation, the nonce is incremented within the EVM Create function during tx execution
|
// NOTE: on contract creation, the nonce is incremented within the EVM Create function during tx execution
|
||||||
@ -469,14 +518,17 @@ func (issd EthIncrementSenderSequenceDecorator) AnteHandle(ctx sdk.Context, tx s
|
|||||||
acc := issd.ak.GetAccount(infCtx, addr)
|
acc := issd.ak.GetAccount(infCtx, addr)
|
||||||
|
|
||||||
if acc == nil {
|
if acc == nil {
|
||||||
return ctx, sdkerrors.Wrapf(
|
return ctx, stacktrace.Propagate(
|
||||||
sdkerrors.ErrUnknownAddress,
|
sdkerrors.Wrapf(
|
||||||
"account %s (%s) is nil", common.BytesToAddress(addr.Bytes()), addr,
|
sdkerrors.ErrUnknownAddress,
|
||||||
|
"account %s (%s) is nil", common.BytesToAddress(addr.Bytes()), addr,
|
||||||
|
),
|
||||||
|
"signer account not found",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := acc.SetSequence(acc.GetSequence() + 1); err != nil {
|
if err := acc.SetSequence(acc.GetSequence() + 1); err != nil {
|
||||||
return ctx, err
|
return ctx, stacktrace.Propagate(err, "failed to set sequence to %d", acc.GetSequence()+1)
|
||||||
}
|
}
|
||||||
|
|
||||||
issd.ak.SetAccount(infCtx, acc)
|
issd.ak.SetAccount(infCtx, acc)
|
||||||
|
Loading…
Reference in New Issue
Block a user