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

feat: [Foundry] Fetch HIP-719 remote state for fungible tokens #143

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
15 changes: 15 additions & 0 deletions scripts/curl
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/HtsSystemContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved
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) }
Expand All @@ -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;
Expand Down
30 changes: 30 additions & 0 deletions src/HtsSystemContractJson.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ contract HtsSystemContractJson is HtsSystemContract {
MirrorNode private _mirrorNode;

bool private initialized;
mapping (address => bool) private relationshipsInitialized;

function setMirrorNodeProvider(MirrorNode mirrorNode_) htsCall external {
_mirrorNode = mirrorNode_;
Expand Down Expand Up @@ -81,6 +82,35 @@ 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\"}]}}";
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved
if (keccak256(bytes(json)) == keccak256(bytes(notFoundError))) {
storeBool(address(this), uint256(slot), false);
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved
} 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);
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved
}

// 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);

Expand Down
10 changes: 10 additions & 0 deletions src/IMirrorNodeResponses.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
2 changes: 2 additions & 0 deletions src/MirrorNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions src/MirrorNodeFFI.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
15 changes: 15 additions & 0 deletions test/IHRC719.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
6 changes: 6 additions & 0 deletions test/lib/MirrorNodeMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading