From 449aae936cc616e638d7e6be7025b50af8f9d950 Mon Sep 17 00:00:00 2001 From: Victor Yanev Date: Wed, 27 Nov 2024 17:03:16 +0200 Subject: [PATCH 1/5] feat: [Foundry] Fetch HIP-719 remote state for fungible tokens Signed-off-by: Victor Yanev --- ...12a87022a4706fc9452c50abd2703ac4fd7d9.json | 16 +++++++++++ ...0000000000000000000000000000000001438.json | 16 +++++++++++ scripts/curl | 15 +++++++++++ src/HtsSystemContract.sol | 6 +++++ src/HtsSystemContractJson.sol | 27 +++++++++++++++++++ src/IMirrorNodeResponses.sol | 10 +++++++ src/MirrorNode.sol | 2 ++ src/MirrorNodeFFI.sol | 9 +++++++ test/IHRC719.t.sol | 15 +++++++++++ test/lib/MirrorNodeMock.sol | 6 +++++ 10 files changed, 122 insertions(+) create mode 100644 @hts-forking/test/data/MFCT/getTokenRelationship_0xa3612a87022a4706fc9452c50abd2703ac4fd7d9.json create mode 100644 @hts-forking/test/data/USDC/getTokenRelationship_0x0000000000000000000000000000000000001438.json diff --git a/@hts-forking/test/data/MFCT/getTokenRelationship_0xa3612a87022a4706fc9452c50abd2703ac4fd7d9.json b/@hts-forking/test/data/MFCT/getTokenRelationship_0xa3612a87022a4706fc9452c50abd2703ac4fd7d9.json new file mode 100644 index 00000000..cb103857 --- /dev/null +++ b/@hts-forking/test/data/MFCT/getTokenRelationship_0xa3612a87022a4706fc9452c50abd2703ac4fd7d9.json @@ -0,0 +1,16 @@ +{ + "tokens": [ + { + "automatic_association": false, + "balance": 5000, + "created_timestamp": "1724382479.562855337", + "decimals": 0, + "token_id": "0.0.4730999", + "freeze_status": "NOT_APPLICABLE", + "kyc_status": "NOT_APPLICABLE" + } + ], + "links": { + "next": null + } +} diff --git a/@hts-forking/test/data/USDC/getTokenRelationship_0x0000000000000000000000000000000000001438.json b/@hts-forking/test/data/USDC/getTokenRelationship_0x0000000000000000000000000000000000001438.json new file mode 100644 index 00000000..2c42f7ba --- /dev/null +++ b/@hts-forking/test/data/USDC/getTokenRelationship_0x0000000000000000000000000000000000001438.json @@ -0,0 +1,16 @@ +{ + "tokens": [ + { + "automatic_association": false, + "balance": 5000000, + "created_timestamp": "1706825561.156945003", + "decimals": 6, + "token_id": "0.0.429274", + "freeze_status": "UNFROZEN", + "kyc_status": "NOT_APPLICABLE" + } + ], + "links": { + "next": null + } +} diff --git a/scripts/curl b/scripts/curl index b3fd504c..043e5bba 100755 --- a/scripts/curl +++ b/scripts/curl @@ -84,6 +84,21 @@ for (const [re, fn] of /** @type {const} */([ return undefined; } }], + [/^accounts\/((0x[0-9a-fA-F]{40})|(0\.0\.\d+))\/tokens/, idOrAliasOrEvmAddress => { + assert(typeof idOrAliasOrEvmAddress === 'string'); + const tokenId = searchParams.get('token.id'); + assert(tokenId !== null); + + const token = tokens[tokenId]; + if (token === undefined) + return { tokens: [], links: { next: null } }; + + try { + return require(`../@hts-forking/test/data/${token.symbol}/getTokenRelationship_${idOrAliasOrEvmAddress.toLowerCase()}.json`); + } catch { + return undefined; + } + }], ])) { const match = endpoint.match(re); if (match !== null) { diff --git a/src/HtsSystemContract.sol b/src/HtsSystemContract.sol index 7fff6079..9217def4 100644 --- a/src/HtsSystemContract.sol +++ b/src/HtsSystemContract.sol @@ -213,14 +213,17 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events { emit Approval(owner, spender, amount); return abi.encode(true); } else if (selector == IHRC719.associate.selector) { + _initTokenRelationships(msg.sender); bytes32 slot = _isAssociatedSlot(msg.sender); assembly { sstore(slot, true) } return abi.encode(true); } else if (selector == IHRC719.dissociate.selector) { + _initTokenRelationships(msg.sender); bytes32 slot = _isAssociatedSlot(msg.sender); assembly { sstore(slot, false) } return abi.encode(true); } else if (selector == IHRC719.isAssociated.selector) { + _initTokenRelationships(msg.sender); bytes32 slot = _isAssociatedSlot(msg.sender); bool res; assembly { res := sload(slot) } @@ -246,6 +249,9 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events { function _initTokenData() internal virtual { } + function _initTokenRelationships(address account) internal virtual { + } + function _balanceOfSlot(address account) internal virtual returns (bytes32) { bytes4 selector = IERC20.balanceOf.selector; uint192 pad = 0x0; diff --git a/src/HtsSystemContractJson.sol b/src/HtsSystemContractJson.sol index 9de6835a..64c60b0d 100644 --- a/src/HtsSystemContractJson.sol +++ b/src/HtsSystemContractJson.sol @@ -14,6 +14,7 @@ contract HtsSystemContractJson is HtsSystemContract { MirrorNode private _mirrorNode; bool private initialized; + bool private relationshipsInitialized; function setMirrorNodeProvider(MirrorNode mirrorNode_) htsCall external { _mirrorNode = mirrorNode_; @@ -81,6 +82,32 @@ contract HtsSystemContractJson is HtsSystemContract { _initTokenInfo(json); } + function _initTokenRelationships(address account) internal override { + bytes32 slot; + assembly { slot := relationshipsInitialized.slot } + if (vm.load(address(this), slot) == bytes32(uint256(1))) { + // Already initialized + return; + } + + slot = _isAssociatedSlot(account); + try mirrorNode().fetchTokenRelationshipOfAccount(vm.toString(account), address(this)) returns (string memory json) { + string memory notFoundError = "{\"_status\":{\"messages\":[{\"message\":\"Not found\"}]}}"; + if (keccak256(bytes(json)) == keccak256(bytes(notFoundError))) { + storeBool(address(this), uint256(slot), false); + } else { + bytes memory tokens = vm.parseJson(json, ".tokens"); + IMirrorNodeResponses.TokenRelationship[] memory relationships = abi.decode(tokens, (IMirrorNodeResponses.TokenRelationship[])); + storeBool(address(this), uint256(slot), relationships.length > 0); + } + } catch { + storeBool(address(this), uint256(slot), false); + } + + assembly { slot := relationshipsInitialized.slot } + storeBool(address(this), uint256(slot), true); + } + function _initTokenInfo(string memory json) internal { TokenInfo memory tokenInfo = _getTokenInfo(json); diff --git a/src/IMirrorNodeResponses.sol b/src/IMirrorNodeResponses.sol index e4ffa1e1..281e26f3 100644 --- a/src/IMirrorNodeResponses.sol +++ b/src/IMirrorNodeResponses.sol @@ -39,4 +39,14 @@ interface IMirrorNodeResponses { int64 amount; string denominating_token_id; } + + struct TokenRelationship { + bool automatic_association; + uint256 balance; + string created_timestamp; + uint256 decimals; + string token_id; + string freeze_status; + string kyc_status; + } } diff --git a/src/MirrorNode.sol b/src/MirrorNode.sol index 78fc8f2a..1d71f03d 100644 --- a/src/MirrorNode.sol +++ b/src/MirrorNode.sol @@ -25,6 +25,8 @@ abstract contract MirrorNode { function fetchAccount(string memory account) external virtual returns (string memory json); + function fetchTokenRelationshipOfAccount(string memory account, address token) external virtual returns (string memory json); + function getBalance(address token, address account) external returns (uint256) { uint32 accountNum = _getAccountNum(account); if (accountNum == 0) return 0; diff --git a/src/MirrorNodeFFI.sol b/src/MirrorNodeFFI.sol index bf0b698b..34de3081 100644 --- a/src/MirrorNodeFFI.sol +++ b/src/MirrorNodeFFI.sol @@ -60,6 +60,15 @@ contract MirrorNodeFFI is MirrorNode { )); } + function fetchTokenRelationshipOfAccount(string memory idOrAliasOrEvmAddress, address token) external override returns (string memory) { + return _get(string.concat( + "accounts/", + idOrAliasOrEvmAddress, + "/tokens?token.id=0.0.", + vm.toString(uint160(token)) + )); + } + /** * @dev Returns the block information by given number. * diff --git a/test/IHRC719.t.sol b/test/IHRC719.t.sol index d415d8fc..4040f325 100644 --- a/test/IHRC719.t.sol +++ b/test/IHRC719.t.sol @@ -65,4 +65,19 @@ contract IHRC719TokenAssociationTest is Test, TestSetup { vm.stopPrank(); } + + function test_IHRC719_with_real_accounts() external { + vm.startPrank(USDC_TREASURY); + assertEq(IHRC719(USDC).isAssociated(), true); + assertEq(IHRC719(USDC).dissociate(), 1); + assertEq(IHRC719(USDC).isAssociated(), false); + + + vm.startPrank(MFCT_TREASURY); + assertEq(IHRC719(MFCT).isAssociated(), true); + assertEq(IHRC719(MFCT).dissociate(), 1); + assertEq(IHRC719(MFCT).isAssociated(), false); + + vm.stopPrank(); + } } diff --git a/test/lib/MirrorNodeMock.sol b/test/lib/MirrorNodeMock.sol index 71146f44..15fd2d4b 100644 --- a/test/lib/MirrorNodeMock.sol +++ b/test/lib/MirrorNodeMock.sol @@ -43,4 +43,10 @@ contract MirrorNodeMock is MirrorNode { string memory path = string.concat("./@hts-forking/test/data/getAccount_", vm.toLowercase(account), ".json"); return vm.readFile(path); } + + function fetchTokenRelationshipOfAccount(string memory idOrAliasOrEvmAddress, address token) external override view returns (string memory) { + string memory symbol = _symbolOf[token]; + string memory path = string.concat("./@hts-forking/test/data/", symbol, "/getTokenRelationship_", vm.toLowercase(idOrAliasOrEvmAddress), ".json"); + return vm.readFile(path); + } } From 50f0ddfbb02764aa8b3f8b60137c9c9309ecdf72 Mon Sep 17 00:00:00 2001 From: Victor Yanev Date: Wed, 27 Nov 2024 17:11:32 +0200 Subject: [PATCH 2/5] feat: [Foundry] Fetch HIP-719 remote state for fungible tokens Signed-off-by: Victor Yanev --- src/HtsSystemContractJson.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/HtsSystemContractJson.sol b/src/HtsSystemContractJson.sol index 64c60b0d..0f84afd7 100644 --- a/src/HtsSystemContractJson.sol +++ b/src/HtsSystemContractJson.sol @@ -14,7 +14,7 @@ contract HtsSystemContractJson is HtsSystemContract { MirrorNode private _mirrorNode; bool private initialized; - bool private relationshipsInitialized; + mapping (address => bool) private relationshipsInitialized; function setMirrorNodeProvider(MirrorNode mirrorNode_) htsCall external { _mirrorNode = mirrorNode_; @@ -83,14 +83,12 @@ contract HtsSystemContractJson is HtsSystemContract { } function _initTokenRelationships(address account) internal override { - bytes32 slot; - assembly { slot := relationshipsInitialized.slot } - if (vm.load(address(this), slot) == bytes32(uint256(1))) { + if (relationshipsInitialized[account]) { // Already initialized return; } - slot = _isAssociatedSlot(account); + bytes32 slot = _isAssociatedSlot(account); try mirrorNode().fetchTokenRelationshipOfAccount(vm.toString(account), address(this)) returns (string memory json) { string memory notFoundError = "{\"_status\":{\"messages\":[{\"message\":\"Not found\"}]}}"; if (keccak256(bytes(json)) == keccak256(bytes(notFoundError))) { @@ -104,8 +102,7 @@ contract HtsSystemContractJson is HtsSystemContract { storeBool(address(this), uint256(slot), false); } - assembly { slot := relationshipsInitialized.slot } - storeBool(address(this), uint256(slot), true); + relationshipsInitialized[account] = true; } function _initTokenInfo(string memory json) internal { From 8fc8a47521025370d4a18a40524001445a859276 Mon Sep 17 00:00:00 2001 From: Victor Yanev Date: Wed, 27 Nov 2024 18:06:00 +0200 Subject: [PATCH 3/5] fix: `EvmError: StateChangeDuringStaticCall` error Signed-off-by: Victor Yanev --- src/HtsSystemContractJson.sol | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/HtsSystemContractJson.sol b/src/HtsSystemContractJson.sol index 0f84afd7..01e3e38c 100644 --- a/src/HtsSystemContractJson.sol +++ b/src/HtsSystemContractJson.sol @@ -102,7 +102,13 @@ contract HtsSystemContractJson is HtsSystemContract { storeBool(address(this), uint256(slot), false); } - relationshipsInitialized[account] = true; + // The value corresponding to a mapping key k is located at keccak256(h(k).p), + // where . is concatenation, p is the base slot and h is a function applied to the key depending on its type: + // - for value types, h pads the value to 32 bytes in the same way as when storing the value in memory. + // - for strings and byte arrays, h(k) is just the non-padded data. + assembly { slot := relationshipsInitialized.slot } + slot = keccak256(abi.encodePacked(bytes32(uint256(uint160(account))), slot)); + storeBool(address(this), uint256(slot), true); } function _initTokenInfo(string memory json) internal { From 308c506b20f63c4b3b29dd67006606b8a56facaf Mon Sep 17 00:00:00 2001 From: Victor Yanev Date: Thu, 28 Nov 2024 12:03:15 +0200 Subject: [PATCH 4/5] chore: address comments Signed-off-by: Victor Yanev --- src/HtsSystemContract.sol | 6 ------ src/HtsSystemContractJson.sol | 39 ++++++++--------------------------- src/MirrorNode.sol | 12 +++++++++++ test/IHRC719.t.sol | 4 ++++ 4 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/HtsSystemContract.sol b/src/HtsSystemContract.sol index 9217def4..7fff6079 100644 --- a/src/HtsSystemContract.sol +++ b/src/HtsSystemContract.sol @@ -213,17 +213,14 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events { emit Approval(owner, spender, amount); return abi.encode(true); } else if (selector == IHRC719.associate.selector) { - _initTokenRelationships(msg.sender); bytes32 slot = _isAssociatedSlot(msg.sender); assembly { sstore(slot, true) } return abi.encode(true); } else if (selector == IHRC719.dissociate.selector) { - _initTokenRelationships(msg.sender); bytes32 slot = _isAssociatedSlot(msg.sender); assembly { sstore(slot, false) } return abi.encode(true); } else if (selector == IHRC719.isAssociated.selector) { - _initTokenRelationships(msg.sender); bytes32 slot = _isAssociatedSlot(msg.sender); bool res; assembly { res := sload(slot) } @@ -249,9 +246,6 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events { function _initTokenData() internal virtual { } - function _initTokenRelationships(address account) internal virtual { - } - function _balanceOfSlot(address account) internal virtual returns (bytes32) { bytes4 selector = IERC20.balanceOf.selector; uint192 pad = 0x0; diff --git a/src/HtsSystemContractJson.sol b/src/HtsSystemContractJson.sol index 01e3e38c..b509502e 100644 --- a/src/HtsSystemContractJson.sol +++ b/src/HtsSystemContractJson.sol @@ -14,7 +14,6 @@ contract HtsSystemContractJson is HtsSystemContract { MirrorNode private _mirrorNode; bool private initialized; - mapping (address => bool) private relationshipsInitialized; function setMirrorNodeProvider(MirrorNode mirrorNode_) htsCall external { _mirrorNode = mirrorNode_; @@ -82,35 +81,6 @@ contract HtsSystemContractJson is HtsSystemContract { _initTokenInfo(json); } - function _initTokenRelationships(address account) internal override { - if (relationshipsInitialized[account]) { - // Already initialized - return; - } - - bytes32 slot = _isAssociatedSlot(account); - try mirrorNode().fetchTokenRelationshipOfAccount(vm.toString(account), address(this)) returns (string memory json) { - string memory notFoundError = "{\"_status\":{\"messages\":[{\"message\":\"Not found\"}]}}"; - if (keccak256(bytes(json)) == keccak256(bytes(notFoundError))) { - storeBool(address(this), uint256(slot), false); - } else { - bytes memory tokens = vm.parseJson(json, ".tokens"); - IMirrorNodeResponses.TokenRelationship[] memory relationships = abi.decode(tokens, (IMirrorNodeResponses.TokenRelationship[])); - storeBool(address(this), uint256(slot), relationships.length > 0); - } - } catch { - storeBool(address(this), uint256(slot), false); - } - - // The value corresponding to a mapping key k is located at keccak256(h(k).p), - // where . is concatenation, p is the base slot and h is a function applied to the key depending on its type: - // - for value types, h pads the value to 32 bytes in the same way as when storing the value in memory. - // - for strings and byte arrays, h(k) is just the non-padded data. - assembly { slot := relationshipsInitialized.slot } - slot = keccak256(abi.encodePacked(bytes32(uint256(uint160(account))), slot)); - storeBool(address(this), uint256(slot), true); - } - function _initTokenInfo(string memory json) internal { TokenInfo memory tokenInfo = _getTokenInfo(json); @@ -453,6 +423,15 @@ contract HtsSystemContractJson is HtsSystemContract { return slot; } + function _isAssociatedSlot(address account) internal override returns (bytes32) { + bytes32 slot = super._isAssociatedSlot(account); + if (vm.load(_scratchAddr(), slot) == bytes32(0)) { + bool associated = mirrorNode().isAssociated(address(this), account); + _setValue(slot, bytes32(uint256(associated ? 1 : 0))); + } + return slot; + } + function _setValue(bytes32 slot, bytes32 value) private { vm.store(address(this), slot, value); vm.store(_scratchAddr(), slot, bytes32(uint(1))); diff --git a/src/MirrorNode.sol b/src/MirrorNode.sol index 1d71f03d..2fbbf49a 100644 --- a/src/MirrorNode.sol +++ b/src/MirrorNode.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +import "./IMirrorNodeResponses.sol"; import {Vm} from "forge-std/Vm.sol"; abstract contract MirrorNode { @@ -53,6 +54,17 @@ abstract contract MirrorNode { return 0; } + function isAssociated(address token, address account) external returns (bool) { + try this.fetchTokenRelationshipOfAccount(vm.toString(account), token) returns (string memory json) { + if (vm.keyExistsJson(json, ".tokens")) { + bytes memory tokens = vm.parseJson(json, ".tokens"); + IMirrorNodeResponses.TokenRelationship[] memory relationships = abi.decode(tokens, (IMirrorNodeResponses.TokenRelationship[])); + return relationships.length > 0; + } + } catch {} + return false; + } + function getAccountAddress(string memory accountId) external returns (address) { if (bytes(accountId).length == 0 || keccak256(bytes(accountId)) == keccak256(bytes("null")) diff --git a/test/IHRC719.t.sol b/test/IHRC719.t.sol index 4040f325..59b1fc0b 100644 --- a/test/IHRC719.t.sol +++ b/test/IHRC719.t.sol @@ -67,6 +67,10 @@ contract IHRC719TokenAssociationTest is Test, TestSetup { } function test_IHRC719_with_real_accounts() external { + if (testMode() == TestMode.JSON_RPC) { + // TODO: Enable this test with https://github.com/hashgraph/hedera-forking/issues/126 + return; + } vm.startPrank(USDC_TREASURY); assertEq(IHRC719(USDC).isAssociated(), true); assertEq(IHRC719(USDC).dissociate(), 1); From f7cc1375a6fdd4a4dddb84ecfe52ffa65e043c39 Mon Sep 17 00:00:00 2001 From: Victor Yanev Date: Tue, 3 Dec 2024 14:52:13 +0200 Subject: [PATCH 5/5] fix: build error in IHRC719.t.sol Signed-off-by: Victor Yanev --- test/IHRC719.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/IHRC719.t.sol b/test/IHRC719.t.sol index 59b1fc0b..33cf8cce 100644 --- a/test/IHRC719.t.sol +++ b/test/IHRC719.t.sol @@ -67,7 +67,7 @@ contract IHRC719TokenAssociationTest is Test, TestSetup { } function test_IHRC719_with_real_accounts() external { - if (testMode() == TestMode.JSON_RPC) { + if (TestMode.JSON_RPC == testMode) { // TODO: Enable this test with https://github.com/hashgraph/hedera-forking/issues/126 return; }