diff --git a/packages/oft-hts/.gitignore b/packages/oft-hts/.gitignore
new file mode 100644
index 0000000000..3072a07426
--- /dev/null
+++ b/packages/oft-hts/.gitignore
@@ -0,0 +1,5 @@
+out
+cache
+
+# artifacts; ignore all files except the local contract artifacts.
+artifacts/*
\ No newline at end of file
diff --git a/packages/oft-hts/README.md b/packages/oft-hts/README.md
new file mode 100644
index 0000000000..a1f0ae9423
--- /dev/null
+++ b/packages/oft-hts/README.md
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+@layerzerolabs/oft-hts
+
+
+
+
+
+
+
+
+
+
+
+## Installation
+
+```bash
+pnpm install @layerzerolabs/oft-hts
+```
+
+```bash
+yarn install @layerzerolabs/oft-hts
+```
+
+```bash
+npm install @layerzerolabs/oft-hts
+```
diff --git a/packages/oft-hts/contracts/HederaTokenService.sol b/packages/oft-hts/contracts/HederaTokenService.sol
new file mode 100644
index 0000000000..24f21da251
--- /dev/null
+++ b/packages/oft-hts/contracts/HederaTokenService.sol
@@ -0,0 +1,159 @@
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity >=0.5.0 <0.9.0;
+pragma experimental ABIEncoderV2;
+
+import { IHederaTokenService } from "./IHederaTokenService.sol";
+
+abstract contract HederaTokenService {
+ // all response codes are defined here https://github.com/hashgraph/hedera-smart-contracts/blob/main/contracts/system-contracts/HederaResponseCodes.sol
+ int32 constant UNKNOWN_CODE = 21;
+ int32 constant SUCCESS_CODE = 22;
+
+ address constant precompileAddress = address(0x167);
+ // 90 days in seconds
+ int32 constant defaultAutoRenewPeriod = 7776000;
+
+ modifier nonEmptyExpiry(IHederaTokenService.HederaToken memory token) {
+ if (token.expiry.second == 0 && token.expiry.autoRenewPeriod == 0) {
+ token.expiry.autoRenewPeriod = defaultAutoRenewPeriod;
+ }
+ _;
+ }
+
+ /// Generic event
+ event CallResponseEvent(bool, bytes);
+
+ /// 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
+ /// INVALID_TOKEN_ID
+ /// @param amount Applicable to tokens of type FUNGIBLE_COMMON. The amount to mint to the Treasury Account.
+ /// Amount must be a positive non-zero number represented in the lowest denomination of the
+ /// token. The new supply must be lower than 2^63.
+ /// @param metadata Applicable to tokens of type NON_FUNGIBLE_UNIQUE. A list of metadata that are being created.
+ /// Maximum allowed size of each metadata is 100 bytes
+ /// @return responseCode The response code for the status of the request. SUCCESS is 22.
+ /// @return newTotalSupply The new supply of tokens. For NFTs it is the total count of NFTs
+ /// @return serialNumbers If the token is an NFT the newly generate serial numbers, otherwise empty.
+ function mintToken(
+ address token,
+ int64 amount,
+ bytes[] memory metadata
+ )
+ internal
+ returns (
+ int responseCode,
+ int64 newTotalSupply,
+ int64[] memory serialNumbers
+ )
+ {
+ (bool success, bytes memory result) = precompileAddress.call(
+ abi.encodeWithSelector(
+ IHederaTokenService.mintToken.selector,
+ token,
+ amount,
+ metadata
+ )
+ );
+ (responseCode, newTotalSupply, serialNumbers) = success
+ ? abi.decode(result, (int32, int64, int64[]))
+ : (HederaTokenService.UNKNOWN_CODE, int64(0), new int64[](0));
+ }
+
+ /// Burns an amount of the token from the defined treasury account
+ /// @param token The token for which to burn tokens. If token does not exist, transaction results in
+ /// INVALID_TOKEN_ID
+ /// @param amount Applicable to tokens of type FUNGIBLE_COMMON. The amount to burn from the Treasury Account.
+ /// Amount must be a positive non-zero number, not bigger than the token balance of the treasury
+ /// account (0; balance], represented in the lowest denomination.
+ /// @param serialNumbers Applicable to tokens of type NON_FUNGIBLE_UNIQUE. The list of serial numbers to be burned.
+ /// @return responseCode The response code for the status of the request. SUCCESS is 22.
+ /// @return newTotalSupply The new supply of tokens. For NFTs it is the total count of NFTs
+ function burnToken(
+ address token,
+ int64 amount,
+ int64[] memory serialNumbers
+ ) internal returns (int responseCode, int64 newTotalSupply) {
+ (bool success, bytes memory result) = precompileAddress.call(
+ abi.encodeWithSelector(
+ IHederaTokenService.burnToken.selector,
+ token,
+ amount,
+ serialNumbers
+ )
+ );
+ (responseCode, newTotalSupply) = success
+ ? abi.decode(result, (int32, int64))
+ : (HederaTokenService.UNKNOWN_CODE, int64(0));
+ }
+
+ /// Creates a Fungible Token with the specified properties
+ /// @param token the basic properties of the token being created
+ /// @param initialTotalSupply Specifies the initial supply of tokens to be put in circulation. The
+ /// initial supply is sent to the Treasury Account. The supply is in the lowest denomination possible.
+ /// @param decimals the number of decimal places a token is divisible by
+ /// @return responseCode The response code for the status of the request. SUCCESS is 22.
+ /// @return tokenAddress the created token's address
+ function createFungibleToken(
+ IHederaTokenService.HederaToken memory token,
+ int64 initialTotalSupply,
+ int32 decimals
+ )
+ internal
+ nonEmptyExpiry(token)
+ returns (int responseCode, address tokenAddress)
+ {
+ (bool success, bytes memory result) = precompileAddress.call{
+ value: msg.value
+ }(
+ abi.encodeWithSelector(
+ IHederaTokenService.createFungibleToken.selector,
+ token,
+ initialTotalSupply,
+ decimals
+ )
+ );
+
+ (responseCode, tokenAddress) = success
+ ? abi.decode(result, (int32, address))
+ : (HederaTokenService.UNKNOWN_CODE, address(0));
+ }
+
+ /// 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
+ /// (positive amount) or receiving (negative amount)
+ /// @param token The token to transfer to/from
+ /// @param sender The sender for the transaction
+ /// @param receiver 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 receiver,
+ int64 amount
+ ) internal returns (int responseCode) {
+ (bool success, bytes memory result) = precompileAddress.call(
+ abi.encodeWithSelector(
+ IHederaTokenService.transferToken.selector,
+ token,
+ sender,
+ receiver,
+ amount
+ )
+ );
+ responseCode = success
+ ? abi.decode(result, (int32))
+ : HederaTokenService.UNKNOWN_CODE;
+ }
+
+ /// Operation to update token keys
+ /// @param token The token address
+ /// @param keys The token keys
+ /// @return responseCode The response code for the status of the request. SUCCESS is 22.
+ function updateTokenKeys(address token, IHederaTokenService.TokenKey[] memory keys)
+ internal returns (int64 responseCode){
+ (bool success, bytes memory result) = precompileAddress.call(
+ abi.encodeWithSelector(IHederaTokenService.updateTokenKeys.selector, token, keys));
+ (responseCode) = success ? abi.decode(result, (int32)) : HederaTokenService.UNKNOWN_CODE;
+ }
+
+}
\ No newline at end of file
diff --git a/packages/oft-hts/contracts/IHederaTokenService.sol b/packages/oft-hts/contracts/IHederaTokenService.sol
new file mode 100644
index 0000000000..8e65057575
--- /dev/null
+++ b/packages/oft-hts/contracts/IHederaTokenService.sol
@@ -0,0 +1,167 @@
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity >=0.4.9 <0.9.0;
+pragma experimental ABIEncoderV2;
+
+interface IHederaTokenService {
+ /// Expiry properties of a Hedera token - second, autoRenewAccount, autoRenewPeriod
+ struct Expiry {
+ // The epoch second at which the token should expire; if an auto-renew account and period are
+ // specified, this is coerced to the current epoch second plus the autoRenewPeriod
+ int64 second;
+ // ID of an account which will be automatically charged to renew the token's expiration, at
+ // autoRenewPeriod interval, expressed as a solidity address
+ address autoRenewAccount;
+ // The interval at which the auto-renew account will be charged to extend the token's expiry
+ int64 autoRenewPeriod;
+ }
+
+ /// A Key can be a public key from either the Ed25519 or ECDSA(secp256k1) signature schemes, where
+ /// in the ECDSA(secp256k1) case we require the 33-byte compressed form of the public key. We call
+ /// these public keys primitive keys.
+ /// A Key can also be the ID of a smart contract instance, which is then authorized to perform any
+ /// precompiled contract action that requires this key to sign.
+ /// Note that when a Key is a smart contract ID, it doesn't mean the contract with that ID
+ /// will actually create a cryptographic signature. It only means that when the contract calls a
+ /// precompiled contract, the resulting "child transaction" will be authorized to perform any action
+ /// controlled by the Key.
+ /// Exactly one of the possible values should be populated in order for the Key to be valid.
+ struct KeyValue {
+ // if set to true, the key of the calling Hedera account will be inherited as the token key
+ bool inheritAccountKey;
+ // smart contract instance that is authorized as if it had signed with a key
+ address contractId;
+ // Ed25519 public key bytes
+ bytes ed25519;
+ // Compressed ECDSA(secp256k1) public key bytes
+ bytes ECDSA_secp256k1;
+ // A smart contract that, if the recipient of the active message frame, should be treated
+ // as having signed. (Note this does not mean the code being executed in the frame
+ // will belong to the given contract, since it could be running another contract's code via
+ // delegatecall. So setting this key is a more permissive version of setting the
+ // contractID key, which also requires the code in the active message frame belong to the
+ // the contract with the given id.)
+ address delegatableContractId;
+ }
+
+ /// A list of token key types the key should be applied to and the value of the key
+ struct TokenKey {
+ // bit field representing the key type. Keys of all types that have corresponding bits set to 1
+ // will be created for the token.
+ // 0th bit: adminKey
+ // 1st bit: kycKey
+ // 2nd bit: freezeKey
+ // 3rd bit: wipeKey
+ // 4th bit: supplyKey
+ // 5th bit: feeScheduleKey
+ // 6th bit: pauseKey
+ // 7th bit: ignored
+ uint keyType;
+ // the value that will be set to the key type
+ KeyValue key;
+ }
+
+ /// Basic properties of a Hedera Token - name, symbol, memo, tokenSupplyType, maxSupply,
+ /// treasury, freezeDefault. These properties are related both to Fungible and NFT token types.
+ struct HederaToken {
+ // The publicly visible name of the token. The token name is specified as a Unicode string.
+ // Its UTF-8 encoding cannot exceed 100 bytes, and cannot contain the 0 byte (NUL).
+ string name;
+ // The publicly visible token symbol. The token symbol is specified as a Unicode string.
+ // Its UTF-8 encoding cannot exceed 100 bytes, and cannot contain the 0 byte (NUL).
+ string symbol;
+ // The ID of the account which will act as a treasury for the token as a solidity address.
+ // This account will receive the specified initial supply or the newly minted NFTs in
+ // the case for NON_FUNGIBLE_UNIQUE Type
+ address treasury;
+ // The memo associated with the token (UTF-8 encoding max 100 bytes)
+ string memo;
+ // IWA compatibility. Specified the token supply type. Defaults to INFINITE
+ bool tokenSupplyType;
+ // IWA Compatibility. Depends on TokenSupplyType. For tokens of type FUNGIBLE_COMMON - the
+ // maximum number of tokens that can be in circulation. For tokens of type NON_FUNGIBLE_UNIQUE -
+ // the maximum number of NFTs (serial numbers) that can be minted. This field can never be changed!
+ int64 maxSupply;
+ // The default Freeze status (frozen or unfrozen) of Hedera accounts relative to this token. If
+ // true, an account must be unfrozen before it can receive the token
+ bool freezeDefault;
+ // list of keys to set to the token
+ TokenKey[] tokenKeys;
+ // expiry properties of a Hedera token - second, autoRenewAccount, autoRenewPeriod
+ Expiry expiry;
+ }
+
+ /**********************
+ * Direct HTS Calls *
+ **********************/
+
+ /// 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
+ /// INVALID_TOKEN_ID
+ /// @param amount Applicable to tokens of type FUNGIBLE_COMMON. The amount to mint to the Treasury Account.
+ /// Amount must be a positive non-zero number represented in the lowest denomination of the
+ /// token. The new supply must be lower than 2^63.
+ /// @param metadata Applicable to tokens of type NON_FUNGIBLE_UNIQUE. A list of metadata that are being created.
+ /// Maximum allowed size of each metadata is 100 bytes
+ /// @return responseCode The response code for the status of the request. SUCCESS is 22.
+ /// @return newTotalSupply The new supply of tokens. For NFTs it is the total count of NFTs
+ /// @return serialNumbers If the token is an NFT the newly generate serial numbers, othersise empty.
+ function mintToken(
+ address token,
+ int64 amount,
+ bytes[] memory metadata
+ )
+ external
+ returns (
+ int64 responseCode,
+ int64 newTotalSupply,
+ int64[] memory serialNumbers
+ );
+
+ /// Burns an amount of the token from the defined treasury account
+ /// @param token The token for which to burn tokens. If token does not exist, transaction results in
+ /// INVALID_TOKEN_ID
+ /// @param amount Applicable to tokens of type FUNGIBLE_COMMON. The amount to burn from the Treasury Account.
+ /// Amount must be a positive non-zero number, not bigger than the token balance of the treasury
+ /// account (0; balance], represented in the lowest denomination.
+ /// @param serialNumbers Applicable to tokens of type NON_FUNGIBLE_UNIQUE. The list of serial numbers to be burned.
+ /// @return responseCode The response code for the status of the request. SUCCESS is 22.
+ /// @return newTotalSupply The new supply of tokens. For NFTs it is the total count of NFTs
+ function burnToken(
+ address token,
+ int64 amount,
+ int64[] memory serialNumbers
+ ) external returns (int64 responseCode, int64 newTotalSupply);
+
+ /// Creates a Fungible Token with the specified properties
+ /// @param token the basic properties of the token being created
+ /// @param initialTotalSupply Specifies the initial supply of tokens to be put in circulation. The
+ /// initial supply is sent to the Treasury Account. The supply is in the lowest denomination possible.
+ /// @param decimals the number of decimal places a token is divisible by
+ /// @return responseCode The response code for the status of the request. SUCCESS is 22.
+ /// @return tokenAddress the created token's address
+ function createFungibleToken(
+ HederaToken memory token,
+ int64 initialTotalSupply,
+ int32 decimals
+ ) external payable returns (int64 responseCode, address tokenAddress);
+
+ /// 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
+ /// (positive amount) or receiving (negative amount)
+ /// @param token The token to transfer to/from
+ /// @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);
+
+ /// Operation to update token keys
+ /// @param token The token address
+ /// @param keys The token keys
+ /// @return responseCode The response code for the status of the request. SUCCESS is 22.
+ function updateTokenKeys(address token, TokenKey[] memory keys) external returns (int64 responseCode);
+}
\ No newline at end of file
diff --git a/packages/oft-hts/contracts/KeyHelper.sol b/packages/oft-hts/contracts/KeyHelper.sol
new file mode 100644
index 0000000000..ec36467eaa
--- /dev/null
+++ b/packages/oft-hts/contracts/KeyHelper.sol
@@ -0,0 +1,82 @@
+
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity >=0.5.0 <0.9.0;
+pragma experimental ABIEncoderV2;
+
+import { IHederaTokenService } from "./IHederaTokenService.sol";
+
+abstract contract KeyHelper {
+ using Bits for uint256;
+ address supplyContract;
+
+ mapping(KeyType => uint256) keyTypes;
+
+ enum KeyType {
+ ADMIN,
+ KYC,
+ FREEZE,
+ WIPE,
+ SUPPLY,
+ FEE,
+ PAUSE
+ }
+ enum KeyValueType {
+ INHERIT_ACCOUNT_KEY,
+ CONTRACT_ID,
+ ED25519,
+ SECP256K1,
+ DELEGETABLE_CONTRACT_ID
+ }
+
+ constructor() {
+ keyTypes[KeyType.ADMIN] = 1;
+ keyTypes[KeyType.KYC] = 2;
+ keyTypes[KeyType.FREEZE] = 4;
+ keyTypes[KeyType.WIPE] = 8;
+ keyTypes[KeyType.SUPPLY] = 16;
+ keyTypes[KeyType.FEE] = 32;
+ keyTypes[KeyType.PAUSE] = 64;
+ }
+
+ function getSingleKey(
+ KeyType keyType,
+ KeyValueType keyValueType,
+ bytes memory key
+ ) internal view returns (IHederaTokenService.TokenKey memory tokenKey) {
+ tokenKey = IHederaTokenService.TokenKey(
+ getKeyType(keyType),
+ getKeyValueType(keyValueType, key)
+ );
+ }
+
+ function getKeyType(KeyType keyType) internal view returns (uint256) {
+ return keyTypes[keyType];
+ }
+
+ function getKeyValueType(
+ KeyValueType keyValueType,
+ bytes memory key
+ ) internal view returns (IHederaTokenService.KeyValue memory keyValue) {
+ if (keyValueType == KeyValueType.INHERIT_ACCOUNT_KEY) {
+ keyValue.inheritAccountKey = true;
+ } else if (keyValueType == KeyValueType.CONTRACT_ID) {
+ keyValue.contractId = supplyContract;
+ } else if (keyValueType == KeyValueType.ED25519) {
+ keyValue.ed25519 = key;
+ } else if (keyValueType == KeyValueType.SECP256K1) {
+ keyValue.ECDSA_secp256k1 = key;
+ } else if (keyValueType == KeyValueType.DELEGETABLE_CONTRACT_ID) {
+ keyValue.delegatableContractId = supplyContract;
+ }
+ }
+}
+
+library Bits {
+ uint256 internal constant ONE = uint256(1);
+
+ // Sets the bit at the given 'index' in 'self' to '1'.
+ // Returns the modified value.
+ function setBit(uint256 self, uint8 index) internal pure returns (uint256) {
+ return self | (ONE << index);
+ }
+}
\ No newline at end of file
diff --git a/packages/oft-hts/contracts/OFTAdapterHTS.sol b/packages/oft-hts/contracts/OFTAdapterHTS.sol
new file mode 100644
index 0000000000..27434cbdc5
--- /dev/null
+++ b/packages/oft-hts/contracts/OFTAdapterHTS.sol
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity ^0.8.20;
+
+import { OFTCore } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol";
+import { HederaTokenService } from "./HederaTokenService.sol";
+import { IHederaTokenService } from "./IHederaTokenService.sol";
+import { KeyHelper } from "./KeyHelper.sol";
+
+/**
+ * @title HTS Connector for existing token
+ * @dev HTSConnectorExistingToken is a contract wrapped for already existing HTS token that extends the functionality of the OFTCore contract.
+ */
+abstract contract OFTAdapterHTS is OFTCore, KeyHelper, HederaTokenService {
+ address public htsTokenAddress;
+
+ /**
+ * @dev Constructor for the HTSConnectorExistingToken contract.
+ * @param _tokenAddress Address of already existing HTS token
+ * @param _lzEndpoint The LayerZero endpoint address.
+ * @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
+ */
+ constructor(
+ address _tokenAddress,
+ address _lzEndpoint,
+ address _delegate
+ ) payable OFTCore(8, _lzEndpoint, _delegate) {
+ htsTokenAddress = _tokenAddress;
+ }
+
+ /**
+ * @dev Retrieves the address of the underlying HTS implementation.
+ * @return The address of the HTS token.
+ */
+ function token() public view returns (address) {
+ return htsTokenAddress;
+ }
+
+ /**
+ * @notice Indicates whether the HTS Connector contract requires approval of the 'token()' to send.
+ * @return requiresApproval Needs approval of the underlying token implementation.
+ */
+ function approvalRequired() external pure virtual returns (bool) {
+ return false;
+ }
+
+ /**
+ * @dev Burns tokens from the sender's specified balance.
+ * @param _from The address to debit the tokens from.
+ * @param _amountLD The amount of tokens to send in local decimals.
+ * @param _minAmountLD The minimum amount to send in local decimals.
+ * @param _dstEid The destination chain ID.
+ * @return amountSentLD The amount sent in local decimals.
+ * @return amountReceivedLD The amount received in local decimals on the remote.
+ */
+ function _debit(
+ address _from,
+ uint256 _amountLD,
+ uint256 _minAmountLD,
+ uint32 _dstEid
+ ) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
+ (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);
+
+ int256 transferResponse = HederaTokenService.transferToken(htsTokenAddress, _from, address(this), int64(uint64(_amountLD)));
+ require(transferResponse == HederaTokenService.SUCCESS_CODE, "HTS: Transfer failed");
+
+ (int256 response,) = HederaTokenService.burnToken(htsTokenAddress, int64(uint64(amountSentLD)), new int64[](0));
+ require(response == HederaTokenService.SUCCESS_CODE, "HTS: Burn failed");
+ }
+
+ /**
+ * @dev Credits tokens to the specified address.
+ * @param _to The address to credit the tokens to.
+ * @param _amountLD The amount of tokens to credit in local decimals.
+ * @dev _srcEid The source chain ID.
+ * @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals.
+ */
+ function _credit(
+ address _to,
+ uint256 _amountLD,
+ uint32 /*_srcEid*/
+ ) internal virtual override returns (uint256) {
+ (int256 response, ,) = HederaTokenService.mintToken(htsTokenAddress, int64(uint64(_amountLD)), new bytes[](0));
+ require(response == HederaTokenService.SUCCESS_CODE, "HTS: Mint failed");
+
+ int256 transferResponse = HederaTokenService.transferToken(htsTokenAddress, address(this), _to, int64(uint64(_amountLD)));
+ require(transferResponse == HederaTokenService.SUCCESS_CODE, "HTS: Transfer failed");
+
+ return _amountLD;
+ }
+}
\ No newline at end of file
diff --git a/packages/oft-hts/contracts/OFTHederaTokenService.sol b/packages/oft-hts/contracts/OFTHederaTokenService.sol
new file mode 100644
index 0000000000..7d60fa254e
--- /dev/null
+++ b/packages/oft-hts/contracts/OFTHederaTokenService.sol
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: Apache-2.0
+pragma solidity ^0.8.20;
+
+import { OFTCore } from "@layerzerolabs/oft-evm/contracts/OFTCore.sol";
+import { HederaTokenService } from "./HederaTokenService.sol";
+import { IHederaTokenService } from "./IHederaTokenService.sol";
+import { KeyHelper } from "./KeyHelper.sol";
+
+/**
+ * @title OFT Hedera Token Service
+ * @dev OFT Hedera Token Service is a OFT token that extends the functionality of the OFTCore contract.
+ */
+abstract contract OFTHederaTokenService is OFTCore, KeyHelper, HederaTokenService {
+ address public htsTokenAddress;
+ bool public finiteTotalSupplyType = true;
+ event TokenCreated(address tokenAddress);
+
+ /**
+ * @dev Constructor for the HTS Connector contract.
+ * @param _name The name of HTS token
+ * @param _symbol The symbol of HTS token
+ * @param _lzEndpoint The LayerZero endpoint address.
+ * @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
+ */
+ constructor(
+ string memory _name,
+ string memory _symbol,
+ address _lzEndpoint,
+ address _delegate
+ ) payable OFTCore(8, _lzEndpoint, _delegate) {
+ IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1);
+ keys[0] = getSingleKey(
+ KeyType.SUPPLY,
+ KeyValueType.INHERIT_ACCOUNT_KEY,
+ bytes("")
+ );
+
+ IHederaTokenService.Expiry memory expiry = IHederaTokenService.Expiry(0, address(this), 8000000);
+ IHederaTokenService.HederaToken memory token = IHederaTokenService.HederaToken(
+ _name, _symbol, address(this), "memo", finiteTotalSupplyType, 5000, false, keys, expiry
+ );
+
+ (int responseCode, address tokenAddress) = HederaTokenService.createFungibleToken(
+ token, 1000, int32(int256(uint256(8)))
+ );
+ require(responseCode == HederaTokenService.SUCCESS_CODE, "Failed to create HTS token");
+
+ int256 transferResponse = HederaTokenService.transferToken(tokenAddress, address(this), msg.sender, 1000);
+ require(transferResponse == HederaTokenService.SUCCESS_CODE, "HTS: Transfer failed");
+
+ htsTokenAddress = tokenAddress;
+
+ emit TokenCreated(tokenAddress);
+ }
+
+ /**
+ * @dev Retrieves the address of the underlying HTS implementation.
+ * @return The address of the HTS token.
+ */
+ function token() public view returns (address) {
+ return htsTokenAddress;
+ }
+
+ /**
+ * @notice Indicates whether the HTS Connector contract requires approval of the 'token()' to send.
+ * @return requiresApproval Needs approval of the underlying token implementation.
+ */
+ function approvalRequired() external pure virtual returns (bool) {
+ return false;
+ }
+
+ /**
+ * @dev Burns tokens from the sender's specified balance.
+ * @param _from The address to debit the tokens from.
+ * @param _amountLD The amount of tokens to send in local decimals.
+ * @param _minAmountLD The minimum amount to send in local decimals.
+ * @param _dstEid The destination chain ID.
+ * @return amountSentLD The amount sent in local decimals.
+ * @return amountReceivedLD The amount received in local decimals on the remote.
+ */
+ function _debit(
+ address _from,
+ uint256 _amountLD,
+ uint256 _minAmountLD,
+ uint32 _dstEid
+ ) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
+ require(_amountLD <= uint64(type(int64).max), "HTSConnector: amount exceeds int64 safe range");
+
+ (amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);
+
+ int256 transferResponse = HederaTokenService.transferToken(htsTokenAddress, _from, address(this), int64(uint64(_amountLD)));
+ require(transferResponse == HederaTokenService.SUCCESS_CODE, "HTS: Transfer failed");
+
+ (int256 response,) = HederaTokenService.burnToken(htsTokenAddress, int64(uint64(amountSentLD)), new int64[](0));
+ require(response == HederaTokenService.SUCCESS_CODE, "HTS: Burn failed");
+ }
+
+ /**
+ * @dev Credits tokens to the specified address.
+ * @param _to The address to credit the tokens to.
+ * @param _amountLD The amount of tokens to credit in local decimals.
+ * @dev _srcEid The source chain ID.
+ * @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals.
+ */
+ function _credit(
+ address _to,
+ uint256 _amountLD,
+ uint32 /*_srcEid*/
+ ) internal virtual override returns (uint256) {
+ require(_amountLD <= uint64(type(int64).max), "HTSConnector: amount exceeds int64 safe range");
+
+ (int256 response, ,) = HederaTokenService.mintToken(htsTokenAddress, int64(uint64(_amountLD)), new bytes[](0));
+ require(response == HederaTokenService.SUCCESS_CODE, "HTS: Mint failed");
+
+ int256 transferResponse = HederaTokenService.transferToken(htsTokenAddress, address(this), _to, int64(uint64(_amountLD)));
+ require(transferResponse == HederaTokenService.SUCCESS_CODE, "HTS: Transfer failed");
+
+ return _amountLD;
+ }
+}
\ No newline at end of file
diff --git a/packages/oft-hts/foundry.toml b/packages/oft-hts/foundry.toml
new file mode 100644
index 0000000000..82e8d82ec5
--- /dev/null
+++ b/packages/oft-hts/foundry.toml
@@ -0,0 +1,26 @@
+[profile.default]
+solc = '0.8.22'
+verbosity = 3
+src = "contracts"
+test = "test"
+out = "artifacts"
+cache_path = "cache"
+optimizer = true
+optimizer_runs = 20_000
+
+libs = [
+ # - forge-std
+ # - ds-test
+ 'node_modules',
+]
+
+remappings = [
+ # Due to a misconfiguration of solidity-bytes-utils, an outdated version
+ # of forge-std is being dragged in
+ #
+ # To remedy this, we'll remap the ds-test and forge-std imports to our own versions
+ '@layerzerolabs/=node_modules/@layerzerolabs/',
+]
+
+[fuzz]
+runs = 1000
diff --git a/packages/oft-hts/package.json b/packages/oft-hts/package.json
new file mode 100644
index 0000000000..659426b54f
--- /dev/null
+++ b/packages/oft-hts/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "@layerzerolabs/oft-hts",
+ "version": "0.0.1",
+ "description": "LayerZero Labs reference HTS OmniChain Fungible Token (OFT) implementation",
+ "keywords": [
+ "LayerZero",
+ "OFT",
+ "OmniChain",
+ "Fungible",
+ "Token",
+ "EndpointV2",
+ "HTS"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/LayerZero-Labs/devtools.git",
+ "directory": "packages/oft-hts"
+ },
+ "license": "MIT",
+ "exports": {
+ "./package.json": "./package.json",
+ "./artifacts/*.json": {
+ "require": "./artifacts/*.json",
+ "imports": "./artifacts/*.json"
+ }
+ },
+ "files": [
+ "artifacts/HederaTokenService.sol/HederaTokenService.json",
+ "artifacts/IHederaTokenService.sol/IHederaTokenService.json",
+ "artifacts/KeyHelper.sol/KeyHelper.json",
+ "artifacts/OFT.sol/OFT.json",
+ "contracts/**/*",
+ "test/**/*"
+ ],
+ "scripts": {
+ "clean": "rimraf .turbo cache out artifacts",
+ "compile": "$npm_execpath compile:forge",
+ "compile:forge": "forge build",
+ "test": "$npm_execpath test:forge",
+ "test:forge": "forge test"
+ },
+ "devDependencies": {
+ "@layerzerolabs/lz-evm-messagelib-v2": "^3.0.75",
+ "@layerzerolabs/lz-evm-protocol-v2": "^3.0.75",
+ "@layerzerolabs/lz-evm-v1-0.7": "^3.0.75",
+ "@layerzerolabs/oapp-evm": "^0.3.2",
+ "@layerzerolabs/oft-evm": "^3.2.0",
+ "@layerzerolabs/test-devtools-evm-foundry": "~7.0.0",
+ "@layerzerolabs/toolbox-foundry": "^0.1.12",
+ "@openzeppelin/contracts": "^5.0.2",
+ "@openzeppelin/contracts-upgradeable": "^5.0.2",
+ "rimraf": "^5.0.5"
+ },
+ "peerDependencies": {
+ "@layerzerolabs/lz-evm-messagelib-v2": "^3.0.75",
+ "@layerzerolabs/lz-evm-protocol-v2": "^3.0.75",
+ "@layerzerolabs/lz-evm-v1-0.7": "^3.0.75",
+ "@layerzerolabs/oapp-evm": "^0.3.2",
+ "@layerzerolabs/oft-evm": "^3.2.0",
+ "@openzeppelin/contracts": "^4.8.1 || ^5.0.0",
+ "@openzeppelin/contracts-upgradeable": "^4.8.1 || ^5.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5801e42d38..a93d06ade7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4568,6 +4568,39 @@ importers:
specifier: ^5.0.5
version: 5.0.9
+ packages/oft-hts:
+ devDependencies:
+ '@layerzerolabs/lz-evm-messagelib-v2':
+ specifier: ^3.0.75
+ version: 3.0.86(@axelar-network/axelar-gmp-sdk-solidity@5.10.0)(@chainlink/contracts-ccip@0.7.6)(@eth-optimism/contracts@0.6.0)(@layerzerolabs/lz-evm-protocol-v2@3.0.86)(@layerzerolabs/lz-evm-v1-0.7@3.0.86)(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4)(solidity-bytes-utils@0.8.2)
+ '@layerzerolabs/lz-evm-protocol-v2':
+ specifier: ^3.0.75
+ version: 3.0.86(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4)(solidity-bytes-utils@0.8.2)
+ '@layerzerolabs/lz-evm-v1-0.7':
+ specifier: ^3.0.75
+ version: 3.0.86(@openzeppelin/contracts-upgradeable@5.1.0)(@openzeppelin/contracts@5.1.0)(hardhat-deploy@0.12.4)
+ '@layerzerolabs/oapp-evm':
+ specifier: ^0.3.2
+ version: link:../oapp-evm
+ '@layerzerolabs/oft-evm':
+ specifier: ^3.2.0
+ version: link:../oft-evm
+ '@layerzerolabs/test-devtools-evm-foundry':
+ specifier: ~7.0.0
+ version: link:../test-devtools-evm-foundry
+ '@layerzerolabs/toolbox-foundry':
+ specifier: ^0.1.12
+ version: link:../toolbox-foundry
+ '@openzeppelin/contracts':
+ specifier: ^5.0.2
+ version: 5.1.0
+ '@openzeppelin/contracts-upgradeable':
+ specifier: ^5.0.2
+ version: 5.1.0(@openzeppelin/contracts@5.1.0)
+ rimraf:
+ specifier: ^5.0.5
+ version: 5.0.9
+
packages/oft-move:
devDependencies:
'@aptos-labs/ts-sdk':