Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add LZ <> Hedera HTS Connector for existing token example #3361

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions tools/layer-zero-example/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ ONFT_ADAPTER_BSC_CONTRACT=0x
HTS_CONNECTOR_HEDERA_CONTRACT=0x
HTS_CONNECTOR_BSC_CONTRACT=0x

# HTS Connector for existing token config
HTS_CONNECTOR_CREATE_HTS_CONTRACT=0x
HTS_CONNECTOR_EXISTING_TOKEN_HEDERA_CONTRACT=0x
HTS_CONNECTOR_EXISTING_TOKEN_BSC_CONTRACT=0x

# HTS Adapter config
HTS_ADAPTER_HTS_HEDERA_CONTRACT=0x
HTS_ADAPTER_ERC20_BSC_CONTRACT=0x
Expand Down
52 changes: 52 additions & 0 deletions tools/layer-zero-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,58 @@ npx hardhat test --grep "HTSConnectorTests @hedera @test" --network hedera_testn
npx hardhat test --grep "HTSConnectorTests @bsc @test" --network bsc_testnet
```

### HTS Connector for existing HTS token

That's a variant of OFT but using an already existing HTS token. Keep in mind that "supply key" of the token must contains the HTS Connector contract's address.

- Create an HTS token
```typescript
npx hardhat create-hts-token --network hedera_testnet
```

- Deploying OFT on an EVM chain and HTS Connector on the Hedera chain. The HTS Connector for existing token extends OFTCore and receives the HTS tokens address as constructor parameter. Also, overrides OFTCore _debit and _credit with related HTS mint and burn precompile calls
```
npx hardhat deploy-hts-connector-existing-token --token <existing_hts_token_address> --network hedera_testnet
npx hardhat deploy-oft --decimals 8 --mint 1000 --network bsc_testnet
```

- In order to connect OFTs together, we need to set the peer of the target OFT, more info can be found here https://docs.layerzero.network/v2/developers/evm/getting-started#connecting-your-contracts
```typescript
npx hardhat set-peer --source <hedera_oft_address> --target <bsc_oft_address> --network hedera_testnet
npx hardhat set-peer --source <bsc_oft_address> --target <hedera_oft_address> --network bsc_testnet
```

- Fill the .env

- Adding the HTSConnectorExistingToken contract's address as a supply key of the existing HTS token
```typescript
npx hardhat test --grep "HTSConnectorExistingToken @hedera @update-keys" --network hedera_testnet
```

- Funding the HTSConnectorExistingToken contract
```typescript
npx hardhat test --grep "HTSConnectorExistingToken @hedera @fund" --network hedera_testnet
```

- Approving HTS Connector to use some signer's tokens
```typescript
npx hardhat test --grep "HTSConnectorExistingToken @hedera @approve" --network hedera_testnet
```

- On these steps, we're sending tokens from an EVM chain to Hedera and receiving HTS tokens and vice versa
```typescript
npx hardhat test --grep "HTSConnectorExistingToken @hedera @send" --network hedera_testnet
npx hardhat test --grep "HTSConnectorExistingToken @bsc @send" --network bsc_testnet
```

- Wait a couple of minutes, the LZ progress can be tracked on https://testnet.layerzeroscan.com/tx/<tx_hash>

- Finally we're checking whether the balances are expected on both source and destination chains
```typescript
npx hardhat test --grep "HTSConnectorExistingToken @hedera @test" --network hedera_testnet
npx hardhat test --grep "HTSConnectorExistingToken @bsc @test" --network bsc_testnet
```

### HTS Adapter

If your HTS token already exists on Hedera and you want to connect it to another chain, you can deploy the OFT Adapter contract to act as an intermediary lockbox for it.
Expand Down
13 changes: 12 additions & 1 deletion tools/layer-zero-example/contracts/CreateHTS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ contract CreateHTS is Ownable, KeyHelper, HederaTokenService {
address public htsTokenAddress;

constructor(string memory _name, string memory _symbol, address _delegate) payable Ownable(_delegate) {
IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1);
IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](2);
keys[0] = getSingleKey(
KeyType.ADMIN,
KeyValueType.INHERIT_ACCOUNT_KEY,
bytes("")
);
keys[1] = getSingleKey(
KeyType.SUPPLY,
KeyValueType.INHERIT_ACCOUNT_KEY,
bytes("")
Expand All @@ -32,4 +37,10 @@ contract CreateHTS is Ownable, KeyHelper, HederaTokenService {

htsTokenAddress = tokenAddress;
}

function updateTokenKeysPublic(IHederaTokenService.TokenKey[] memory keys) public returns (int64 responseCode) {
(responseCode) = HederaTokenService.updateTokenKeys(htsTokenAddress, keys);

require(responseCode == HederaTokenService.SUCCESS_CODE, "HTS: Update keys reverted");
}
}
Original file line number Diff line number Diff line change
@@ -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) {}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
12 changes: 12 additions & 0 deletions tools/layer-zero-example/contracts/hts/HederaTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,16 @@ abstract contract HederaTokenService {
? 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,10 @@ interface IHederaTokenService {
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);
}
36 changes: 26 additions & 10 deletions tools/layer-zero-example/hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ task('deploy-whbar', 'Deploy WHBAR')
const contract = await contractFactory.deploy();
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) WHBAR to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) WHBAR to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-erc20', 'Deploy ERC20 token')
Expand All @@ -82,7 +82,7 @@ task('deploy-erc20', 'Deploy ERC20 token')
const contract = await contractFactory.deploy(taskArgs.mint, taskArgs.decimals);
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ERC20 deployed to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) ERC20 deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-erc721', 'Deploy ERC721 token')
Expand All @@ -91,7 +91,7 @@ task('deploy-erc721', 'Deploy ERC721 token')
const contract = await contractFactory.deploy();
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ERC721 deployed to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) ERC721 deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-oapp', 'Deploy OApp contract')
Expand All @@ -104,7 +104,7 @@ task('deploy-oapp', 'Deploy OApp contract')
const contract = await contractFactory.deploy(ENDPOINT_V2, signers[0].address);
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ExampleOApp deployed to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) ExampleOApp deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-oft', 'Deploy OFT contract')
Expand All @@ -119,7 +119,7 @@ task('deploy-oft', 'Deploy OFT contract')
const contract = await contractFactory.deploy('T_NAME', 'T_SYMBOL', ENDPOINT_V2, signers[0].address, taskArgs.mint, taskArgs.decimals);
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ExampleOFT deployed to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) ExampleOFT deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-hts-connector', 'Deploy HTS connector contract')
Expand All @@ -135,7 +135,23 @@ task('deploy-hts-connector', 'Deploy HTS connector contract')
});
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ExampleHTSConnector deployed to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) ExampleHTSConnector deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-hts-connector-existing-token', 'Deploy HTS connector for existing token contract')
.addParam('token', 'Already existing token address')
.setAction(async (taskArgs, hre) => {
const ethers = hre.ethers;
const signers = await ethers.getSigners();
const ENDPOINT_V2 = getEndpointAddress(hre.network.name);

const contractFactory = await ethers.getContractFactory('ExampleHTSConnectorExistingToken');
const contract = await contractFactory.deploy(taskArgs.token, ENDPOINT_V2, signers[0].address, {
gasLimit: 10_000_000,
});
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ExampleHTSConnectorExistingToken deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('create-hts-token', 'Create a HTS token')
Expand All @@ -150,7 +166,7 @@ task('create-hts-token', 'Create a HTS token')
});
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) Token address ${await contract.htsTokenAddress()}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) Token address ${await contract.htsTokenAddress()} contract address ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-oft-adapter', 'Deploy OFT adapter contract')
Expand All @@ -164,7 +180,7 @@ task('deploy-oft-adapter', 'Deploy OFT adapter contract')
const contract = await contractFactory.deploy(taskArgs.token, ENDPOINT_V2, signers[0].address);
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ExampleOFTAdapter for token ${taskArgs.token} deployed to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) ExampleOFTAdapter for token ${taskArgs.token} deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-onft', 'Deploy OFT contract')
Expand All @@ -184,7 +200,7 @@ task('deploy-onft', 'Deploy OFT contract')
const contract = await contractFactory.deploy('T_NAME', 'T_SYMBOL', ENDPOINT_V2, signers[0].address, tokenId);
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ExampleONFT deployed to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) ExampleONFT deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('deploy-onft-adapter', 'Deploy OFT contract')
Expand All @@ -198,7 +214,7 @@ task('deploy-onft-adapter', 'Deploy OFT contract')
const contract = await contractFactory.deploy(taskArgs.token, ENDPOINT_V2, signers[0].address);
await contract.deployTransaction.wait();

console.log(`(${hre.network.name}) ExampleONFTAdapter deployed to ${contract.address}, txHash ${contract.deployTransaction.hash}`);
console.log(`(${hre.network.name}) ExampleONFTAdapter deployed to ${contract.address} txHash ${contract.deployTransaction.hash}`);
});

task('set-peer', 'Set peer')
Expand Down
30 changes: 30 additions & 0 deletions tools/layer-zero-example/test/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*-
*
* Hedera JSON RPC Relay - Hardhat Example
*
* Copyright (C) 2025 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

module.exports = {
// we're using the official LZ endpoints
// and a list of all EIDs can be found here
// EIDs are defined in the layer zero documentation https://docs.layerzero.network/v2/developers/evm/technical-reference/deployed-contracts#contract-address-table
HEDERA_EID: 40285,
BSC_EID: 40102,

// a random account
RECEIVER_ADDRESS: '0xF51c7a9407217911d74e91642dbC58F18E51Deac'
};
Loading
Loading