Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions hathor/nanocontracts/runner/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
CallInfo,
CallRecord,
CallType,
IndexUpdateRecordType,
SyscallCreateContractRecord,
SyscallUpdateTokenRecord,
UpdateAuthoritiesRecord,
Expand Down Expand Up @@ -437,7 +438,13 @@ def _unsafe_call_another_contract_public_method(
# Update the balances with the fee payment amount. Since some tokens could be created during contract
# execution, the verification of the tokens and amounts will be done after it
for fee in fees:
previous_changes_tracker.add_balance(fee.token_uid, -fee.amount)
self._update_tokens_amount([
SyscallUpdateTokenRecord(
token_uid=fee.token_uid,
amount=-fee.amount,
type=IndexUpdateRecordType.MELT_TOKENS
)
])
self._register_paid_fee(fee.token_uid, fee.amount)

ctx_actions = Context.__group_actions__(actions)
Expand Down Expand Up @@ -534,10 +541,6 @@ def _validate_balances(self, ctx: Context) -> None:
case _: # pragma: no cover
assert_never(action)

# Account for fees paid during execution
for fee_token_uid, amount in self._paid_actions_fees.items():
total_diffs[fee_token_uid] += amount

assert all(diff == 0 for diff in total_diffs.values()), (
f'change tracker diffs do not match actions: {total_diffs}'
)
Expand Down
155 changes: 153 additions & 2 deletions tests/nanocontracts/test_actions_fee.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
from hathor.nanocontracts.storage.contract_storage import Balance, BalanceKey
from hathor.nanocontracts.types import ContractId, NCDepositAction, NCFee, NCWithdrawalAction, TokenUid, public
from hathor.nanocontracts.utils import derive_child_token_id
from hathor.transaction import Transaction
from tests.dag_builder.builder import TestDAGBuilder
from tests.nanocontracts.blueprints.unittest import BlueprintTestCase
from tests.nanocontracts.test_reentrancy import HTR_TOKEN_UID


class MyBlueprint(Blueprint):
Expand Down Expand Up @@ -63,16 +66,64 @@ def move_tokens_to_nc(


class MyOtherBlueprint(Blueprint):
fbt_uid: TokenUid
ftt_uid: TokenUid
dbt_uid: TokenUid

@public(allow_deposit=True, allow_withdrawal=True, allow_grant_authority=True)
def initialize(self, ctx: Context) -> None:
self.syscall.create_fee_token(
self.fbt_uid = self.syscall.create_fee_token(
token_name='FBT',
token_symbol='FBT',
amount=1_000_000,
mint_authority=True,
melt_authority=True,
)
self.ftt_uid = self.syscall.create_fee_token(
token_name='FTT',
token_symbol='FTT',
amount=500_000,
mint_authority=True,
melt_authority=True,
)
self.dbt_uid = self.syscall.create_deposit_token(
token_name='DBT',
token_symbol='DBT',
amount=1000,
mint_authority=True,
melt_authority=True,
)

@public(allow_deposit=True, allow_withdrawal=True)
def move_tokens_to_nc(
self,
ctx: Context,
nc_id: ContractId,
pay_with_dbt: bool,
) -> None:
if pay_with_dbt:
fees = [NCFee(token_uid=TokenUid(self.dbt_uid), amount=100)]
else:
fees = [NCFee(token_uid=TokenUid(HTR_TOKEN_UID), amount=1)]
self.syscall.get_contract(nc_id, blueprint_id=None).public(
NCDepositAction(token_uid=self.fbt_uid, amount=1000),
fees=fees
).noop()

@public(allow_deposit=True, allow_withdrawal=True)
def move_tokens_to_nc_paying_with_htr_and_dbt(
self,
ctx: Context,
nc_id: ContractId
) -> None:
self.syscall.get_contract(nc_id, blueprint_id=None).public(
NCDepositAction(token_uid=self.fbt_uid, amount=1000),
NCDepositAction(token_uid=self.ftt_uid, amount=1000),
fees=[
NCFee(token_uid=TokenUid(self.dbt_uid), amount=100),
NCFee(token_uid=TokenUid(HTR_TOKEN_UID), amount=1)
]
).noop()


class NCActionsFeeTestCase(BlueprintTestCase):
Expand Down Expand Up @@ -328,21 +379,121 @@ def test_create_and_actions(self) -> None:
# Check the contract creation with a token creation syscall and withdrawal of the created token
self.nc3_id = self.gen_random_contract_id()
fbt_token_uid = derive_child_token_id(self.nc3_id, 'FBT')
ftt_token_uid = derive_child_token_id(self.nc3_id, 'FTT')
dbt_token_uid = derive_child_token_id(self.nc3_id, 'DBT')

htr_token_uid = TokenUid(HATHOR_TOKEN_UID)
ctx = self.create_context(
actions=[
NCDepositAction(token_uid=htr_token_uid, amount=1),
NCDepositAction(token_uid=htr_token_uid, amount=12),
NCWithdrawalAction(token_uid=fbt_token_uid, amount=100_000)
],
)
self.runner.create_contract(self.nc3_id, self.my_other_blueprint_id, ctx)

fbt_balance_key = BalanceKey(self.nc3_id, token_uid=fbt_token_uid)
ftt_balance_key = BalanceKey(self.nc3_id, token_uid=ftt_token_uid)
dbt_balance_key = BalanceKey(self.nc3_id, token_uid=dbt_token_uid)
htr_balance_key = BalanceKey(self.nc3_id, token_uid=htr_token_uid)

storage = self.runner.get_storage(self.nc3_id)
assert storage.get_all_balances() == {
htr_balance_key: Balance(value=0, can_mint=False, can_melt=False),
fbt_balance_key: Balance(value=900_000, can_mint=True, can_melt=True),
ftt_balance_key: Balance(value=500_000, can_mint=True, can_melt=True),
dbt_balance_key: Balance(value=1000, can_mint=True, can_melt=True),
}

def test_token_index_updates(self) -> None:
"""Test token creation, token movement between contracts, and verify token indexes."""
# Register the blueprint
self.dag_builder = TestDAGBuilder.from_manager(self.manager)

# Build the DAG: create two contracts, create tokens in first, then move tokens to second
artifacts = self.dag_builder.build_from_str(f'''
blockchain genesis b[1..15]
b10 < dummy

tx1.nc_id = "{self.my_other_blueprint_id.hex()}"
tx1.nc_method = initialize()
tx1.nc_deposit = 100 HTR

tx2.nc_id = "{self.my_blueprint_id.hex()}"
tx2.nc_method = initialize()

tx3.nc_id = tx1
tx3.nc_method = move_tokens_to_nc(`tx2`, false)

tx4.nc_id = tx1
tx4.nc_method = move_tokens_to_nc(`tx2`, true)

tx5.nc_id = tx1
tx5.nc_method = move_tokens_to_nc_paying_with_htr_and_dbt(`tx2`)

tx1 < b11 < tx2 < b12 < tx3 < b13 < tx4 < b14 < tx5 < b15
tx1 <-- b11
tx2 <-- b12
tx3 <-- b13
tx4 <-- b14
tx5 <-- b15
''')

# Propagate transactions and blocks
artifacts.propagate_with(self.manager)

tx1, tx2, tx3, tx4, tx5 = artifacts.get_typed_vertices(('tx1', 'tx2', 'tx3', 'tx4', 'tx5'), Transaction)

# Get tokens index
tokens_index = self.manager.tx_storage.indexes.tokens
assert tokens_index is not None

fbt_id = derive_child_token_id(ContractId(tx1.hash), token_symbol='FBT')
ftt_id = derive_child_token_id(ContractId(tx1.hash), token_symbol='FTT')
dbt_id = derive_child_token_id(ContractId(tx1.hash), token_symbol='DBT')

fbt_token_info = tokens_index.get_token_info(fbt_id)
assert fbt_token_info.get_total() == 1_000_000

ftt_token_info = tokens_index.get_token_info(ftt_id)
assert ftt_token_info.get_total() == 500_000

dbt_token_info = tokens_index.get_token_info(dbt_id)
assert dbt_token_info.get_total() == 800

# Verify HTR total (genesis + mined blocks - fees paid)
# Fees during initialize (tx1): 1 HTR (FBT) + 1 HTR (FTT) + 10 HTR (DBT, 1% of 1000) = 12 HTR
# Fees during tx3: 1 HTR (move_tokens_to_nc paying with HTR)
# Fees during tx4: 100 DBT (move_tokens_to_nc paying with DBT, doesn't affect HTR)
# Fees during tx5: 1 HTR + 100 DBT (move_tokens_to_nc_paying_with_htr_and_dbt)
# Total HTR fees: 12 + 1 + 1 = 14 HTR
htr_token_info = tokens_index.get_token_info(HATHOR_TOKEN_UID)
expected_htr_total = (
self._settings.GENESIS_TOKENS
+ 15 * self._settings.INITIAL_TOKENS_PER_BLOCK
- 12 # 12 HTR fee for initialize (1 FBT + 1 FTT + 10 DBT)
- 1 # 1 HTR fee for tx3 (move_tokens_to_nc)
- 1 # 1 HTR fee for tx5 (move_tokens_to_nc_paying_with_htr_and_dbt)
)
assert htr_token_info.get_total() == expected_htr_total

# Verify contract balances after all operations
nc1_storage = self.manager.get_best_block_nc_storage(tx1.hash)
nc2_storage = self.manager.get_best_block_nc_storage(tx2.hash)

# nc1 should have:
# - HTR: 100 - 12 (initialize) - 1 (tx3) - 1 (tx5) = 86 HTR
# - FBT: 1_000_000 - 1000 (tx3) - 1000 (tx4) - 1000 (tx5) = 997_000 FBT
# - FTT: 500_000 - 1000 (tx5) = 499_000 FTT
# - DBT: 1000 - 100 (tx4) - 100 (tx5) = 800 DBT
assert nc1_storage.get_balance(HATHOR_TOKEN_UID) == Balance(value=86, can_mint=False, can_melt=False)
assert nc1_storage.get_balance(fbt_id) == Balance(value=997_000, can_mint=True, can_melt=True)
assert nc1_storage.get_balance(ftt_id) == Balance(value=499_000, can_mint=True, can_melt=True)
assert nc1_storage.get_balance(dbt_id) == Balance(value=800, can_mint=True, can_melt=True)

# nc2 should have:
# - HTR: 0
# - FBT: 1000 (tx3) + 1000 (tx4) + 1000 (tx5) = 3000 FBT
# - FTT: 1000 (tx5) = 1000 FTT
assert nc2_storage.get_balance(HATHOR_TOKEN_UID) == Balance(value=0, can_mint=False, can_melt=False)
assert nc2_storage.get_balance(fbt_id) == Balance(value=3000, can_mint=False, can_melt=False)
assert nc2_storage.get_balance(ftt_id) == Balance(value=1000, can_mint=False, can_melt=False)
Loading