Skip to content

Commit

Permalink
feat: crypto transfer support (#195)
Browse files Browse the repository at this point in the history
Signed-off-by: Mariusz Jasuwienas <mariusz.jasuwienas@arianelabs.com>
  • Loading branch information
arianejasuwienas committed Jan 30, 2025
1 parent fd99e8a commit cfc904c
Show file tree
Hide file tree
Showing 14 changed files with 8,597 additions and 8 deletions.
8 changes: 8 additions & 0 deletions contracts/HederaResponseCodes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,13 @@
pragma solidity ^0.8.0;

library HederaResponseCodes {
int32 internal constant NOT_SUPPORTED = 13;
int32 internal constant SUCCESS = 22; // The transaction succeeded
int32 internal constant INSUFFICIENT_ACCOUNT_BALANCE = 28;
int32 internal constant INVALID_ACCOUNT_AMOUNTS = 48;
int32 internal constant ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS = 74;
int32 internal constant INSUFFICIENT_TOKEN_BALANCE = 178;
int32 internal constant SENDER_DOES_NOT_OWN_NFT_SERIAL_NO = 237;
int32 internal constant SPENDER_DOES_NOT_HAVE_ALLOWANCE = 292;
int32 internal constant MAX_ALLOWANCES_EXCEEDED = 294;
}
152 changes: 152 additions & 0 deletions contracts/HtsSystemContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,82 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events {
assembly { accountId := sload(slot) }
}

function cryptoTransfer(TransferList memory transferList, TokenTransferList[] memory tokenTransfers)
htsCall external returns (int64) {
int64 responseCode = _checkCryptoFungibleTransfers(address(0), transferList.transfers);
if (responseCode != HederaResponseCodes.SUCCESS) return responseCode;

for (uint256 tokenIndex = 0; tokenIndex < tokenTransfers.length; tokenIndex++) {
require(tokenTransfers[tokenIndex].token != address(0), "cryptoTransfer: invalid token");

// Processing fungible token transfers
responseCode = _checkCryptoFungibleTransfers(tokenTransfers[tokenIndex].token, tokenTransfers[tokenIndex].transfers);
if (responseCode != HederaResponseCodes.SUCCESS) return responseCode;
AccountAmount[] memory transfers = tokenTransfers[tokenIndex].transfers;
for (uint256 from = 0; from < transfers.length; from++) {
if (transfers[from].amount >= 0) continue;
for (uint256 to = 0; to < transfers.length; to++) {
if (transfers[to].amount <= 0) continue;
int64 transferAmount = transfers[to].amount < -transfers[from].amount
? transfers[to].amount : -transfers[from].amount;
transferToken(
tokenTransfers[tokenIndex].token,
transfers[from].accountID,
transfers[to].accountID,
transferAmount
);
transfers[from].amount += transferAmount;
transfers[to].amount -= transferAmount;
if (transfers[from].amount == 0) break;
}
}

// Processing non-fungible token transfers
// The IERC721 interface already handles all crucial validations and operations,
// but HTS interface need to return correct response code instead of reverting the whole operations.
for (uint256 nftIndex = 0; nftIndex < tokenTransfers[tokenIndex].nftTransfers.length; nftIndex++) {
NftTransfer memory transfer = tokenTransfers[tokenIndex].nftTransfers[nftIndex];

// Check if NFT transfer is feasible.
if (transfer.senderAccountID != msg.sender) {
if (!transfer.isApproval) return HederaResponseCodes.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO;
address token = tokenTransfers[tokenIndex].token;
bool approvedForAll = IERC721(token).isApprovedForAll(transfer.senderAccountID, msg.sender);
bool approved = IERC721(token).getApproved(uint256(uint64(transfer.serialNumber))) == msg.sender;
if (!approvedForAll && !approved) return HederaResponseCodes.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO;
}
transferNFT(
tokenTransfers[tokenIndex].token,
transfer.senderAccountID,
transfer.receiverAccountID,
transfer.serialNumber
);
}
}

// Processing HBAR transfers
// To ensure stability, this operation is performed last since the Foundry library implementation
// uses forge cheat codes and FT or NFT transfers may potentially trigger reverts.
for (uint256 i = 0; i < transferList.transfers.length; i++) {
bool from = transferList.transfers[i].amount < 0;
address account = transferList.transfers[i].isApproval || !from
? transferList.transfers[i].accountID
: msg.sender;
uint256 amount = uint256(uint64(
from ? -transferList.transfers[i].amount : transferList.transfers[i].amount
));
int64 updatingResult = _updateHbarBalanceOnAccount(
account, from ? account.balance - amount : account.balance + amount
);
if (updatingResult != HederaResponseCodes.SUCCESS) revert("cryptoTransfer: hbar transfer is not supported without vm cheatcodes in forked network");
if (from && account != msg.sender) {
_approve(account, msg.sender, __allowance(account, msg.sender) - amount);
}
}

return HederaResponseCodes.SUCCESS;
}

function mintToken(address token, int64 amount, bytes[] memory) htsCall external returns (
int64 responseCode,
int64 newTotalSupply,
Expand Down Expand Up @@ -832,4 +908,80 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events, IERC721Events {
assembly { sstore(slot, approved) }
emit ApprovalForAll(sender, operator, approved);
}

function _cryptoFungibleTransfers(address token, AccountAmount[] memory transfers) internal {
for (uint256 from = 0; from < transfers.length; from++) {
if (transfers[from].amount >= 0) continue;
for (uint256 to = 0; to < transfers.length; to++) {
if (transfers[to].amount <= 0) continue;
int64 transferAmount = transfers[to].amount < -transfers[from].amount ? transfers[to].amount : -transfers[from].amount;
transferToken(
token,
transfers[from].accountID,
transfers[to].accountID,
transferAmount
);
transfers[from].amount += transferAmount;
transfers[to].amount -= transferAmount;
if (transfers[from].amount == 0) break;
}
}
}

function _checkCryptoFungibleTransfers(address token, AccountAmount[] memory transfers) internal returns (int64) {
int64 total = 0;
AccountAmount[] memory spends = new AccountAmount[](transfers.length);
uint256 spendsCount = 0;
for (uint256 i = 0; i < transfers.length; i++) {
total += transfers[i].amount;
if (transfers[i].amount > 0) {
continue;
}
uint256 accountSpendIndex;
for (accountSpendIndex = 0; accountSpendIndex <= spendsCount; accountSpendIndex++) {
if (accountSpendIndex == spendsCount) {
spends[spendsCount] = AccountAmount(transfers[i].accountID, -transfers[i].amount, false);
spendsCount++;
break;
}
if (spends[accountSpendIndex].accountID == transfers[i].accountID) {
spends[accountSpendIndex].amount -= transfers[i].amount;
break;
}
}
bool isApproval = transfers[i].accountID != msg.sender;
if (transfers[i].isApproval != isApproval) return HederaResponseCodes.SPENDER_DOES_NOT_HAVE_ALLOWANCE;
uint256 totalAccountSpend = uint256(uint64(spends[accountSpendIndex].amount));
if (isApproval) {
uint256 allowanceLimit = token == address(0) ?
__allowance(transfers[i].accountID, msg.sender) :
IERC20(token).allowance(transfers[i].accountID, msg.sender);
if (allowanceLimit < totalAccountSpend) return HederaResponseCodes.MAX_ALLOWANCES_EXCEEDED;
}
if (token == address(0) && transfers[i].accountID.balance < totalAccountSpend) {
return HederaResponseCodes.INSUFFICIENT_ACCOUNT_BALANCE;
}
if (token != address(0) && IERC20(token).balanceOf(transfers[i].accountID) < totalAccountSpend) {
return HederaResponseCodes.INSUFFICIENT_TOKEN_BALANCE;
}
}

for (uint256 first = 0; first < transfers.length; first++) {
for (uint256 second = 0; second < transfers.length; second++) {
if (first == second) continue;
bool bothToOrFrom = (transfers[first].amount > 0) == (transfers[second].amount > 0);
if (transfers[first].accountID == transfers[second].accountID && bothToOrFrom) {
return HederaResponseCodes.ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS;
}
}
}

return total == 0 ? HederaResponseCodes.SUCCESS : HederaResponseCodes.INVALID_ACCOUNT_AMOUNTS;
}

function _updateHbarBalanceOnAccount(address account, uint256 newBalance) internal virtual returns (int64) {
if (newBalance == account.balance) return HederaResponseCodes.SUCCESS; // No change required anyway.

return HederaResponseCodes.NOT_SUPPORTED;
}
}
6 changes: 6 additions & 0 deletions contracts/HtsSystemContractJson.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {decode} from './Base64.sol';
import {HederaResponseCodes} from "./HederaResponseCodes.sol";
import {HtsSystemContract, HTS_ADDRESS} from "./HtsSystemContract.sol";
import {IERC20} from "./IERC20.sol";
import {MirrorNode} from "./MirrorNode.sol";
Expand Down Expand Up @@ -473,4 +474,9 @@ contract HtsSystemContractJson is HtsSystemContract {
function _scratchAddr() private view returns (address) {
return address(bytes20(keccak256(abi.encode(address(this)))));
}

function _updateHbarBalanceOnAccount(address account, uint256 newBalance) internal override returns (int64) {
vm.deal(account, newBalance);
return HederaResponseCodes.SUCCESS;
}
}
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
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
2 changes: 2 additions & 0 deletions examples/hardhat-hts-crypto-transfer-hbar/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# If you want to deploy on testnet go to https://portal.hedera.com/ to setup your private key
TESTNET_OPERATOR_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
11 changes: 11 additions & 0 deletions examples/hardhat-hts-crypto-transfer-hbar/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules
.env
coverage
coverage.json
typechain
typechain-types
types

#Hardhat files
cache
artifacts
21 changes: 21 additions & 0 deletions examples/hardhat-hts-crypto-transfer-hbar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# HTS crypto transfer hbars example

Simple scripts to show and investigate how the crypto transfer actually works for the hbar operations on the testnet.

Requires testnet account with non-empty balance. Each test run will result in decreasing the balance.

## Configuration

Create `.env` file based on `.env.example`

```
# Alias accounts keys
TESTNET_OPERATOR_PRIVATE_KEY=
```

## Setup & Install

In the project directory:

1. Run `npm install`
2. Run `npx hardhat test`
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;
// pragma experimental ABIEncoderV2;

interface IHederaTokenService {

// /// Transfers cryptocurrency among two or more accounts by making the desired adjustments to their
// /// balances. Each transfer list can specify up to 10 adjustments. Each negative amount is withdrawn
// /// from the corresponding account (a sender), and each positive one is added to the corresponding
// /// account (a receiver). The amounts list must sum to zero. Each amount is a number of tinybars
// /// (there are 100,000,000 tinybars in one hbar). If any sender account fails to have sufficient
// /// hbars, then the entire transaction fails, and none of those transfers occur, though the
// /// transaction fee is still charged. This transaction must be signed by the keys for all the sending
// /// 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;

// 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;
}

// /// 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;

// The solidity address of the receiver
address receiverAccountID;

// 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;
}

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 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;
}

/// Performs transfers among combinations of tokens and hbars
/// @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);
}
50 changes: 50 additions & 0 deletions examples/hardhat-hts-crypto-transfer-hbar/hardhat.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*-
* Hedera Hardhat Forking Plugin
*
* Copyright (C) 2024 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

require('dotenv').config();
require('@nomicfoundation/hardhat-toolbox');
require('@nomicfoundation/hardhat-chai-matchers');

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
mocha: {
timeout: 3600000,
},
solidity: {
version: '0.8.9',
settings: {
optimizer: {
enabled: true,
runs: 500,
},
},
},
defaultNetwork: 'testnet',
networks: {
hardhat: {
allowUnlimitedContractSize: true,
},
testnet: {
url: 'https://testnet.hashio.io/api',
accounts: process.env.TESTNET_OPERATOR_PRIVATE_KEY
? [process.env.TESTNET_OPERATOR_PRIVATE_KEY]
: [],
chainId: 296,
},
},
};
Loading

0 comments on commit cfc904c

Please sign in to comment.