diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index b9e7043..df40f8d 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 {IERC165} from "../lib/forge-std/src/interfaces/IERC165.sol"; address constant HTS_ADDRESS = address(0x167); @@ -48,6 +49,265 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { (responseCode, tokenInfo) = IHederaTokenService(token).getTokenInfo(token); } + function getNonFungibleTokenInfo(address token, int64 serialNumber) + htsCall external + returns (int64, NonFungibleTokenInfo memory) { + require(token != address(0), "getNonFungibleTokenInfo: invalid token"); + + (int64 responseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token); + require(responseCode == 22, "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 getFungibleTokenInfo(address token) htsCall external returns (int64, FungibleTokenInfo memory) { + require(token != address(0), "getFungibleTokenInfo: invalid token"); + + (int64 responseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token); + require(responseCode == 22, "getFungibleTokenInfo: failed to get token data"); + FungibleTokenInfo memory fungibleTokenInfo; + fungibleTokenInfo.tokenInfo = tokenInfo; + fungibleTokenInfo.decimals = int32(int8(IERC20(token).decimals())); + + return (responseCode, fungibleTokenInfo); + } + + 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 == 22, "associateTokens: Failed to associate token"); + } + responseCode = 22; // HederaResponseCodes.SUCCESS + } + + function transferTokens( + address token, + address[] memory accountId, + int64[] memory amount + ) htsCall public returns (int64 responseCode) { + require(token != address(0), "transferTokens: invalid token"); + require(accountId.length > 0, "transferTokens: missing recipients"); + require(amount.length == accountId.length, "transferTokens: inconsistent input"); + + int64 total = 0; + for (uint256 i = 0; i < accountId.length; i++) { + total += amount[i]; + } + require(total == 0, "transferTokens: total amount must balance"); + + for (uint256 from = 0; from < amount.length; from++) { + if (amount[from] >= 0) { + continue; + } + for (uint256 to = 0; to < amount.length; to++) { + if (amount[to] <= 0) { + continue; + } + int64 transferAmount = amount[to] < -amount[from] ? amount[to] : -amount[from]; + transferToken(token, accountId[from], accountId[to], transferAmount); + amount[from] += transferAmount; + amount[to] -= transferAmount; + if (amount[from] == 0) { + break; + } + } + } + for (uint256 i = 0; i < amount.length; i++) { // Ensure all amounts are fully balanced after processing + require(amount[i] == 0, "transferTokens: unmatched transfers"); + } + responseCode = 22; // 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 transferFromNFT( + address token, + address from, + address to, + uint256 serialNumber + ) htsCall external returns (int64) { + return transferNFT(token, from, to, int64(int256(serialNumber))); + } + + 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)._transferAsHTS(from, to, uint256(uint64(amount))); + responseCode = 22; // HederaResponseCodes.SUCCESS + } + + function approve(address token, address spender, uint256 amount) htsCall public returns (int64 responseCode) { + HtsSystemContract(token).approve(msg.sender, spender, amount); + responseCode = 22; // HederaResponseCodes.SUCCESS + } + + function approveNFT(address token, address approved, uint256 serialNumber) htsCall public returns (int64 responseCode) { + HtsSystemContract(token).approveNFT(msg.sender, approved, serialNumber); + responseCode = 22; // HederaResponseCodes.SUCCESS + } + + function setApprovalForAll( + address token, + address operator, + bool approved + ) htsCall external returns (int64 responseCode) { + HtsSystemContract(token).setApprovalForAll(msg.sender, operator, approved); + responseCode = 22; // HederaResponseCodes.SUCCESS + } + + 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 = 22; // HederaResponseCodes.SUCCESS + } + + function transferNFT( + address token, + address sender, + address recipient, + int64 serialNumber + ) htsCall public returns (int64 responseCode) { + uint256 serialId = uint256(uint64(serialNumber)); + require( + IERC721(token).ownerOf(serialId) == msg.sender || + IERC721(token).getApproved(serialId) == msg.sender || + IERC721(token).isApprovedForAll(sender, msg.sender), + "transferNFT: unauthorized" + ); + HtsSystemContract(token)._transferNFTAsHTS(sender, recipient, serialId); + responseCode = 22; // HederaResponseCodes.SUCCESS + } + + 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 == 22, "dissociateTokens: Failed to dissociate token"); + } + responseCode = 22; // 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 dissociateToken(address account, address token) htsCall external returns (int64 responseCode) { + address[] memory tokens = new address[](1); + tokens[0] = token; + return dissociateTokens(account, tokens); + } + + function getTokenExpiryInfo(address token) htsCall external returns (int64 responseCode, Expiry memory tokenInfo) { + require(token != address(0), "getTokenExpiryInfo: invalid token"); + + (responseCode, tokenInfo) = IHederaTokenService(token).getTokenExpiryInfo(token); + } + + function getApproved(address token, uint256 serialNumber) + htsCall external view returns (int64 responseCode, address approved) { + require(token != address(0), "getApproved: invalid token"); + (responseCode, approved) = (int64(22), IERC721(token).getApproved(serialNumber)); + } + + function isApprovedForAll( + address token, + address owner, + address operator + ) htsCall external view returns (int64, bool) { + require(token != address(0), "isApprovedForAll: invalid token"); + return (int64(22), IERC721(token).isApprovedForAll(owner, operator)); + } + + function getTokenDefaultFreezeStatus(address token) htsCall external returns (int64, bool) { + require(token != address(0), "getTokenDefaultFreezeStatus: invalid address"); + return IHederaTokenService(token).getTokenDefaultFreezeStatus(token); + } + + function getTokenCustomFees( + address token + ) htsCall external returns (int64, FixedFee[] memory, FractionalFee[] memory, RoyaltyFee[] memory) { + require(token != address(0), "getTokenCustomFees: invalid token"); + return IHederaTokenService(token).getTokenCustomFees(token); + } + + function getTokenDefaultKycStatus(address token) htsCall external returns (int64, bool) { + require(token != address(0), "getTokenDefaultKycStatus: invalid address"); + return IHederaTokenService(token).getTokenDefaultKycStatus(token); + } + + function getTokenKey(address token, uint keyType) htsCall external returns (int64, KeyValue memory) { + require(token != address(0), "getTokenKey: invalid token"); + (int64 responseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token); + require(responseCode == 22, "getTokenKey: failed to get token data"); + for (uint256 i = 0; i < tokenInfo.token.tokenKeys.length; i++) { + if (tokenInfo.token.tokenKeys[i].keyType == keyType) { + return (22, tokenInfo.token.tokenKeys[i].key); + } + } + KeyValue memory emptyKey; + return (22, emptyKey); + } + + function getTokenType(address token) htsCall external returns (int64, int32) { + require(token != address(0), "getTokenType: invalid address"); + return IHederaTokenService(token).getTokenType(token); + } + + function isToken(address token) external returns (int64, bool) { + bytes memory payload = abi.encodeWithSignature("getTokenType(address)", token); + (bool success, bytes memory returnData) = token.call(payload); + return (22, success && returnData.length > 0); + } + function mintToken(address token, int64 amount, bytes[] memory) htsCall external returns ( int64 responseCode, int64 newTotalSupply, @@ -175,6 +435,87 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { require(msg.data.length >= 28, "getTokenInfo: Not enough calldata"); return abi.encode(22, _tokenInfo); } + if (selector == this.getTokenCustomFees.selector) { + require(msg.data.length >= 28, "getTokenCustomFees: Not enough calldata"); + return abi.encode(22, _tokenInfo.fixedFees, _tokenInfo.fractionalFees, _tokenInfo.royaltyFees); + } + if (selector == this.getTokenDefaultKycStatus.selector) { + require(msg.data.length >= 28, "getTokenDefaultKycStatus: Not enough calldata"); + return abi.encode(22, _tokenInfo.defaultKycStatus); + } + if (selector == this.getTokenDefaultFreezeStatus.selector) { + require(msg.data.length >= 28, "getTokenDefaultFreezeStatus: Not enough calldata"); + return abi.encode(22, _tokenInfo.token.freezeDefault); + } + if (selector == this.getTokenExpiryInfo.selector) { + require(msg.data.length >= 28, "getTokenExpiryInfo: Not enough calldata"); + return abi.encode(22, _tokenInfo.token.expiry); + } + 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(22); + } + 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(22); + } + 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(22, int32(0)); + } + if (keccak256(abi.encodePacked(tokenType)) == keccak256("NON_FUNGIBLE_UNIQUE")) { + return abi.encode(22, int32(1)); + } + return abi.encode(22, int32(-1)); + } + if (selector == this._transferAsHTS.selector) { + require(msg.data.length >= 124, "transferAsHTS: 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])); + _transferAsHTS(from, to, amount); + return abi.encode(true); + } + if (selector == this._transferNFTAsHTS.selector) { + require(msg.data.length >= 124, "transferNFTAsHTS: 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])); + _transferNFTAsHTS(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])); + _approveAsHTS(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; + _setApprovalForAllAsHTS(from, to, approved); + return abi.encode(true); + } if (selector == this._update.selector) { require(msg.data.length >= 124, "update: Not enough calldata"); address from = address(bytes20(msg.data[40:60])); @@ -417,9 +758,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,6 +770,16 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { emit Transfer(from, to, amount); } + function _transferAsHTS(address from, address to, uint256 amount) public { + require(msg.sender == HTS_ADDRESS, "hts: not permitted"); + _transfer(from, to, amount); + } + + function _transferNFTAsHTS(address from, address to, uint256 serialId) public { + require(msg.sender == HTS_ADDRESS, "hts: not permitted"); + _transferNFT(from, to, serialId); + } + function _transferNFT(address from, address to, uint256 serialId) private { require(from != address(0), "hts: invalid sender"); require(to != address(0), "hts: invalid receiver"); @@ -439,9 +790,14 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { assembly { owner := sload(slot) } require(owner == from, "hts: sender is not owner"); + address sender = msg.sender; // 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 == HTS_ADDRESS) { + sender = from; + require(sender == owner || __isApprovedForAll(owner, sender), "hts: unauthorized"); + } + if (sender != owner) { + require(sender == __getApproved(serialId) || __isApprovedForAll(from, sender), "hts: unauthorized"); } // Clear approval @@ -488,6 +844,28 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { assembly { sstore(allowanceSlot, amount) } } + function _approveAsHTS(address caller, address spender, uint256 serialId, bool isApproved) private { + require(msg.sender == HTS_ADDRESS, "_approveAsHTS: only allowed for HTS"); + // The caller must own the token or be an approved operator. + address owner = __ownerOf(serialId); + require(caller == owner || __getApproved(serialId) == caller || __isApprovedForAll(owner, caller), "_approveAsHTS: unauthorized"); + + bytes32 slot = _getApprovedSlot(uint32(serialId)); + address newApproved = isApproved ? spender : address(0); + assembly { sstore(slot, newApproved) } + + emit Approval(owner, spender, serialId); + } + + + function _setApprovalForAllAsHTS(address owner, address operator, bool approved) private { + require(msg.sender == HTS_ADDRESS, "_setApprovalForAllAsHTS: only allowed for HTS"); + require(operator != address(0) && operator != owner, "_setApprovalForAllAsHTS: invalid operator"); + bytes32 slot = _isApprovedForAllSlot(owner, operator); + assembly { sstore(slot, approved) } + emit ApprovalForAll(owner, operator, approved); + } + function _approve(address spender, uint256 serialId, bool isApproved) private { // The caller must own the token or be an approved operator. address owner = __ownerOf(serialId); diff --git a/contracts/IHederaTokenService.sol b/contracts/IHederaTokenService.sol index c8d618d..5958f64 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 @@ -305,7 +305,7 @@ interface IHederaTokenService { /// @param tokenTransfers the list of token transfers to do /// @custom:version 0.3.0 the signature of the previous version was cryptoTransfer(TokenTransferList[] memory tokenTransfers) // function cryptoTransfer(TransferList memory transferList, TokenTransferList[] memory tokenTransfers) - // external + // payable external // returns (int64 responseCode); /// Mints an amount of the token to the defined treasury account @@ -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 @@ -462,23 +462,23 @@ interface IHederaTokenService { /// @param token The ID of the token as a solidity address /// @param accountId account to do a transfer to/from /// @param amount The amount from the accountId at the same index - // function transferTokens( - // address token, - // address[] memory accountId, - // int64[] memory amount - // ) external returns (int64 responseCode); + function transferTokens( + address token, + address[] memory accountId, + int64[] memory amount + ) external returns (int64 responseCode); /// Initiates a Non-Fungable Token Transfer /// @param token The ID of the token as a solidity address /// @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 @@ -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/test/HTS.t.sol b/test/HTS.t.sol index f160507..f0717f2 100644 --- a/test/HTS.t.sol +++ b/test/HTS.t.sol @@ -4,7 +4,9 @@ pragma solidity ^0.8.0; import {Test} from "forge-std/Test.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 { @@ -134,7 +136,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); @@ -303,4 +305,479 @@ 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, 22); + 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, 22); + 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, 22); + 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, 22); + + 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, 22); + 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, 22); + 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, 22); + 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, 22); + 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, 22); + 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, 22); + 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(22, ftTypeStatusCode); + assertEq(ftType, int32(0)); + + (int64 nftTypeStatusCode, int32 nftType) = HtsSystemContract(HTS_ADDRESS).getTokenType(CFNFTFF); + assertEq(22, nftTypeStatusCode); + 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(22, ftIsTokenStatusCode); + assertTrue(ftIsToken); + + (int64 nftIsTokenStatusCode, bool nftIsToken) = HtsSystemContract(HTS_ADDRESS).isToken(CFNFTFF); + assertEq(22, nftIsTokenStatusCode); + assertTrue(nftIsToken); + + (int64 accountIsTokenCode, bool accountIsToken) = HtsSystemContract(HTS_ADDRESS).isToken(CFNFTFF_TREASURY); + assertEq(22, accountIsTokenCode); + assertFalse(accountIsToken); + + (int64 randomIsTokenCode, bool randomIsToken) = HtsSystemContract(HTS_ADDRESS).isToken(address(123)); + assertEq(22, randomIsTokenCode); + 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, 22); + assertTrue(IHRC719(USDC).isAssociated()); + + // Dissociate this token. + int64 dissociationResponseCode = HtsSystemContract(HTS_ADDRESS).dissociateToken(bob, USDC); + assertEq(dissociationResponseCode, 22); + 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, 22); + assertTrue(IHRC719(USDC).isAssociated()); + assertTrue(IHRC719(MFCT).isAssociated()); + + // Dissociate multiple tokens at once. + int64 multiDissociateResponseCode = HtsSystemContract(HTS_ADDRESS).dissociateTokens(bob, tokens); + assertEq(multiDissociateResponseCode, 22); + 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, 22); + 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, 22); + 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); // https://book.getfoundry.sh/cheatcodes/prank + 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); // https://book.getfoundry.sh/cheatcodes/prank + 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_transferTokens_with_correct_dataset() external { + // https://hashscan.io/testnet/account/0.0.1421 + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + uint256 amountToBob = 1_000000; + uint256 amountToAlice = 3_000000; + int64[] memory amount = new int64[](3); + amount[0] = -4_000000; + amount[1] = int64(int256(1_000000)); + amount[2] = int64(int256(amountToAlice)); + address[] memory to = new address[](3); + to[0] = owner; + to[1] = makeAddr("bob"); + to[2] = makeAddr("alice"); + uint256 balanceOfOwner = IERC20(USDC).balanceOf(owner); + assertGt(balanceOfOwner, 0); + assertEq(IERC20(USDC).balanceOf(to[1]), 0); + assertEq(IERC20(USDC).balanceOf(to[2]), 0); + vm.prank(owner); // https://book.getfoundry.sh/cheatcodes/prank + vm.expectEmit(USDC); + emit IERC20Events.Transfer(owner, to[1], amountToBob); + emit IERC20Events.Transfer(owner, to[2], amountToAlice); + IHederaTokenService(HTS_ADDRESS).transferTokens(USDC, to, amount); + assertEq(IERC20(USDC).balanceOf(owner), balanceOfOwner - amountToBob - amountToAlice); + assertEq(IERC20(USDC).balanceOf(to[1]), amountToBob); + assertEq(IERC20(USDC).balanceOf(to[2]), amountToAlice); + } + + function test_HTS_transferTokens_insufficient_balance() external { + // https://hashscan.io/testnet/account/0.0.1421 + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + uint256 amountToBob = 1_000000; + uint256 amountToAlice = 300_000000; + int64[] memory amount = new int64[](2); + amount[0] = int64(int256(1_000000)); + amount[1] = int64(int256(amountToAlice)); + address[] memory to = new address[](2); + to[0] = makeAddr("bob"); + to[1] = makeAddr("alice"); + uint256 balanceOfOwner = IERC20(USDC).balanceOf(owner); + assertGt(balanceOfOwner, 0); + assertEq(IERC20(USDC).balanceOf(to[0]), 0); + assertEq(IERC20(USDC).balanceOf(to[1]), 0); + vm.prank(owner); // https://book.getfoundry.sh/cheatcodes/prank + emit IERC20Events.Transfer(owner, to[0], amountToBob); + emit IERC20Events.Transfer(owner, to[1], amountToAlice); + vm.expectRevert(); + IHederaTokenService(HTS_ADDRESS).transferTokens(USDC, 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_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, 22); + 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, 22); + 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, 22); + assertTrue(IERC721(CFNFTFF).isApprovedForAll(CFNFTFF_TREASURY, operator)); + } }