Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Clawback feature for assetft #804

Merged
merged 13 commits into from
Apr 29, 2024
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ vendor
/bin
*.iml
go.work*
.DS_Store
1 change: 1 addition & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ func New(
// pointer is used here because there is cycle in keeper dependencies:
// AssetFTKeeper -> WasmKeeper -> BankKeeper -> AssetFTKeeper
&app.WasmKeeper,
&app.AccountKeeper,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)

Expand Down
38 changes: 38 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [MintAuthorization](#coreum.asset.ft.v1.MintAuthorization)

- [coreum/asset/ft/v1/event.proto](#coreum/asset/ft/v1/event.proto)
- [EventAmountClawedBack](#coreum.asset.ft.v1.EventAmountClawedBack)
- [EventFrozenAmountChanged](#coreum.asset.ft.v1.EventFrozenAmountChanged)
- [EventIssued](#coreum.asset.ft.v1.EventIssued)
- [EventWhitelistedAmountChanged](#coreum.asset.ft.v1.EventWhitelistedAmountChanged)
Expand Down Expand Up @@ -56,6 +57,7 @@
- [coreum/asset/ft/v1/tx.proto](#coreum/asset/ft/v1/tx.proto)
- [EmptyResponse](#coreum.asset.ft.v1.EmptyResponse)
- [MsgBurn](#coreum.asset.ft.v1.MsgBurn)
- [MsgClawback](#coreum.asset.ft.v1.MsgClawback)
- [MsgFreeze](#coreum.asset.ft.v1.MsgFreeze)
- [MsgGloballyFreeze](#coreum.asset.ft.v1.MsgGloballyFreeze)
- [MsgGloballyUnfreeze](#coreum.asset.ft.v1.MsgGloballyUnfreeze)
Expand Down Expand Up @@ -1579,6 +1581,23 @@ the granter's account.



<a name="coreum.asset.ft.v1.EventAmountClawedBack"></a>

### EventAmountClawedBack



| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `account` | [string](#string) | | |
| `denom` | [string](#string) | | |
| `amount` | [string](#string) | | |






<a name="coreum.asset.ft.v1.EventFrozenAmountChanged"></a>

### EventFrozenAmountChanged
Expand Down Expand Up @@ -2236,6 +2255,7 @@ Feature defines possible features of fungible token.
| whitelisting | 3 | |
| ibc | 4 | |
| block_smart_contracts | 5 | |
| clawback | 6 | |


<!-- end enums -->
Expand Down Expand Up @@ -2279,6 +2299,23 @@ Feature defines possible features of fungible token.



<a name="coreum.asset.ft.v1.MsgClawback"></a>

### MsgClawback



| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `sender` | [string](#string) | | |
| `account` | [string](#string) | | |
| `coin` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | |






<a name="coreum.asset.ft.v1.MsgFreeze"></a>

### MsgFreeze
Expand Down Expand Up @@ -2487,6 +2524,7 @@ Msg defines the Msg service.
| `SetFrozen` | [MsgSetFrozen](#coreum.asset.ft.v1.MsgSetFrozen) | [EmptyResponse](#coreum.asset.ft.v1.EmptyResponse) | `SetFrozen sets the absolute value of frozen amount.` | |
| `GloballyFreeze` | [MsgGloballyFreeze](#coreum.asset.ft.v1.MsgGloballyFreeze) | [EmptyResponse](#coreum.asset.ft.v1.EmptyResponse) | `GloballyFreeze freezes fungible token so no operations are allowed with it before unfrozen. This operation is idempotent so global freeze of already frozen token does nothing.` | |
| `GloballyUnfreeze` | [MsgGloballyUnfreeze](#coreum.asset.ft.v1.MsgGloballyUnfreeze) | [EmptyResponse](#coreum.asset.ft.v1.EmptyResponse) | `GloballyUnfreeze unfreezes fungible token and unblocks basic operations on it. This operation is idempotent so global unfreezing of non-frozen token does nothing.` | |
| `Clawback` | [MsgClawback](#coreum.asset.ft.v1.MsgClawback) | [EmptyResponse](#coreum.asset.ft.v1.EmptyResponse) | `Clawback confiscates a part of fungible tokens from an account to the issuer, only if the clawback feature is enabled on that token.` | |
| `SetWhitelistedLimit` | [MsgSetWhitelistedLimit](#coreum.asset.ft.v1.MsgSetWhitelistedLimit) | [EmptyResponse](#coreum.asset.ft.v1.EmptyResponse) | `SetWhitelistedLimit sets the limit of how many tokens a specific account may hold.` | |
| `UpgradeTokenV1` | [MsgUpgradeTokenV1](#coreum.asset.ft.v1.MsgUpgradeTokenV1) | [EmptyResponse](#coreum.asset.ft.v1.EmptyResponse) | `TokenUpgradeV1 upgrades token to version V1.` | |
| `UpdateParams` | [MsgUpdateParams](#coreum.asset.ft.v1.MsgUpdateParams) | [EmptyResponse](#coreum.asset.ft.v1.EmptyResponse) | `UpdateParams is a governance operation to modify the parameters of the module. NOTE: all parameters must be provided.` | |
Expand Down
3 changes: 2 additions & 1 deletion docs/static/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9090,7 +9090,8 @@
"freezing",
"whitelisting",
"ibc",
"block_smart_contracts"
"block_smart_contracts",
"clawback"
],
"default": "minting",
"description": "Feature defines possible features of fungible token."
Expand Down
133 changes: 133 additions & 0 deletions integration-tests/modules/assetft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2031,6 +2031,139 @@ func TestAssetFTFreeze(t *testing.T) {
}, fungibleTokenFreezeEvts[0])
}

// TestAssetFTClawback checks clawback functionality of fungible tokens.
func TestAssetFTClawback(t *testing.T) {
t.Parallel()

ctx, chain := integrationtests.NewCoreumTestingContext(t)

requireT := require.New(t)
assertT := assert.New(t)
clientCtx := chain.ClientContext

bankClient := banktypes.NewQueryClient(clientCtx)

issuer := chain.GenAccount()
from := chain.GenAccount()
randomAddress := chain.GenAccount()
chain.FundAccountWithOptions(ctx, t, issuer, integration.BalancesOptions{
Messages: []sdk.Msg{
&assetfttypes.MsgIssue{},
&banktypes.MsgSend{},
&assetfttypes.MsgClawback{},
&assetfttypes.MsgClawback{},
},
Amount: chain.QueryAssetFTParams(ctx, t).IssueFee.Amount,
})
chain.FundAccountWithOptions(ctx, t, randomAddress, integration.BalancesOptions{
Messages: []sdk.Msg{
&assetfttypes.MsgClawback{},
},
})

// Issue the new fungible token
msg := &assetfttypes.MsgIssue{
Issuer: issuer.String(),
Symbol: "ABC",
Subunit: "uabc",
Precision: 6,
Description: "ABC Description",
InitialAmount: sdkmath.NewInt(1000),
Features: []assetfttypes.Feature{
assetfttypes.Feature_clawback,
},
}

msgSend := &banktypes.MsgSend{
FromAddress: issuer.String(),
ToAddress: from.String(),
Amount: sdk.NewCoins(
sdk.NewCoin(assetfttypes.BuildDenom(msg.Subunit, issuer), sdkmath.NewInt(1000)),
),
}

msgList := []sdk.Msg{
msg, msgSend,
}

res, err := client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(msgList...)),
msgList...,
)

requireT.NoError(err)
fungibleTokenIssuedEvts, err := event.FindTypedEvents[*assetfttypes.EventIssued](res.Events)
requireT.NoError(err)
denom := fungibleTokenIssuedEvts[0].Denom

// query account balance before clawback
bankRes, err := bankClient.Balance(ctx, banktypes.NewQueryBalanceRequest(from, denom))
requireT.NoError(err)
requireT.EqualValues(sdk.NewCoin(denom, sdkmath.NewInt(1000)).String(), bankRes.Balance.String())

// try to pass non-issuer signature to clawback msg
clawbackMsg := &assetfttypes.MsgClawback{
Sender: randomAddress.String(),
Account: from.String(),
Coin: sdk.NewCoin(denom, sdkmath.NewInt(1000)),
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(randomAddress),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(clawbackMsg)),
clawbackMsg,
)
requireT.Error(err)
requireT.ErrorIs(err, cosmoserrors.ErrUnauthorized)

// clawback 400 tokens
clawbackMsg = &assetfttypes.MsgClawback{
Sender: issuer.String(),
Account: from.String(),
Coin: sdk.NewCoin(denom, sdkmath.NewInt(400)),
}
res, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(clawbackMsg)),
clawbackMsg,
)
requireT.NoError(err)
assertT.EqualValues(res.GasUsed, chain.GasLimitByMsgs(clawbackMsg))

fungibleTokenClawbackEvts, err := event.FindTypedEvents[*assetfttypes.EventAmountClawedBack](res.Events)
requireT.NoError(err)
assertT.EqualValues(&assetfttypes.EventAmountClawedBack{
Account: from.String(),
Denom: denom,
Amount: sdkmath.NewInt(400),
}, fungibleTokenClawbackEvts[0])

// query account balance after clawback
bankRes, err = bankClient.Balance(ctx, banktypes.NewQueryBalanceRequest(from, denom))
requireT.NoError(err)
requireT.EqualValues(sdk.NewCoin(denom, sdkmath.NewInt(600)).String(), bankRes.Balance.String())

// try to clawback more than available (650) (600 is available)
coinsToClawback := sdk.NewCoin(denom, sdkmath.NewInt(650))

clawbackMsg = &assetfttypes.MsgClawback{
Sender: issuer.String(),
Account: from.String(),
Coin: coinsToClawback,
}
_, err = client.BroadcastTx(
ctx,
chain.ClientContext.WithFromAddress(issuer),
chain.TxFactory().WithGas(chain.GasLimitByMsgs(clawbackMsg)),
clawbackMsg,
)
requireT.Error(err)
requireT.ErrorIs(err, cosmoserrors.ErrInsufficientFunds)
}

// TestAssetFTFreezeUnfreezable checks freeze functionality on unfreezable fungible tokens.
func TestAssetFTFreezeUnfreezable(t *testing.T) {
t.Parallel()
Expand Down
9 changes: 9 additions & 0 deletions proto/coreum/asset/ft/v1/event.proto
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ message EventFrozenAmountChanged {
];
}

message EventAmountClawedBack {
string account = 1;
string denom = 2;
string amount = 3 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.nullable) = false
];
}

message EventWhitelistedAmountChanged {
string account = 1;
string denom = 2;
Expand Down
1 change: 1 addition & 0 deletions proto/coreum/asset/ft/v1/token.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum Feature {
whitelisting = 3;
ibc = 4;
block_smart_contracts = 5;
clawback = 6;
}

// Definition defines the fungible token settings to store.
Expand Down
10 changes: 10 additions & 0 deletions proto/coreum/asset/ft/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ service Msg {
// This operation is idempotent so global unfreezing of non-frozen token does nothing.
rpc GloballyUnfreeze(MsgGloballyUnfreeze) returns (EmptyResponse);

// Clawback confiscates a part of fungible tokens from an account
// to the issuer, only if the clawback feature is enabled on that token.
rpc Clawback(MsgClawback) returns (EmptyResponse);

// SetWhitelistedLimit sets the limit of how many tokens a specific account may hold.
rpc SetWhitelistedLimit(MsgSetWhitelistedLimit) returns (EmptyResponse);

Expand Down Expand Up @@ -116,6 +120,12 @@ message MsgGloballyUnfreeze {
string denom = 2;
}

message MsgClawback {
string sender = 1;
string account = 2;
cosmos.base.v1beta1.Coin coin = 3 [(gogoproto.nullable) = false];
}

message MsgSetWhitelistedLimit {
string sender = 1;
string account = 2;
Expand Down
45 changes: 45 additions & 0 deletions x/asset/ft/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func GetTxCmd() *cobra.Command {
CmdTxSetFrozen(),
CmdTxGloballyFreeze(),
CmdTxGloballyUnfreeze(),
CmdTxClawback(),
CmdTxSetWhitelistedLimit(),
CmdTxUpgradeV1(),
CmdGrantAuthorization(),
Expand Down Expand Up @@ -415,6 +416,50 @@ $ %s tx %s set-frozen [account_address] 100000ABC-%s --from [sender]
return cmd
}

// CmdTxClawback returns Clawback cobra command.
//
//nolint:dupl // most code is identical, but reusing logic is not beneficial here.
func CmdTxClawback() *cobra.Command {
cmd := &cobra.Command{
Use: "clawback [account_address] [amount] --from [sender]",
Args: cobra.ExactArgs(2),
Short: "Confiscates any amount of fungible token from the specific account",
Long: strings.TrimSpace(
fmt.Sprintf(`Confiscate a portion of fungible token.

Example:
$ %s tx %s clawback [account_address] 100000ABC-%s --from [sender]
`,
version.AppName, types.ModuleName, constant.AddressSampleTest,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return errors.WithStack(err)
}

sender := clientCtx.GetFromAddress()
account := args[0]
amount, err := sdk.ParseCoinNormalized(args[1])
if err != nil {
return sdkerrors.Wrap(err, "invalid amount")
}

msg := &types.MsgClawback{
Sender: sender.String(),
Account: account,
Coin: amount,
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)

return cmd
}

// CmdTxSetWhitelistedLimit returns SetWhitelistedLimit cobra command.
//
//nolint:dupl // most code is identical, but reusing logic is not beneficial here.
Expand Down
40 changes: 40 additions & 0 deletions x/asset/ft/client/cli/tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,46 @@ func TestGloballyFreezeUnfreeze(t *testing.T) {
requireT.False(resp.Token.GloballyFrozen)
}

func TestClawback(t *testing.T) {
requireT := require.New(t)
testNetwork := network.New(t)

token := types.Token{
Symbol: "btc" + uuid.NewString()[:4],
Subunit: "satoshi" + uuid.NewString()[:4],
Precision: 8,
Description: "description",
Features: []types.Feature{
types.Feature_clawback,
},
}

ctx := testNetwork.Validators[0].ClientCtx
initialAmount := sdkmath.NewInt(777)
denom := issue(requireT, ctx, token, initialAmount, testNetwork)
account := sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address())

coin := sdk.NewInt64Coin(denom, 100)

valAddr := testNetwork.Validators[0].Address.String()
args := append([]string{valAddr, account.String(), coin.String()}, txValidator1Args(testNetwork)...)
_, err := coreumclitestutil.ExecTxCmd(ctx, testNetwork, bankcli.NewSendTxCmd(), args)
requireT.NoError(err)

var balanceRsp banktypes.QueryAllBalancesResponse
args = []string{account.String()}
requireT.NoError(coreumclitestutil.ExecQueryCmd(ctx, bankcli.GetBalancesCmd(), args, &balanceRsp))
requireT.Equal(sdkmath.NewInt(100).String(), balanceRsp.Balances.AmountOf(denom).String())

args = append([]string{account.String(), coin.String()}, txValidator1Args(testNetwork)...)
_, err = coreumclitestutil.ExecTxCmd(ctx, testNetwork, cli.CmdTxClawback(), args)
requireT.NoError(err)

args = []string{account.String()}
requireT.NoError(coreumclitestutil.ExecQueryCmd(ctx, bankcli.GetBalancesCmd(), args, &balanceRsp))
requireT.Equal(sdkmath.NewInt(0).String(), balanceRsp.Balances.AmountOf(denom).String())
}

func TestWhitelistAndQueryWhitelisted(t *testing.T) {
requireT := require.New(t)
testNetwork := network.New(t)
Expand Down
Loading
Loading