From 33a75f4be3ddba146dadba4d614cb7f6f058b941 Mon Sep 17 00:00:00 2001 From: Mariusz Jasuwienas Date: Wed, 22 Jan 2025 16:26:47 +0100 Subject: [PATCH] feat: crypto transfer method implemented (#195) Signed-off-by: Mariusz Jasuwienas --- contracts/HtsSystemContract.sol | 113 ++++++++++++++++ contracts/IHederaTokenService.sol | 6 +- test/HTS.t.sol | 206 ++++++++++++++++++++++++++++++ 3 files changed, 322 insertions(+), 3 deletions(-) diff --git a/contracts/HtsSystemContract.sol b/contracts/HtsSystemContract.sol index 4d03956b..a6695de1 100644 --- a/contracts/HtsSystemContract.sol +++ b/contracts/HtsSystemContract.sol @@ -43,6 +43,119 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events { assembly { accountId := sload(slot) } } + function cryptoTransfer(TransferList memory transferList, TokenTransferList[] memory tokenTransfers) + payable htsCall external returns (int64 responseCode) { + uint256 hbarsReceived = msg.value; + int64 hbarBalance = 0; + for (uint256 hbarIndex = 0; hbarIndex < transferList.transfers.length; hbarIndex++) { + require(!transferList.transfers[hbarIndex].isApproval, "cryptoTransfer: hbar approval is not supported"); + hbarBalance += transferList.transfers[hbarIndex].amount; + if (transferList.transfers[hbarIndex].amount < 0) { + require( + transferList.transfers[hbarIndex].accountID == msg.sender, + "cryptoTransfer: hbar transfer allowed only from the msg sender account" + ); + continue; + } + require(transferList.transfers[hbarIndex].amount > 0, "cryptoTransfer: invalid amount"); + uint256 value = uint256(uint64(transferList.transfers[hbarIndex].amount)); + require(hbarsReceived >= value, "cryptoTransfer: insufficient balance"); + hbarsReceived -= value; + (bool success, ) = transferList.transfers[hbarIndex].accountID.call{value: value}(""); + require(success, "cryptoTransfer: hbar transfer failure"); + } + require(hbarBalance == 0 && hbarsReceived == 0, "cryptoTransfer: unmatched hbar transfers "); + for (uint256 tokenIndex = 0; tokenIndex < tokenTransfers.length; tokenIndex++) { + require(tokenTransfers[tokenIndex].token != address(0), "cryptoTransfer: invalid token"); + uint256 validFungibleTransfersCount = 0; + for (uint256 ftIndex = 0; ftIndex < tokenTransfers[tokenIndex].transfers.length; ftIndex++) { + if (!tokenTransfers[tokenIndex].transfers[ftIndex].isApproval) { + validFungibleTransfersCount++; + } else { + require( + tokenTransfers[tokenIndex].transfers[ftIndex].amount >= 0, + "cryptoTransfer: only positive approvals allowed" + ); + int64 approveResponseCode = approve( + tokenTransfers[tokenIndex].token, + tokenTransfers[tokenIndex].transfers[ftIndex].accountID, + uint256(uint64(tokenTransfers[tokenIndex].transfers[ftIndex].amount)) + ); + require( + approveResponseCode == HederaResponseCodes.SUCCESS, + "cryptoTransfer: failed to approve fungible token" + ); + } + } + address[] memory ftAccountIds = new address[](validFungibleTransfersCount); + int64[] memory ftAmounts = new int64[](validFungibleTransfersCount); + uint256 validFungibleIndex = 0; + for (uint256 ftIndex = 0; ftIndex < tokenTransfers[tokenIndex].transfers.length; ftIndex++) { + if (!tokenTransfers[tokenIndex].transfers[ftIndex].isApproval) { + ftAccountIds[validFungibleIndex] = tokenTransfers[tokenIndex].transfers[ftIndex].accountID; + ftAmounts[validFungibleIndex] = tokenTransfers[tokenIndex].transfers[ftIndex].amount; + validFungibleIndex++; + } + } + if (ftAccountIds.length > 0) { + int64 total = 0; + for (uint256 i = 0; i < ftAccountIds.length; i++) { + total += ftAmounts[i]; + } + require(total == 0, "cryptoTransfer: total amount must balance"); + + for (uint256 from = 0; from < ftAmounts.length; from++) { + if (ftAmounts[from] >= 0) { + continue; + } + for (uint256 to = 0; to < ftAmounts.length; to++) { + if (ftAmounts[to] <= 0) { + continue; + } + int64 transferAmount = ftAmounts[to] < -ftAmounts[from] ? ftAmounts[to] : -ftAmounts[from]; + transferToken( + tokenTransfers[tokenIndex].token, + ftAccountIds[from], + ftAccountIds[to], + transferAmount + ); + ftAmounts[from] += transferAmount; + ftAmounts[to] -= transferAmount; + if (ftAmounts[from] == 0) { + break; + } + } + } + // Ensure all amounts are fully balanced after processing + for (uint256 i = 0; i < ftAmounts.length; i++) { + require(ftAmounts[i] == 0, "cryptoTransfer: unmatched transfers"); + } + } + for (uint256 nftIndex = 0; nftIndex < tokenTransfers[tokenIndex].nftTransfers.length; nftIndex++) { + if (!tokenTransfers[tokenIndex].nftTransfers[nftIndex].isApproval) { + transferNFT( + tokenTransfers[tokenIndex].token, + tokenTransfers[tokenIndex].nftTransfers[nftIndex].senderAccountID, + tokenTransfers[tokenIndex].nftTransfers[nftIndex].receiverAccountID, + tokenTransfers[tokenIndex].nftTransfers[nftIndex].serialNumber + ); + } else { + int64 nftApproveResponseCode = approveNFT( + tokenTransfers[tokenIndex].token, + tokenTransfers[tokenIndex].nftTransfers[nftIndex].receiverAccountID, + uint256(uint64(tokenTransfers[tokenIndex].nftTransfers[nftIndex].serialNumber)) + ); + require( + nftApproveResponseCode == HederaResponseCodes.SUCCESS, + "cryptoTransfer: failed to approve nft" + ); + } + } + } + + return HederaResponseCodes.SUCCESS; + } + function mintToken(address token, int64 amount, bytes[] memory) htsCall external returns ( int64 responseCode, int64 newTotalSupply, diff --git a/contracts/IHederaTokenService.sol b/contracts/IHederaTokenService.sol index 7c98547b..b85fd2de 100644 --- a/contracts/IHederaTokenService.sol +++ b/contracts/IHederaTokenService.sol @@ -304,9 +304,9 @@ interface IHederaTokenService { /// @param transferList the list of hbar transfers to do /// @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 - // returns (int64 responseCode); + function cryptoTransfer(TransferList memory transferList, TokenTransferList[] memory tokenTransfers) + external payable + returns (int64 responseCode); /// Mints an amount of the token to the defined treasury account /// @param token The token for which to mint tokens. If token does not exist, transaction results in diff --git a/test/HTS.t.sol b/test/HTS.t.sol index 5b54e04d..d9519dbd 100644 --- a/test/HTS.t.sol +++ b/test/HTS.t.sol @@ -830,4 +830,210 @@ contract HTSTest is Test, TestSetup { assertEq(setApprovalForAllResponseCode, HederaResponseCodes.SUCCESS); assertTrue(IERC721(CFNFTFF).isApprovedForAll(CFNFTFF_TREASURY, operator)); } + + function test_HTS_cryptoTransfer() external { + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + address bob = makeAddr("bob"); + address alice = makeAddr("alice"); + uint256 amountToBob = 1_000000; + uint256 amountToAlice = 3_000000; + address token = USDC; + IHederaTokenService.AccountAmount memory transfer1 = IHederaTokenService.AccountAmount( + owner, + -int64(uint64(amountToBob + amountToAlice)), + false + ); + IHederaTokenService.AccountAmount memory transfer2 = IHederaTokenService.AccountAmount( + bob, + int64(uint64(amountToBob)), + false + ); + IHederaTokenService.AccountAmount memory transfer3 = IHederaTokenService.AccountAmount( + alice, + int64(uint64(amountToAlice)), + false + ); + IHederaTokenService.AccountAmount memory transfer4 = IHederaTokenService.AccountAmount( + makeAddr("ignored"), + 500000, + true + ); + IHederaTokenService.TokenTransferList[] memory tokenTransfers = new IHederaTokenService.TokenTransferList[](1); + IHederaTokenService.TransferList memory hbarTransfers; + IHederaTokenService.AccountAmount[] memory transfers = new IHederaTokenService.AccountAmount[](4); + tokenTransfers[0] = IHederaTokenService.TokenTransferList( + token, + transfers, + new IHederaTokenService.NftTransfer[](0) + ); + tokenTransfers[0].transfers[0] = transfer1; + tokenTransfers[0].transfers[1] = transfer2; + tokenTransfers[0].transfers[2] = transfer3; + tokenTransfers[0].transfers[3] = transfer4; + vm.prank(owner); + vm.expectEmit(true, true, true, true, token); + emit IERC20Events.Transfer(owner, bob, amountToBob); + vm.expectEmit(true, true, true, true, token); + emit IERC20Events.Transfer(owner, alice, amountToAlice); + int64 responseCode = IHederaTokenService(HTS_ADDRESS).cryptoTransfer(hbarTransfers, tokenTransfers); + assertEq(responseCode, HederaResponseCodes.SUCCESS); + assertEq(IERC20(token).balanceOf(bob), amountToBob); + assertEq(IERC20(token).balanceOf(alice), amountToAlice); + } + + function test_HTS_cryptoTransfer_hbar() external { + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + address recipient1 = makeAddr("recipient1"); + address recipient2 = makeAddr("recipient2"); + uint256 hbarToRecipient1 = 1 ether; + uint256 hbarToRecipient2 = 2 ether; + IHederaTokenService.AccountAmount memory transfer1 = IHederaTokenService.AccountAmount( + owner, + -int64(uint64(hbarToRecipient1 + hbarToRecipient2)), + false + ); + IHederaTokenService.AccountAmount memory transfer2 = IHederaTokenService.AccountAmount( + recipient1, + int64(uint64(hbarToRecipient1)), + false + ); + IHederaTokenService.AccountAmount memory transfer3 = IHederaTokenService.AccountAmount( + recipient2, + int64(uint64(hbarToRecipient2)), + false + ); + IHederaTokenService.TransferList memory transferList; + transferList.transfers = new IHederaTokenService.AccountAmount[](3); + transferList.transfers[0] = transfer1; + transferList.transfers[1] = transfer2; + transferList.transfers[2] = transfer3; + vm.deal(owner, hbarToRecipient1 + hbarToRecipient2 + 100); + uint256 initialOwnerBalance = address(owner).balance; + uint256 initialRecipient1Balance = address(recipient1).balance; + uint256 initialRecipient2Balance = address(recipient2).balance; + assertGt(initialOwnerBalance, hbarToRecipient1 + hbarToRecipient2); + assertEq(initialRecipient1Balance, 0); + assertEq(initialRecipient2Balance, 0); + vm.prank(owner); + vm.expectCall(recipient1, hbarToRecipient1, ""); + vm.expectCall(recipient2, hbarToRecipient2, ""); + int64 code = IHederaTokenService(HTS_ADDRESS).cryptoTransfer{value: hbarToRecipient1 + hbarToRecipient2}( + transferList, + new IHederaTokenService.TokenTransferList[](0) + ); + assertEq(code, HederaResponseCodes.SUCCESS); + assertEq(address(owner).balance, initialOwnerBalance - hbarToRecipient1 - hbarToRecipient2); + assertEq(address(recipient1).balance, initialRecipient1Balance + hbarToRecipient1); + assertEq(address(recipient2).balance, initialRecipient2Balance + hbarToRecipient2); + } + + function test_HTS_cryptoTransfer_test_invalid_sender() external { + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + address sender = makeAddr("sender"); + address recipient = makeAddr("recipient"); + uint256 hbarToRecipient = 1 ether; + IHederaTokenService.AccountAmount memory transfer1 = IHederaTokenService.AccountAmount( + sender, + -int64(uint64(hbarToRecipient)), + false + ); + IHederaTokenService.AccountAmount memory transfer2 = IHederaTokenService.AccountAmount( + recipient, + int64(uint64(hbarToRecipient)), + false + ); + IHederaTokenService.TransferList memory transferList; + transferList.transfers = new IHederaTokenService.AccountAmount[](2); + transferList.transfers[0] = transfer1; + transferList.transfers[1] = transfer2; + vm.deal(owner, hbarToRecipient); + vm.deal(sender, hbarToRecipient); + vm.expectRevert("cryptoTransfer: hbar transfer allowed only from the msg sender account"); + vm.prank(owner); + IHederaTokenService(HTS_ADDRESS).cryptoTransfer{value: hbarToRecipient}( + transferList, + new IHederaTokenService.TokenTransferList[](0) + ); + } + + function test_HTS_cryptoTransfer_test_reject_insufficient_value_send() external { + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + address recipient = makeAddr("recipient"); + uint256 hbarToRecipient = 1 ether; + IHederaTokenService.AccountAmount memory transfer1 = IHederaTokenService.AccountAmount( + owner, + -int64(uint64(hbarToRecipient)), + false + ); + IHederaTokenService.AccountAmount memory transfer2 = IHederaTokenService.AccountAmount( + recipient, + int64(uint64(hbarToRecipient)), + false + ); + IHederaTokenService.TransferList memory transferList; + transferList.transfers = new IHederaTokenService.AccountAmount[](2); + transferList.transfers[0] = transfer1; + transferList.transfers[1] = transfer2; + vm.deal(owner, hbarToRecipient); + vm.expectRevert("cryptoTransfer: insufficient balance"); + vm.prank(owner); + IHederaTokenService(HTS_ADDRESS).cryptoTransfer{value: hbarToRecipient - 0.5 ether}( + transferList, + new IHederaTokenService.TokenTransferList[](0) + ); + } + + function test_HTS_cryptoTransfer_test_reject_hbar_approval() external { + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + address recipient = makeAddr("recipient"); + uint256 hbarToRecipient = 1 ether; + IHederaTokenService.AccountAmount memory transfer1 = IHederaTokenService.AccountAmount( + owner, + -int64(uint64(hbarToRecipient)), + false + ); + IHederaTokenService.AccountAmount memory transfer2 = IHederaTokenService.AccountAmount( + recipient, + int64(uint64(hbarToRecipient)), + true + ); + IHederaTokenService.TransferList memory transferList; + transferList.transfers = new IHederaTokenService.AccountAmount[](2); + transferList.transfers[0] = transfer1; + transferList.transfers[1] = transfer2; + vm.deal(owner, hbarToRecipient); + vm.expectRevert("cryptoTransfer: hbar approval is not supported"); + vm.prank(owner); + IHederaTokenService(HTS_ADDRESS).cryptoTransfer{value: hbarToRecipient}( + transferList, + new IHederaTokenService.TokenTransferList[](0) + ); + } + + function test_HTS_cryptoTransfer_test_reject_wrong_value() external { + address owner = 0x4D1c823b5f15bE83FDf5adAF137c2a9e0E78fE15; + address recipient = makeAddr("recipient"); + uint256 hbarToRecipient = 1 ether; + IHederaTokenService.AccountAmount memory transfer1 = IHederaTokenService.AccountAmount( + owner, + -int64(uint64(hbarToRecipient)), + false + ); + IHederaTokenService.AccountAmount memory transfer2 = IHederaTokenService.AccountAmount( + recipient, + int64(uint64(hbarToRecipient)), + false + ); + IHederaTokenService.TransferList memory transferList; + transferList.transfers = new IHederaTokenService.AccountAmount[](2); + transferList.transfers[0] = transfer1; + transferList.transfers[1] = transfer2; + vm.deal(owner, hbarToRecipient); + vm.expectRevert(); + vm.prank(owner); + IHederaTokenService(HTS_ADDRESS).cryptoTransfer{value: hbarToRecipient + 0.5 ether}( + transferList, + new IHederaTokenService.TokenTransferList[](0) + ); + } }