mirror of
https://github.com/0glabs/0g-chain.git
synced 2025-04-04 15:55:23 +00:00
Compare commits
6 Commits
e3c17a030a
...
691cc06686
Author | SHA1 | Date | |
---|---|---|---|
![]() |
691cc06686 | ||
![]() |
32601927cf | ||
![]() |
9bc8d568df | ||
![]() |
6bca51e34c | ||
![]() |
559d1beb03 | ||
![]() |
4db9ee49b4 |
4
go.mod
4
go.mod
@ -242,7 +242,7 @@ replace (
|
||||
github.com/cometbft/cometbft-db => github.com/kava-labs/cometbft-db v0.9.1-kava.2
|
||||
// Use cosmos-sdk fork with backported fix for unsafe-reset-all, staking transfer events, and custom tally handler support
|
||||
// github.com/cosmos/cosmos-sdk => github.com/0glabs/cosmos-sdk v0.46.11-kava.3
|
||||
github.com/cosmos/cosmos-sdk => github.com/0glabs/cosmos-sdk v0.47.10-0glabs.7
|
||||
github.com/cosmos/cosmos-sdk => github.com/0glabs/cosmos-sdk v0.0.0-20241226164042-2543aa53eee1
|
||||
github.com/cosmos/iavl => github.com/kava-labs/iavl v1.2.0-kava.1
|
||||
// See https://github.com/cosmos/cosmos-sdk/pull/13093
|
||||
github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt/v4 v4.4.2
|
||||
@ -250,7 +250,7 @@ replace (
|
||||
// TODO: Tag before release
|
||||
github.com/ethereum/go-ethereum => github.com/evmos/go-ethereum v1.10.26-evmos-rc2
|
||||
// Use ethermint fork that respects min-gas-price with NoBaseFee true and london enabled, and includes eip712 support
|
||||
github.com/evmos/ethermint => github.com/0glabs/ethermint v0.21.0-0g.v3.1.7
|
||||
github.com/evmos/ethermint => github.com/0glabs/ethermint v0.0.0-20241231151651-f7d1b1499e5a
|
||||
// See https://github.com/cosmos/cosmos-sdk/pull/10401, https://github.com/cosmos/cosmos-sdk/commit/0592ba6158cd0bf49d894be1cef4faeec59e8320
|
||||
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.9.0
|
||||
// Downgraded to avoid bugs in following commits which causes "version does not exist" errors
|
||||
|
8
go.sum
8
go.sum
@ -211,10 +211,10 @@ git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFN
|
||||
git.sr.ht/~sircmpwn/go-bare v0.0.0-20210406120253-ab86bc2846d9/go.mod h1:BVJwbDfVjCjoFiKrhkei6NdGcZYpkDkdyCdg1ukytRA=
|
||||
github.com/0glabs/cometbft v0.37.9-0glabs.1 h1:KQJG17Y21suKP3QNICLto4b5Ak73XbSmKxeLbg0ZM68=
|
||||
github.com/0glabs/cometbft v0.37.9-0glabs.1/go.mod h1:j0Q3RqrCd+cztWCugs3obbzC4NyHGBPZZjtm/fWV00I=
|
||||
github.com/0glabs/cosmos-sdk v0.47.10-0glabs.7 h1:6+JquK9BaZZdIA3gx1AXhPBAdYCG+FQ94Y7FN35CvB4=
|
||||
github.com/0glabs/cosmos-sdk v0.47.10-0glabs.7/go.mod h1:KskIVnhXTFqrw7CDccMvx7To5KzUsOomIsQV7sPGOog=
|
||||
github.com/0glabs/ethermint v0.21.0-0g.v3.1.7 h1:wzr6z/LTsbjoAaBf0JkMtIDl/+B5KLd0GrU5brSZCY0=
|
||||
github.com/0glabs/ethermint v0.21.0-0g.v3.1.7/go.mod h1:S1Ahmqpzo1XUsfmmpGT7ok0hu5Fekz/pD6EDtXaBg9Q=
|
||||
github.com/0glabs/cosmos-sdk v0.0.0-20241226164042-2543aa53eee1 h1:5bREC0rOIpQDay71Weorv26WivJGRYnEk8kRH+jMPZg=
|
||||
github.com/0glabs/cosmos-sdk v0.0.0-20241226164042-2543aa53eee1/go.mod h1:KskIVnhXTFqrw7CDccMvx7To5KzUsOomIsQV7sPGOog=
|
||||
github.com/0glabs/ethermint v0.0.0-20241231151651-f7d1b1499e5a h1:C7edknSFFmB0PdXApWryR+3NjK8Lr/gHiYQ156rUahw=
|
||||
github.com/0glabs/ethermint v0.0.0-20241231151651-f7d1b1499e5a/go.mod h1:WD4qMi5eOSa34aMeNS7aYMdI5Yby+PK57sWmNqdKqJM=
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM=
|
||||
|
62
precompiles/common/common.go
Normal file
62
precompiles/common/common.go
Normal file
@ -0,0 +1,62 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/evmos/ethermint/x/evm/statedb"
|
||||
)
|
||||
|
||||
type PrecompileCommon interface {
|
||||
Abi() *abi.ABI
|
||||
IsTx(string) bool
|
||||
KVGasConfig() storetypes.GasConfig
|
||||
}
|
||||
|
||||
func InitializePrecompileCall(
|
||||
p PrecompileCommon,
|
||||
evm *vm.EVM,
|
||||
contract *vm.Contract,
|
||||
readonly bool,
|
||||
) (
|
||||
ctx sdk.Context,
|
||||
stateDB *statedb.StateDB,
|
||||
method *abi.Method,
|
||||
initialGas storetypes.Gas,
|
||||
args []interface{},
|
||||
err error,
|
||||
) {
|
||||
// parse input
|
||||
if len(contract.Input) < 4 {
|
||||
return sdk.Context{}, nil, nil, uint64(0), nil, vm.ErrExecutionReverted
|
||||
}
|
||||
method, err = p.Abi().MethodById(contract.Input[:4])
|
||||
if err != nil {
|
||||
return sdk.Context{}, nil, nil, uint64(0), nil, vm.ErrExecutionReverted
|
||||
}
|
||||
args, err = method.Inputs.Unpack(contract.Input[4:])
|
||||
if err != nil {
|
||||
return sdk.Context{}, nil, nil, uint64(0), nil, err
|
||||
}
|
||||
// readonly check
|
||||
if readonly && p.IsTx(method.Name) {
|
||||
return sdk.Context{}, nil, nil, uint64(0), nil, errors.New(ErrWriteOnReadOnly)
|
||||
}
|
||||
// get state db and context
|
||||
stateDB, ok := evm.StateDB.(*statedb.StateDB)
|
||||
if !ok {
|
||||
return sdk.Context{}, nil, nil, uint64(0), nil, errors.New(ErrGetStateDB)
|
||||
}
|
||||
ctx, err = stateDB.GetCachedContextForPrecompile()
|
||||
if err != nil {
|
||||
return sdk.Context{}, nil, nil, uint64(0), nil, err
|
||||
}
|
||||
// initial gas
|
||||
initialGas = ctx.GasMeter().GasConsumed()
|
||||
ctx = ctx.WithKVGasConfig(p.KVGasConfig())
|
||||
|
||||
return ctx, stateDB, method, initialGas, args, nil
|
||||
}
|
@ -4,4 +4,5 @@ const (
|
||||
ErrGetStateDB = "get EVM StateDB failed"
|
||||
ErrInvalidNumberOfArgs = "invalid number of arguments; expected %d; got: %d"
|
||||
ErrSenderNotOrigin = "msg.sender is not from tx origin"
|
||||
ErrWriteOnReadOnly = "read only call to write functions"
|
||||
)
|
||||
|
@ -1,7 +1,6 @@
|
||||
package dasigners
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
precompiles_common "github.com/0glabs/0g-chain/precompiles/common"
|
||||
@ -10,7 +9,6 @@ import (
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/evmos/ethermint/x/evm/statedb"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -47,17 +45,8 @@ var RequiredGasBasic = map[string]uint64{
|
||||
DASignersFunctionRegisteredEpoch: 10000,
|
||||
}
|
||||
|
||||
var KVGasConfig storetypes.GasConfig = storetypes.GasConfig{
|
||||
HasCost: 0,
|
||||
DeleteCost: 0,
|
||||
ReadCostFlat: 0,
|
||||
ReadCostPerByte: 0,
|
||||
WriteCostFlat: 0,
|
||||
WriteCostPerByte: 0,
|
||||
IterNextCostFlat: 0,
|
||||
}
|
||||
|
||||
var _ vm.PrecompiledContract = &DASignersPrecompile{}
|
||||
var _ precompiles_common.PrecompileCommon = &DASignersPrecompile{}
|
||||
|
||||
type DASignersPrecompile struct {
|
||||
abi abi.ABI
|
||||
@ -92,29 +81,31 @@ func (d *DASignersPrecompile) RequiredGas(input []byte) uint64 {
|
||||
return RequiredGasMax
|
||||
}
|
||||
|
||||
func (d *DASignersPrecompile) IsTx(method string) bool {
|
||||
switch method {
|
||||
case DASignersFunctionUpdateSocket,
|
||||
DASignersFunctionRegisterSigner,
|
||||
DASignersFunctionRegisterNextEpoch:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DASignersPrecompile) Abi() *abi.ABI {
|
||||
return &d.abi
|
||||
}
|
||||
|
||||
func (d *DASignersPrecompile) KVGasConfig() storetypes.GasConfig {
|
||||
return storetypes.KVGasConfig()
|
||||
}
|
||||
|
||||
// Run implements vm.PrecompiledContract.
|
||||
func (d *DASignersPrecompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) {
|
||||
// parse input
|
||||
if len(contract.Input) < 4 {
|
||||
return nil, vm.ErrExecutionReverted
|
||||
}
|
||||
method, err := d.abi.MethodById(contract.Input[:4])
|
||||
if err != nil {
|
||||
return nil, vm.ErrExecutionReverted
|
||||
}
|
||||
args, err := method.Inputs.Unpack(contract.Input[4:])
|
||||
ctx, stateDB, method, initialGas, args, err := precompiles_common.InitializePrecompileCall(d, evm, contract, readonly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// get state db and context
|
||||
stateDB, ok := evm.StateDB.(*statedb.StateDB)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrGetStateDB)
|
||||
}
|
||||
ctx := stateDB.GetContext()
|
||||
// reset gas config
|
||||
ctx = ctx.WithKVGasConfig(KVGasConfig)
|
||||
initialGas := ctx.GasMeter().GasConsumed()
|
||||
|
||||
var bz []byte
|
||||
switch method.Name {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package dasigners_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -24,6 +25,7 @@ import (
|
||||
"cosmossdk.io/math"
|
||||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/evmos/ethermint/x/evm/statedb"
|
||||
)
|
||||
|
||||
type DASignersTestSuite struct {
|
||||
@ -99,7 +101,11 @@ func (suite *DASignersTestSuite) runTx(input []byte, signer *testutil.TestSigner
|
||||
precompiles := suite.EvmKeeper.GetPrecompiles()
|
||||
evm.WithPrecompiles(precompiles, []common.Address{suite.addr})
|
||||
|
||||
return suite.dasigners.Run(evm, contract, false)
|
||||
bz, err := suite.dasigners.Run(evm, contract, false)
|
||||
if err == nil {
|
||||
evm.StateDB.(*statedb.StateDB).Commit()
|
||||
}
|
||||
return bz, err
|
||||
}
|
||||
|
||||
func (suite *DASignersTestSuite) registerSigner(testSigner *testutil.TestSigner, sk *big.Int) *types.Signer {
|
||||
@ -308,8 +314,11 @@ func (suite *DASignersTestSuite) Test_DASigners() {
|
||||
suite.AddDelegation(suite.signerOne.HexAddr, suite.signerOne.HexAddr, keeper.BondedConversionRate.Mul(sdk.NewIntFromUint64(params.TokensPerVote)))
|
||||
suite.AddDelegation(suite.signerTwo.HexAddr, suite.signerOne.HexAddr, keeper.BondedConversionRate.Mul(sdk.NewIntFromUint64(params.TokensPerVote)).Mul(sdk.NewIntFromUint64(2)))
|
||||
// tx test
|
||||
fmt.Println("registering signer 1..")
|
||||
signer1 := suite.registerSigner(suite.signerOne, big.NewInt(1))
|
||||
fmt.Println("registering signer 2..")
|
||||
signer2 := suite.registerSigner(suite.signerTwo, big.NewInt(11))
|
||||
fmt.Println("signers registered..")
|
||||
suite.updateSocket(suite.signerOne, signer1)
|
||||
suite.updateSocket(suite.signerTwo, signer2)
|
||||
suite.registerEpoch(suite.signerOne, big.NewInt(1))
|
||||
|
@ -1,6 +1,7 @@
|
||||
package dasigners
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
@ -29,7 +30,7 @@ func (d *DASignersPrecompile) RegisterSigner(
|
||||
return nil, fmt.Errorf(ErrInvalidSender, sender, msg.Signer.Account)
|
||||
}
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
// execute
|
||||
_, err = d.dasignersKeeper.RegisterSigner(sdk.WrapSDKContext(ctx), msg)
|
||||
@ -58,7 +59,7 @@ func (d *DASignersPrecompile) RegisterNextEpoch(
|
||||
}
|
||||
// validation
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
// execute
|
||||
_, err = d.dasignersKeeper.RegisterNextEpoch(sdk.WrapSDKContext(ctx), msg)
|
||||
@ -82,7 +83,7 @@ func (d *DASignersPrecompile) UpdateSocket(
|
||||
}
|
||||
// validation
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
// execute
|
||||
_, err = d.dasignersKeeper.UpdateSocket(sdk.WrapSDKContext(ctx), msg)
|
||||
|
@ -1,16 +1,14 @@
|
||||
package staking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
precompiles_common "github.com/0glabs/0g-chain/precompiles/common"
|
||||
"github.com/cosmos/cosmos-sdk/store/types"
|
||||
storetypes "github.com/cosmos/cosmos-sdk/store/types"
|
||||
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/evmos/ethermint/x/evm/statedb"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -40,6 +38,7 @@ const (
|
||||
)
|
||||
|
||||
var _ vm.PrecompiledContract = &StakingPrecompile{}
|
||||
var _ precompiles_common.PrecompileCommon = &StakingPrecompile{}
|
||||
|
||||
type StakingPrecompile struct {
|
||||
abi abi.ABI
|
||||
@ -67,29 +66,34 @@ func (s *StakingPrecompile) RequiredGas(input []byte) uint64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *StakingPrecompile) Abi() *abi.ABI {
|
||||
return &s.abi
|
||||
}
|
||||
|
||||
func (s *StakingPrecompile) IsTx(method string) bool {
|
||||
switch method {
|
||||
case StakingFunctionCreateValidator,
|
||||
StakingFunctionEditValidator,
|
||||
StakingFunctionDelegate,
|
||||
StakingFunctionBeginRedelegate,
|
||||
StakingFunctionUndelegate,
|
||||
StakingFunctionCancelUnbondingDelegation:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StakingPrecompile) KVGasConfig() storetypes.GasConfig {
|
||||
return storetypes.KVGasConfig()
|
||||
}
|
||||
|
||||
// Run implements vm.PrecompiledContract.
|
||||
func (s *StakingPrecompile) Run(evm *vm.EVM, contract *vm.Contract, readonly bool) ([]byte, error) {
|
||||
// parse input
|
||||
if len(contract.Input) < 4 {
|
||||
return nil, vm.ErrExecutionReverted
|
||||
}
|
||||
method, err := s.abi.MethodById(contract.Input[:4])
|
||||
if err != nil {
|
||||
return nil, vm.ErrExecutionReverted
|
||||
}
|
||||
args, err := method.Inputs.Unpack(contract.Input[4:])
|
||||
ctx, stateDB, method, initialGas, args, err := precompiles_common.InitializePrecompileCall(s, evm, contract, readonly)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// get state db and context
|
||||
stateDB, ok := evm.StateDB.(*statedb.StateDB)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrGetStateDB)
|
||||
}
|
||||
ctx := stateDB.GetContext()
|
||||
// reset gas config
|
||||
ctx = ctx.WithKVGasConfig(types.KVGasConfig())
|
||||
initialGas := ctx.GasMeter().GasConsumed()
|
||||
|
||||
var bz []byte
|
||||
switch method.Name {
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/evmos/ethermint/x/evm/statedb"
|
||||
evmtypes "github.com/evmos/ethermint/x/evm/types"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
@ -137,7 +138,11 @@ func (suite *StakingTestSuite) runTx(input []byte, signer *testutil.TestSigner,
|
||||
precompiles := suite.EvmKeeper.GetPrecompiles()
|
||||
evm.WithPrecompiles(precompiles, []common.Address{suite.addr})
|
||||
|
||||
return suite.staking.Run(evm, contract, false)
|
||||
bz, err := suite.staking.Run(evm, contract, false)
|
||||
if err == nil {
|
||||
evm.StateDB.(*statedb.StateDB).Commit()
|
||||
}
|
||||
return bz, err
|
||||
}
|
||||
|
||||
func TestKeeperSuite(t *testing.T) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package staking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
precompiles_common "github.com/0glabs/0g-chain/precompiles/common"
|
||||
@ -26,7 +26,7 @@ func (s *StakingPrecompile) CreateValidator(
|
||||
}
|
||||
// validation
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
// execute
|
||||
_, err = stakingkeeper.NewMsgServerImpl(s.stakingKeeper).CreateValidator(ctx, msg)
|
||||
@ -51,7 +51,7 @@ func (s *StakingPrecompile) EditValidator(
|
||||
}
|
||||
// validation
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
// execute
|
||||
_, err = stakingkeeper.NewMsgServerImpl(s.stakingKeeper).EditValidator(ctx, msg)
|
||||
@ -77,7 +77,7 @@ func (s *StakingPrecompile) Delegate(
|
||||
// validation
|
||||
/*
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
*/
|
||||
// execute
|
||||
@ -104,7 +104,7 @@ func (s *StakingPrecompile) BeginRedelegate(
|
||||
// validation
|
||||
/*
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
*/
|
||||
// execute
|
||||
@ -131,7 +131,7 @@ func (s *StakingPrecompile) Undelegate(
|
||||
// validation
|
||||
/*
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
*/
|
||||
// execute
|
||||
@ -158,7 +158,7 @@ func (s *StakingPrecompile) CancelUnbondingDelegation(
|
||||
// validation
|
||||
/*
|
||||
if contract.CallerAddress != evm.Origin {
|
||||
return nil, fmt.Errorf(precompiles_common.ErrSenderNotOrigin)
|
||||
return nil, errors.New(precompiles_common.ErrSenderNotOrigin)
|
||||
}
|
||||
*/
|
||||
// execute
|
||||
|
Loading…
Reference in New Issue
Block a user