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 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/oft-hts

+ + +

+ + NPM Version + + Downloads + + NPM License +

+ +## 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':