From 8533630770865fc5e3ae0341aee2b71b7d229350 Mon Sep 17 00:00:00 2001 From: Mariusz Jasuwienas Date: Mon, 20 Jan 2025 08:08:25 +0100 Subject: [PATCH] feat: methods for transfer association and approval from the IHTS (#188) Signed-off-by: Mariusz Jasuwienas --- contracts/HederaResponseCodes.sol | 6 + contracts/HtsSystemContract.sol | 356 ++++++++++++++++-- contracts/IHederaTokenService.sol | 290 +++++++-------- examples/hardhat-hts/package-lock.json | 2 +- test/HTS.t.sol | 487 ++++++++++++++++++++++++- 5 files changed, 956 insertions(+), 185 deletions(-) create mode 100644 contracts/HederaResponseCodes.sol diff --git a/contracts/HederaResponseCodes.sol b/contracts/HederaResponseCodes.sol new file mode 100644 index 00000000..53b8e4ac --- /dev/null +++ b/contracts/HederaResponseCodes.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +library HederaResponseCodes { + int32 internal constant SUCCESS = 22; // The transaction succeeded +} diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index b9e7043b..ab740d91 100644 --- a/contracts/HtsSystemContract.sol +++ b/contracts/HtsSystemContract.sol @@ -5,6 +5,7 @@ import {IERC20Events, IERC20} from "./IERC20.sol"; import {IERC721, IERC721Events} from "./IERC721.sol"; import {IHRC719} from "./IHRC719.sol"; import {IHederaTokenService} from "./IHederaTokenService.sol"; +import {HederaResponseCodes} from "./HederaResponseCodes.sol"; address constant HTS_ADDRESS = address(0x167); @@ -42,12 +43,6 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { assembly { accountId := sload(slot) } } - function getTokenInfo(address token) htsCall external returns (int64 responseCode, TokenInfo memory tokenInfo) { - require(token != address(0), "getTokenInfo: invalid token"); - - (responseCode, tokenInfo) = IHederaTokenService(token).getTokenInfo(token); - } - function mintToken(address token, int64 amount, bytes[] memory) htsCall external returns ( int64 responseCode, int64 newTotalSupply, @@ -57,14 +52,14 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { require(amount > 0, "mintToken: invalid amount"); (int64 tokenInfoResponseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token); - require(tokenInfoResponseCode == 22, "mintToken: failed to get token info"); + require(tokenInfoResponseCode == HederaResponseCodes.SUCCESS, "mintToken: failed to get token info"); address treasuryAccount = tokenInfo.token.treasury; require(treasuryAccount != address(0), "mintToken: invalid account"); HtsSystemContract(token)._update(address(0), treasuryAccount, uint256(uint64(amount))); - responseCode = 22; // HederaResponseCodes.SUCCESS + responseCode = HederaResponseCodes.SUCCESS; newTotalSupply = int64(uint64(IERC20(token).totalSupply())); serialNumbers = new int64[](0); require(newTotalSupply >= 0, "mintToken: invalid total supply"); @@ -78,18 +73,247 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { require(amount > 0, "burnToken: invalid amount"); (int64 tokenInfoResponseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token); - require(tokenInfoResponseCode == 22, "burnToken: failed to get token info"); + require(tokenInfoResponseCode == HederaResponseCodes.SUCCESS, "burnToken: failed to get token info"); address treasuryAccount = tokenInfo.token.treasury; require(treasuryAccount != address(0), "burnToken: invalid account"); HtsSystemContract(token)._update(treasuryAccount, address(0), uint256(uint64(amount))); - responseCode = 22; // HederaResponseCodes.SUCCESS + responseCode = HederaResponseCodes.SUCCESS; newTotalSupply = int64(uint64(IERC20(token).totalSupply())); require(newTotalSupply >= 0, "burnToken: invalid total supply"); } + function associateTokens(address account, address[] memory tokens) htsCall public returns (int64 responseCode) { + require(tokens.length > 0, "associateTokens: missing tokens"); + require( + account == msg.sender, + "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, + "associateTokens: Failed to associate token" + ); + } + responseCode = HederaResponseCodes.SUCCESS; + } + + function associateToken(address account, address token) htsCall external returns (int64 responseCode) { + address[] memory tokens = new address[](1); + tokens[0] = token; + return associateTokens(account, tokens); + } + + function dissociateTokens(address account, address[] memory tokens) htsCall public returns (int64 responseCode) { + 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"); + } + responseCode = HederaResponseCodes.SUCCESS; + } + + function dissociateToken(address account, address token) htsCall external returns (int64 responseCode) { + address[] memory tokens = new address[](1); + tokens[0] = token; + return dissociateTokens(account, tokens); + } + + function transferNFTs( + address token, + address[] memory sender, + address[] memory receiver, + int64[] memory serialNumber + ) htsCall external returns (int64 responseCode) { + require(token != address(0), "transferNFTs: invalid token"); + require(sender.length > 0, "transferNFTs: missing recipients"); + require(receiver.length == sender.length, "transferNFTs: inconsistent input"); + require(serialNumber.length == sender.length, "transferNFTs: inconsistent input"); + for (uint256 i = 0; i < sender.length; i++) { + transferNFT(token, sender[i], receiver[i], serialNumber[i]); + } + responseCode = HederaResponseCodes.SUCCESS; + } + + function transferToken( + address token, + address sender, + address recipient, + int64 amount + ) htsCall public returns (int64 responseCode) { + require(token != address(0), "transferToken: invalid token"); + address from = sender; + address to = recipient; + if (amount < 0) { + from = recipient; + to = sender; + amount *= -1; + } + require( + from == msg.sender || + IERC20(token).allowance(from, msg.sender) >= uint256(uint64(amount)), + "transferNFT: unauthorized" + ); + HtsSystemContract(token).transferFrom(msg.sender, from, to, uint256(uint64(amount))); + responseCode = HederaResponseCodes.SUCCESS; + } + + function transferNFT( + address token, + address sender, + address recipient, + int64 serialNumber + ) htsCall public returns (int64 responseCode) { + 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) { + HtsSystemContract(token).approve(msg.sender, spender, amount); + responseCode = HederaResponseCodes.SUCCESS; + } + + function transferFrom( + address token, + address sender, + address recipient, + uint256 amount + ) htsCall external returns (int64) { + return transferToken(token, sender, recipient, int64(int256(amount))); + } + + function allowance(address token, address owner, address spender) htsCall external view returns (int64, uint256) { + return (HederaResponseCodes.SUCCESS, IERC20(token).allowance(owner, spender)); + } + + function approveNFT( + address token, + address approved, + uint256 serialNumber + ) htsCall public returns (int64 responseCode) { + HtsSystemContract(token).approveNFT(msg.sender, approved, serialNumber); + responseCode = HederaResponseCodes.SUCCESS; + } + + function transferFromNFT( + address token, + address from, + address to, + uint256 serialNumber + ) htsCall external returns (int64) { + return transferNFT(token, from, to, int64(int256(serialNumber))); + } + + function getApproved(address token, uint256 serialNumber) + htsCall external view returns (int64 responseCode, address approved) { + require(token != address(0), "getApproved: invalid token"); + (responseCode, approved) = (HederaResponseCodes.SUCCESS, IERC721(token).getApproved(serialNumber)); + } + + function setApprovalForAll( + address token, + address operator, + bool approved + ) htsCall external returns (int64 responseCode) { + HtsSystemContract(token).setApprovalForAll(msg.sender, operator, approved); + responseCode = HederaResponseCodes.SUCCESS; + } + + function isApprovedForAll( + address token, + address owner, + address operator + ) htsCall external view returns (int64, bool) { + require(token != address(0), "isApprovedForAll: invalid token"); + return (HederaResponseCodes.SUCCESS, IERC721(token).isApprovedForAll(owner, operator)); + } + + function getTokenCustomFees( + address token + ) htsCall external returns (int64, FixedFee[] memory, FractionalFee[] memory, RoyaltyFee[] memory) { + (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); + return (responseCode, tokenInfo.fixedFees, tokenInfo.fractionalFees, tokenInfo.royaltyFees); + } + + function getTokenDefaultFreezeStatus(address token) htsCall external returns (int64, bool) { + (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); + return (responseCode, tokenInfo.token.freezeDefault); + } + + function getTokenDefaultKycStatus(address token) htsCall external returns (int64, bool) { + (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); + return (responseCode, tokenInfo.defaultKycStatus); + } + + function getTokenExpiryInfo(address token) htsCall external returns (int64, Expiry memory expiry) { + (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); + return (responseCode, tokenInfo.token.expiry); + } + + function getFungibleTokenInfo(address token) htsCall external returns (int64, FungibleTokenInfo memory) { + (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); + require(responseCode == HederaResponseCodes.SUCCESS, "getFungibleTokenInfo: failed to get token data"); + FungibleTokenInfo memory fungibleTokenInfo; + fungibleTokenInfo.tokenInfo = tokenInfo; + fungibleTokenInfo.decimals = int32(int8(IERC20(token).decimals())); + + return (responseCode, fungibleTokenInfo); + } + + function getTokenInfo(address token) htsCall public returns (int64, TokenInfo memory) { + require(token != address(0), "getTokenInfo: invalid token"); + + return IHederaTokenService(token).getTokenInfo(token); + } + + function getTokenKey(address token, uint keyType) htsCall external returns (int64, KeyValue memory) { + (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); + require(responseCode == HederaResponseCodes.SUCCESS, "getTokenKey: failed to get token data"); + for (uint256 i = 0; i < tokenInfo.token.tokenKeys.length; i++) { + if (tokenInfo.token.tokenKeys[i].keyType == keyType) { + return (HederaResponseCodes.SUCCESS, tokenInfo.token.tokenKeys[i].key); + } + } + KeyValue memory emptyKey; + return (HederaResponseCodes.SUCCESS, emptyKey); + } + + function getNonFungibleTokenInfo(address token, int64 serialNumber) + htsCall external + returns (int64, NonFungibleTokenInfo memory) { + (int64 responseCode, TokenInfo memory tokenInfo) = getTokenInfo(token); + require(responseCode == HederaResponseCodes.SUCCESS, "getNonFungibleTokenInfo: failed to get token data"); + NonFungibleTokenInfo memory nonFungibleTokenInfo; + nonFungibleTokenInfo.tokenInfo = tokenInfo; + nonFungibleTokenInfo.serialNumber = serialNumber; + nonFungibleTokenInfo.spenderId = IERC721(token).getApproved(uint256(uint64(serialNumber))); + nonFungibleTokenInfo.ownerId = IERC721(token).ownerOf(uint256(uint64(serialNumber))); + + // ToDo: + // nonFungibleTokenInfo.metadata = bytes(IERC721(token).tokenURI(uint256(uint64(serialNumber)))); + // nonFungibleTokenInfo.creationTime = int64(0); + + return (responseCode, nonFungibleTokenInfo); + } + + function isToken(address token) htsCall external returns (int64, bool) { + bytes memory payload = abi.encodeWithSignature("getTokenType(address)", token); + (bool success, bytes memory returnData) = token.call(payload); + return (HederaResponseCodes.SUCCESS, success && returnData.length > 0); + } + + function getTokenType(address token) htsCall external returns (int64, int32) { + require(token != address(0), "getTokenType: invalid address"); + return IHederaTokenService(token).getTokenType(token); + } + /** * @dev Validates `redirectForToken(address,bytes)` dispatcher arguments. * @@ -173,7 +397,77 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { if (msg.sender == HTS_ADDRESS) { if (selector == this.getTokenInfo.selector) { require(msg.data.length >= 28, "getTokenInfo: Not enough calldata"); - return abi.encode(22, _tokenInfo); + return abi.encode(HederaResponseCodes.SUCCESS, _tokenInfo); + } + if (selector == this.associateToken.selector) { + require(msg.data.length >= 48, "associateToken: Not enough calldata"); + address account = address(bytes20(msg.data[40:60])); + bytes32 slot = _isAssociatedSlot(account); + assembly { sstore(slot, true) } + return abi.encode(HederaResponseCodes.SUCCESS); + } + if (selector == this.dissociateToken.selector) { + require(msg.data.length >= 48, "dissociateToken: Not enough calldata"); + address account = address(bytes20(msg.data[40:60])); + bytes32 slot = _isAssociatedSlot(account); + assembly { sstore(slot, false) } + return abi.encode(HederaResponseCodes.SUCCESS); + } + if (selector == this.getTokenType.selector) { + require(msg.data.length >= 28, "getTokenType: Not enough calldata"); + if (keccak256(abi.encodePacked(tokenType)) == keccak256("FUNGIBLE_COMMON")) { + return abi.encode(HederaResponseCodes.SUCCESS, int32(0)); + } + if (keccak256(abi.encodePacked(tokenType)) == keccak256("NON_FUNGIBLE_UNIQUE")) { + return abi.encode(HederaResponseCodes.SUCCESS, int32(1)); + } + return abi.encode(HederaResponseCodes.SUCCESS, int32(-1)); + } + if (selector == this.transferFrom.selector) { + require(msg.data.length >= 156, "transferFrom: Not enough calldata"); + address sender = address(bytes20(msg.data[40:60])); + address from = address(bytes20(msg.data[72:92])); + address to = address(bytes20(msg.data[104:124])); + uint256 amount = uint256(bytes32(msg.data[124:156])); + if (from != sender) { + _spendAllowance(from, sender, amount); + } + _transfer(from, to, amount); + return abi.encode(true); + } + if (selector == this.transferFromNFT.selector) { + require(msg.data.length >= 156, "transferFromNFT: Not enough calldata"); + address sender = address(bytes20(msg.data[40:60])); + address from = address(bytes20(msg.data[72:92])); + address to = address(bytes20(msg.data[104:124])); + uint256 serialId = uint256(bytes32(msg.data[124:156])); + _transferNFT(sender, from, to, serialId); + return abi.encode(true); + } + if (selector == this.approve.selector) { + require(msg.data.length >= 124, "approve: Not enough calldata"); + address from = address(bytes20(msg.data[40:60])); + address to = address(bytes20(msg.data[72:92])); + uint256 amount = uint256(bytes32(msg.data[92:124])); + _approve(from, to, amount); + emit Approval(from, to, amount); + return abi.encode(true); + } + if (selector == this.approveNFT.selector) { + require(msg.data.length >= 124, "approveNFT: Not enough calldata"); + address from = address(bytes20(msg.data[40:60])); + address to = address(bytes20(msg.data[72:92])); + uint256 serialId = uint256(bytes32(msg.data[92:124])); + _approve(from, to, serialId, true); + return abi.encode(true); + } + if (selector == this.setApprovalForAll.selector) { + require(msg.data.length >= 124, "setApprovalForAll: Not enough calldata"); + address from = address(bytes20(msg.data[40:60])); + address to = address(bytes20(msg.data[72:92])); + bool approved = uint256(bytes32(msg.data[92:124])) == 1; + _setApprovalForAll(from, to, approved); + return abi.encode(true); } if (selector == this._update.selector) { require(msg.data.length >= 124, "update: Not enough calldata"); @@ -288,21 +582,21 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { address from = address(bytes20(msg.data[40:60])); address to = address(bytes20(msg.data[72:92])); uint256 serialId = uint256(bytes32(msg.data[92:124])); - _transferNFT(from, to, serialId); + _transferNFT(msg.sender, from, to, serialId); return abi.encode(true); } if (selector == IERC721.approve.selector) { require(msg.data.length >= 92, "approve: Not enough calldata"); address spender = address(bytes20(msg.data[40:60])); uint256 serialId = uint256(bytes32(msg.data[60:92])); - _approve(spender, serialId, true); + _approve(msg.sender, spender, serialId, true); return abi.encode(true); } if (selector == IERC721.setApprovalForAll.selector) { require(msg.data.length >= 92, "setApprovalForAll: Not enough calldata"); address operator = address(bytes20(msg.data[40:60])); bool approved = uint256(bytes32(msg.data[60:92])) == 1; - _setApprovalForAll(operator, approved); + _setApprovalForAll(msg.sender, operator, approved); return abi.encode(true); } if (selector == IERC721.getApproved.selector) { @@ -417,9 +711,9 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { assembly { approved := sload(slot) } } - function __isApprovedForAll(address owner, address operator) private returns (bool isApprovedForAll) { + function __isApprovedForAll(address owner, address operator) private returns (bool approvedForAll) { bytes32 slot = _isApprovedForAllSlot(owner, operator); - assembly { isApprovedForAll := sload(slot) } + assembly { approvedForAll := sload(slot) } } function _transfer(address from, address to, uint256 amount) private { @@ -429,7 +723,7 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { emit Transfer(from, to, amount); } - function _transferNFT(address from, address to, uint256 serialId) private { + function _transferNFT(address sender, address from, address to, uint256 serialId) private { require(from != address(0), "hts: invalid sender"); require(to != address(0), "hts: invalid receiver"); @@ -438,10 +732,8 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { address owner; assembly { owner := sload(slot) } require(owner == from, "hts: sender is not owner"); - - // If the sender is not the owner, check if the sender is approved - if (msg.sender != from) { - require(msg.sender == __getApproved(serialId) || __isApprovedForAll(from, msg.sender), "hts: unauthorized"); + if (sender != owner) { + require(sender == __getApproved(serialId) || __isApprovedForAll(owner, sender), "hts: unauthorized"); } // Clear approval @@ -449,8 +741,10 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { assembly { sstore(approvalSlot, 0) } // Clear approval for all - bytes32 isApprovedForAllSlot = _isApprovedForAllSlot(from, to); - assembly { sstore(isApprovedForAllSlot, false) } + if (owner != sender) { + bytes32 isApprovedForAllSlot = _isApprovedForAllSlot(owner, sender); + assembly { sstore(isApprovedForAllSlot, false) } + } // Set the new owner assembly { sstore(slot, to) } @@ -488,10 +782,13 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { assembly { sstore(allowanceSlot, amount) } } - function _approve(address spender, uint256 serialId, bool isApproved) private { + function _approve(address sender, address spender, uint256 serialId, bool isApproved) private { // The caller must own the token or be an approved operator. address owner = __ownerOf(serialId); - require(msg.sender == owner || __getApproved(serialId) == msg.sender || __isApprovedForAll(owner, msg.sender), "_approve: unauthorized"); + require( + sender == owner || __getApproved(serialId) == sender || __isApprovedForAll(owner, sender), + "_approve: unauthorized" + ); bytes32 slot = _getApprovedSlot(uint32(serialId)); address newApproved = isApproved ? spender : address(0); @@ -514,11 +811,10 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { } } - function _setApprovalForAll(address operator, bool approved) private { - address owner = msg.sender; - require(operator != address(0) && operator != owner, "setApprovalForAll: invalid operator"); - bytes32 slot = _isApprovedForAllSlot(owner, operator); + function _setApprovalForAll(address sender, address operator, bool approved) private { + require(operator != address(0) && operator != sender, "setApprovalForAll: invalid operator"); + bytes32 slot = _isApprovedForAllSlot(sender, operator); assembly { sstore(slot, approved) } - emit ApprovalForAll(owner, operator, approved); + emit ApprovalForAll(sender, operator, approved); } } diff --git a/contracts/IHederaTokenService.sol b/contracts/IHederaTokenService.sol index c8d618d1..4e8fb2c9 100644 --- a/contracts/IHederaTokenService.sol +++ b/contracts/IHederaTokenService.sol @@ -14,56 +14,56 @@ interface IHederaTokenService { // /// accounts, and for any receiving accounts that have receiverSigRequired == true. The signatures // /// are in the same order as the accounts, skipping those accounts that don't need a signature. // /// @custom:version 0.3.0 previous version did not include isApproval - // struct AccountAmount { - // // The Account ID, as a solidity address, that sends/receives cryptocurrency or tokens - // address accountID; + struct AccountAmount { + // The Account ID, as a solidity address, that sends/receives cryptocurrency or tokens + address accountID; - // // The amount of the lowest denomination of the given token that - // // the account sends(negative) or receives(positive) - // int64 amount; + // The amount of the lowest denomination of the given token that + // the account sends(negative) or receives(positive) + int64 amount; - // // If true then the transfer is expected to be an approved allowance and the - // // accountID is expected to be the owner. The default is false (omitted). - // bool isApproval; - // } + // If true then the transfer is expected to be an approved allowance and the + // accountID is expected to be the owner. The default is false (omitted). + bool isApproval; + } // /// A sender account, a receiver account, and the serial number of an NFT of a Token with // /// NON_FUNGIBLE_UNIQUE type. When minting NFTs the sender will be the default AccountID instance // /// (0.0.0 aka 0x0) and when burning NFTs, the receiver will be the default AccountID instance. // /// @custom:version 0.3.0 previous version did not include isApproval - // struct NftTransfer { - // // The solidity address of the sender - // address senderAccountID; + struct NftTransfer { + // The solidity address of the sender + address senderAccountID; - // // The solidity address of the receiver - // address receiverAccountID; + // The solidity address of the receiver + address receiverAccountID; - // // The serial number of the NFT - // int64 serialNumber; + // The serial number of the NFT + int64 serialNumber; - // // If true then the transfer is expected to be an approved allowance and the - // // accountID is expected to be the owner. The default is false (omitted). - // bool isApproval; - // } + // If true then the transfer is expected to be an approved allowance and the + // accountID is expected to be the owner. The default is false (omitted). + bool isApproval; + } - // struct TokenTransferList { - // // The ID of the token as a solidity address - // address token; + struct TokenTransferList { + // The ID of the token as a solidity address + address token; - // // Applicable to tokens of type FUNGIBLE_COMMON. Multiple list of AccountAmounts, each of which - // // has an account and amount. - // AccountAmount[] transfers; + // Applicable to tokens of type FUNGIBLE_COMMON. Multiple list of AccountAmounts, each of which + // has an account and amount. + AccountAmount[] transfers; - // // Applicable to tokens of type NON_FUNGIBLE_UNIQUE. Multiple list of NftTransfers, each of - // // which has a sender and receiver account, including the serial number of the NFT - // NftTransfer[] nftTransfers; - // } + // Applicable to tokens of type NON_FUNGIBLE_UNIQUE. Multiple list of NftTransfers, each of + // which has a sender and receiver account, including the serial number of the NFT + NftTransfer[] nftTransfers; + } - // struct TransferList { - // // Multiple list of AccountAmounts, each of which has an account and amount. - // // Used to transfer hbars between the accounts in the list. - // AccountAmount[] transfers; - // } + struct TransferList { + // Multiple list of AccountAmounts, each of which has an account and amount. + // Used to transfer hbars between the accounts in the list. + AccountAmount[] transfers; + } /// Expiry properties of a Hedera token - second, autoRenewAccount, autoRenewPeriod struct Expiry { @@ -199,35 +199,35 @@ interface IHederaTokenService { string ledgerId; } - // /// Additional fungible properties of a Hedera Token. - // struct FungibleTokenInfo { - // /// The shared hedera token info - // TokenInfo tokenInfo; + /// Additional fungible properties of a Hedera Token. + struct FungibleTokenInfo { + /// The shared hedera token info + TokenInfo tokenInfo; - // /// The number of decimal places a token is divisible by - // int32 decimals; - // } + /// The number of decimal places a token is divisible by + int32 decimals; + } - // /// Additional non fungible properties of a Hedera Token. - // struct NonFungibleTokenInfo { - // /// The shared hedera token info - // TokenInfo tokenInfo; + /// Additional non fungible properties of a Hedera Token. + struct NonFungibleTokenInfo { + /// The shared hedera token info + TokenInfo tokenInfo; - // /// The serial number of the nft - // int64 serialNumber; + /// The serial number of the nft + int64 serialNumber; - // /// The account id specifying the owner of the non fungible token - // address ownerId; + /// The account id specifying the owner of the non fungible token + address ownerId; - // /// The epoch second at which the token was created. - // int64 creationTime; + /// The epoch second at which the token was created. + int64 creationTime; - // /// The unique metadata of the NFT - // bytes metadata; + /// The unique metadata of the NFT + bytes metadata; - // /// The account id specifying an account that has been granted spending permissions on this nft - // address spenderId; - // } + /// The account id specifying an account that has been granted spending permissions on this nft + address spenderId; + } /// A fixed number of units (hbar or token) to assess as a fee during a transfer of /// units of the token to which this fixed fee is attached. The denomination of @@ -363,16 +363,16 @@ interface IHederaTokenService { /// Type, once an account is associated, it can hold any number of NFTs (serial numbers) of that /// token type /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function associateTokens(address account, address[] memory tokens) - // external - // returns (int64 responseCode); + function associateTokens(address account, address[] memory tokens) + external + returns (int64 responseCode); /// Single-token variant of associateTokens. Will be mapped to a single entry array call of associateTokens /// @param account The account to be associated with the provided token /// @param token The token to be associated with the provided account - // function associateToken(address account, address token) - // external - // returns (int64 responseCode); + function associateToken(address account, address token) + external + returns (int64 responseCode); /// Dissociates the provided account with the provided tokens. Must be signed by the provided /// Account's key. @@ -392,16 +392,16 @@ interface IHederaTokenService { /// @param account The account to be dissociated from the provided tokens /// @param tokens The tokens to be dissociated from the provided account. /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function dissociateTokens(address account, address[] memory tokens) - // external - // returns (int64 responseCode); + function dissociateTokens(address account, address[] memory tokens) + external + returns (int64 responseCode); /// Single-token variant of dissociateTokens. Will be mapped to a single entry array call of dissociateTokens /// @param account The account to be associated with the provided token /// @param token The token to be associated with the provided account - // function dissociateToken(address account, address token) - // external - // returns (int64 responseCode); + function dissociateToken(address account, address token) + external + returns (int64 responseCode); /// Creates a Fungible Token with the specified properties /// @param token the basic properties of the token being created @@ -473,12 +473,12 @@ interface IHederaTokenService { /// @param sender the sender of an nft /// @param receiver the receiver of the nft sent by the same index at sender /// @param serialNumber the serial number of the nft sent by the same index at sender - // function transferNFTs( - // address token, - // address[] memory sender, - // address[] memory receiver, - // int64[] memory serialNumber - // ) external returns (int64 responseCode); + function transferNFTs( + address token, + address[] memory sender, + address[] memory receiver, + int64[] memory serialNumber + ) external returns (int64 responseCode); /// Transfers tokens where the calling account/contract is implicitly the first entry in the token transfer list, /// where the amount is the value needed to zero balance the transfers. Regular signing rules apply for sending @@ -487,12 +487,12 @@ interface IHederaTokenService { /// @param sender The sender for the transaction /// @param recipient The receiver of the transaction /// @param amount Non-negative value to send. a negative value will result in a failure. - // function transferToken( - // address token, - // address sender, - // address recipient, - // int64 amount - // ) external returns (int64 responseCode); + function transferToken( + address token, + address sender, + address recipient, + int64 amount + ) external returns (int64 responseCode); /// Transfers tokens where the calling account/contract is implicitly the first entry in the token transfer list, /// where the amount is the value needed to zero balance the transfers. Regular signing rules apply for sending @@ -501,12 +501,12 @@ interface IHederaTokenService { /// @param sender The sender for the transaction /// @param recipient The receiver of the transaction /// @param serialNumber The serial number of the NFT to transfer. - // function transferNFT( - // address token, - // address sender, - // address recipient, - // int64 serialNumber - // ) external returns (int64 responseCode); + function transferNFT( + address token, + address sender, + address recipient, + int64 serialNumber + ) external returns (int64 responseCode); /// Allows spender to withdraw from your account multiple times, up to the value amount. If this function is called /// again it overwrites the current allowance with value. @@ -515,11 +515,11 @@ interface IHederaTokenService { /// @param spender the account address authorized to spend /// @param amount the amount of tokens authorized to spend. /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function approve( - // address token, - // address spender, - // uint256 amount - // ) external returns (int64 responseCode); + function approve( + address token, + address spender, + uint256 amount + ) external returns (int64 responseCode); /// Transfers `amount` tokens from `from` to `to` using the // allowance mechanism. `amount` is then deducted from the caller's allowance. @@ -529,7 +529,7 @@ interface IHederaTokenService { /// @param to The account address of the receiver of the `amount` tokens /// @param amount The amount of tokens to transfer from `from` to `to` /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function transferFrom(address token, address from, address to, uint256 amount) external returns (int64 responseCode); + function transferFrom(address token, address from, address to, uint256 amount) external returns (int64 responseCode); /// Returns the amount which spender is still allowed to withdraw from owner. /// Only Applicable to Fungible Tokens @@ -538,11 +538,11 @@ interface IHederaTokenService { /// @param spender the spender of the tokens /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return allowance The amount which spender is still allowed to withdraw from owner. - // function allowance( - // address token, - // address owner, - // address spender - // ) external returns (int64 responseCode, uint256 allowance); + function allowance( + address token, + address owner, + address spender + ) external returns (int64 responseCode, uint256 allowance); /// Allow or reaffirm the approved address to transfer an NFT the approved address does not own. /// Only Applicable to NFT Tokens @@ -550,11 +550,11 @@ interface IHederaTokenService { /// @param approved The new approved NFT controller. To revoke approvals pass in the zero address. /// @param serialNumber The NFT serial number to approve /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function approveNFT( - // address token, - // address approved, - // uint256 serialNumber - // ) external returns (int64 responseCode); + function approveNFT( + address token, + address approved, + uint256 serialNumber + ) external returns (int64 responseCode); /// Transfers `serialNumber` of `token` from `from` to `to` using the allowance mechanism. /// Only applicable to NFT tokens @@ -563,7 +563,7 @@ interface IHederaTokenService { /// @param to The account address of the receiver of `serialNumber` /// @param serialNumber The NFT serial number to transfer /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function transferFromNFT(address token, address from, address to, uint256 serialNumber) external returns (int64 responseCode); + function transferFromNFT(address token, address from, address to, uint256 serialNumber) external returns (int64 responseCode); /// Get the approved address for a single NFT /// Only Applicable to NFT Tokens @@ -571,9 +571,9 @@ interface IHederaTokenService { /// @param serialNumber The NFT to find the approved address for /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return approved The approved address for this NFT, or the zero address if there is none - // function getApproved(address token, uint256 serialNumber) - // external - // returns (int64 responseCode, address approved); + function getApproved(address token, uint256 serialNumber) + external + returns (int64 responseCode, address approved); /// Enable or disable approval for a third party ("operator") to manage /// all of `msg.sender`'s assets @@ -581,11 +581,11 @@ interface IHederaTokenService { /// @param operator Address to add to the set of authorized operators /// @param approved True if the operator is approved, false to revoke approval /// @return responseCode The response code for the status of the request. SUCCESS is 22. - // function setApprovalForAll( - // address token, - // address operator, - // bool approved - // ) external returns (int64 responseCode); + function setApprovalForAll( + address token, + address operator, + bool approved + ) external returns (int64 responseCode); /// Query if an address is an authorized operator for another address /// Only Applicable to NFT Tokens @@ -594,11 +594,11 @@ interface IHederaTokenService { /// @param operator The address that acts on behalf of the owner /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return approved True if `operator` is an approved operator for `owner`, false otherwise - // function isApprovedForAll( - // address token, - // address owner, - // address operator - // ) external returns (int64 responseCode, bool approved); + function isApprovedForAll( + address token, + address owner, + address operator + ) external returns (int64 responseCode, bool approved); /// Query if token account is frozen /// @param token The token address to check @@ -629,41 +629,41 @@ interface IHederaTokenService { /// @return fixedFees Set of fixed fees for `token` /// @return fractionalFees Set of fractional fees for `token` /// @return royaltyFees Set of royalty fees for `token` - // function getTokenCustomFees(address token) - // external - // returns (int64 responseCode, FixedFee[] memory fixedFees, FractionalFee[] memory fractionalFees, RoyaltyFee[] memory royaltyFees); + function getTokenCustomFees(address token) + external + returns (int64 responseCode, FixedFee[] memory fixedFees, FractionalFee[] memory fractionalFees, RoyaltyFee[] memory royaltyFees); /// Query token default freeze status /// @param token The token address to check /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return defaultFreezeStatus True if `token` default freeze status is frozen. - // function getTokenDefaultFreezeStatus(address token) - // external - // returns (int64 responseCode, bool defaultFreezeStatus); + 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. /// @return defaultKycStatus True if `token` default kyc status is KycNotApplicable and false if Revoked. - // function getTokenDefaultKycStatus(address token) - // external - // returns (int64 responseCode, bool defaultKycStatus); + function getTokenDefaultKycStatus(address token) + external + returns (int64 responseCode, bool defaultKycStatus); /// Query token expiry info /// @param token The token address to check /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return expiry Expiry info for `token` - // function getTokenExpiryInfo(address token) - // external - // returns (int64 responseCode, Expiry memory expiry); + function getTokenExpiryInfo(address token) + external + returns (int64 responseCode, Expiry memory expiry); /// Query fungible token info /// @param token The token address to check /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return fungibleTokenInfo FungibleTokenInfo info for `token` - // function getFungibleTokenInfo(address token) - // external - // returns (int64 responseCode, FungibleTokenInfo memory fungibleTokenInfo); + function getFungibleTokenInfo(address token) + external + returns (int64 responseCode, FungibleTokenInfo memory fungibleTokenInfo); /// Query token info /// @param token The token address to check @@ -678,18 +678,18 @@ interface IHederaTokenService { /// @param keyType The keyType of the desired KeyValue /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return key KeyValue info for key of type `keyType` - // function getTokenKey(address token, uint keyType) - // external - // returns (int64 responseCode, KeyValue memory key); + function getTokenKey(address token, uint keyType) + external + returns (int64 responseCode, KeyValue memory key); /// Query non fungible token info /// @param token The token address to check /// @param serialNumber The NFT serialNumber to check /// @return responseCode The response code for the status of the request. SUCCESS is 22. /// @return nonFungibleTokenInfo NonFungibleTokenInfo info for `token` `serialNumber` - // function getNonFungibleTokenInfo(address token, int64 serialNumber) - // external - // returns (int64 responseCode, NonFungibleTokenInfo memory nonFungibleTokenInfo); + function getNonFungibleTokenInfo(address token, int64 serialNumber) + external + returns (int64 responseCode, NonFungibleTokenInfo memory nonFungibleTokenInfo); /// Operation to freeze token account /// @param token The token address @@ -783,17 +783,17 @@ interface IHederaTokenService { /// @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 - // (int64 responseCode, bool isToken); + 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 - // function getTokenType(address token) - // external returns - // (int64 responseCode, int32 tokenType); + function getTokenType(address token) + external returns + (int64 responseCode, int32 tokenType); /// Initiates a Redirect For Token /// @param token The token address diff --git a/examples/hardhat-hts/package-lock.json b/examples/hardhat-hts/package-lock.json index 09e720ee..8ec1a07f 100644 --- a/examples/hardhat-hts/package-lock.json +++ b/examples/hardhat-hts/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "devDependencies": { - "@hashgraph/system-contracts-forking": "hashgraph/hedera-forking#165-htssystemcontractjson", + "@hashgraph/system-contracts-forking": "hashgraph/hedera-forking", "@nomicfoundation/hardhat-toolbox": "^5.0.0" } }, diff --git a/test/HTS.t.sol b/test/HTS.t.sol index f160507f..bb7872bb 100644 --- a/test/HTS.t.sol +++ b/test/HTS.t.sol @@ -2,9 +2,12 @@ pragma solidity ^0.8.0; import {Test} from "forge-std/Test.sol"; +import {HederaResponseCodes} from "../contracts/HederaResponseCodes.sol"; import {HtsSystemContract, HTS_ADDRESS} from "../contracts/HtsSystemContract.sol"; import {IHederaTokenService} from "../contracts/IHederaTokenService.sol"; -import {IERC20} from "../contracts/IERC20.sol"; +import {IERC20Events, IERC20} from "../contracts/IERC20.sol"; +import {IERC721Events, IERC721} from "../contracts/IERC721.sol"; +import {IHRC719} from "../contracts/IHRC719.sol"; import {TestSetup} from "./lib/TestSetup.sol"; contract HTSTest is Test, TestSetup { @@ -60,7 +63,7 @@ contract HTSTest is Test, TestSetup { function test_HTS_getTokenInfo_should_return_token_info_for_valid_token() external { address token = USDC; (int64 responseCode, HtsSystemContract.TokenInfo memory tokenInfo) = HtsSystemContract(HTS_ADDRESS).getTokenInfo(token); - assertEq(responseCode, 22); + assertEq(responseCode, HederaResponseCodes.SUCCESS); assertEq(tokenInfo.token.name, "USD Coin"); assertEq(tokenInfo.token.symbol, "USDC"); assertEq(tokenInfo.token.treasury, address(0x0000000000000000000000000000000000001438)); @@ -106,7 +109,7 @@ contract HTSTest is Test, TestSetup { function test_HTS_getTokenInfo_should_return_custom_fees_for_valid_token() external { (int64 responseCode, HtsSystemContract.TokenInfo memory tokenInfo) = HtsSystemContract(HTS_ADDRESS).getTokenInfo(CTCF); - assertEq(responseCode, 22); + assertEq(responseCode, HederaResponseCodes.SUCCESS); assertEq(tokenInfo.token.name, "Crypto Token with Custom Fees"); assertEq(tokenInfo.token.symbol, "CTCF"); assertEq(tokenInfo.token.memo, ""); @@ -134,7 +137,7 @@ contract HTSTest is Test, TestSetup { assertEq(tokenInfo.fixedFees[2].useCurrentTokenForPayment, true); assertEq(tokenInfo.fractionalFees.length, 2); - + assertEq(tokenInfo.fractionalFees[0].netOfTransfers, false); assertEq(tokenInfo.fractionalFees[0].numerator, 1); assertEq(tokenInfo.fractionalFees[0].denominator, 100); @@ -197,7 +200,7 @@ contract HTSTest is Test, TestSetup { bytes[] memory metadata = new bytes[](0); (int64 responseCode, int64 newTotalSupply, int64[] memory serialNumbers) = HtsSystemContract(HTS_ADDRESS).mintToken(token, amount, metadata); - assertEq(responseCode, 22); + assertEq(responseCode, HederaResponseCodes.SUCCESS); assertEq(serialNumbers.length, 0); assertEq(newTotalSupply, initialTotalSupply + amount); assertEq(IERC20(token).balanceOf(treasury), uint64(initialTreasuryBalance) + uint64(amount)); @@ -221,7 +224,11 @@ contract HTSTest is Test, TestSetup { IHederaTokenService.Expiry(0, address(0), 0) ); - vm.mockCall(token, abi.encode(HtsSystemContract.getTokenInfo.selector), abi.encode(22, tokenInfo)); + vm.mockCall( + token, + abi.encode(HtsSystemContract.getTokenInfo.selector), + abi.encode(HederaResponseCodes.SUCCESS, tokenInfo) + ); vm.expectRevert(bytes("mintToken: invalid account")); HtsSystemContract(HTS_ADDRESS).mintToken(token, amount, metadata); } @@ -252,13 +259,13 @@ contract HTSTest is Test, TestSetup { uint256 initialTreasuryBalance = IERC20(token).balanceOf(treasury); (int64 responseCodeMint, int64 newTotalSupplyAfterMint, int64[] memory serialNumbers) = HtsSystemContract(HTS_ADDRESS).mintToken(token, amount, new bytes[](0)); - assertEq(responseCodeMint, 22); + assertEq(responseCodeMint, HederaResponseCodes.SUCCESS); assertEq(serialNumbers.length, 0); assertEq(newTotalSupplyAfterMint, initialTotalSupply + amount); assertEq(IERC20(token).balanceOf(treasury), uint64(initialTreasuryBalance) + uint64(amount)); (int64 responseCodeBurn, int64 newTotalSupplyAfterBurn) = HtsSystemContract(HTS_ADDRESS).burnToken(token, amount, serialNumbers); - assertEq(responseCodeBurn, 22); + assertEq(responseCodeBurn, HederaResponseCodes.SUCCESS); assertEq(newTotalSupplyAfterBurn, initialTotalSupply); assertEq(IERC20(token).balanceOf(treasury), uint64(initialTreasuryBalance)); } @@ -281,7 +288,11 @@ contract HTSTest is Test, TestSetup { IHederaTokenService.Expiry(0, address(0), 0) ); - vm.mockCall(token, abi.encode(HtsSystemContract.getTokenInfo.selector), abi.encode(22, tokenInfo)); + vm.mockCall( + token, + abi.encode(HtsSystemContract.getTokenInfo.selector), + abi.encode(HederaResponseCodes.SUCCESS, tokenInfo) + ); vm.expectRevert(bytes("burnToken: invalid account")); HtsSystemContract(HTS_ADDRESS).burnToken(token, amount, serialNumbers); } @@ -303,4 +314,462 @@ contract HTSTest is Test, TestSetup { vm.expectRevert(bytes("burnToken: invalid amount")); HtsSystemContract(HTS_ADDRESS).burnToken(token, amount, serialNumbers); } + + function test_HTS_getApproved_should_return_correct_address() external view { + address token = CFNFTFF; + (int64 responseCodeGetApproved, address approved) = HtsSystemContract(HTS_ADDRESS) + .getApproved(token, 1); + assertEq(responseCodeGetApproved, HederaResponseCodes.SUCCESS); + assertEq(approved, CFNFTFF_ALLOWED_SPENDER); + } + + function test_HTS_getApproved_should_return_nothing_when_no_approval_granted() external view { + address token = CFNFTFF; + (int64 responseCodeGetApproved, address approved) = HtsSystemContract(HTS_ADDRESS).getApproved(token, 2); + assertEq(responseCodeGetApproved, HederaResponseCodes.SUCCESS); + assertEq(approved, address(0)); + } + + function test_HTS_isApprovedForAll() view external { + address token = CFNFTFF; + (int64 isApprovedForAllResponseCode, bool isApproved) = HtsSystemContract(HTS_ADDRESS) + .isApprovedForAll(token, CFNFTFF_TREASURY, CFNFTFF_ALLOWED_SPENDER); + assertEq(isApprovedForAllResponseCode, HederaResponseCodes.SUCCESS); + assertFalse(isApproved); + } + + function test_HTS_getTokenCustomFees_should_return_custom_fees_for_valid_token() external { + ( + int64 responseCode, + HtsSystemContract.FixedFee[] memory fixedFees, + HtsSystemContract.FractionalFee[] memory fractionalFees, + HtsSystemContract.RoyaltyFee[] memory royaltyFees + ) = HtsSystemContract(HTS_ADDRESS).getTokenCustomFees(CTCF); + assertEq(responseCode, HederaResponseCodes.SUCCESS); + + assertEq(fixedFees.length, 3); + + assertEq(fixedFees[0].feeCollector, 0xa3612A87022a4706FC9452C50abd2703ac4Fd7d9); + assertEq(fixedFees[0].amount, 1); + assertEq(fixedFees[0].tokenId, address(0)); + assertEq(fixedFees[0].useHbarsForPayment, true); + assertEq(fixedFees[0].useCurrentTokenForPayment, false); + + assertEq(fixedFees[1].feeCollector, 0x0000000000000000000000000000000000000D89); + assertEq(fixedFees[1].amount, 2); + assertEq(fixedFees[1].tokenId, 0x0000000000000000000000000000000000068cDa); + assertEq(fixedFees[1].useHbarsForPayment, false); + assertEq(fixedFees[1].useCurrentTokenForPayment, false); + + assertEq(fixedFees[2].feeCollector, 0xa3612A87022a4706FC9452C50abd2703ac4Fd7d9); + assertEq(fixedFees[2].amount, 3); + assertEq(fixedFees[2].tokenId, CTCF); + assertEq(fixedFees[2].useHbarsForPayment, false); + assertEq(fixedFees[2].useCurrentTokenForPayment, true); + + assertEq(fractionalFees.length, 2); + + assertEq(fractionalFees[0].netOfTransfers, false); + assertEq(fractionalFees[0].numerator, 1); + assertEq(fractionalFees[0].denominator, 100); + assertEq(fractionalFees[0].minimumAmount, 3); + assertEq(fractionalFees[0].maximumAmount, 4); + assertEq(fractionalFees[0].feeCollector, 0xa3612A87022a4706FC9452C50abd2703ac4Fd7d9); + + assertEq(fractionalFees[1].netOfTransfers, true); + assertEq(fractionalFees[1].numerator, 5); + assertEq(fractionalFees[1].denominator, 100); + assertEq(fractionalFees[1].minimumAmount, 3); + assertEq(fractionalFees[1].maximumAmount, 4); + assertEq(fractionalFees[1].feeCollector, 0xa3612A87022a4706FC9452C50abd2703ac4Fd7d9); + + assertEq(royaltyFees.length, 0); + } + + function test_HTS_getTokenDefaultFreezeStatus_should_return_correct_value_for_valid_token() external { + address token = CFNFTFF; + (int64 freezeStatus, bool defaultFreeze) = HtsSystemContract(HTS_ADDRESS).getTokenDefaultFreezeStatus(token); + assertEq(freezeStatus, HederaResponseCodes.SUCCESS); + assertFalse(defaultFreeze); + } + + function test_HTS_getTokenDefaultKycStatus_should_return_correct_value_for_valid_token() external { + address token = CFNFTFF; + (int64 kycStatus, bool defaultKyc) = HtsSystemContract(HTS_ADDRESS).getTokenDefaultKycStatus(token); + assertEq(kycStatus, HederaResponseCodes.SUCCESS); + assertFalse(defaultKyc); + } + + function test_HTS_getTokenExpiryInfo_should_return_correct_value_for_valid_token() external { + (int64 expiryStatusCode, HtsSystemContract.Expiry memory expiry) + = HtsSystemContract(HTS_ADDRESS).getTokenExpiryInfo(USDC); + assertEq(expiryStatusCode, HederaResponseCodes.SUCCESS); + assertEq(expiry.second, 1706825707000718000); + assertEq(expiry.autoRenewAccount, address(0)); + assertEq(expiry.autoRenewPeriod, 0); + } + + function test_HTS_getTokenKey_should_return_correct_key_value() external { + address token = USDC; + + // AdminKey + (int64 adminKeyStatusCode, HtsSystemContract.KeyValue memory adminKey) + = HtsSystemContract(HTS_ADDRESS).getTokenKey(token, 0x1); + assertEq(adminKeyStatusCode, HederaResponseCodes.SUCCESS); + assertEq(adminKey.inheritAccountKey, false); + assertEq(adminKey.contractId, address(0)); + assertEq(adminKey.ed25519, hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc"); + assertEq(adminKey.ECDSA_secp256k1, bytes("")); + assertEq(adminKey.delegatableContractId, address(0)); + // FreezeKey + (int64 freezeKeyStatusCode, HtsSystemContract.KeyValue memory freezeKey) + = HtsSystemContract(HTS_ADDRESS).getTokenKey(token, 0x4); + assertEq(freezeKeyStatusCode, HederaResponseCodes.SUCCESS); + assertEq(freezeKey.inheritAccountKey, false); + assertEq(freezeKey.contractId, address(0)); + assertEq(freezeKey.ed25519, hex"baa2dd1684d8445d41b22f2b2c913484a7d885cf25ce525f8bf3fe8d5c8cb85d"); + assertEq(freezeKey.ECDSA_secp256k1, bytes("")); + assertEq(freezeKey.delegatableContractId, address(0)); + // SupplyKey + (int64 supplyKeyStatusCode, HtsSystemContract.KeyValue memory supplyKey) + = HtsSystemContract(HTS_ADDRESS).getTokenKey(token, 0x10); + assertEq(supplyKeyStatusCode, HederaResponseCodes.SUCCESS); + assertEq(supplyKey.inheritAccountKey, false); + assertEq(supplyKey.contractId, address(0)); + assertEq(supplyKey.ed25519, hex"4e4658983980d1b25a634eeeb26cb2b0f0e2e9c83263ba5b056798d35f2139a8"); + assertEq(supplyKey.ECDSA_secp256k1, bytes("")); + assertEq(supplyKey.delegatableContractId, address(0)); + } + + function test_HTS_getTokenType_should_return_correct_token_type_for_existing_token() external { + (int64 ftTypeStatusCode, int32 ftType) = HtsSystemContract(HTS_ADDRESS).getTokenType(USDC); + assertEq(ftTypeStatusCode, HederaResponseCodes.SUCCESS); + assertEq(ftType, int32(0)); + + (int64 nftTypeStatusCode, int32 nftType) = HtsSystemContract(HTS_ADDRESS).getTokenType(CFNFTFF); + assertEq(nftTypeStatusCode, HederaResponseCodes.SUCCESS); + assertEq(nftType, int32(1)); + } + + function test_HTS_isToken_should_return_correct_is_token_info() external { + (int64 ftIsTokenStatusCode, bool ftIsToken) = HtsSystemContract(HTS_ADDRESS).isToken(USDC); + assertEq(ftIsTokenStatusCode, HederaResponseCodes.SUCCESS); + assertTrue(ftIsToken); + + (int64 nftIsTokenStatusCode, bool nftIsToken) = HtsSystemContract(HTS_ADDRESS).isToken(CFNFTFF); + assertEq(nftIsTokenStatusCode, HederaResponseCodes.SUCCESS); + assertTrue(nftIsToken); + + (int64 accountIsTokenCode, bool accountIsToken) = HtsSystemContract(HTS_ADDRESS).isToken(CFNFTFF_TREASURY); + assertEq(accountIsTokenCode, HederaResponseCodes.SUCCESS); + assertFalse(accountIsToken); + + (int64 randomIsTokenCode, bool randomIsToken) = HtsSystemContract(HTS_ADDRESS).isToken(address(123)); + assertEq(randomIsTokenCode, HederaResponseCodes.SUCCESS); + assertFalse(randomIsToken); + } + + function test_HTS_associations_with_correct_privileges() external { + address bob = CFNFTFF_TREASURY; + vm.startPrank(bob); // https://book.getfoundry.sh/cheatcodes/prank + assertFalse(IHRC719(USDC).isAssociated()); + + // Associate the token. + int64 associationResponseCode = HtsSystemContract(HTS_ADDRESS).associateToken(bob, USDC); + assertEq(associationResponseCode, HederaResponseCodes.SUCCESS); + assertTrue(IHRC719(USDC).isAssociated()); + + // Dissociate this token. + int64 dissociationResponseCode = HtsSystemContract(HTS_ADDRESS).dissociateToken(bob, USDC); + assertEq(dissociationResponseCode, HederaResponseCodes.SUCCESS); + assertFalse(IHRC719(USDC).isAssociated()); + + // Associate multiple tokens at once. + assertFalse(IHRC719(MFCT).isAssociated()); + + address[] memory tokens = new address[](2); + tokens[0] = USDC; + tokens[1] = MFCT; + int64 multiAssociateResponseCode = HtsSystemContract(HTS_ADDRESS).associateTokens(bob, tokens); + assertEq(multiAssociateResponseCode, HederaResponseCodes.SUCCESS); + assertTrue(IHRC719(USDC).isAssociated()); + assertTrue(IHRC719(MFCT).isAssociated()); + + // Dissociate multiple tokens at once. + int64 multiDissociateResponseCode = HtsSystemContract(HTS_ADDRESS).dissociateTokens(bob, tokens); + assertEq(multiDissociateResponseCode, HederaResponseCodes.SUCCESS); + assertFalse(IHRC719(USDC).isAssociated()); + assertFalse(IHRC719(MFCT).isAssociated()); + + vm.stopPrank(); + } + + function test_HTS_associations_without_correct_privileges() external { + address bob = CFNFTFF_TREASURY; + vm.expectRevert(); + HtsSystemContract(HTS_ADDRESS).associateToken(bob, USDC); + } + + function test_HTS_dissociation_without_correct_privileges() external { + address bob = CFNFTFF_TREASURY; + vm.expectRevert(); + HtsSystemContract(HTS_ADDRESS).dissociateToken(bob, USDC); + } + + function test_HTS_mass_associations_without_correct_privileges() external { + address bob = CFNFTFF_TREASURY; + address[] memory tokens = new address[](2); + tokens[0] = USDC; + tokens[1] = MFCT; + vm.expectRevert(); + HtsSystemContract(HTS_ADDRESS).associateTokens(bob, tokens); + } + + function test_HTS_mass_dissociation_without_correct_privileges() external { + address bob = CFNFTFF_TREASURY; + address[] memory tokens = new address[](2); + tokens[0] = USDC; + tokens[1] = MFCT; + vm.expectRevert(); + HtsSystemContract(HTS_ADDRESS).dissociateTokens(bob, tokens); + } + + function test_HTS_get_fungible_token_info() external { + (int64 fungibleResponseCode, HtsSystemContract.FungibleTokenInfo memory fungibleTokenInfo) + = HtsSystemContract(HTS_ADDRESS).getFungibleTokenInfo(USDC); + assertEq(fungibleResponseCode, HederaResponseCodes.SUCCESS); + assertEq(fungibleTokenInfo.decimals, 6); + assertEq(fungibleTokenInfo.tokenInfo.token.name, "USD Coin"); + assertEq(fungibleTokenInfo.tokenInfo.token.symbol, "USDC"); + assertEq(fungibleTokenInfo.tokenInfo.token.treasury, address(0x0000000000000000000000000000000000001438)); + assertEq(fungibleTokenInfo.tokenInfo.token.memo, "USDC HBAR"); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenSupplyType, false); + assertEq(fungibleTokenInfo.tokenInfo.token.maxSupply, 0); + assertEq(fungibleTokenInfo.tokenInfo.token.freezeDefault, false); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys.length, 7); + + // AdminKey + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].keyType, 0x1); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.inheritAccountKey, false); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.contractId, address(0)); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.ed25519, hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc"); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.ECDSA_secp256k1, bytes("")); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.delegatableContractId, address(0)); + // FreezeKey + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].keyType, 0x4); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.inheritAccountKey, false); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.contractId, address(0)); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.ed25519, hex"baa2dd1684d8445d41b22f2b2c913484a7d885cf25ce525f8bf3fe8d5c8cb85d"); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.ECDSA_secp256k1, bytes("")); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[2].key.delegatableContractId, address(0)); + // SupplyKey + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].keyType, 0x10); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.inheritAccountKey, false); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.contractId, address(0)); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.ed25519, hex"4e4658983980d1b25a634eeeb26cb2b0f0e2e9c83263ba5b056798d35f2139a8"); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.ECDSA_secp256k1, bytes("")); + assertEq(fungibleTokenInfo.tokenInfo.token.tokenKeys[4].key.delegatableContractId, address(0)); + // Expiry + assertEq(fungibleTokenInfo.tokenInfo.token.expiry.second, 1706825707000718000); + assertEq(fungibleTokenInfo.tokenInfo.token.expiry.autoRenewAccount, address(0)); + assertEq(fungibleTokenInfo.tokenInfo.token.expiry.autoRenewPeriod, 0); + assertEq(fungibleTokenInfo.tokenInfo.totalSupply, 10000000005000000); + assertEq(fungibleTokenInfo.tokenInfo.deleted, false); + assertEq(fungibleTokenInfo.tokenInfo.defaultKycStatus, false); + assertEq(fungibleTokenInfo.tokenInfo.pauseStatus, false); + assertEq(fungibleTokenInfo.tokenInfo.fixedFees.length, 0); + assertEq(fungibleTokenInfo.tokenInfo.fractionalFees.length, 0); + assertEq(fungibleTokenInfo.tokenInfo.royaltyFees.length, 0); + assertEq(fungibleTokenInfo.tokenInfo.ledgerId, testMode == TestMode.FFI ? "0x01" : "0x00"); + } + + function test_HTS_get_non_fungible_token_info() external { + (int64 nonFungibleResponseCode, HtsSystemContract.NonFungibleTokenInfo memory nonFungibleTokenInfo) + = HtsSystemContract(HTS_ADDRESS).getNonFungibleTokenInfo(CFNFTFF, int64(1)); + assertEq(nonFungibleResponseCode, HederaResponseCodes.SUCCESS); + assertEq(nonFungibleTokenInfo.serialNumber, int64(1)); + assertEq(nonFungibleTokenInfo.ownerId, CFNFTFF_TREASURY); + assertEq(nonFungibleTokenInfo.spenderId, CFNFTFF_ALLOWED_SPENDER); + assertEq(nonFungibleTokenInfo.tokenInfo.token.name, "Custom Fee NFT (Fixed Fee)"); + assertEq(nonFungibleTokenInfo.tokenInfo.token.symbol, "CFNFTFF"); + assertEq(nonFungibleTokenInfo.tokenInfo.token.treasury, CFNFTFF_TREASURY); + assertEq(nonFungibleTokenInfo.tokenInfo.token.memo, ""); + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenSupplyType, true); + assertEq(nonFungibleTokenInfo.tokenInfo.token.maxSupply, 2); + assertEq(nonFungibleTokenInfo.tokenInfo.token.freezeDefault, false); + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys.length, 7); + + // AdminKey + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].keyType, 0x1); + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.inheritAccountKey, false); + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.contractId, address(0)); + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.ed25519, bytes("")); + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.ECDSA_secp256k1, hex"0242b7c3beea2af6dfcc874c41d1332463407e283f602ce8ef2cbe324823561b6f"); + assertEq(nonFungibleTokenInfo.tokenInfo.token.tokenKeys[0].key.delegatableContractId, address(0)); + + // Expiry + assertEq(nonFungibleTokenInfo.tokenInfo.token.expiry.autoRenewAccount, address(0)); + assertEq(nonFungibleTokenInfo.tokenInfo.totalSupply, 2); + assertEq(nonFungibleTokenInfo.tokenInfo.deleted, false); + assertEq(nonFungibleTokenInfo.tokenInfo.defaultKycStatus, false); + assertEq(nonFungibleTokenInfo.tokenInfo.pauseStatus, false); + assertEq(nonFungibleTokenInfo.tokenInfo.fixedFees.length, 0); + assertEq(nonFungibleTokenInfo.tokenInfo.fractionalFees.length, 0); + assertEq(nonFungibleTokenInfo.tokenInfo.royaltyFees.length, 0); + assertEq(nonFungibleTokenInfo.tokenInfo.ledgerId, testMode == TestMode.FFI ? "0x01" : "0x00"); + } + + function test_HTS_transferToken() external { + // https://hashscan.io/testnet/account/0.0.1421 + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + address to = makeAddr("bob"); + uint256 amount = 4_000000; + + uint256 balanceOfOwner = IERC20(USDC).balanceOf(owner); + assertGt(balanceOfOwner, 0); + assertEq(IERC20(USDC).balanceOf(to), 0); + + vm.prank(owner); + vm.expectEmit(USDC); + emit IERC20Events.Transfer(owner, to, amount); + IHederaTokenService(HTS_ADDRESS).transferToken(USDC, owner, to, int64(int256(amount))); + + assertEq(IERC20(USDC).balanceOf(owner), balanceOfOwner - amount); + assertEq(IERC20(USDC).balanceOf(to), amount); + } + + function test_HTS_transferFrom() external { + // https://hashscan.io/testnet/account/0.0.1421 + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + address to = makeAddr("bob"); + uint256 amount = 4_000000; + + uint256 balanceOfOwner = IERC20(USDC).balanceOf(owner); + assertGt(balanceOfOwner, 0); + assertEq(IERC20(USDC).balanceOf(to), 0); + + vm.prank(owner); + vm.expectEmit(USDC); + emit IERC20Events.Transfer(owner, to, amount); + IHederaTokenService(HTS_ADDRESS).transferFrom(USDC, owner, to, amount); + + assertEq(IERC20(USDC).balanceOf(owner), balanceOfOwner - amount); + assertEq(IERC20(USDC).balanceOf(to), amount); + } + + function test_HTS_transferToken_insufficient_balance() external { + // https://hashscan.io/testnet/account/0.0.1421 + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + uint256 amount = 300_000000; + address to = makeAddr("bob"); + uint256 balanceOfOwner = IERC20(USDC).balanceOf(owner); + assertGt(balanceOfOwner, 0); + assertEq(IERC20(USDC).balanceOf(to), 0); + vm.prank(owner); + vm.expectRevert(); + IHederaTokenService(HTS_ADDRESS).transferFrom(USDC, owner, to, amount); + } + + function test_HTS_transferNFT() external { + address to = makeAddr("recipient"); + uint256 serialId = 1; + vm.startPrank(CFNFTFF_TREASURY); + vm.expectEmit(CFNFTFF); + emit IERC20Events.Transfer(CFNFTFF_TREASURY, to, serialId); + IHederaTokenService(HTS_ADDRESS).transferNFT(CFNFTFF, CFNFTFF_TREASURY, to, int64(int256(serialId))); + vm.stopPrank(); + assertEq(IERC721(CFNFTFF).ownerOf(serialId), to); + } + + function test_HTS_transferFromNFT() external { + address to = makeAddr("recipient"); + uint256 serialId = 1; + vm.startPrank(CFNFTFF_TREASURY); + vm.expectEmit(CFNFTFF); + emit IERC20Events.Transfer(CFNFTFF_TREASURY, to, serialId); + IHederaTokenService(HTS_ADDRESS).transferFromNFT(CFNFTFF, CFNFTFF_TREASURY, to, serialId); + vm.stopPrank(); + assertEq(IERC721(CFNFTFF).ownerOf(serialId), to); + } + + function test_HTS_transferNFTs_for_allowed_user() external { + uint256[] memory serialId = new uint256[](1); + serialId[0] = 1; + address[] memory from = new address[](1); + from[0] = CFNFTFF_TREASURY; + address[] memory to = new address[](1); + to[0] = makeAddr("recipient"); + vm.startPrank(CFNFTFF_TREASURY); + vm.expectEmit(CFNFTFF); + emit IERC20Events.Transfer(CFNFTFF_TREASURY, to[0], serialId[0]); + IHederaTokenService(HTS_ADDRESS).transferNFT(CFNFTFF, from[0], to[0], int64(int256(serialId[0]))); + vm.stopPrank(); + assertEq(IERC721(CFNFTFF).ownerOf(serialId[0]), to[0]); + } + + function test_HTS_transferNFT_fail_when_not_allowed() external { + address from = makeAddr("bob"); + address to = makeAddr("recipient"); + uint256 serialId = 2; + vm.startPrank(from); + vm.expectRevert(); + IHederaTokenService(HTS_ADDRESS).transferNFT(CFNFTFF, from, to, int64(int256(serialId))); + vm.stopPrank(); + } + + function test_HTS_allowance_from_remote() external { + // https://hashscan.io/testnet/account/0.0.4233295 + address owner = address(0x000000000000000000000000000000000040984F); + // https://hashscan.io/testnet/account/0.0.1335 + address spender = 0x0000000000000000000000000000000000000537; + (int64 responseCode, uint256 allowance) = IHederaTokenService(HTS_ADDRESS).allowance(USDC, owner, spender); + assertEq(responseCode, HederaResponseCodes.SUCCESS); + assertEq(allowance, 5_000000); + } + + function test_HTS_allowance_empty() external { + // https://hashscan.io/testnet/account/0.0.4233295 + address owner = address(0x000000000000000000000000000000000040984F); + address spender = makeAddr("alice"); + (int64 responseCode, uint256 allowance) = IHederaTokenService(HTS_ADDRESS).allowance(USDC, owner, spender); + assertEq(responseCode, HederaResponseCodes.SUCCESS); + assertEq(allowance, 0); + } + + function test_HTS_approveNFT() external { + address token = CFNFTFF; + address newSpender = makeAddr("NEW_SPENDER"); + assertNotEq(IERC721(token).getApproved(1), newSpender); + vm.prank(CFNFTFF_TREASURY); + vm.expectEmit(token); + emit IERC20Events.Approval(CFNFTFF_TREASURY, newSpender, 1); + int64 responseCodeApprove = HtsSystemContract(HTS_ADDRESS).approveNFT(token, newSpender, 1); + assertEq(responseCodeApprove, HederaResponseCodes.SUCCESS); + assertEq(IERC721(token).getApproved(1), newSpender); + } + + function test_HTS_approve() external { + address owner = makeAddr("alice"); + address spender = makeAddr("bob"); + uint256 amount = 4_000000; + assertEq(IERC20(USDC).allowance(owner, spender), 0); + vm.prank(owner); + vm.expectEmit(USDC); + emit IERC20Events.Approval(owner, spender, amount); + int64 responseCodeApprove = HtsSystemContract(HTS_ADDRESS).approve(USDC, spender, amount); + assertEq(responseCodeApprove, HederaResponseCodes.SUCCESS); + assertEq(IERC20(USDC).allowance(owner, spender), amount); + } + + function test_HTS_setIsApprovedForAll() external { + address operator = makeAddr("operator"); + assertFalse(IERC721(CFNFTFF).isApprovedForAll(CFNFTFF_TREASURY, operator)); + vm.prank(CFNFTFF_TREASURY); + vm.expectEmit(CFNFTFF); + emit IERC721Events.ApprovalForAll(CFNFTFF_TREASURY, operator, true); + int64 setApprovalForAllResponseCode = HtsSystemContract(HTS_ADDRESS) + .setApprovalForAll(CFNFTFF, operator, true); + assertEq(setApprovalForAllResponseCode, HederaResponseCodes.SUCCESS); + assertTrue(IERC721(CFNFTFF).isApprovedForAll(CFNFTFF_TREASURY, operator)); + } }