diff --git a/integration_tests/test_feegrant.py b/integration_tests/test_feegrant.py new file mode 100644 index 000000000..7fff67d2a --- /dev/null +++ b/integration_tests/test_feegrant.py @@ -0,0 +1,270 @@ +import datetime +from time import sleep + +import pytest + +from .utils import ( + BASECRO_DENOM, + SUCCESS_CODE, + grant_fee_allowance, + revoke_fee_grant, + transfer, +) + +pytestmark = pytest.mark.normal + + +def test_basic_fee_allowance(cluster): + """ + check basic fee allowance with no limit or grant expiry + """ + transaction_coins = 100 + fee_coins = 10 + + fee_granter_address = cluster.address("community") + fee_grantee_address = cluster.address("ecosystem") + receiver_address = cluster.address("reserve") + + fee_granter_balance = cluster.balance(fee_granter_address) + fee_grantee_balance = cluster.balance(fee_grantee_address) + receiver_balance = cluster.balance(receiver_address) + + grant_fee_allowance(cluster, fee_granter_address, fee_grantee_address) + + transfer( + cluster, + fee_grantee_address, + receiver_address, + "%s%s" % (transaction_coins, BASECRO_DENOM), + fees="%s%s" % (fee_coins, BASECRO_DENOM), + fee_account=fee_granter_address, + ) + + assert cluster.balance(fee_granter_address) == fee_granter_balance - fee_coins + assert ( + cluster.balance(fee_grantee_address) == fee_grantee_balance - transaction_coins + ) + assert cluster.balance(receiver_address) == receiver_balance + transaction_coins + + +def test_tx_failed_when_exceeds_grant_fee(cluster): + """ + check transaction should fail when tx fee exceeds fee limit in basic fee allowance + """ + transaction_coins = 100 + fee_coins = 10 + fee_grant_spend_limit = 5 + + fee_granter_address = cluster.address("community") + fee_grantee_address = cluster.address("ecosystem") + receiver_address = cluster.address("reserve") + + fee_granter_balance = cluster.balance(fee_granter_address) + fee_grantee_balance = cluster.balance(fee_grantee_address) + receiver_balance = cluster.balance(receiver_address) + + revoke_fee_grant(cluster, fee_granter_address, fee_grantee_address) + grant_fee_allowance( + cluster, + fee_granter_address, + fee_grantee_address, + spend_limit="%s%s" % (fee_grant_spend_limit, BASECRO_DENOM), + ) + + tx = transfer( + cluster, + fee_grantee_address, + receiver_address, + "%s%s" % (transaction_coins, BASECRO_DENOM), + fees="%s%s" % (fee_coins, BASECRO_DENOM), + fee_account=fee_granter_address, + ) + assert tx["code"] != SUCCESS_CODE, "should fail as fee limit exceeded" + + assert cluster.balance(fee_granter_address) == fee_granter_balance + assert cluster.balance(fee_grantee_address) == fee_grantee_balance + assert cluster.balance(receiver_address) == receiver_balance + + +def test_tx_failed_after_grant_expiration(cluster): + """ + check transaction should fail when tx happens after grant expiry + """ + transaction_coins = 100 + fee_coins = 10 + + # RFC 3339 timestamp + grant_expiration = datetime.datetime.utcnow().isoformat() + "Z" + + fee_granter_address = cluster.address("community") + fee_grantee_address = cluster.address("ecosystem") + receiver_address = cluster.address("reserve") + + fee_granter_balance = cluster.balance(fee_granter_address) + fee_grantee_balance = cluster.balance(fee_grantee_address) + receiver_balance = cluster.balance(receiver_address) + + revoke_fee_grant(cluster, fee_granter_address, fee_grantee_address) + grant_fee_allowance( + cluster, fee_granter_address, fee_grantee_address, expiration=grant_expiration + ) + + tx = transfer( + cluster, + fee_grantee_address, + receiver_address, + "%s%s" % (transaction_coins, BASECRO_DENOM), + fees="%s%s" % (fee_coins, BASECRO_DENOM), + fee_account=fee_granter_address, + ) + assert tx["code"] != SUCCESS_CODE, "should fail as fee allowance expired" + + assert cluster.balance(fee_granter_address) == fee_granter_balance + assert cluster.balance(fee_grantee_address) == fee_grantee_balance + assert cluster.balance(receiver_address) == receiver_balance + + +def test_periodic_fee_allowance(cluster): + """ + check periodic fee allowance with no expiration + """ + transaction_coins = 100 + fee_coins = 10 + + period = 5 + period_limit = 11 + number_of_periods = 3 + + fee_granter_address = cluster.address("community") + fee_grantee_address = cluster.address("ecosystem") + receiver_address = cluster.address("reserve") + + fee_granter_balance = cluster.balance(fee_granter_address) + fee_grantee_balance = cluster.balance(fee_grantee_address) + receiver_balance = cluster.balance(receiver_address) + + revoke_fee_grant(cluster, fee_granter_address, fee_grantee_address) + grant_fee_allowance( + cluster, + fee_granter_address, + fee_grantee_address, + period_limit="%s%s" % (period_limit, BASECRO_DENOM), + period=period, + ) + + for _ in range(number_of_periods): + transfer( + cluster, + fee_grantee_address, + receiver_address, + "%s%s" % (transaction_coins, BASECRO_DENOM), + fees="%s%s" % (fee_coins, BASECRO_DENOM), + fee_account=fee_granter_address, + ) + sleep(period) + + assert ( + cluster.balance(fee_granter_address) + == fee_granter_balance - fee_coins * number_of_periods + ) + assert ( + cluster.balance(fee_grantee_address) + == fee_grantee_balance - transaction_coins * number_of_periods + ) + assert ( + cluster.balance(receiver_address) + == receiver_balance + transaction_coins * number_of_periods + ) + + +def test_exceed_period_limit_should_not_affect_the_next_period(cluster): + """ + check exceeding periodic fee should not affect next period + """ + transaction_coins = 100 + fee_coins = 10 + + period = 5 + period_limit = 11 + + fee_granter_address = cluster.address("community") + fee_grantee_address = cluster.address("ecosystem") + receiver_address = cluster.address("reserve") + + fee_granter_balance = cluster.balance(fee_granter_address) + fee_grantee_balance = cluster.balance(fee_grantee_address) + receiver_balance = cluster.balance(receiver_address) + + revoke_fee_grant(cluster, fee_granter_address, fee_grantee_address) + grant_fee_allowance( + cluster, + fee_granter_address, + fee_grantee_address, + period_limit="%s%s" % (period_limit, BASECRO_DENOM), + period=period, + ) + + transfer( + cluster, + fee_grantee_address, + receiver_address, + "%s%s" % (transaction_coins, BASECRO_DENOM), + fees="%s%s" % (fee_coins, BASECRO_DENOM), + fee_account=fee_granter_address, + ) + + failed_tx = transfer( + cluster, + fee_grantee_address, + receiver_address, + "%s%s" % (transaction_coins, BASECRO_DENOM), + fees="%s%s" % (fee_coins, BASECRO_DENOM), + fee_account=fee_granter_address, + ) + assert failed_tx["code"] != SUCCESS_CODE, "should fail as fee exceeds period limit" + sleep(period) + + transfer( + cluster, + fee_grantee_address, + receiver_address, + "%s%s" % (transaction_coins, BASECRO_DENOM), + fees="%s%s" % (fee_coins, BASECRO_DENOM), + fee_account=fee_granter_address, + ) + + # transaction only happened two times + assert cluster.balance(fee_granter_address) == fee_granter_balance - fee_coins * 2 + assert ( + cluster.balance(fee_grantee_address) + == fee_grantee_balance - transaction_coins * 2 + ) + assert cluster.balance(receiver_address) == receiver_balance + transaction_coins * 2 + + +def test_revoke_fee_grant(cluster): + """ + check tx should fail after fee grant is revoked + """ + transaction_coins = 100 + fee_coins = 10 + + fee_granter_address = cluster.address("community") + fee_grantee_address = cluster.address("ecosystem") + receiver_address = cluster.address("reserve") + + revoke_fee_grant(cluster, fee_granter_address, fee_grantee_address) + grant_fee_allowance(cluster, fee_granter_address, fee_grantee_address) + + revoke_fee_grant(cluster, fee_granter_address, fee_grantee_address) + + failed_tx = transfer( + cluster, + fee_grantee_address, + receiver_address, + "%s%s" % (transaction_coins, BASECRO_DENOM), + fees="%s%s" % (fee_coins, BASECRO_DENOM), + fee_account=fee_granter_address, + ) + + assert failed_tx["code"] != SUCCESS_CODE, "should fail as grant is revoked" diff --git a/integration_tests/utils.py b/integration_tests/utils.py index 73f8a5c2e..a3c91ddcc 100644 --- a/integration_tests/utils.py +++ b/integration_tests/utils.py @@ -13,6 +13,15 @@ from pystarport import cluster, ledger from pystarport.ports import rpc_port +################# +# CONSTANTS +# Reponse code +SUCCESS_CODE = 0 + +# Denomination +CRO_DENOM = "cro" +BASECRO_DENOM = "basecro" + def wait_for_block(cli, height, timeout=240): for i in range(timeout * 2): @@ -202,3 +211,61 @@ def find_balance(balances, denom): if balance["denom"] == denom: return int(balance["amount"]) return 0 + + +def transfer(cli, from_, to, coins, i=0, *k_options, **kv_options): + return json.loads( + cli.cosmos_cli(i).raw( + "tx", + "bank", + "send", + from_, + to, + coins, + "-y", + home=cli.cosmos_cli(i).data_dir, + keyring_backend="test", + chain_id=cli.cosmos_cli(i).chain_id, + node=cli.cosmos_cli(i).node_rpc, + *k_options, + **kv_options, + ) + ) + + +def grant_fee_allowance(cli, granter_address, grantee, i=0, *k_options, **kv_options): + return json.loads( + cli.cosmos_cli(i).raw( + "tx", + "feegrant", + "grant", + granter_address, + grantee, + "-y", + home=cli.cosmos_cli(i).data_dir, + keyring_backend="test", + chain_id=cli.cosmos_cli(i).chain_id, + node=cli.cosmos_cli(i).node_rpc, + *k_options, + **kv_options, + ) + ) + + +def revoke_fee_grant(cli, granter_address, grantee, i=0, *k_options, **kv_options): + return json.loads( + cli.cosmos_cli(i).raw( + "tx", + "feegrant", + "revoke", + granter_address, + grantee, + "-y", + home=cli.cosmos_cli(i).data_dir, + keyring_backend="test", + chain_id=cli.cosmos_cli(i).chain_id, + node=cli.cosmos_cli(i).node_rpc, + *k_options, + **kv_options, + ) + )