Skip to content

Commit

Permalink
chore: add hts connector for existing token example
Browse files Browse the repository at this point in the history
Signed-off-by: nikolay <[email protected]>
  • Loading branch information
natanasow committed Jan 10, 2025
1 parent bc38bcd commit a4c5e58
Show file tree
Hide file tree
Showing 10 changed files with 381 additions and 15 deletions.
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) {}
}
6 changes: 2 additions & 4 deletions tools/layer-zero-example/contracts/ExampleOApp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,13 @@ contract ExampleOApp is OApp {
* @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.
* @param address Executor address as specified by the OApp.
* @param calldata Any extra data or options to trigger on receipt.
*/
function _lzReceive(
Origin calldata _origin,
bytes32 _guid,
bytes calldata payload,
address,
bytes calldata
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!
Expand Down
90 changes: 90 additions & 0 deletions tools/layer-zero-example/contracts/HTSConnectorExistingToken.sol
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
Loading

0 comments on commit a4c5e58

Please sign in to comment.