From 386d06039a1751afa90293f7a256ac43eec09d75 Mon Sep 17 00:00:00 2001 From: Mariusz Jasuwienas Date: Mon, 3 Mar 2025 16:58:08 +0100 Subject: [PATCH] feat: token freeze kyc and pause implemented (#197) Signed-off-by: Mariusz Jasuwienas --- contracts/HederaResponseCodes.sol | 15 +++ contracts/HtsSystemContract.sol | 191 +++++++++++++++++++++++++++- contracts/HtsSystemContractJson.sol | 19 +++ contracts/IHederaTokenService.sol | 40 +++--- contracts/MirrorNode.sol | 26 ++++ src/index.d.ts | 2 + src/index.js | 35 +++++ test/Freeze.t.sol | 63 +++++++++ test/HTS.t.sol | 4 +- test/KYC.t.sol | 76 +++++++++++ test/Pause.t.sol | 57 +++++++++ 11 files changed, 501 insertions(+), 27 deletions(-) create mode 100644 test/Freeze.t.sol create mode 100644 test/KYC.t.sol create mode 100644 test/Pause.t.sol diff --git a/contracts/HederaResponseCodes.sol b/contracts/HederaResponseCodes.sol index a46b065e..e0fc0ff7 100644 --- a/contracts/HederaResponseCodes.sol +++ b/contracts/HederaResponseCodes.sol @@ -2,6 +2,21 @@ pragma solidity ^0.8.0; library HederaResponseCodes { + int32 internal constant NOT_SUPPORTED = 13; int32 internal constant SUCCESS = 22; // The transaction succeeded int32 internal constant TOKEN_HAS_NO_SUPPLY_KEY = 180; + int32 internal constant INSUFFICIENT_ACCOUNT_BALANCE = 28; + int32 internal constant INVALID_ACCOUNT_AMOUNTS = 48; + int32 internal constant ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS = 74; + int32 internal constant ACCOUNT_FROZEN_FOR_TOKEN = 165; + int32 internal constant INVALID_TOKEN_ID = 167; + int32 internal constant TOKEN_HAS_NO_FREEZE_KEY = 172; + int32 internal constant ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN = 176; + int32 internal constant TOKEN_HAS_NO_KYC_KEY = 177; + int32 internal constant INSUFFICIENT_TOKEN_BALANCE = 178; + int32 internal constant SENDER_DOES_NOT_OWN_NFT_SERIAL_NO = 237; + int32 internal constant TOKEN_IS_PAUSED = 265; + int32 internal constant TOKEN_HAS_NO_PAUSE_KEY = 266; + int32 internal constant SPENDER_DOES_NOT_HAVE_ALLOWANCE = 292; + int32 internal constant MAX_ALLOWANCES_EXCEEDED = 294; } diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index fd03ecd4..4e8a0585 100644 --- a/contracts/HtsSystemContract.sol +++ b/contracts/HtsSystemContract.sol @@ -125,7 +125,6 @@ contract HtsSystemContract is IHederaTokenService { "associateTokens: Must be signed by the provided Account's key or called from the accounts contract key" ); for (uint256 i = 0; i < tokens.length; i++) { - require(tokens[i] != address(0), "associateTokens: invalid token"); int64 associationResponseCode = IHederaTokenService(tokens[i]).associateToken(account, tokens[i]); require( associationResponseCode == HederaResponseCodes.SUCCESS, @@ -136,6 +135,8 @@ contract HtsSystemContract is IHederaTokenService { } function associateToken(address account, address token) htsCall external returns (int64 responseCode) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; address[] memory tokens = new address[](1); tokens[0] = token; return associateTokens(account, tokens); @@ -145,7 +146,6 @@ contract HtsSystemContract is IHederaTokenService { require(tokens.length > 0, "dissociateTokens: missing tokens"); require(account == msg.sender, "dissociateTokens: Must be signed by the provided Account's key or called from the accounts contract key"); for (uint256 i = 0; i < tokens.length; i++) { - require(tokens[i] != address(0), "dissociateTokens: invalid token"); int64 dissociationResponseCode = IHederaTokenService(tokens[i]).dissociateToken(account, tokens[i]); require(dissociationResponseCode == HederaResponseCodes.SUCCESS, "dissociateTokens: Failed to dissociate token"); } @@ -153,6 +153,8 @@ contract HtsSystemContract is IHederaTokenService { } function dissociateToken(address account, address token) htsCall external returns (int64 responseCode) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; address[] memory tokens = new address[](1); tokens[0] = token; return dissociateTokens(account, tokens); @@ -263,7 +265,8 @@ contract HtsSystemContract is IHederaTokenService { address[] memory accountId, int64[] memory amount ) htsCall external returns (int64 responseCode) { - require(token != address(0), "transferTokens: invalid token"); + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; require(accountId.length > 0, "transferTokens: missing recipients"); require(amount.length == accountId.length, "transferTokens: inconsistent input"); for (uint256 i = 0; i < accountId.length; i++) { @@ -279,6 +282,8 @@ contract HtsSystemContract is IHederaTokenService { address[] memory receiver, int64[] memory serialNumber ) htsCall external returns (int64 responseCode) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; require(token != address(0), "transferNFTs: invalid token"); require(sender.length > 0, "transferNFTs: missing recipients"); require(receiver.length == sender.length, "transferNFTs: inconsistent input"); @@ -295,7 +300,8 @@ contract HtsSystemContract is IHederaTokenService { address recipient, int64 amount ) htsCall public returns (int64 responseCode) { - require(token != address(0), "transferToken: invalid token"); + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; address from = sender; address to = recipient; if (amount < 0) { @@ -318,12 +324,17 @@ contract HtsSystemContract is IHederaTokenService { address recipient, int64 serialNumber ) htsCall public returns (int64 responseCode) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; uint256 serialId = uint256(uint64(serialNumber)); HtsSystemContract(token).transferFromNFT(msg.sender, sender, recipient, serialId); responseCode = HederaResponseCodes.SUCCESS; } - function approve(address token, address spender, uint256 amount) htsCall public returns (int64 responseCode) { + function approve(address token, address spender, uint256 amount) htsCall + public returns (int64 responseCode) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; HtsSystemContract(token).approve(msg.sender, spender, amount); responseCode = HederaResponseCodes.SUCCESS; } @@ -334,6 +345,8 @@ contract HtsSystemContract is IHederaTokenService { address recipient, uint256 amount ) htsCall external returns (int64) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; return transferToken(token, sender, recipient, int64(int256(amount))); } @@ -346,6 +359,8 @@ contract HtsSystemContract is IHederaTokenService { address approved, uint256 serialNumber ) htsCall public returns (int64 responseCode) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; HtsSystemContract(token).approveNFT(msg.sender, approved, serialNumber); responseCode = HederaResponseCodes.SUCCESS; } @@ -356,6 +371,8 @@ contract HtsSystemContract is IHederaTokenService { address to, uint256 serialNumber ) htsCall external returns (int64) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; return transferNFT(token, from, to, int64(int256(serialNumber))); } @@ -370,6 +387,8 @@ contract HtsSystemContract is IHederaTokenService { address operator, bool approved ) htsCall external returns (int64 responseCode) { + int64 tokenStatus = _checkToken(token); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; HtsSystemContract(token).setApprovalForAll(msg.sender, operator, approved); responseCode = HederaResponseCodes.SUCCESS; } @@ -383,6 +402,39 @@ contract HtsSystemContract is IHederaTokenService { return (HederaResponseCodes.SUCCESS, IERC721(token).isApprovedForAll(owner, operator)); } + function isKyc(address token, address account) htsCall public view returns (int64, bool) { + return IHederaTokenService(token).isKyc(msg.sender, account); + } + + function isFrozen(address token, address account) htsCall public view returns (int64, bool) { + if (token == address(0)) return (HederaResponseCodes.SUCCESS, false); + return IHederaTokenService(token).isFrozen(token, account); + } + + function _checkToken(address token) private view returns (int64 tokenStatus) { + if (address(0) == token) return HederaResponseCodes.INVALID_TOKEN_ID; + (int64 responseCode, TokenInfo memory info) = getTokenInfo(token); + if (responseCode != HederaResponseCodes.SUCCESS) return HederaResponseCodes.INVALID_TOKEN_ID; + if (KeyLib.keyExists(0x4, info)) { + (, bool frozen) = isFrozen(token, msg.sender); + if (frozen) return HederaResponseCodes.ACCOUNT_FROZEN_FOR_TOKEN; + } + if (KeyLib.keyExists(0x2, info)) { + (, bool hasKyc) = isKyc(token, msg.sender); + if (!hasKyc) return HederaResponseCodes.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN; + } + if (info.pauseStatus) return HederaResponseCodes.TOKEN_IS_PAUSED; + return HederaResponseCodes.SUCCESS; + } + + function _checkTokenKey(address token, uint256 requiredKey, int32 missingKeyCode) private view returns (int32) { + if (token == address(0)) return HederaResponseCodes.INVALID_TOKEN_ID; + (int64 responseCode, TokenInfo memory info) = getTokenInfo(token); + if (responseCode != HederaResponseCodes.SUCCESS) return HederaResponseCodes.INVALID_TOKEN_ID; + if (!KeyLib.keyExists(requiredKey, info)) return missingKeyCode; + return HederaResponseCodes.SUCCESS; + } + function getTokenCustomFees( address token ) htsCall external view returns (int64, FixedFee[] memory, FractionalFee[] memory, RoyaltyFee[] memory) { @@ -457,6 +509,42 @@ contract HtsSystemContract is IHederaTokenService { return (HederaResponseCodes.SUCCESS, success && returnData.length > 0); } + function freezeToken(address token, address account) htsCall external returns (int64 responseCode) { + int32 tokenStatus = _checkTokenKey(token, 0x4, HederaResponseCodes.TOKEN_HAS_NO_FREEZE_KEY); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; + responseCode = IHederaTokenService(token).freezeToken(token, account); + } + + function unfreezeToken(address token, address account) htsCall external returns (int64 responseCode) { + int32 tokenStatus = _checkTokenKey(token, 0x4, HederaResponseCodes.TOKEN_HAS_NO_FREEZE_KEY); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; + responseCode = IHederaTokenService(token).unfreezeToken(token, account); + } + + function grantTokenKyc(address token, address account) htsCall external returns (int64 responseCode) { + int32 tokenStatus = _checkTokenKey(token, 0x2, HederaResponseCodes.TOKEN_HAS_NO_KYC_KEY); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; + responseCode = IHederaTokenService(token).grantTokenKyc(token, account); + } + + function revokeTokenKyc(address token, address account) htsCall external returns (int64 responseCode) { + int32 tokenStatus = _checkTokenKey(token, 0x2, HederaResponseCodes.TOKEN_HAS_NO_KYC_KEY); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; + responseCode = IHederaTokenService(token).revokeTokenKyc(token, account); + } + + function pauseToken(address token) htsCall external returns (int64 responseCode) { + int32 tokenStatus = _checkTokenKey(token, 0x40, HederaResponseCodes.TOKEN_HAS_NO_PAUSE_KEY); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; + responseCode = IHederaTokenService(token).pauseToken(token); + } + + function unpauseToken(address token) htsCall external returns (int64 responseCode) { + int32 tokenStatus = _checkTokenKey(token, 0x40, HederaResponseCodes.TOKEN_HAS_NO_PAUSE_KEY); + if (tokenStatus != HederaResponseCodes.SUCCESS) return tokenStatus; + responseCode = IHederaTokenService(token).unpauseToken(token); + } + function getTokenType(address token) htsCall external view returns (int64, int32) { require(token != address(0), "getTokenType: invalid address"); return IHederaTokenService(token).getTokenType(token); @@ -579,6 +667,11 @@ contract HtsSystemContract is IHederaTokenService { } return abi.encode(HederaResponseCodes.SUCCESS, int32(-1)); } + if (selector == this.isFrozen.selector) { + require(msg.data.length >= 92, "isFrozen: Not enough calldata"); + address account = address(bytes20(msg.data[72:92])); + return abi.encode(HederaResponseCodes.SUCCESS, __isFrozen(account)); + } if (selector == this.transferFrom.selector) { require(msg.data.length >= 156, "transferFrom: Not enough calldata"); address sender = address(bytes20(msg.data[40:60])); @@ -617,6 +710,11 @@ contract HtsSystemContract is IHederaTokenService { _approve(from, to, serialId, true); return abi.encode(true); } + if (selector == this.isKyc.selector) { + require(msg.data.length >= 48, "associateToken: Not enough calldata"); + address account = address(bytes20(msg.data[40:60])); + return abi.encode(HederaResponseCodes.SUCCESS, __hasKycGranted(account)); + } if (selector == this.setApprovalForAll.selector) { require(msg.data.length >= 124, "setApprovalForAll: Not enough calldata"); address from = address(bytes20(msg.data[40:60])); @@ -625,6 +723,38 @@ contract HtsSystemContract is IHederaTokenService { _setApprovalForAll(from, to, approved); return abi.encode(true); } + if (selector == this.freezeToken.selector) { + require(msg.data.length >= 92, "freezeToken: Not enough calldata"); + address account = address(bytes20(msg.data[72:92])); + _updateFreeze(account, true); + return abi.encode(HederaResponseCodes.SUCCESS); + } + if (selector == this.unfreezeToken.selector) { + require(msg.data.length >= 92, "unfreezeToken: Not enough calldata"); + address account = address(bytes20(msg.data[72:92])); + _updateFreeze(account, false); + return abi.encode(HederaResponseCodes.SUCCESS); + } + if (selector == this.grantTokenKyc.selector) { + require(msg.data.length >= 92, "grantTokenKyc: Not enough calldata"); + address account = address(bytes20(msg.data[72:92])); + _updateKyc(account, true); + return abi.encode(HederaResponseCodes.SUCCESS); + } + if (selector == this.revokeTokenKyc.selector) { + require(msg.data.length >= 92, "revokeTokenKyc: Not enough calldata"); + address account = address(bytes20(msg.data[72:92])); + _updateKyc(account, false); + return abi.encode(HederaResponseCodes.SUCCESS); + } + if (selector == this.pauseToken.selector) { + _tokenInfo.pauseStatus = true; + return abi.encode(HederaResponseCodes.SUCCESS); + } + if (selector == this.unpauseToken.selector) { + _tokenInfo.pauseStatus = false; + return abi.encode(HederaResponseCodes.SUCCESS); + } if (selector == this._update.selector) { require(msg.data.length >= 124, "update: Not enough calldata"); address from = address(bytes20(msg.data[40:60])); @@ -634,6 +764,14 @@ contract HtsSystemContract is IHederaTokenService { return abi.encode(true); } } + if (_isTokenInteraction(selector)) { + require(!__isFrozen(msg.sender), "__redirectForToken: frozen"); + require( + !KeyLib.keyExists(0x2, _tokenInfo) || __hasKycGranted(msg.sender), + "__redirectForToken: no kyc granted" + ); + require(!_tokenInfo.pauseStatus, "__redirectForToken: paused"); + } // Redirect to the appropriate ERC20 method if the token type is fungible. if (keccak256(bytes(tokenType)) == keccak256(bytes("FUNGIBLE_COMMON"))) { @@ -648,6 +786,15 @@ contract HtsSystemContract is IHederaTokenService { revert ("redirectForToken: token type not supported"); } + function _isTokenInteraction(bytes4 selector) private pure returns (bool) { + return selector == IERC20.transfer.selector || + selector == IERC20.transferFrom.selector || + selector == IERC20.approve.selector || + selector == IERC721.transferFrom.selector || + selector == IERC721.approve.selector || + selector == IERC721.setApprovalForAll.selector; + } + function _redirectForERC20(bytes4 selector) private returns (bytes memory) { if (selector == IERC20.name.selector) { return abi.encode(_tokenInfo.token.name); @@ -900,6 +1047,20 @@ contract HtsSystemContract is IHederaTokenService { return bytes32(abi.encodePacked(selector, pad, ownerId, operatorId)); } + function _isFrozenSlot(address account) internal virtual returns (bytes32) { + bytes4 selector = IHederaTokenService.isFrozen.selector; + uint192 pad = 0x0; + (uint32 accountId, ) = HtsSystemContract(HTS_ADDRESS).getAccountId(account); + return bytes32(abi.encodePacked(selector, pad, accountId)); + } + + function _hasKycGrantedSlot(address account) internal virtual returns (bytes32) { + bytes4 selector = IHederaTokenService.isKyc.selector; + uint192 pad = 0x0; + (uint32 accountId, ) = HtsSystemContract(HTS_ADDRESS).getAccountId(account); + return bytes32(abi.encodePacked(selector, pad, accountId)); + } + function __balanceOf(address account) private returns (uint256 amount) { bytes32 slot = _balanceOfSlot(account); assembly { amount := sload(slot) } @@ -932,6 +1093,16 @@ contract HtsSystemContract is IHederaTokenService { assembly { approvedForAll := sload(slot) } } + function __isFrozen(address account) private returns (bool frozenStatus) { + bytes32 slot = _isFrozenSlot(account); + assembly { frozenStatus := sload(slot) } + } + + function __hasKycGranted(address account) private returns (bool hasKycGranted) { + bytes32 slot = _hasKycGrantedSlot(account); + assembly { hasKycGranted := sload(slot) } + } + function _transfer(address from, address to, uint256 amount) private { require(from != address(0), "hts: invalid sender"); require(to != address(0), "hts: invalid receiver"); @@ -967,6 +1138,16 @@ contract HtsSystemContract is IHederaTokenService { emit IERC721.Transfer(from, to, serialId); } + function _updateKyc(address account, bool hasKycGranted) public { + bytes32 kycSlot = _hasKycGrantedSlot(account); + assembly { sstore(kycSlot, hasKycGranted) } + } + + function _updateFreeze(address account, bool frozenStatus) public { + bytes32 isFrozenSlot = _isFrozenSlot(account); + assembly { sstore(isFrozenSlot, frozenStatus) } + } + function _update(address from, address to, uint256 amount) public { if (from == address(0)) { _tokenInfo.totalSupply += int64(int256(amount)); diff --git a/contracts/HtsSystemContractJson.sol b/contracts/HtsSystemContractJson.sol index 9168c593..b5bc9f99 100644 --- a/contracts/HtsSystemContractJson.sol +++ b/contracts/HtsSystemContractJson.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import {Vm} from "forge-std/Vm.sol"; import {decode} from './Base64.sol'; import {IHederaTokenService} from "./IHederaTokenService.sol"; +import {HederaResponseCodes} from "./HederaResponseCodes.sol"; import {HtsSystemContract, HTS_ADDRESS} from "./HtsSystemContract.sol"; import {IERC20} from "./IERC20.sol"; import {MirrorNode} from "./MirrorNode.sol"; @@ -477,6 +478,24 @@ contract HtsSystemContractJson is HtsSystemContract { return slot; } + function _isFrozenSlot(address account) internal override returns (bytes32) { + bytes32 slot = super._isFrozenSlot(account); + if (_shouldFetch(slot)) { + string memory freezeStatus = mirrorNode().getFreezeStatus(address(this), account); + _setValue(slot, bytes32(keccak256(bytes(freezeStatus)) == keccak256("FROZEN") ? uint256(1) : uint256(0))); + } + return slot; + } + + function _hasKycGrantedSlot(address account) internal override returns (bytes32) { + bytes32 slot = super._hasKycGrantedSlot(account); + if (_shouldFetch(slot)) { + string memory kycStatus = mirrorNode().getKycStatus(address(this), account); + _setValue(slot, bytes32(keccak256(bytes(kycStatus)) == keccak256("GRANTED") ? uint256(1) : uint256(0))); + } + return slot; + } + function _allowanceSlot(address owner, address spender) internal override returns (bytes32) { bytes32 slot = super._allowanceSlot(owner, spender); if (_shouldFetch(slot)) { diff --git a/contracts/IHederaTokenService.sol b/contracts/IHederaTokenService.sol index 1cc9fb5f..cbdd6f14 100644 --- a/contracts/IHederaTokenService.sol +++ b/contracts/IHederaTokenService.sol @@ -605,18 +605,18 @@ interface IHederaTokenService { /// @param account The account address associated with the token /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return frozen True if `account` is frozen for `token` - // function isFrozen(address token, address account) - // external - // returns (int64 responseCode, bool frozen); + function isFrozen(address token, address account) + external view + returns (int64 responseCode, bool frozen); /// Query if token account has kyc granted /// @param token The token address to check /// @param account The account address associated with the token /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return kycGranted True if `account` has kyc granted for `token` - // function isKyc(address token, address account) - // external - // returns (int64 responseCode, bool kycGranted); + function isKyc(address token, address account) + external view + returns (int64 responseCode, bool kycGranted); /// Operation to delete token /// @param token The token address to be deleted @@ -695,43 +695,43 @@ interface IHederaTokenService { /// @param token The token address /// @param account The account address to be frozen /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function freezeToken(address token, address account) - // external - // returns (int64 responseCode); + function freezeToken(address token, address account) + external + returns (int64 responseCode); /// Operation to unfreeze token account /// @param token The token address /// @param account The account address to be unfrozen /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function unfreezeToken(address token, address account) - // external - // returns (int64 responseCode); + function unfreezeToken(address token, address account) + external + returns (int64 responseCode); /// Operation to grant kyc to token account /// @param token The token address /// @param account The account address to grant kyc /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function grantTokenKyc(address token, address account) - // external - // returns (int64 responseCode); + function grantTokenKyc(address token, address account) + external + returns (int64 responseCode); /// Operation to revoke kyc to token account /// @param token The token address /// @param account The account address to revoke kyc /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function revokeTokenKyc(address token, address account) - // external - // returns (int64 responseCode); + function revokeTokenKyc(address token, address account) + external + returns (int64 responseCode); /// Operation to pause token /// @param token The token address to be paused /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function pauseToken(address token) external returns (int64 responseCode); + function pauseToken(address token) external returns (int64 responseCode); /// Operation to unpause token /// @param token The token address to be unpaused /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function unpauseToken(address token) external returns (int64 responseCode); + function unpauseToken(address token) external returns (int64 responseCode); /// Operation to wipe fungible tokens from account /// @param token The token address diff --git a/contracts/MirrorNode.sol b/contracts/MirrorNode.sol index 8aef3422..706bd562 100644 --- a/contracts/MirrorNode.sol +++ b/contracts/MirrorNode.sol @@ -101,6 +101,32 @@ abstract contract MirrorNode { return false; } + function getFreezeStatus(address token, address account) external returns (string memory) { + 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[])); + if (relationships.length > 0) { + return relationships[0].freeze_status; + } + } + } catch {} + return "NOT_APPLICABLE"; + } + + function getKycStatus(address token, address account) external returns (string memory) { + 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[])); + if (relationships.length > 0) { + return relationships[0].kyc_status; + } + } + } catch {} + return "NOT_APPLICABLE"; + } + function getAccountAddress(string memory accountId) public returns (address) { if (bytes(accountId).length == 0 || keccak256(bytes(accountId)) == keccak256(bytes("null")) diff --git a/src/index.d.ts b/src/index.d.ts index e3855b89..c8e9d937 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -48,6 +48,8 @@ interface IMirrorNodeClient { tokens: { token_id: string; automatic_association: boolean; + kyc_status: 'NOT_APPLICABLE' | 'GRANTED' | 'REVOKED'; + frozen_status: 'NOT_APPLICABLE' | 'FROZEN' | 'UNFROZEN'; }[]; } | null>; diff --git a/src/index.js b/src/index.js index c5f7b497..827a9ee3 100644 --- a/src/index.js +++ b/src/index.js @@ -246,6 +246,41 @@ async function getHtsStorageAt(address, requestedSlot, blockNumber, mirrorNodeCl ); persistentStorage.store(tokenId, blockNumber, nrequestedSlot, atob(metadata)); } + + // Encoded `address(tokenId).isKyc(tokenId, accountId)` slot + // slot(256) = `isKyc`selector(32) + padding(192) + accountId(32) + if ( + nrequestedSlot >> 32n === + 0xf2c31ff4_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000n + ) { + const accountId = `0.0.${parseInt(requestedSlot.slice(-8), 16)}`; + const { tokens } = (await mirrorNodeClient.getTokenRelationship(accountId, tokenId)) ?? { + tokens: [], + }; + const kycGranted = tokens.length > 0 && tokens[0].kyc_status === 'GRANTED'; + return ret( + `0x${toIntHex256(kycGranted ? 1 : 0)}`, + `Token ${tokenId} kyc for ${accountId} is ${kycGranted ? 'granted' : 'not granted'}` + ); + } + + // Encoded `address(tokenId).isFrozen(tokenId, accountId)` slot + // slot(256) = `isFrozen`selector(32) + padding(192) + accountId(32) + if ( + nrequestedSlot >> 32n === + 0x46de0fb1_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000n + ) { + const accountId = `0.0.${parseInt(requestedSlot.slice(-8), 16)}`; + const { tokens } = (await mirrorNodeClient.getTokenRelationship(accountId, tokenId)) ?? { + tokens: [], + }; + const isFrozen = tokens.length > 0 && tokens[0].frozen_status === 'FROZEN'; + return ret( + `0x${toIntHex256(isFrozen ? 1 : 0)}`, + `Token ${tokenId} is ${isFrozen ? 'frozen' : 'not frozen'} for account ${accountId}` + ); + } + let unresolvedValues = persistentStorage.load(tokenId, blockNumber, nrequestedSlot); if (unresolvedValues === undefined) { const token = await mirrorNodeClient.getTokenById(tokenId, blockNumber); diff --git a/test/Freeze.t.sol b/test/Freeze.t.sol new file mode 100644 index 00000000..33f3e28f --- /dev/null +++ b/test/Freeze.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import {TestSetup} from "./lib/TestSetup.sol"; +import {HtsSystemContract, HTS_ADDRESS} from "../contracts/HtsSystemContract.sol"; +import {IHederaTokenService} from "../contracts/IHederaTokenService.sol"; +import {HederaResponseCodes} from "../contracts/HederaResponseCodes.sol"; +import {IERC20} from "../contracts/IERC20.sol"; +import {IERC721} from "../contracts/IERC721.sol"; + +contract FreezeTest is Test, TestSetup { + + address private token; + address private owner; + address private to; + uint256 private amount = 4_000000; + + function setUp() external { + setUpMockStorageForNonFork(); + IHederaTokenService.HederaToken memory hederaToken; + hederaToken.name = "Token name"; + hederaToken.symbol = "Token symbol"; + hederaToken.treasury = makeAddr("Token treasury"); + hederaToken.tokenKeys = new IHederaTokenService.TokenKey[](1); + owner = makeAddr("freeze"); + hederaToken.tokenKeys[0].keyType = 0x4; + hederaToken.tokenKeys[0].key.ed25519 = hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc"; + (, token) = IHederaTokenService(HTS_ADDRESS).createFungibleToken{value: 1000}(hederaToken, 1000000, 4); + vm.assertNotEq(token, address(0)); + to = makeAddr("bob"); + } + + function test_HTS_transferToken_success_when_not_frozen() public { + address from = makeAddr("from"); + deal(token, from, amount); + vm.prank(from); + IHederaTokenService(HTS_ADDRESS).transferToken(token, from, to, int64(int256(amount))); + assertEq(IERC20(token).balanceOf(to), amount); + } + + function test_HTS_freezing_success_with_correct_key() public { + address from = makeAddr("bob"); + vm.startPrank(owner); + int64 freezeCode = IHederaTokenService(HTS_ADDRESS).freezeToken(token, from); + assertEq(freezeCode, HederaResponseCodes.SUCCESS); + int64 unfreezeCode = IHederaTokenService(HTS_ADDRESS).unfreezeToken(token, from); + assertEq(unfreezeCode, HederaResponseCodes.SUCCESS); + vm.stopPrank(); + } + + function test_HTS_transferToken_failure_when_frozen() public { + address from = makeAddr("bob"); + deal(token, from, amount); + vm.prank(owner); + IHederaTokenService(HTS_ADDRESS).freezeToken(token, from); + + vm.prank(from); + (int64 code) = IHederaTokenService(HTS_ADDRESS).transferToken(token, from, to, int64(int256(amount))); + assertEq(code, HederaResponseCodes.ACCOUNT_FROZEN_FOR_TOKEN); + } +} diff --git a/test/HTS.t.sol b/test/HTS.t.sol index b3e58de8..088ca318 100644 --- a/test/HTS.t.sol +++ b/test/HTS.t.sol @@ -793,8 +793,8 @@ contract HTSTest is Test, TestSetup { int64[] memory amounts = new int64[](1); amounts[0] = 4_000000; vm.prank(owner); - vm.expectRevert("transferTokens: invalid token"); - IHederaTokenService(HTS_ADDRESS).transferTokens(address(0), to, amounts); + (int64 code) = IHederaTokenService(HTS_ADDRESS).transferTokens(address(0), to, amounts); + assertEq(code, HederaResponseCodes.INVALID_TOKEN_ID); } function test_HTS_transferTokens_inconsistent_input() public { diff --git a/test/KYC.t.sol b/test/KYC.t.sol new file mode 100644 index 00000000..ce986ca3 --- /dev/null +++ b/test/KYC.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import {TestSetup} from "./lib/TestSetup.sol"; +import {HtsSystemContract, HTS_ADDRESS} from "../contracts/HtsSystemContract.sol"; +import {IHederaTokenService} from "../contracts/IHederaTokenService.sol"; +import {HederaResponseCodes} from "../contracts/HederaResponseCodes.sol"; +import {IERC20} from "../contracts/IERC20.sol"; +import {IERC721} from "../contracts/IERC721.sol"; + +contract KYCTest is Test, TestSetup { + + address private token; + address private owner; + address private to; + uint256 private amount = 4_000000; + + function setUp() external { + setUpMockStorageForNonFork(); + IHederaTokenService.HederaToken memory hederaToken; + hederaToken.name = "Token name"; + hederaToken.symbol = "Token symbol"; + hederaToken.treasury = makeAddr("Token treasury"); + hederaToken.tokenKeys = new IHederaTokenService.TokenKey[](1); + owner = makeAddr("kyc"); + hederaToken.tokenKeys[0].keyType = 0x2; + hederaToken.tokenKeys[0].key.ed25519 = hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc"; + (, token) = IHederaTokenService(HTS_ADDRESS).createFungibleToken{value: 1000}(hederaToken, 1000000, 4); + vm.assertNotEq(token, address(0)); + to = makeAddr("bob"); + } + + function test_HTS_transferToken_success_with_kyc() public { + deal(token, owner, amount); + uint256 balanceOfOwner = IERC20(token).balanceOf(owner); + IHederaTokenService(HTS_ADDRESS).grantTokenKyc(token, owner); + vm.prank(owner); + IHederaTokenService(HTS_ADDRESS).transferToken(token, owner, to, int64(int256(amount))); + assertEq(IERC20(token).balanceOf(owner), balanceOfOwner - amount); + assertEq(IERC20(token).balanceOf(to), amount); + } + + function test_ERC20_transferToken_success_with_kyc() public { + deal(token, owner, amount); + uint256 balanceOfOwner = IERC20(token).balanceOf(owner); + IHederaTokenService(HTS_ADDRESS).grantTokenKyc(token, owner); + vm.prank(owner); + IERC20(token).transfer(to, amount); + assertEq(IERC20(token).balanceOf(owner), balanceOfOwner - amount); + assertEq(IERC20(token).balanceOf(to), amount); + } + + function test_HTS_transferToken_failure_without_kyc() public { + address from = makeAddr("from"); + deal(token, from, amount); + vm.prank(from); + (int64 code) = IHederaTokenService(HTS_ADDRESS).transferToken(token, from, to, int64(int256(amount))); + assertEq(code, HederaResponseCodes.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN); + } + + function test_HTS_transferToken_success_with_kyc_granted() public { + address from = makeAddr("from"); + deal(token, from, amount); + uint256 balanceOfFrom = IERC20(token).balanceOf(from); + vm.prank(owner); + + IHederaTokenService(HTS_ADDRESS).grantTokenKyc(token, from); + vm.prank(from); + IHederaTokenService(HTS_ADDRESS).transferToken(token, from, to, int64(int256(amount))); + + assertEq(IERC20(token).balanceOf(from), balanceOfFrom - amount); + assertEq(IERC20(token).balanceOf(to), amount); + } +} diff --git a/test/Pause.t.sol b/test/Pause.t.sol new file mode 100644 index 00000000..4bd99d80 --- /dev/null +++ b/test/Pause.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import {Test} from "forge-std/Test.sol"; +import {console} from "forge-std/console.sol"; +import {TestSetup} from "./lib/TestSetup.sol"; +import {HtsSystemContract, HTS_ADDRESS} from "../contracts/HtsSystemContract.sol"; +import {IHederaTokenService} from "../contracts/IHederaTokenService.sol"; +import {HederaResponseCodes} from "../contracts/HederaResponseCodes.sol"; +import {IERC20} from "../contracts/IERC20.sol"; +import {IERC721} from "../contracts/IERC721.sol"; + +contract PauseTest is Test, TestSetup { + + address private token; + address private owner; + address private to; + uint256 private amount = 4_000000; + + function setUp() external { + setUpMockStorageForNonFork(); + IHederaTokenService.HederaToken memory hederaToken; + hederaToken.name = "Token name"; + hederaToken.symbol = "Token symbol"; + hederaToken.treasury = makeAddr("Token treasury"); + hederaToken.tokenKeys = new IHederaTokenService.TokenKey[](1); + owner = makeAddr("pause"); + hederaToken.tokenKeys[0].keyType = 0x40; + hederaToken.tokenKeys[0].key.ed25519 = hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc"; + (, token) = IHederaTokenService(HTS_ADDRESS).createFungibleToken{value: 1000}(hederaToken, 1000000, 4); + vm.assertNotEq(token, address(0)); + to = makeAddr("bob"); + } + + function test_HTS_transferToken_success_when_not_paused() public { + deal(token, owner, amount); + uint256 balanceOfOwner = IERC20(token).balanceOf(owner); + vm.prank(owner); + IHederaTokenService(HTS_ADDRESS).transferToken(token, owner, to, int64(int256(amount))); + assertEq(IERC20(token).balanceOf(owner), balanceOfOwner - amount); + assertEq(IERC20(token).balanceOf(to), amount); + } + + function test_HTS_pausing_success_with_correct_key() public { + vm.prank(owner); + int64 code = IHederaTokenService(HTS_ADDRESS).pauseToken(token); + assertEq(code, HederaResponseCodes.SUCCESS); + } + + function test_HTS_transferToken_failure_when_paused() public { + deal(token, owner, amount); + vm.startPrank(owner); + IHederaTokenService(HTS_ADDRESS).pauseToken(token); + (int64 code) = IHederaTokenService(HTS_ADDRESS).transferToken(token, owner, to, int64(int256(amount))); + assertEq(code, HederaResponseCodes.TOKEN_IS_PAUSED); + } +}