From 2f1afc23203ffc870e525c778d83c3c32480be19 Mon Sep 17 00:00:00 2001 From: Victor Yanev Date: Tue, 12 Nov 2024 18:08:55 +0200 Subject: [PATCH] test: token airdrops Signed-off-by: Victor Yanev --- .../IHederaTokenService.json | 198 ++++++++++++++++++ .../HederaTokenService.sol | 38 +++- .../hedera-token-service/IHRC904.sol | 12 ++ .../IHederaTokenService.sol | 21 ++ .../examples/hrc-904/Airdrop.sol | 175 ++++++++++++++++ .../examples/hrc-904/CancelAirdrop.sol | 65 ++++++ .../examples/hrc-904/ClaimAirdrop.sol | 65 ++++++ .../examples/hrc-904/HRC904Contract.sol | 49 +++++ .../examples/hrc-904/TokenReject.sol | 26 +++ hardhat.config.js | 3 +- test/constants.js | 2 + .../hts-precompile/HtsSystemContractMock.sol | 26 ++- .../token-airdrop/tokenAirdropContract.js | 97 +++++++++ .../hedera-token-service/utils.js | 52 +++++ 14 files changed, 816 insertions(+), 13 deletions(-) create mode 100644 contracts/system-contracts/hedera-token-service/IHRC904.sol create mode 100644 contracts/system-contracts/hedera-token-service/examples/hrc-904/Airdrop.sol create mode 100644 contracts/system-contracts/hedera-token-service/examples/hrc-904/CancelAirdrop.sol create mode 100644 contracts/system-contracts/hedera-token-service/examples/hrc-904/ClaimAirdrop.sol create mode 100644 contracts/system-contracts/hedera-token-service/examples/hrc-904/HRC904Contract.sol create mode 100644 contracts/system-contracts/hedera-token-service/examples/hrc-904/TokenReject.sol create mode 100644 test/system-contracts/hedera-token-service/token-airdrop/tokenAirdropContract.js diff --git a/contracts-abi/contracts/system-contracts/hedera-token-service/IHederaTokenService.sol/IHederaTokenService.json b/contracts-abi/contracts/system-contracts/hedera-token-service/IHederaTokenService.sol/IHederaTokenService.json index d60dc3fe2..d793540b1 100644 --- a/contracts-abi/contracts/system-contracts/hedera-token-service/IHederaTokenService.sol/IHederaTokenService.json +++ b/contracts-abi/contracts/system-contracts/hedera-token-service/IHederaTokenService.sol/IHederaTokenService.json @@ -1,4 +1,79 @@ [ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "components": [ + { + "internalType": "address", + "name": "accountID", + "type": "address" + }, + { + "internalType": "int64", + "name": "amount", + "type": "int64" + }, + { + "internalType": "bool", + "name": "isApproval", + "type": "bool" + } + ], + "internalType": "struct IHederaTokenService.AccountAmount[]", + "name": "transfers", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "senderAccountID", + "type": "address" + }, + { + "internalType": "address", + "name": "receiverAccountID", + "type": "address" + }, + { + "internalType": "int64", + "name": "serialNumber", + "type": "int64" + }, + { + "internalType": "bool", + "name": "isApproval", + "type": "bool" + } + ], + "internalType": "struct IHederaTokenService.NftTransfer[]", + "name": "nftTransfers", + "type": "tuple[]" + } + ], + "internalType": "struct IHederaTokenService.TokenTransferList[]", + "name": "tokenTransfers", + "type": "tuple[]" + } + ], + "name": "airdropTokens", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -173,6 +248,88 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "int64", + "name": "serial", + "type": "int64" + } + ], + "internalType": "struct IHederaTokenService.PendingAirdrop[]", + "name": "pendingAirdrops", + "type": "tuple[]" + } + ], + "name": "cancelAirdrops", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "int64", + "name": "serial", + "type": "int64" + } + ], + "internalType": "struct IHederaTokenService.PendingAirdrop[]", + "name": "pendingAirdrops", + "type": "tuple[]" + } + ], + "name": "claimAirdrops", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -2428,6 +2585,47 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "rejectingAddress", + "type": "address" + }, + { + "internalType": "address[]", + "name": "ftAddresses", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "nft", + "type": "address" + }, + { + "internalType": "int64", + "name": "serial", + "type": "int64" + } + ], + "internalType": "struct IHederaTokenService.NftID[]", + "name": "nftIDs", + "type": "tuple[]" + } + ], + "name": "rejectTokens", + "outputs": [ + { + "internalType": "int64", + "name": "responseCode", + "type": "int64" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/system-contracts/hedera-token-service/HederaTokenService.sol b/contracts/system-contracts/hedera-token-service/HederaTokenService.sol index 92f47946d..524a7834c 100644 --- a/contracts/system-contracts/hedera-token-service/HederaTokenService.sol +++ b/contracts/system-contracts/hedera-token-service/HederaTokenService.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.5.0 <0.9.0; -pragma experimental ABIEncoderV2; -import "../HederaResponseCodes.sol"; -import "./IHederaTokenService.sol"; +import {HederaResponseCodes} from "../HederaResponseCodes.sol"; +import {IHederaTokenService} from "./IHederaTokenService.sol"; +pragma experimental ABIEncoderV2; abstract contract HederaTokenService { address constant precompileAddress = address(0x167); @@ -704,4 +704,36 @@ abstract contract HederaTokenService { abi.encodeWithSelector(IHederaTokenService.updateNonFungibleTokenCustomFees.selector, token, fixedFees, royaltyFees)); responseCode = success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN; } + + function airdropTokens(IHederaTokenService.TokenTransferList[] memory tokenTransfers) + internal returns (int64 responseCode) { + (bool success, bytes memory result) = precompileAddress.call( + abi.encodeWithSelector(IHederaTokenService.airdropTokens.selector, tokenTransfers) + ); + (responseCode) = success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN; + } + + function cancelAirdrops(IHederaTokenService.PendingAirdrop[] memory pendingAirdrops) + internal returns (int64 responseCode) { + (bool success, bytes memory result) = precompileAddress.call( + abi.encodeWithSelector(IHederaTokenService.cancelAirdrops.selector, pendingAirdrops) + ); + (responseCode) = success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN; + } + + function claimAirdrops(IHederaTokenService.PendingAirdrop[] memory pendingAirdrops) + internal returns (int64 responseCode) { + (bool success, bytes memory result) = precompileAddress.call( + abi.encodeWithSelector(IHederaTokenService.claimAirdrops.selector, pendingAirdrops) + ); + (responseCode) = success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN; + } + + function rejectTokens(address rejectingAddress, address[] memory ftAddresses, IHederaTokenService.NftID[] memory nftIds) + internal returns (int64 responseCode) { + (bool success, bytes memory result) = precompileAddress.call( + abi.encodeWithSelector(IHederaTokenService.rejectTokens.selector, rejectingAddress, ftAddresses, nftIds) + ); + (responseCode) = success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN; + } } diff --git a/contracts/system-contracts/hedera-token-service/IHRC904.sol b/contracts/system-contracts/hedera-token-service/IHRC904.sol new file mode 100644 index 000000000..977b6c768 --- /dev/null +++ b/contracts/system-contracts/hedera-token-service/IHRC904.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.4.9 <0.9.0; + +interface IHRC904 { + function cancelAirdropFT(address receiverAddress) external returns (int64 responseCode); + function cancelAirdropNFT(address receiverAddress, int64 serialNumber) external returns (int64 responseCode); + function claimAirdropFT(address senderAddress) external returns (int64 responseCode); + function claimAirdropNFT(address senderAddress, int64 serialNumber) external returns (int64 responseCode); + function rejectTokenFT() external returns (int64 responseCode); + function rejectTokenNFTs(int64[] memory serialNumbers) external returns (int64 responseCode); + function setUnlimitedAutomaticAssociations(bool enableAutoAssociations) external returns (int64 responseCode); +} diff --git a/contracts/system-contracts/hedera-token-service/IHederaTokenService.sol b/contracts/system-contracts/hedera-token-service/IHederaTokenService.sol index 7cfed6928..921245157 100644 --- a/contracts/system-contracts/hedera-token-service/IHederaTokenService.sol +++ b/contracts/system-contracts/hedera-token-service/IHederaTokenService.sol @@ -296,6 +296,19 @@ interface IHederaTokenService { address feeCollector; } + struct PendingAirdrop { + address sender; + address receiver; + + address token; + int64 serial; + } + + struct NftID { + address nft; + int64 serial; + } + /********************** * Direct HTS Calls * **********************/ @@ -815,4 +828,12 @@ interface IHederaTokenService { /// @param royaltyFees Set of royalty fees for `token` /// @return responseCode The response code for the status of the request. SUCCESS is 22. function updateNonFungibleTokenCustomFees(address token, IHederaTokenService.FixedFee[] memory fixedFees, IHederaTokenService.RoyaltyFee[] memory royaltyFees) external returns (int64 responseCode); + + function airdropTokens(TokenTransferList[] memory tokenTransfers) external returns (int64 responseCode); + + function cancelAirdrops(PendingAirdrop[] memory pendingAirdrops) external returns (int64 responseCode); + + function claimAirdrops(PendingAirdrop[] memory pendingAirdrops) external returns (int64 responseCode); + + function rejectTokens(address rejectingAddress, address[] memory ftAddresses, NftID[] memory nftIDs) external returns (int64 responseCode); } diff --git a/contracts/system-contracts/hedera-token-service/examples/hrc-904/Airdrop.sol b/contracts/system-contracts/hedera-token-service/examples/hrc-904/Airdrop.sol new file mode 100644 index 000000000..f7756a756 --- /dev/null +++ b/contracts/system-contracts/hedera-token-service/examples/hrc-904/Airdrop.sol @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; + +import {HederaResponseCodes} from "../../../HederaResponseCodes.sol"; +import {HederaTokenService} from "../../HederaTokenService.sol"; +import {IHederaTokenService} from "../../IHederaTokenService.sol"; +pragma experimental ABIEncoderV2; + +contract Airdrop is HederaTokenService { + function tokenAirdrop(address token, address sender, address receiver, int64 amount) public payable returns (int64 responseCode) { + IHederaTokenService.TokenTransferList[] memory tokenTransfers = new IHederaTokenService.TokenTransferList[](1); + IHederaTokenService.TokenTransferList memory airdrop; + + airdrop.token = token; + airdrop.transfers = prepareAA(sender, receiver, amount); + tokenTransfers[0] = airdrop; + responseCode = airdropTokens(tokenTransfers); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function nftAirdrop(address token, address sender, address receiver, int64 serial) public payable returns (int64 responseCode) { + IHederaTokenService.TokenTransferList[] memory tokenTransfers = new IHederaTokenService.TokenTransferList[](1); + IHederaTokenService.TokenTransferList memory airdrop; + + airdrop.token = token; + IHederaTokenService.NftTransfer memory nftTransfer = prepareNftTransfer(sender, receiver, serial); + IHederaTokenService.NftTransfer[] memory nftTransfers = new IHederaTokenService.NftTransfer[](1); + nftTransfers[0] = nftTransfer; + airdrop.nftTransfers = nftTransfers; + tokenTransfers[0] = airdrop; + responseCode = airdropTokens(tokenTransfers); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function tokenNAmountAirdrops(address[] memory tokens, address[] memory senders, address[] memory receivers, int64 amount) public payable returns (int64 responseCode) { + uint256 length = senders.length; + IHederaTokenService.TokenTransferList[] memory tokenTransfers = new IHederaTokenService.TokenTransferList[](length); + for (uint256 i = 0; i < length; i++) + { + IHederaTokenService.TokenTransferList memory airdrop; + airdrop.token = tokens[i]; + airdrop.transfers = prepareAA(senders[i], receivers[i], amount); + tokenTransfers[i] = airdrop; + } + responseCode = airdropTokens(tokenTransfers); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function nftNAmountAirdrops(address[] memory nft, address[] memory senders, address[] memory receivers, int64[] memory serials) public returns (int64 responseCode) { + uint256 length = nft.length; + IHederaTokenService.TokenTransferList[] memory tokenTransfers = new IHederaTokenService.TokenTransferList[](length); + for (uint256 i = 0; i < length; i++) + { + IHederaTokenService.TokenTransferList memory airdrop; + airdrop.token = nft[i]; + IHederaTokenService.NftTransfer[] memory nftTransfers = new IHederaTokenService.NftTransfer[](1); + nftTransfers[0] = prepareNftTransfer(senders[i], receivers[i], serials[i]); + airdrop.nftTransfers = nftTransfers; + tokenTransfers[i] = airdrop; + } + responseCode = airdropTokens(tokenTransfers); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function tokenAirdropDistribute(address token, address sender, address[] memory receivers, int64 amount) public payable returns (int64 responseCode) { + uint256 length = receivers.length + 1; + IHederaTokenService.TokenTransferList[] memory tokenTransfers = new IHederaTokenService.TokenTransferList[](1); + IHederaTokenService.TokenTransferList memory airdrop; + airdrop.token = token; + IHederaTokenService.AccountAmount memory senderAA; + senderAA.accountID = sender; + int64 totalAmount = 0; + for (uint i = 0; i < receivers.length; i++) { + totalAmount += amount; + } + senderAA.amount = -totalAmount; + IHederaTokenService.AccountAmount[] memory transfers = new IHederaTokenService.AccountAmount[](length); + transfers[0] = senderAA; + for (uint i = 1; i < length; i++) + { + IHederaTokenService.AccountAmount memory receiverAA; + receiverAA.accountID = receivers[i]; + receiverAA.amount = amount; + transfers[i] = receiverAA; + } + airdrop.transfers = transfers; + tokenTransfers[0] = airdrop; + responseCode = airdropTokens(tokenTransfers); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function nftAirdropDistribute(address token, address sender, address[] memory receivers) public payable returns (int64 responseCode) { + uint256 length = receivers.length; + IHederaTokenService.TokenTransferList[] memory tokenTransfers = new IHederaTokenService.TokenTransferList[](1); + IHederaTokenService.TokenTransferList memory airdrop; + airdrop.token = token; + IHederaTokenService.NftTransfer[] memory nftTransfers = new IHederaTokenService.NftTransfer[](length); + for (uint i = 0; i < length; i++) { + int64 serial = 1; + nftTransfers[i] = prepareNftTransfer(sender, receivers[i], serial); + serial++; + } + airdrop.nftTransfers = nftTransfers; + tokenTransfers[0] = airdrop; + + responseCode = airdropTokens(tokenTransfers); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function mixedAirdrop(address[] memory token, address[] memory nft, address[] memory tokenSenders, address[] memory tokenReceivers, address[] memory nftSenders, address[] memory nftReceivers, int64 tokenAmount, int64[] memory serials) public payable returns (int64 responseCode) { + uint256 length = tokenSenders.length + nftSenders.length; + IHederaTokenService.TokenTransferList[] memory tokenTransfers = new IHederaTokenService.TokenTransferList[](length); + for (uint i = 0; i < tokenSenders.length; i++) + { + IHederaTokenService.TokenTransferList memory airdrop; + airdrop.token = token[i]; + airdrop.transfers = prepareAA(tokenSenders[i], tokenReceivers[i], tokenAmount); + tokenTransfers[i] = airdrop; + } + uint nftIndex = tokenSenders.length; + for (uint v = 0; nftIndex < length; v++) + { + IHederaTokenService.TokenTransferList memory airdrop; + airdrop.token = nft[v]; + IHederaTokenService.NftTransfer[] memory nftTransfers = new IHederaTokenService.NftTransfer[](1); + nftTransfers[0] = prepareNftTransfer(nftSenders[v], nftReceivers[v], serials[v]); + airdrop.nftTransfers = nftTransfers; + tokenTransfers[nftIndex] = airdrop; + nftIndex++; + } + responseCode = airdropTokens(tokenTransfers); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function prepareAA(address sender, address receiver, int64 amount) internal pure returns (IHederaTokenService.AccountAmount[] memory transfers) { + IHederaTokenService.AccountAmount memory aa1; + aa1.accountID = sender; + aa1.amount = -amount; + IHederaTokenService.AccountAmount memory aa2; + aa2.accountID = receiver; + aa2.amount = amount; + transfers = new IHederaTokenService.AccountAmount[](2); + transfers[0] = aa1; + transfers[1] = aa2; + return transfers; + } + + function prepareNftTransfer(address sender, address receiver, int64 serial) internal pure returns (IHederaTokenService.NftTransfer memory nftTransfer) { + nftTransfer.senderAccountID = sender; + nftTransfer.receiverAccountID = receiver; + nftTransfer.serialNumber = serial; + return nftTransfer; + } +} diff --git a/contracts/system-contracts/hedera-token-service/examples/hrc-904/CancelAirdrop.sol b/contracts/system-contracts/hedera-token-service/examples/hrc-904/CancelAirdrop.sol new file mode 100644 index 000000000..7e3e494fe --- /dev/null +++ b/contracts/system-contracts/hedera-token-service/examples/hrc-904/CancelAirdrop.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; + +import {HederaResponseCodes} from "../../../HederaResponseCodes.sol"; +import {HederaTokenService} from "../../HederaTokenService.sol"; +import {IHederaTokenService} from "../../IHederaTokenService.sol"; +pragma experimental ABIEncoderV2; + +contract CancelAirdrop is HederaTokenService { + + function cancelAirdrop(address sender, address receiver, address token) public returns(int64 responseCode){ + IHederaTokenService.PendingAirdrop[] memory pendingAirdrops = new IHederaTokenService.PendingAirdrop[](1); + + IHederaTokenService.PendingAirdrop memory pendingAirdrop; + pendingAirdrop.sender = sender; + pendingAirdrop.receiver = receiver; + pendingAirdrop.token = token; + + pendingAirdrops[0] = pendingAirdrop; + + responseCode = cancelAirdrops(pendingAirdrops); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function cancelNFTAirdrop(address sender, address receiver, address token, int64 serial) public returns(int64 responseCode){ + IHederaTokenService.PendingAirdrop[] memory pendingAirdrops = new IHederaTokenService.PendingAirdrop[](1); + + IHederaTokenService.PendingAirdrop memory pendingAirdrop; + pendingAirdrop.sender = sender; + pendingAirdrop.receiver = receiver; + pendingAirdrop.token = token; + pendingAirdrop.serial = serial; + + pendingAirdrops[0] = pendingAirdrop; + + responseCode = cancelAirdrops(pendingAirdrops); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function cancelAirdrops(address[] memory senders, address[] memory receivers, address[] memory tokens, int64[] memory serials) public returns (int64 responseCode) { + uint length = senders.length; + IHederaTokenService.PendingAirdrop[] memory pendingAirdrops = new IHederaTokenService.PendingAirdrop[](length); + for (uint i = 0; i < length; i++) { + IHederaTokenService.PendingAirdrop memory pendingAirdrop; + pendingAirdrop.sender = senders[i]; + pendingAirdrop.receiver = receivers[i]; + pendingAirdrop.token = tokens[i]; + pendingAirdrop.serial = serials[i]; + + pendingAirdrops[i] = pendingAirdrop; + } + + responseCode = cancelAirdrops(pendingAirdrops); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } +} diff --git a/contracts/system-contracts/hedera-token-service/examples/hrc-904/ClaimAirdrop.sol b/contracts/system-contracts/hedera-token-service/examples/hrc-904/ClaimAirdrop.sol new file mode 100644 index 000000000..ffa5c0676 --- /dev/null +++ b/contracts/system-contracts/hedera-token-service/examples/hrc-904/ClaimAirdrop.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; + +import {HederaResponseCodes} from "../../../HederaResponseCodes.sol"; +import {HederaTokenService} from "../../HederaTokenService.sol"; +import {IHederaTokenService} from "../../IHederaTokenService.sol"; +pragma experimental ABIEncoderV2; + +contract ClaimAirdrop is HederaTokenService { + + function claim(address sender, address receiver, address token) public returns(int64 responseCode){ + IHederaTokenService.PendingAirdrop[] memory pendingAirdrops = new IHederaTokenService.PendingAirdrop[](1); + + IHederaTokenService.PendingAirdrop memory pendingAirdrop; + pendingAirdrop.sender = sender; + pendingAirdrop.receiver = receiver; + pendingAirdrop.token = token; + + pendingAirdrops[0] = pendingAirdrop; + + responseCode = claimAirdrops(pendingAirdrops); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function claimNFTAirdrop(address sender, address receiver, address token, int64 serial) public returns(int64 responseCode){ + IHederaTokenService.PendingAirdrop[] memory pendingAirdrops = new IHederaTokenService.PendingAirdrop[](1); + + IHederaTokenService.PendingAirdrop memory pendingAirdrop; + pendingAirdrop.sender = sender; + pendingAirdrop.receiver = receiver; + pendingAirdrop.token = token; + pendingAirdrop.serial = serial; + + pendingAirdrops[0] = pendingAirdrop; + + responseCode = claimAirdrops(pendingAirdrops); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } + + function claimAirdrops(address[] memory senders, address[] memory receivers, address[] memory tokens, int64[] memory serials) public returns (int64 responseCode) { + uint length = senders.length; + IHederaTokenService.PendingAirdrop[] memory pendingAirdrops = new IHederaTokenService.PendingAirdrop[](length); + for (uint i = 0; i < length; i++) { + IHederaTokenService.PendingAirdrop memory pendingAirdrop; + pendingAirdrop.sender = senders[i]; + pendingAirdrop.receiver = receivers[i]; + pendingAirdrop.token = tokens[i]; + pendingAirdrop.serial = serials[i]; + + pendingAirdrops[i] = pendingAirdrop; + } + + responseCode = claimAirdrops(pendingAirdrops); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } +} diff --git a/contracts/system-contracts/hedera-token-service/examples/hrc-904/HRC904Contract.sol b/contracts/system-contracts/hedera-token-service/examples/hrc-904/HRC904Contract.sol new file mode 100644 index 000000000..e83619827 --- /dev/null +++ b/contracts/system-contracts/hedera-token-service/examples/hrc-904/HRC904Contract.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.7; + +import {IHRC719} from "../../IHRC719.sol"; +import {IHRC904} from "../../IHRC904.sol"; + +contract HRC904Contract { + event IsAssociated(bool status); + + function cancelAirdropFT(address token, address receiver) public returns (int64 responseCode) { + return IHRC904(token).cancelAirdropFT(receiver); + } + + function cancelAirdropNFT(address token, address receiver, int64 serial) public returns (int64 responseCode) { + return IHRC904(token).cancelAirdropNFT(receiver, serial); + } + + function claimAirdropFT(address token, address sender) public returns (int64 responseCode) { + return IHRC904(token).claimAirdropFT(sender); + } + + function claimAirdropNFT(address token, address sender, int64 serial) public returns (int64 responseCode) { + return IHRC904(token).claimAirdropNFT(sender, serial); + } + + function rejectTokenFT(address token) public returns (int64 responseCode) { + return IHRC904(token).rejectTokenFT(); + } + + function rejectTokenNFTs(address token, int64[] memory serialNumbers) public returns (int64 responseCode) { + return IHRC904(token).rejectTokenNFTs(serialNumbers); + } + + function setUnlimitedAssociations(address account, bool enableAutoAssociations) public returns (int64 responseCode) { + return IHRC904(account).setUnlimitedAutomaticAssociations(enableAutoAssociations); + } + + function associate(address token) public returns (uint256 responseCode) { + return IHRC719(token).associate(); + } + + function dissociate(address token) public returns (uint256 responseCode) { + return IHRC719(token).dissociate(); + } + + function isAssociated(address token) public view returns (bool associated) { + return IHRC719(token).isAssociated(); + } +} diff --git a/contracts/system-contracts/hedera-token-service/examples/hrc-904/TokenReject.sol b/contracts/system-contracts/hedera-token-service/examples/hrc-904/TokenReject.sol new file mode 100644 index 000000000..f43cfd6cc --- /dev/null +++ b/contracts/system-contracts/hedera-token-service/examples/hrc-904/TokenReject.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; + +import {HederaResponseCodes} from "../../../HederaResponseCodes.sol"; +import {HederaTokenService} from "../../HederaTokenService.sol"; +import {IHederaTokenService} from "../../IHederaTokenService.sol"; +pragma experimental ABIEncoderV2; + +contract TokenReject is HederaTokenService { + + function rejectTokens(address rejectingAddress, address[] memory ftAddresses, address[] memory nftAddresses) public returns(int64 responseCode) { + IHederaTokenService.NftID[] memory nftIDs = new IHederaTokenService.NftID[](nftAddresses.length); + for (uint i; i < nftAddresses.length; i++) + { + IHederaTokenService.NftID memory nftId; + nftId.nft = nftAddresses[i]; + nftId.serial = 1; + nftIDs[i] = nftId; + } + responseCode = rejectTokens(rejectingAddress, ftAddresses, nftIDs); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert(); + } + return responseCode; + } +} diff --git a/hardhat.config.js b/hardhat.config.js index 3504d710d..3aa43f335 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -61,7 +61,7 @@ module.exports = { path: './contracts-abi', runOnCompile: true, }, - defaultNetwork: NETWORKS.local.name, + defaultNetwork: NETWORKS.previewnet.name, networks: { local: { url: NETWORKS.local.url, @@ -98,6 +98,7 @@ module.exports = { nodeId: NETWORKS.previewnet.nodeId, mirrorNode: NETWORKS.previewnet.mirrorNode, }, + timeout: 60_000, }, besu_local: { url: NETWORKS.besu.url, diff --git a/test/constants.js b/test/constants.js index 1e07319ec..3c961418c 100644 --- a/test/constants.js +++ b/test/constants.js @@ -76,6 +76,7 @@ const Contract = { ERC20Mock: 'ERC20Mock', OZERC20Mock: 'OZERC20Mock', OZERC721Mock: 'OZERC721Mock', + Airdrop: 'Airdrop', TokenCreateContract: 'TokenCreateContract', DiamondCutFacet: 'DiamondCutFacet', Diamond: 'Diamond', @@ -104,6 +105,7 @@ const Contract = { ERC20CappedMock: 'ERC20CappedMock', ERC20PausableMock: 'ERC20PausableMock', HRC719Contract: 'HRC719Contract', + HRC904Contract: 'HRC904Contract', ExchangeRateMock: 'ExchangeRateMock', PrngSystemContract: 'PrngSystemContract', Concatenation: 'Concatenation', diff --git a/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol b/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol index 1bd5c02f5..e9e021fbf 100644 --- a/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol +++ b/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.9; -import '../../../../contracts/system-contracts/HederaResponseCodes.sol'; -import '../../../../contracts/system-contracts/hedera-token-service/KeyHelper.sol'; -import './HederaFungibleToken.sol'; -import './HederaNonFungibleToken.sol'; -import '../../../../contracts/base/NoDelegateCall.sol'; -import '../../../../contracts/libraries/Constants.sol'; - -import '../interfaces/IHtsSystemContractMock.sol'; -import '../libraries/HederaTokenValidation.sol'; +import {NoDelegateCall} from "../../../../contracts/base/NoDelegateCall.sol"; +import {Constants} from "../../../../contracts/libraries/Constants.sol"; +import {HederaResponseCodes} from "../../../../contracts/system-contracts/HederaResponseCodes.sol"; +import {IHederaTokenService} from "../../../../contracts/system-contracts/hedera-token-service/IHederaTokenService.sol"; +import {KeyHelper} from "../../../../contracts/system-contracts/hedera-token-service/KeyHelper.sol"; +import {IHtsSystemContractMock} from "../interfaces/IHtsSystemContractMock.sol"; +import {HederaTokenValidation} from "../libraries/HederaTokenValidation.sol"; +import {HederaFungibleToken} from "./HederaFungibleToken.sol"; +import {HederaNonFungibleToken} from "./HederaNonFungibleToken.sol"; contract HtsSystemContractMock is NoDelegateCall, KeyHelper, IHtsSystemContractMock { @@ -1890,6 +1890,14 @@ contract HtsSystemContractMock is NoDelegateCall, KeyHelper, IHtsSystemContractM // TODO function redirectForToken(address token, bytes memory encodedFunctionSelector) external noDelegateCall override returns (int64 responseCode, bytes memory response) {} + function airdropTokens(TokenTransferList[] memory tokenTransfers) external returns (int64 responseCode) {} + + function cancelAirdrops(PendingAirdrop[] memory pendingAirdrops) external returns (int64 responseCode) {} + + function claimAirdrops(PendingAirdrop[] memory pendingAirdrops) external returns (int64 responseCode) {} + + function rejectTokens(address rejectingAddress, address[] memory ftAddresses, NftID[] memory nftIDs) external returns (int64 responseCode) {} + // Additional(not in IHederaTokenService) public/external state-changing functions: function isAssociated(address account, address token) external view returns (bool associated) { associated = _association[token][account]; diff --git a/test/system-contracts/hedera-token-service/token-airdrop/tokenAirdropContract.js b/test/system-contracts/hedera-token-service/token-airdrop/tokenAirdropContract.js new file mode 100644 index 000000000..321008f3d --- /dev/null +++ b/test/system-contracts/hedera-token-service/token-airdrop/tokenAirdropContract.js @@ -0,0 +1,97 @@ +const { expect } = require('chai'); +const { ethers } = require('hardhat'); +const utils = require('../utils'); +const Constants = require('../../../constants'); +const IHRC904Contract = require('../../../../artifacts/contracts/system-contracts/hedera-token-service/IHRC904.sol/IHRC904.json'); + +describe.only('HRC904Contract Test Suite', function () { + let hrc904Contract; + let hrc904Address; + let hrc904Interface; + let airdropContract; + let tokenCreateContract; + let tokenAddress; + let signers; + + before(async function () { + signers = await ethers.getSigners(); + airdropContract = await utils.deployAirdropContract(); + tokenCreateContract = await utils.deployTokenCreateContract(); + hrc904Contract = await utils.deployHRC904Contract(); + hrc904Address = await hrc904Contract.getAddress(); + hrc904Interface = new ethers.Interface(IHRC904Contract.abi); + await utils.updateAccountKeysViaHapi([ + await airdropContract.getAddress(), + await tokenCreateContract.getAddress(), + await hrc904Contract.getAddress(), + ]); + tokenAddress = await utils.createFungibleTokenWithSECP256K1AdminKey( + tokenCreateContract, + signers[0].address, + utils.getSignerCompressedPublicKey() + ); + await utils.updateTokenKeysViaHapi(tokenAddress, [ + await airdropContract.getAddress(), + await tokenCreateContract.getAddress(), + await hrc904Contract.getAddress(), + ]); + }); + + it('should be able to create an HTS fungible token', async function () { + expect(tokenAddress).to.exist; + }); + + it('should be able to associate receiver with a token', async function () { + const tx = await hrc904Contract.associate(tokenAddress, Constants.GAS_LIMIT_1_000_000); + const receipt = await tx.wait(); + expect(receipt.status).to.eq(1); + }); + + it('should be able to set the auto associate setting to true', async function () { + const tx = await hrc904Contract.setUnlimitedAssociations(signers[1].address, true, { + gasLimit: 1_000_000, + }); + const receipt = await tx.wait(); + expect(receipt.status).to.eq(1); + }); + + it('should be able to airdrop any token in its balance based on token address', async function () { + const tx = await airdropContract.tokenAirdrop(tokenAddress, signers[0].address, signers[1].address, 1, { + gasLimit: 2_000_000, + value: 100_000, + }); + const receipt = await tx.wait(); + expect(receipt.status).to.eq(1); + }); + + it('should be able to claim any token airdropped to it based on token address', async function () { + const data = hrc904Interface.encodeFunctionData('claimAirdropFT', [signers[0].address]); + const tx = await signers[1].sendTransaction({ + to: tokenAddress, + data: data, + gasLimit: 1_000_000 + }); + const receipt = await tx.wait(); + expect(receipt.status).to.eq(1); + }); + + it('should be able to cancel a token it airdropped', async function () { + const tx = await hrc904Contract.cancelAirdropFT(tokenAddress, hrc904Address); + const receipt = await tx.wait(); + expect(receipt.status).to.eq(1); + }); + + it('should be able to revoke any token in its balance', async function () { + const tx = await hrc904Contract.rejectTokenFT(tokenAddress); + const receipt = await tx.wait(); + expect(receipt.status).to.eq(1); + }); + + it('should be able to set the auto associate setting back to false', async function () { + const tx = await hrc904Contract.setUnlimitedAssociations(signers[1].address, false, { + gasLimit: 1_000_000 + }); + const receipt = await tx.wait(); + expect(receipt.status).to.eq(1); + }); +}); diff --git a/test/system-contracts/hedera-token-service/utils.js b/test/system-contracts/hedera-token-service/utils.js index 415472d52..77f7fdb4c 100644 --- a/test/system-contracts/hedera-token-service/utils.js +++ b/test/system-contracts/hedera-token-service/utils.js @@ -92,6 +92,20 @@ class Utils { ); } + static async deployAirdropContract() { + const tokenAirdropFactory = await ethers.getContractFactory( + Constants.Contract.Airdrop + ); + const tokenAirdrop = await tokenAirdropFactory.deploy( + Constants.GAS_LIMIT_1_000_000 + ); + + return await ethers.getContractAt( + Constants.Contract.Airdrop, + await tokenAirdrop.getAddress() + ); + } + static async deployTokenCreateContract() { const tokenCreateFactory = await ethers.getContractFactory( Constants.Contract.TokenCreateContract @@ -176,6 +190,20 @@ class Utils { ); } + static async deployHRC904Contract() { + const hrcContractFactory = await ethers.getContractFactory( + Constants.Contract.HRC904Contract + ); + const hrcContract = await hrcContractFactory.deploy( + Constants.GAS_LIMIT_1_000_000 + ); + + return await ethers.getContractAt( + Constants.Contract.HRC904Contract, + await hrcContract.getAddress() + ); + } + static async deployERC20Contract() { const erc20ContractFactory = await ethers.getContractFactory( Constants.Contract.ERC20Contract @@ -485,6 +513,30 @@ class Utils { return tokenAddress; } + static async createNonFungibleTokenWithSECP256K1AdminKeyAssociateAndTransferToAddress( + contract, + treasury, + adminKey, + initialBalance = 300 + ) { + const tokenAddressTx = + await contract.createNonFungibleTokenWithSECP256K1AdminKeyAssociateAndTransferToAddressPublic( + treasury, + adminKey, + initialBalance, + { + value: BigInt(this.createTokenCost), + gasLimit: 1_000_000, + } + ); + const tokenAddressReceipt = await tokenAddressTx.wait(); + const { tokenAddress } = tokenAddressReceipt.logs.filter( + (e) => e.fragment.name === Constants.Events.CreatedToken + )[0].args; + + return tokenAddress; + } + static async createNonFungibleTokenWithSECP256K1AdminKeyWithoutKYC( contract, treasury,