From 77e71e6613c0dd6a5f1e45f7d0bcc4cadf19628a Mon Sep 17 00:00:00 2001 From: nikolay Date: Fri, 17 Jan 2025 12:54:18 +0200 Subject: [PATCH] chore: add LZ contracts Signed-off-by: nikolay --- lib/layer-zero/ExampleHTSConnector.sol | 17 ++ .../ExampleHTSConnectorExistingToken.sol | 16 ++ lib/layer-zero/ExampleOApp.sol | 74 ++++++++ lib/layer-zero/ExampleOFT.sol | 25 +++ lib/layer-zero/ExampleOFTAdapter.sol | 13 ++ lib/layer-zero/ExampleONFT.sol | 17 ++ lib/layer-zero/ExampleONFTAdapter.sol | 13 ++ lib/layer-zero/HTSConnector.sol | 116 ++++++++++++ lib/layer-zero/HTSConnectorExistingToken.sol | 90 ++++++++++ lib/layer-zero/hts/HederaTokenService.sol | 159 +++++++++++++++++ lib/layer-zero/hts/IHederaTokenService.sol | 167 ++++++++++++++++++ lib/layer-zero/hts/KeyHelper.sol | 81 +++++++++ 12 files changed, 788 insertions(+) create mode 100644 lib/layer-zero/ExampleHTSConnector.sol create mode 100644 lib/layer-zero/ExampleHTSConnectorExistingToken.sol create mode 100644 lib/layer-zero/ExampleOApp.sol create mode 100644 lib/layer-zero/ExampleOFT.sol create mode 100644 lib/layer-zero/ExampleOFTAdapter.sol create mode 100644 lib/layer-zero/ExampleONFT.sol create mode 100644 lib/layer-zero/ExampleONFTAdapter.sol create mode 100644 lib/layer-zero/HTSConnector.sol create mode 100644 lib/layer-zero/HTSConnectorExistingToken.sol create mode 100644 lib/layer-zero/hts/HederaTokenService.sol create mode 100644 lib/layer-zero/hts/IHederaTokenService.sol create mode 100644 lib/layer-zero/hts/KeyHelper.sol diff --git a/lib/layer-zero/ExampleHTSConnector.sol b/lib/layer-zero/ExampleHTSConnector.sol new file mode 100644 index 000000000..8623904af --- /dev/null +++ b/lib/layer-zero/ExampleHTSConnector.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./hts/HederaTokenService.sol"; +import "./hts/IHederaTokenService.sol"; +import "./hts/KeyHelper.sol"; +import "./HTSConnector.sol"; + +contract ExampleHTSConnector is Ownable, HTSConnector { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate + ) payable HTSConnector(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} +} diff --git a/lib/layer-zero/ExampleHTSConnectorExistingToken.sol b/lib/layer-zero/ExampleHTSConnectorExistingToken.sol new file mode 100644 index 000000000..416deb29a --- /dev/null +++ b/lib/layer-zero/ExampleHTSConnectorExistingToken.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./hts/HederaTokenService.sol"; +import "./hts/IHederaTokenService.sol"; +import "./hts/KeyHelper.sol"; +import "./HTSConnectorExistingToken.sol"; + +contract ExampleHTSConnectorExistingToken is Ownable, HTSConnectorExistingToken { + constructor( + address _tokenAddress, + address _lzEndpoint, + address _delegate + ) payable HTSConnectorExistingToken(_tokenAddress, _lzEndpoint, _delegate) Ownable(_delegate) {} +} diff --git a/lib/layer-zero/ExampleOApp.sol b/lib/layer-zero/ExampleOApp.sol new file mode 100644 index 000000000..7899bd62d --- /dev/null +++ b/lib/layer-zero/ExampleOApp.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import {OApp, Origin, MessagingFee} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oapp/OApp.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract ExampleOApp is OApp { + constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {} + + // Some arbitrary data you want to deliver to the destination chain! + string public data; + + /** + * @notice Sends a message from the source to destination chain. + * @param _dstEid Destination chain's endpoint ID. + * @param _message The message to send. + * @param _options Message execution options (e.g., for sending gas to destination). + */ + function send( + uint32 _dstEid, + string memory _message, + bytes calldata _options + ) external payable { + // Encodes the message before invoking _lzSend. + // Replace with whatever data you want to send! + bytes memory _payload = abi.encode(_message); + _lzSend( + _dstEid, + _payload, + _options, + // Fee in native gas and ZRO token. + MessagingFee(msg.value, 0), + // Refund address in case of failed source message. + payable(msg.sender) + ); + } + + /** + * @dev Called when data is received from the protocol. It overrides the equivalent function in the parent contract. + * Protocol messages are defined as packets, comprised of the following parameters. + * @param _origin A struct containing information about where the packet came from. + * @param _guid A global unique identifier for tracking the packet. + * @param payload Encoded message. + */ + function _lzReceive( + Origin calldata _origin, + bytes32 _guid, + bytes calldata payload, + address, // Executor address as specified by the OApp. + bytes calldata // Any extra data or options to trigger on receipt. + ) internal override { + // Decode the payload to get the message + // In this case, type is string, but depends on your encoding! + data = abi.decode(payload, (string)); + } + + /** + * @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token. + * @param _dstEid Destination chain's endpoint ID. + * @param _message The message. + * @param _options Message execution options (e.g., for sending gas to destination). + * @param _payInLzToken Whether to return fee in ZRO token. + * @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token. + */ + function quote( + uint32 _dstEid, + string memory _message, + bytes memory _options, + bool _payInLzToken + ) public view returns (MessagingFee memory fee) { + bytes memory payload = abi.encode(_message); + fee = _quote(_dstEid, payload, _options, _payInLzToken); + } +} diff --git a/lib/layer-zero/ExampleOFT.sol b/lib/layer-zero/ExampleOFT.sol new file mode 100644 index 000000000..22f5cc6ac --- /dev/null +++ b/lib/layer-zero/ExampleOFT.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {OFT} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFT.sol"; + +contract ExampleOFT is OFT { + uint8 decimalsArg = 8; + + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate, + uint256 _initialMint, + uint8 _decimals + ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) { + _mint(msg.sender, _initialMint); + decimalsArg = _decimals; + } + + function decimals() public view override returns (uint8) { + return decimalsArg; + } +} diff --git a/lib/layer-zero/ExampleOFTAdapter.sol b/lib/layer-zero/ExampleOFTAdapter.sol new file mode 100644 index 000000000..c3021e0f9 --- /dev/null +++ b/lib/layer-zero/ExampleOFTAdapter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import {OFTAdapter} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTAdapter.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract ExampleOFTAdapter is OFTAdapter { + constructor( + address _token, + address _lzEndpoint, + address _owner + ) OFTAdapter(_token, _lzEndpoint, _owner) Ownable(_owner) {} +} diff --git a/lib/layer-zero/ExampleONFT.sol b/lib/layer-zero/ExampleONFT.sol new file mode 100644 index 000000000..95924ef2d --- /dev/null +++ b/lib/layer-zero/ExampleONFT.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ONFT721} from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721.sol"; + +contract ExampleONFT is ONFT721 { + constructor( + string memory _name, + string memory _symbol, + address _lzEndpoint, + address _delegate, + uint256 tokenId + ) ONFT721(_name, _symbol, _lzEndpoint, _delegate) { + _mint(msg.sender, tokenId); + } +} diff --git a/lib/layer-zero/ExampleONFTAdapter.sol b/lib/layer-zero/ExampleONFTAdapter.sol new file mode 100644 index 000000000..da94fb6a7 --- /dev/null +++ b/lib/layer-zero/ExampleONFTAdapter.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ONFT721Adapter} from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721Adapter.sol"; + +contract ExampleONFTAdapter is ONFT721Adapter { + constructor( + address _token, + address _lzEndpoint, + address _owner + ) ONFT721Adapter(_token, _lzEndpoint, _owner) { } +} diff --git a/lib/layer-zero/HTSConnector.sol b/lib/layer-zero/HTSConnector.sol new file mode 100644 index 000000000..da21587fb --- /dev/null +++ b/lib/layer-zero/HTSConnector.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {OFTCore} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTCore.sol"; +import "./hts/HederaTokenService.sol"; +import "./hts/IHederaTokenService.sol"; +import "./hts/KeyHelper.sol"; + +/** + * @title HTS Connector + * @dev HTS Connector is a HTS token that extends the functionality of the OFTCore contract. + */ +abstract contract HTSConnector is OFTCore, KeyHelper, HederaTokenService { + address public htsTokenAddress; + + 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", true, 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) { + (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; + } +} diff --git a/lib/layer-zero/HTSConnectorExistingToken.sol b/lib/layer-zero/HTSConnectorExistingToken.sol new file mode 100644 index 000000000..51597af3f --- /dev/null +++ b/lib/layer-zero/HTSConnectorExistingToken.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.20; + +import {OFTCore} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTCore.sol"; +import "./hts/HederaTokenService.sol"; +import "./hts/IHederaTokenService.sol"; +import "./hts/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 HTSConnectorExistingToken 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; + } +} diff --git a/lib/layer-zero/hts/HederaTokenService.sol b/lib/layer-zero/hts/HederaTokenService.sol new file mode 100644 index 000000000..c7f4332c7 --- /dev/null +++ b/lib/layer-zero/hts/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.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; + } + +} diff --git a/lib/layer-zero/hts/IHederaTokenService.sol b/lib/layer-zero/hts/IHederaTokenService.sol new file mode 100644 index 000000000..065b70055 --- /dev/null +++ b/lib/layer-zero/hts/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); +} diff --git a/lib/layer-zero/hts/KeyHelper.sol b/lib/layer-zero/hts/KeyHelper.sol new file mode 100644 index 000000000..696366cc9 --- /dev/null +++ b/lib/layer-zero/hts/KeyHelper.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; +pragma experimental ABIEncoderV2; + +import "./HederaTokenService.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); + } +}