From a10d2549d6e93d0e4d34e195b6b8d2821a7ec142 Mon Sep 17 00:00:00 2001 From: Milad Zahedi Date: Tue, 31 Oct 2023 10:55:46 +0330 Subject: [PATCH] added tests --- integration-tests/modules/assetnft_test.go | 85 ++++++++++++++++ x/asset/nft/keeper/keeper.go | 6 -- x/asset/nft/keeper/keeper_test.go | 109 +++++++++++++++++++++ 3 files changed, 194 insertions(+), 6 deletions(-) diff --git a/integration-tests/modules/assetnft_test.go b/integration-tests/modules/assetnft_test.go index e0f92ac78..a8efe73ab 100644 --- a/integration-tests/modules/assetnft_test.go +++ b/integration-tests/modules/assetnft_test.go @@ -2054,6 +2054,91 @@ func TestAssetNFTClassWhitelist(t *testing.T) { requireT.NoError(err) } +func TestAssetNFTSoulbound(t *testing.T) { + t.Parallel() + + ctx, chain := integrationtests.NewCoreumTestingContext(t) + + requireT := require.New(t) + issuer := chain.GenAccount() + recipient := chain.GenAccount() + + chain.FundAccountWithOptions(ctx, t, issuer, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &assetnfttypes.MsgIssueClass{}, + &assetnfttypes.MsgMint{}, + &nft.MsgSend{}, + }, + Amount: chain.QueryAssetNFTParams(ctx, t).MintFee.Amount, + }) + + chain.FundAccountWithOptions(ctx, t, recipient, integration.BalancesOptions{ + Messages: []sdk.Msg{ + &nft.MsgSend{}, + }, + }) + + // issue new NFT class + issueMsg := &assetnfttypes.MsgIssueClass{ + Issuer: issuer.String(), + Symbol: "NFTClassSymbol", + Features: []assetnfttypes.ClassFeature{ + assetnfttypes.ClassFeature_soulbound, + }, + } + + // mint new token in that class + classID := assetnfttypes.BuildClassID(issueMsg.Symbol, issuer) + nftID := "id-1" + mintMsg1 := &assetnfttypes.MsgMint{ + Sender: issuer.String(), + ID: nftID, + ClassID: classID, + } + + msgList := []sdk.Msg{issueMsg, mintMsg1} + _, err := client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgList...)), + msgList..., + ) + requireT.NoError(err) + + // try to send from issuer to recipient (it is allowed) + sendMsg := &nft.MsgSend{ + ClassId: classID, + Id: nftID, + Sender: issuer.String(), + Receiver: recipient.String(), + } + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(issuer), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(sendMsg)), + sendMsg, + ) + requireT.NoError(err) + + // try to send from recipient to issuer (it is not allowed) + sendMsg = &nft.MsgSend{ + ClassId: classID, + Id: nftID, + Sender: recipient.String(), + Receiver: issuer.String(), + } + + _, err = client.BroadcastTx( + ctx, + chain.ClientContext.WithFromAddress(recipient), + chain.TxFactory().WithGas(chain.GasLimitByMsgs(sendMsg)), + sendMsg, + ) + requireT.Error(err) + requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) +} + // TestAssetNFTSendAuthorization tests that assetnft SendAuthorization works as expected. func TestAssetNFTSendAuthorization(t *testing.T) { t.Parallel() diff --git a/x/asset/nft/keeper/keeper.go b/x/asset/nft/keeper/keeper.go index a7092b4da..6e9ed4288 100644 --- a/x/asset/nft/keeper/keeper.go +++ b/x/asset/nft/keeper/keeper.go @@ -992,12 +992,6 @@ func (k Keeper) isNFTReceivable(ctx sdk.Context, classID, nftID string, receiver return sdkerrors.Wrapf(types.ErrNFTNotFound, "nft with classID:%s and ID:%s not found", classID, nftID) } - // we check for soulbound before the check for issuer, since the issuer should not be able to receive the token. - // Or in other words, the non-issuer sender should not be able to send to issuer. - if classDefinition.IsFeatureEnabled(types.ClassFeature_soulbound) { - return sdkerrors.Wrapf(cosmoserrors.ErrUnauthorized, "nft with classID:%s and ID:%s is soulbound and cannot be sent", classID, nftID) - } - // always allow issuer to receive NFTs issued by them. if classDefinition.IsIssuer(receiver) { return nil diff --git a/x/asset/nft/keeper/keeper_test.go b/x/asset/nft/keeper/keeper_test.go index e8bab54a2..0afd1ddd0 100644 --- a/x/asset/nft/keeper/keeper_test.go +++ b/x/asset/nft/keeper/keeper_test.go @@ -13,6 +13,7 @@ import ( cosmoserrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/types/query" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/nft" "github.com/stretchr/testify/require" "github.com/CoreumFoundation/coreum/v3/pkg/config/constant" @@ -20,6 +21,7 @@ import ( "github.com/CoreumFoundation/coreum/v3/testutil/simapp" "github.com/CoreumFoundation/coreum/v3/x/asset/nft/keeper" "github.com/CoreumFoundation/coreum/v3/x/asset/nft/types" + wnftkeeper "github.com/CoreumFoundation/coreum/v3/x/wnft/keeper" ) func TestKeeper_IssueClass(t *testing.T) { @@ -1281,6 +1283,107 @@ func TestKeeper_ClassFreeze_Nonexistent(t *testing.T) { requireT.ErrorIs(err, types.ErrClassNotFound) } +func TestKeeper_Soulbound(t *testing.T) { + requireT := require.New(t) + testApp := simapp.New() + ctx := testApp.NewContext(false, tmproto.Header{}) + assetNFTKeeper := testApp.AssetNFTKeeper + nftKeeper := testApp.NFTKeeper + + nftParams := types.Params{ + MintFee: sdk.NewInt64Coin(constant.DenomDev, 0), + } + requireT.NoError(assetNFTKeeper.SetParams(ctx, nftParams)) + + issuer := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + classSettings := types.IssueClassSettings{ + Issuer: issuer, + Symbol: "symbol", + Features: []types.ClassFeature{ + types.ClassFeature_soulbound, + }, + } + + classID, err := assetNFTKeeper.IssueClass(ctx, classSettings) + requireT.NoError(err) + + // mint NFT + recipient := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + settings := types.MintSettings{ + Sender: issuer, + Recipient: recipient, + ClassID: classID, + ID: "my-id", + URI: "https://my-nft-meta.invalid/1", + URIHash: "content-hash", + } + + requireT.NoError(assetNFTKeeper.Mint(ctx, settings)) + nftID := settings.ID + + // transfer must be rejected + recipient2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + err = nftKeeper.Transfer(ctx, classID, nftID, recipient2) + requireT.Error(err) + requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) + + // transfer to issuer must also be rejected + err = nftKeeper.Transfer(ctx, classID, nftID, issuer) + requireT.Error(err) + requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) +} + +func TestKeeper_Soulbound_Burning(t *testing.T) { + requireT := require.New(t) + testApp := simapp.New() + ctx := testApp.NewContext(false, tmproto.Header{}) + assetNFTKeeper := testApp.AssetNFTKeeper + nftKeeper := testApp.NFTKeeper + + nftParams := types.Params{ + MintFee: sdk.NewInt64Coin(constant.DenomDev, 0), + } + requireT.NoError(assetNFTKeeper.SetParams(ctx, nftParams)) + + issuer := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + classSettings := types.IssueClassSettings{ + Issuer: issuer, + Symbol: "symbol", + Features: []types.ClassFeature{ + types.ClassFeature_soulbound, + types.ClassFeature_burning, + }, + } + + classID, err := assetNFTKeeper.IssueClass(ctx, classSettings) + requireT.NoError(err) + + // mint NFT + recipient := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + settings := types.MintSettings{ + Sender: issuer, + Recipient: recipient, + ClassID: classID, + ID: "my-id", + URI: "https://my-nft-meta.invalid/1", + URIHash: "content-hash", + } + + requireT.NoError(assetNFTKeeper.Mint(ctx, settings)) + nftID := settings.ID + + // transfer must be rejected + recipient2 := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address()) + err = nftKeeper.Transfer(ctx, classID, nftID, recipient2) + requireT.Error(err) + requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized) + + // burning is allowed + err = assetNFTKeeper.Burn(ctx, recipient, classID, nftID) + requireT.NoError(err) + requireT.False(nftKeeper.HasNFT(ctx, classID, nftID)) +} + func genNFTData(requireT *require.Assertions) *codectypes.Any { dataString := "metadata" dataValue, err := codectypes.NewAnyWithValue(&types.DataBytes{Data: []byte(dataString)}) @@ -1309,3 +1412,9 @@ func assertFrozen(t *testing.T, ctx sdk.Context, k keeper.Keeper, classID, nftID require.NoError(t, err) require.EqualValues(t, frozen, expected) } + +func assertOwner(t *testing.T, ctx sdk.Context, k wnftkeeper.Wrapper, classID, nftID string, expectedAddress sdk.AccAddress) { + res, err := k.Owner(ctx, &nft.QueryOwnerRequest{ClassId: classID, Id: nftID}) + require.NoError(t, err) + require.EqualValues(t, expectedAddress.String(), res.Owner) +}