From 350806cf8e5e2a42f9332873866c04974597704e Mon Sep 17 00:00:00 2001 From: Mo Shaikjee Date: Fri, 12 Jan 2024 21:42:29 +0200 Subject: [PATCH] implement IHRC in HTS mocks Signed-off-by: Mo Shaikjee --- .../IExchangeRate.sol | 30 ++-- contracts/hts-precompile/IHRC.sol | 10 +- .../hts-precompile/IHederaTokenService.sol | 16 +-- test/foundry/HederaFungibleToken.t.sol | 90 ++++++++++++ test/foundry/HederaNonFungibleToken.t.sol | 131 ++++++++++++++++++ .../hts-precompile/HederaFungibleToken.sol | 19 ++- .../hts-precompile/HederaNonFungibleToken.sol | 19 ++- .../hts-precompile/HtsSystemContractMock.sol | 37 +++++ .../mocks/libraries/HederaTokenValidation.sol | 58 ++++++++ test/foundry/utils/HederaTokenUtils.sol | 62 +++++++++ 10 files changed, 446 insertions(+), 26 deletions(-) diff --git a/contracts/exchange-rate-precompile/IExchangeRate.sol b/contracts/exchange-rate-precompile/IExchangeRate.sol index c7606f544..c7698a0f3 100644 --- a/contracts/exchange-rate-precompile/IExchangeRate.sol +++ b/contracts/exchange-rate-precompile/IExchangeRate.sol @@ -2,24 +2,24 @@ pragma solidity >=0.4.9 <0.9.0; interface IExchangeRate { - // Given a value in tinycents (1e-8 US cents or 1e-10 USD), returns the - // equivalent value in tinybars (1e-8 HBAR) at the current exchange rate - // stored in system file 0.0.112. - // - // This rate is a weighted median of the the recent" HBAR-USD exchange - // rate on major exchanges, but should _not_ be treated as a live price - // oracle! It is important primarily because the network will use it to - // compute the tinybar fees for the active transaction. - // - // So a "self-funding" contract can use this rate to compute how much + // Given a value in tinycents (1e-8 US cents or 1e-10 USD), returns the + // equivalent value in tinybars (1e-8 HBAR) at the current exchange rate + // stored in system file 0.0.112. + // + // This rate is a weighted median of the the recent" HBAR-USD exchange + // rate on major exchanges, but should _not_ be treated as a live price + // oracle! It is important primarily because the network will use it to + // compute the tinybar fees for the active transaction. + // + // So a "self-funding" contract can use this rate to compute how much // tinybar its users must send to cover the Hedera fees for the transaction. function tinycentsToTinybars(uint256 tinycents) external returns (uint256); - // Given a value in tinybars (1e-8 HBAR), returns the equivalent value in - // tinycents (1e-8 US cents or 1e-10 USD) at the current exchange rate - // stored in system file 0.0.112. - // - // This rate tracks the the HBAR-USD rate on public exchanges, but + // Given a value in tinybars (1e-8 HBAR), returns the equivalent value in + // tinycents (1e-8 US cents or 1e-10 USD) at the current exchange rate + // stored in system file 0.0.112. + // + // This rate tracks the the HBAR-USD rate on public exchanges, but // should _not_ be treated as a live price oracle! This conversion is // less likely to be needed than the above conversion from tinycent to // tinybars, but we include it for completeness. diff --git a/contracts/hts-precompile/IHRC.sol b/contracts/hts-precompile/IHRC.sol index 4f79192ed..86d3a2a14 100644 --- a/contracts/hts-precompile/IHRC.sol +++ b/contracts/hts-precompile/IHRC.sol @@ -1,7 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.7; +pragma solidity >=0.4.9 <0.9.0; + +interface IERCCommonToken { + function balanceOf(address account) external view returns (uint256); +} interface IHRC { function associate() external returns (uint256 responseCode); function dissociate() external returns (uint256 responseCode); + + function isAssociated(address evmAddress) external view returns (bool); // TODO: pending completion of https://hips.hedera.com/hip/hip-719 ?? } + +interface IHRCCommon is IHRC, IERCCommonToken {} \ No newline at end of file diff --git a/contracts/hts-precompile/IHederaTokenService.sol b/contracts/hts-precompile/IHederaTokenService.sol index 951cfe0ec..3a7327d0c 100644 --- a/contracts/hts-precompile/IHederaTokenService.sol +++ b/contracts/hts-precompile/IHederaTokenService.sol @@ -640,7 +640,7 @@ interface IHederaTokenService { function getTokenDefaultFreezeStatus(address token) external returns (int64 responseCode, bool defaultFreezeStatus); - + /// Query token default kyc status /// @param token The token address to check /// @return responseCode The response code for the status of the request. SUCCESS is 22. @@ -781,18 +781,18 @@ interface IHederaTokenService { /// Query if valid token found for the given address /// @param token The token address - /// @return responseCode The response code for the status of the request. SUCCESS is 22. - /// @return isToken True if valid token found for the given address - function isToken(address token) - external returns + /// @return responseCode The response code for the status of the request. SUCCESS is 22. + /// @return isToken True if valid token found for the given address + function isToken(address token) + external returns (int64 responseCode, bool isToken); /// Query to return the token type for a given address /// @param token The token address - /// @return responseCode The response code for the status of the request. SUCCESS is 22. - /// @return tokenType the token type. 0 is FUNGIBLE_COMMON, 1 is NON_FUNGIBLE_UNIQUE, -1 is UNRECOGNIZED + /// @return responseCode The response code for the status of the request. SUCCESS is 22. + /// @return tokenType the token type. 0 is FUNGIBLE_COMMON, 1 is NON_FUNGIBLE_UNIQUE, -1 is UNRECOGNIZED function getTokenType(address token) - external returns + external returns (int64 responseCode, int32 tokenType); /// Initiates a Redirect For Token diff --git a/test/foundry/HederaFungibleToken.t.sol b/test/foundry/HederaFungibleToken.t.sol index 976589888..a6fbcbc79 100644 --- a/test/foundry/HederaFungibleToken.t.sol +++ b/test/foundry/HederaFungibleToken.t.sol @@ -234,6 +234,48 @@ contract HederaFungibleTokenTest is HederaTokenUtils, HederaFungibleTokenUtils { assertEq(success, true, "expected burn to succeed"); } + function test_CanAssociateAndDissociateDirectly() public { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + bool success; + uint256 amount = 1e8; + + TransferParams memory transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: amount + }); + + TransferParams memory transferFromBobToAlice = TransferParams({ + sender: bob, + token: tokenAddress, + from: bob, + to: alice, + amountOrSerialNumber: amount + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected transfer to fail since recipient is not associated with token'); + + success = _doAssociateDirectly(bob, tokenAddress); + assertEq(success, true, 'expected bob to associate with token'); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, true, 'expected transfer to succeed'); + + success = _doDissociateDirectly(bob, tokenAddress); + assertEq(success, false, 'expected bob to not dissociate with token while postive balance'); + + (success, ) = _doTransferViaHtsPrecompile(transferFromBobToAlice); + assertEq(success, true, 'expected transfer to succeed'); + + success = _doDissociateDirectly(bob, tokenAddress); + assertEq(success, true, 'expected bob to dissociate'); + } + // negative cases function test_CannotApproveIfSpenderNotAssociated() public { /// @dev already demonstrated in some of the postive test cases @@ -244,6 +286,54 @@ contract HederaFungibleTokenTest is HederaTokenUtils, HederaFungibleTokenUtils { /// @dev already demonstrated in some of the postive test cases // cannot transfer to recipient if recipient is not associated with HederaFungibleToken BOTH directly and viaHtsPrecompile } + + function test_CannotRepeatedlyAssociateAndDissociateDirectly() public { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + bool success; + uint256 amount = 1e8; + + TransferParams memory transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: amount + }); + + TransferParams memory transferFromBobToAlice = TransferParams({ + sender: bob, + token: tokenAddress, + from: bob, + to: alice, + amountOrSerialNumber: amount + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected transfer to fail since recipient is not associated with token'); + + success = _doAssociateDirectly(bob, tokenAddress); + assertEq(success, true, 'expected bob to associate with token'); + + success = _doAssociateDirectly(bob, tokenAddress); + assertEq(success, false, 'expected bob to not re-associate with already associated token'); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, true, 'expected transfer to succeed'); + + (success, ) = _doTransferViaHtsPrecompile(transferFromBobToAlice); + assertEq(success, true, 'expected transfer to succeed'); + + success = _doDissociateDirectly(bob, tokenAddress); + assertEq(success, true, 'expected bob to dissociate with token'); + + success = _doDissociateDirectly(bob, tokenAddress); + assertEq(success, false, 'expected bob to not re-dissociate with already unassociated token'); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected transfer to fail since bob is not associated'); + } } // forge test --match-contract HederaFungibleTokenTest --match-test test_CanBurnViaHtsPrecompile -vv diff --git a/test/foundry/HederaNonFungibleToken.t.sol b/test/foundry/HederaNonFungibleToken.t.sol index 6efa25d30..a53c1384e 100644 --- a/test/foundry/HederaNonFungibleToken.t.sol +++ b/test/foundry/HederaNonFungibleToken.t.sol @@ -515,6 +515,68 @@ contract HederaNonFungibleTokenTest is HederaNonFungibleTokenUtils { assertEq(success, true, "burn should succeed"); } + function test_CanAssociateAndDissociateDirectly() public { + + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + uint256 serialIdU256; + + MintResponse memory mintResponse; + MintParams memory mintParams; + BurnParams memory burnParams; + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + serialIdU256 = uint64(mintResponse.serialId); + + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + success = _doAssociateDirectly(bob, tokenAddress); + assertEq(success, true, 'expected bob to associate with token'); + + TransferParams memory transferParams; + TransferParams memory transferFromBobToAlice; + + transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + transferFromBobToAlice = TransferParams({ + sender: bob, + token: tokenAddress, + from: bob, + to: alice, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, true, 'expected success'); + + success = _doDissociateDirectly(bob, tokenAddress); + assertEq(success, false, 'expected bob to not dissociate with token while postive balance'); + + (success, ) = _doTransferDirectly(transferFromBobToAlice); + assertEq(success, true, 'expected transfer to succeed'); + + success = _doDissociateDirectly(bob, tokenAddress); + assertEq(success, true, 'expected bob to dissociate'); + + } + // negative cases function test_CannotApproveIfSpenderNotAssociated() public { /// @dev already demonstrated in some of the postive test cases @@ -525,6 +587,75 @@ contract HederaNonFungibleTokenTest is HederaNonFungibleTokenUtils { /// @dev already demonstrated in some of the postive test cases // cannot transfer to recipient if recipient is not associated with HederaNonFungibleToken BOTH directly and viaHtsPrecompile } + + function test_CannotRepeatedlyAssociateAndDissociateDirectly() public { + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + uint256 serialIdU256; + + MintResponse memory mintResponse; + MintParams memory mintParams; + BurnParams memory burnParams; + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + serialIdU256 = uint64(mintResponse.serialId); + + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + TransferParams memory transferParams; + TransferParams memory transferFromBobToAlice; + + transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + transferFromBobToAlice = TransferParams({ + sender: bob, + token: tokenAddress, + from: bob, + to: alice, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, false, 'expected transfer to fail since recipient is not associated with token'); + + success = _doAssociateDirectly(bob, tokenAddress); + assertEq(success, true, 'expected bob to associate with token'); + + success = _doAssociateDirectly(bob, tokenAddress); + assertEq(success, false, 'expected bob to not re-associate with already associated token'); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, true, 'expected transfer to succeed'); + + (success, ) = _doTransferDirectly(transferFromBobToAlice); + assertEq(success, true, 'expected transfer to succeed'); + + success = _doDissociateDirectly(bob, tokenAddress); + assertEq(success, true, 'expected bob to dissociate with token'); + + success = _doDissociateDirectly(bob, tokenAddress); + assertEq(success, false, 'expected bob to not re-dissociate with already unassociated token'); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, false, 'expected transfer to fail since bob is not associated'); + } } // forge test --match-contract HederaNonFungibleTokenTest --match-test test_TransferUsingAllowanceDirectly -vv diff --git a/test/foundry/mocks/hts-precompile/HederaFungibleToken.sol b/test/foundry/mocks/hts-precompile/HederaFungibleToken.sol index 3a5c97881..73ee05e18 100644 --- a/test/foundry/mocks/hts-precompile/HederaFungibleToken.sol +++ b/test/foundry/mocks/hts-precompile/HederaFungibleToken.sol @@ -5,10 +5,11 @@ import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; import '../../../../contracts/hts-precompile/HederaResponseCodes.sol'; import '../../../../contracts/hts-precompile/IHederaTokenService.sol'; +import '../../../../contracts/hts-precompile/IHRC.sol'; import './HtsSystemContractMock.sol'; import '../../../../contracts/libraries/Constants.sol'; -contract HederaFungibleToken is ERC20, Constants { +contract HederaFungibleToken is IHRC, ERC20, Constants { error HtsPrecompileError(int64 responseCode); HtsSystemContractMock internal constant HtsPrecompile = HtsSystemContractMock(HTS_PRECOMPILE); @@ -103,4 +104,20 @@ contract HederaFungibleToken is ERC20, Constants { function decimals() public view override returns (uint8) { return _decimals; } + + // IHRC setters: + + function associate() external returns (uint256 responseCode) { + responseCode = uint64(HtsPrecompile.preAssociate(msg.sender)); + } + + function dissociate() external returns (uint256 responseCode) { + responseCode = uint64(HtsPrecompile.preDissociate(msg.sender)); + } + + // IHRC getters: + + function isAssociated(address evmAddress) external view override returns (bool) { + return HtsPrecompile.isAssociated(evmAddress, address(this)); + } } diff --git a/test/foundry/mocks/hts-precompile/HederaNonFungibleToken.sol b/test/foundry/mocks/hts-precompile/HederaNonFungibleToken.sol index 331983616..1bb2628d1 100644 --- a/test/foundry/mocks/hts-precompile/HederaNonFungibleToken.sol +++ b/test/foundry/mocks/hts-precompile/HederaNonFungibleToken.sol @@ -5,10 +5,11 @@ import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; import '../../../../contracts/hts-precompile/HederaResponseCodes.sol'; import '../../../../contracts/hts-precompile/IHederaTokenService.sol'; +import '../../../../contracts/hts-precompile/IHRC.sol'; import './HtsSystemContractMock.sol'; import '../../../../contracts/libraries/Constants.sol'; -contract HederaNonFungibleToken is ERC721, Constants { +contract HederaNonFungibleToken is IHRC, ERC721, Constants { error HtsPrecompileError(int64 responseCode); HtsSystemContractMock internal constant HtsPrecompile = HtsSystemContractMock(HTS_PRECOMPILE); @@ -164,4 +165,20 @@ contract HederaNonFungibleToken is ERC721, Constants { burned = nftCount.burned; } + // IHRC setters: + + function associate() external returns (uint256 responseCode) { + responseCode = uint64(HtsPrecompile.preAssociate(msg.sender)); + } + + function dissociate() external returns (uint256 responseCode) { + responseCode = uint64(HtsPrecompile.preDissociate(msg.sender)); + } + + // IHRC getters: + + function isAssociated(address evmAddress) external view override returns (bool) { + return HtsPrecompile.isAssociated(evmAddress, address(this)); + } + } diff --git a/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol b/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol index 252e0c7ea..f325289af 100644 --- a/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol +++ b/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol @@ -645,6 +645,7 @@ contract HtsSystemContractMock is NoDelegateCall, KeyHelper, IHtsPrecompileMock success = true; (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); (success, responseCode) = success ? HederaTokenValidation._validateTokenAssociation(token, account, _association) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateTokenDissociation(token, account, _association, _isFungible, _isNonFungible) : (success, responseCode); } /// @dev doesPassKyc if KYC is not enabled or if enabled then account is KYCed explicitly or by default @@ -709,6 +710,20 @@ contract HtsSystemContractMock is NoDelegateCall, KeyHelper, IHtsPrecompileMock } } + function _postAssociate( + address token, + address sender + ) internal { + _association[token][sender] = true; + } + + function _postDissociate( + address token, + address sender + ) internal { + _association[token][sender] = false; + } + function _postApprove( address token, address sender, @@ -755,6 +770,28 @@ contract HtsSystemContractMock is NoDelegateCall, KeyHelper, IHtsPrecompileMock } } + function preAssociate( + address sender // msg.sender in the context of the Hedera{Non|}FungibleToken; it should be owner for SUCCESS + ) external onlyHederaToken returns (int64 responseCode) { + address token = msg.sender; + bool success; + (success, responseCode) = _precheckAssociateToken(sender, token); + if (success) { + _postAssociate(token, sender); + } + } + + function preDissociate( + address sender // msg.sender in the context of the Hedera{Non|}FungibleToken; it should be owner for SUCCESS + ) external onlyHederaToken returns (int64 responseCode) { + address token = msg.sender; + bool success; + (success, responseCode) = _precheckDissociateToken(sender, token); + if (success) { + _postDissociate(token, sender); + } + } + function preApprove( address sender, // msg.sender in the context of the Hedera{Non|}FungibleToken; it should be owner for SUCCESS address spender, diff --git a/test/foundry/mocks/libraries/HederaTokenValidation.sol b/test/foundry/mocks/libraries/HederaTokenValidation.sol index 2cd53e5e7..97dd28603 100644 --- a/test/foundry/mocks/libraries/HederaTokenValidation.sol +++ b/test/foundry/mocks/libraries/HederaTokenValidation.sol @@ -207,6 +207,44 @@ library HederaTokenValidation { responseCode = HederaResponseCodes.SUCCESS; } + function _validateEmptyFungibleBalance( + address token, + address owner, + mapping(address => bool) storage _isFungible + ) internal view returns (bool success, int64 responseCode) { + if (_isFungible[token]) { + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(token); + + bool emptyBalance = hederaFungibleToken.balanceOf(owner) == 0; + + if (!emptyBalance) { + return (false, HederaResponseCodes.TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES); + } + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateEmptyNonFungibleBalance( + address token, + address owner, + mapping(address => bool) storage _isNonFungible + ) internal view returns (bool success, int64 responseCode) { + if (_isNonFungible[token]) { + HederaNonFungibleToken hederaNonFungibleToken = HederaNonFungibleToken(token); + + bool emptyBalance = hederaNonFungibleToken.balanceOf(owner) == 0; + + if (!emptyBalance) { + return (false, HederaResponseCodes.TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES); + } + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + function _validateTokenSufficiency( address token, address owner, @@ -331,4 +369,24 @@ library HederaTokenValidation { success = true; responseCode = HederaResponseCodes.SUCCESS; } + + function _validateTokenDissociation( + address token, + address account, + mapping(address => mapping(address => bool)) storage _association, + mapping(address => bool) storage _isFungible, + mapping(address => bool) storage _isNonFungible + ) internal view returns (bool success, int64 responseCode) { + + if (_isFungible[token]) { + return _validateEmptyFungibleBalance(token, account, _isFungible); + } + + if (_isNonFungible[token]) { + return _validateEmptyNonFungibleBalance(token, account, _isNonFungible); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } } diff --git a/test/foundry/utils/HederaTokenUtils.sol b/test/foundry/utils/HederaTokenUtils.sol index 31d26e2cb..4b97e6667 100644 --- a/test/foundry/utils/HederaTokenUtils.sol +++ b/test/foundry/utils/HederaTokenUtils.sol @@ -5,6 +5,7 @@ import 'forge-std/Test.sol'; import '../mocks/hts-precompile/HtsSystemContractMock.sol'; import '../../../contracts/hts-precompile/IHederaTokenService.sol'; +import '../../../contracts/hts-precompile/IHRC.sol'; import './CommonUtils.sol'; /// for testing actions common to both HTS token types i.e FUNGIBLE and NON_FUNGIBLE @@ -53,6 +54,67 @@ abstract contract HederaTokenUtils is Test, CommonUtils, Constants { assertEq(isFinallyAssociated, true, 'expected account to always be finally associated'); } + function _doAssociateDirectly( + address sender, + address token + ) internal setPranker(sender) returns (bool success) { + + IHRCCommon htsToken = IHRCCommon(token); + + bool isInitiallyAssociated = htsToken.isAssociated(sender); + int64 responseCode = int64(uint64(htsToken.associate())); + success = responseCode == HederaResponseCodes.SUCCESS; + + int64 expectedResponseCode; + + if (isInitiallyAssociated) { + expectedResponseCode = HederaResponseCodes.TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT; + } + + if (!isInitiallyAssociated) { + expectedResponseCode = HederaResponseCodes.SUCCESS; + } + + bool isFinallyAssociated = htsToken.isAssociated(sender); + + assertEq(responseCode, expectedResponseCode, 'expected response code does not match actual response code'); + assertEq(isFinallyAssociated, true, 'expected account to always be finally associated'); + } + + function _doDissociateDirectly( + address sender, + address token + ) internal setPranker(sender) returns (bool success) { + + IHRCCommon htsToken = IHRCCommon(token); + + bool isInitiallyAssociated = htsToken.isAssociated(sender); + bool hasPositiveBalance = htsToken.balanceOf(sender) > 0; + int64 responseCode = int64(uint64(htsToken.dissociate())); + success = responseCode == HederaResponseCodes.SUCCESS; + + int64 expectedResponseCode; + + bool isFinallyAssociated = htsToken.isAssociated(sender); + + if (hasPositiveBalance) { + expectedResponseCode = HederaResponseCodes.TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES; + assertEq(isFinallyAssociated, true, 'expected account to be remain associated'); + } else { + if (isInitiallyAssociated) { + expectedResponseCode = HederaResponseCodes.SUCCESS; + assertEq(isFinallyAssociated, false, 'expected account to be finally dissociated'); + } + + if (!isInitiallyAssociated) { + expectedResponseCode = HederaResponseCodes.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; + assertEq(isFinallyAssociated, false, 'expected account to be remain unassociated'); + } + } + + assertEq(responseCode, expectedResponseCode, 'expected response code does not match actual response code'); + } + struct MintKeys { address supplyKey; address treasury;