Skip to content

Commit 1a615bf

Browse files
committed
Refactor EVM bridge module to integrate AccountKeeper and BankKeeper, enhancing token locking functionality. Implement validation for creator address and token amounts in LockTokens method, and introduce event emission for token locking. Add error handling for insufficient funds and invalid EVM addresses. Update expected keeper interfaces accordingly.
1 parent 592e04c commit 1a615bf

File tree

7 files changed

+200
-13
lines changed

7 files changed

+200
-13
lines changed

testutil/keeper/evmbridge.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,27 @@ import (
1010
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
1111
dbm "github.com/cosmos/cosmos-db"
1212
"github.com/cosmos/cosmos-sdk/codec"
13+
"github.com/cosmos/cosmos-sdk/codec/address" // Import address codec
1314
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
1415
"github.com/cosmos/cosmos-sdk/runtime"
1516
sdk "github.com/cosmos/cosmos-sdk/types"
17+
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
1618
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
19+
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
1720
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
1821
"github.com/stretchr/testify/require"
1922

2023
"github.com/airchains-network/junction/x/evmbridge/keeper"
2124
"github.com/airchains-network/junction/x/evmbridge/types"
2225
)
2326

27+
// Define necessary module account permissions for the test setup
28+
var ModuleAccountPerms = []*authtypes.ModuleAccount{
29+
// Add module accounts used by your module or dependencies if any
30+
// For example: {Name: types.ModuleName, Permissions: []string{authtypes.Minter, authtypes.Burner}},
31+
{Name: types.ModuleName, Permissions: []string{authtypes.Minter, authtypes.Burner, authtypes.Staking}},
32+
}
33+
2434
func EvmbridgeKeeper(t testing.TB) (keeper.Keeper, sdk.Context) {
2535
storeKey := storetypes.NewKVStoreKey(types.StoreKey)
2636

@@ -33,12 +43,53 @@ func EvmbridgeKeeper(t testing.TB) (keeper.Keeper, sdk.Context) {
3343
cdc := codec.NewProtoCodec(registry)
3444
authority := authtypes.NewModuleAddress(govtypes.ModuleName)
3545

46+
// Bech32 Prefix (Ensure this is set for your tests, e.g., in TestMain)
47+
// sdk.GetConfig().SetBech32Prefix("yourprefix")
48+
bech32Prefix := sdk.GetConfig().GetBech32AccountAddrPrefix()
49+
if bech32Prefix == "" {
50+
// Set a default if not configured for the test run
51+
bech32Prefix = "air"
52+
config := sdk.GetConfig()
53+
config.SetBech32PrefixForAccount(bech32Prefix, bech32Prefix+"pub")
54+
config.SetBech32PrefixForValidator(bech32Prefix+"val", bech32Prefix+"valpub")
55+
config.SetBech32PrefixForConsensusNode(bech32Prefix+"cons", bech32Prefix+"conspub")
56+
config.Seal()
57+
}
58+
59+
// Account Keeper (Needs authStoreKey, proper ModuleAccountPerms)
60+
// Create a map from the ModuleAccountPerms slice
61+
maccPerms := make(map[string][]string)
62+
for _, perm := range ModuleAccountPerms {
63+
maccPerms[perm.Name] = perm.Permissions
64+
}
65+
// If your module has its own module account, add it here too
66+
maccPerms[types.ModuleName] = nil // Example: No special permissions needed by default
67+
68+
accountKeeper := authkeeper.NewAccountKeeper(
69+
cdc,
70+
runtime.NewKVStoreService(storeKey),
71+
authtypes.ProtoBaseAccount,
72+
maccPerms,
73+
address.NewBech32Codec(bech32Prefix),
74+
bech32Prefix,
75+
authority.String(),
76+
)
77+
78+
bankKeeper := bankkeeper.NewBaseKeeper(
79+
cdc,
80+
runtime.NewKVStoreService(storeKey),
81+
accountKeeper,
82+
map[string]bool{},
83+
authority.String(),
84+
log.NewNopLogger())
85+
3686
k := keeper.NewKeeper(
3787
cdc,
3888
runtime.NewKVStoreService(storeKey),
3989
log.NewNopLogger(),
4090
authority.String(),
41-
nil,
91+
accountKeeper,
92+
bankKeeper,
4293
)
4394

4495
ctx := sdk.NewContext(stateStore, cmtproto.Header{}, false, log.NewNopLogger())

x/evmbridge/keeper/keeper.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ type (
2121
// should be the x/gov module account.
2222
authority string
2323

24-
bankKeeper types.BankKeeper
24+
accountKeeper types.AccountKeeper
25+
bankKeeper types.BankKeeper
2526
}
2627
)
2728

@@ -30,20 +31,20 @@ func NewKeeper(
3031
storeService store.KVStoreService,
3132
logger log.Logger,
3233
authority string,
33-
34+
accountKeeper types.AccountKeeper,
3435
bankKeeper types.BankKeeper,
3536
) Keeper {
3637
if _, err := sdk.AccAddressFromBech32(authority); err != nil {
3738
panic(fmt.Sprintf("invalid authority address: %s", authority))
3839
}
3940

4041
return Keeper{
41-
cdc: cdc,
42-
storeService: storeService,
43-
authority: authority,
44-
logger: logger,
45-
46-
bankKeeper: bankKeeper,
42+
cdc: cdc,
43+
storeService: storeService,
44+
authority: authority,
45+
logger: logger,
46+
accountKeeper: accountKeeper,
47+
bankKeeper: bankKeeper,
4748
}
4849
}
4950

x/evmbridge/keeper/msg_server_lock_tokens.go

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,112 @@ package keeper
22

33
import (
44
"context"
5+
"fmt"
6+
"strconv"
7+
"time"
58

69
"github.com/airchains-network/junction/x/evmbridge/types"
710
sdk "github.com/cosmos/cosmos-sdk/types"
11+
"google.golang.org/grpc/codes"
12+
"google.golang.org/grpc/status"
813
)
914

1015
func (k msgServer) LockTokens(goCtx context.Context, msg *types.MsgLockTokens) (*types.MsgLockTokensResponse, error) {
1116
ctx := sdk.UnwrapSDKContext(goCtx)
1217

13-
// TODO: Handling the message
14-
_ = ctx
18+
// Validate creator address
19+
creatorAddr, err := sdk.AccAddressFromBech32(msg.Creator)
20+
if err != nil {
21+
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid creator address: %s", err))
22+
}
23+
24+
// Validate amount
25+
if msg.Amount == "" {
26+
return nil, status.Error(codes.InvalidArgument, "amount is required")
27+
}
28+
29+
// Parse amount as coins
30+
amount, err := sdk.ParseCoinsNormalized(msg.Amount)
31+
if err != nil {
32+
return nil, types.ErrInvalidAmount.Wrap(err.Error())
33+
}
34+
35+
if !amount.IsValid() || amount.IsZero() {
36+
return nil, types.ErrInvalidAmount.Wrap("amount must be positive")
37+
}
38+
39+
// Validate EVM address format
40+
if err := k.validateEVMAddress(msg.ToAddress); err != nil {
41+
return nil, err
42+
}
43+
44+
// Check if user has sufficient balance
45+
userBalance := k.bankKeeper.SpendableCoins(ctx, creatorAddr)
46+
if !userBalance.IsAllGTE(amount) {
47+
return nil, types.ErrInsufficientFunds.Wrapf("required: %s, available: %s", amount.String(), userBalance.String())
48+
}
49+
// Get module account
50+
moduleAccount := k.accountKeeper.GetModuleAccount(ctx, types.ModuleName)
51+
if moduleAccount == nil {
52+
return nil, types.ErrModuleAccount.Wrap("module account not found")
53+
}
54+
55+
// Transfer tokens from user to module account (this locks them)
56+
if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, creatorAddr, types.ModuleName, amount); err != nil {
57+
return nil, types.ErrFailedToLockTokens.Wrap(err.Error())
58+
}
59+
60+
// Get current block info
61+
sdkCtx := sdk.UnwrapSDKContext(ctx)
62+
blockHeight := sdkCtx.BlockHeight()
63+
blockTime := sdkCtx.BlockTime()
64+
65+
amountUint64, err := strconv.ParseUint(msg.Amount, 10, 64)
66+
if err != nil {
67+
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("invalid amount: %s", err))
68+
}
69+
70+
if err := k.SetAddressLockedAmount(ctx, msg.Creator, amountUint64); err != nil {
71+
return nil, types.ErrFailedToLockTokens.Wrap(err.Error())
72+
}
73+
74+
// Emit simple event for bridge relayer
75+
sdkCtx.EventManager().EmitEvent(
76+
sdk.NewEvent(
77+
types.EventTypeTokensLocked,
78+
sdk.NewAttribute(types.AttributeKeyCreator, msg.Creator),
79+
sdk.NewAttribute(types.AttributeKeyAmount, amount.String()),
80+
sdk.NewAttribute(types.AttributeKeyToAddress, msg.ToAddress),
81+
sdk.NewAttribute(types.AttributeKeyBlockHeight, fmt.Sprintf("%d", blockHeight)),
82+
sdk.NewAttribute(types.AttributeKeyTimestamp, blockTime.Format(time.RFC3339)),
83+
),
84+
)
1585

1686
return &types.MsgLockTokensResponse{}, nil
1787
}
88+
89+
// validateEVMAddress performs basic EVM address validation
90+
func (k msgServer) validateEVMAddress(address string) error {
91+
if address == "" {
92+
return types.ErrInvalidEVMAddr.Wrap("address cannot be empty")
93+
}
94+
95+
// Basic EVM address validation (0x prefix + 40 hex chars)
96+
if len(address) != 42 {
97+
return types.ErrInvalidEVMAddr.Wrap("address must be 42 characters long")
98+
}
99+
100+
if address[:2] != "0x" {
101+
return types.ErrInvalidEVMAddr.Wrap("address must start with 0x")
102+
}
103+
104+
// Validate hex characters
105+
for i := 2; i < len(address); i++ {
106+
c := address[i]
107+
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
108+
return types.ErrInvalidEVMAddr.Wrap("address contains invalid hex characters")
109+
}
110+
}
111+
112+
return nil
113+
}

x/evmbridge/module/module.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ func ProvideModule(in ModuleInputs) ModuleOutputs {
202202
in.StoreService,
203203
in.Logger,
204204
authority.String(),
205+
in.AccountKeeper,
205206
in.BankKeeper,
206207
)
207208
m := NewAppModule(

x/evmbridge/types/errors.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import (
88

99
// x/evmbridge module sentinel errors
1010
var (
11-
ErrInvalidSigner = sdkerrors.Register(ModuleName, 1100, "expected gov account as only signer for proposal message")
12-
ErrSample = sdkerrors.Register(ModuleName, 1101, "sample error")
11+
ErrInvalidSigner = sdkerrors.Register(ModuleName, 1100, "expected gov account as only signer for proposal message")
12+
ErrSample = sdkerrors.Register(ModuleName, 1101, "sample error")
13+
ErrInvalidAmount = sdkerrors.Register(ModuleName, 1101, "invalid token amount")
14+
ErrInvalidEVMAddr = sdkerrors.Register(ModuleName, 1102, "invalid EVM address")
15+
ErrInsufficientFunds = sdkerrors.Register(ModuleName, 1103, "insufficient funds")
16+
ErrModuleAccount = sdkerrors.Register(ModuleName, 1104, "module account error")
17+
ErrUnauthorized = sdkerrors.Register(ModuleName, 1105, "unauthorized operation")
18+
ErrInvalidRecipient = sdkerrors.Register(ModuleName, 1106, "invalid recipient address")
19+
ErrInsufficientUserBalance = sdkerrors.Register(ModuleName, 1107, "user has insufficient locked balance")
20+
ErrFailedToLockTokens = sdkerrors.Register(ModuleName, 1108, "failed to lock tokens")
1321
)

x/evmbridge/types/events.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package types
2+
3+
// Event types for the evmbridge module
4+
const (
5+
// EventTypeTokensLocked is emitted when tokens are locked in the bridge
6+
EventTypeTokensLocked = "tokens_locked"
7+
// EventTypeTokensUnlocked is emitted when tokens are unlocked from the bridge
8+
EventTypeTokensUnlocked = "tokens_unlocked"
9+
)
10+
11+
// Event attribute keys
12+
const (
13+
// AttributeKeyCreator is the address that initiated the lock
14+
AttributeKeyCreator = "creator"
15+
// AttributeKeyAmount is the amount of tokens locked/unlocked
16+
AttributeKeyAmount = "amount"
17+
// AttributeKeyToAddress is the EVM address to mint tokens to
18+
AttributeKeyToAddress = "to_address"
19+
// AttributeKeyBlockHeight is the block height when operation occurred
20+
AttributeKeyBlockHeight = "block_height"
21+
// AttributeKeyTimestamp is the timestamp when operation occurred
22+
AttributeKeyTimestamp = "timestamp"
23+
// AttributeKeyRecipient is the recipient address for unlock operations
24+
AttributeKeyRecipient = "recipient"
25+
// AttributeKeyEVMTxHash is the EVM transaction hash that triggered the unlock
26+
AttributeKeyEVMTxHash = "evm_tx_hash"
27+
)

x/evmbridge/types/expected_keepers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ import (
1010
type AccountKeeper interface {
1111
GetAccount(context.Context, sdk.AccAddress) sdk.AccountI // only used for simulation
1212
// Methods imported from account should be defined here
13+
GetModuleAccount(ctx context.Context, moduleName string) sdk.ModuleAccountI
1314
}
1415

1516
// BankKeeper defines the expected interface for the Bank module.
1617
type BankKeeper interface {
1718
SpendableCoins(context.Context, sdk.AccAddress) sdk.Coins
1819
// Methods imported from bank should be defined here
20+
SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
21+
SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
1922
}
2023

2124
// ParamSubspace defines the expected Subspace interface for parameters.

0 commit comments

Comments
 (0)