Skip to content

Commit

Permalink
feat: crypto transfer method implemented (#195)
Browse files Browse the repository at this point in the history
Signed-off-by: Mariusz Jasuwienas <[email protected]>
  • Loading branch information
arianejasuwienas committed Jan 22, 2025
1 parent fd99e8a commit 33a75f4
Show file tree
Hide file tree
Showing 3 changed files with 322 additions and 3 deletions.
113 changes: 113 additions & 0 deletions contracts/HtsSystemContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions contracts/IHederaTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
206 changes: 206 additions & 0 deletions test/HTS.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
}

0 comments on commit 33a75f4

Please sign in to comment.