Skip to content

Commit bd078cf

Browse files
feat: [hedera-crosschain-bridge] Case B – Custom HTS Token Bridging Implementation and Testing (#3809)
Signed-off-by: Logan Nguyen <[email protected]> Signed-off-by: nikolay <[email protected]> Signed-off-by: Quiet Node <[email protected]> Co-authored-by: Logan Nguyen <[email protected]>
1 parent d45744b commit bd078cf

20 files changed

+10033
-3858
lines changed

tools/hedera-crosschain-bridge/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ The project includes comprehensive test suites for both supported use cases:
7575

7676
### HTS Bridge Testing (`hts-bridge-test.js`)
7777

78+
⚠️ ⚠️ ⚠️ The deployer must have "Auto. Associations" enabled or must execute `npx hardhat run scripts/utils/update-account-associations.ts --network hedera` beforehand. ⚠️ ⚠️ ⚠️
79+
7880
1. Deploy ERC20 token on Sepolia
7981
2. Deploy HTSConnector on Hedera (creates HTS token)
8082
3. Approve HTSConnector to manage HTS tokens
@@ -100,7 +102,7 @@ Create a .env file based on the .env.example file and fill out the configuration
100102
# Hedera Network Configuration
101103
HEDERA_CHAIN_ID=
102104
HEDERA_RPC=
103-
HEDERA_PK=
105+
HEDERA_PK= # for HTS-related operations, the deployer account should have enabled "Auto Associations"
104106
HEDERA_LZ_ENDPOINT_V2=
105107
HEDERA_LZ_EID_V2=
106108

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
import "@openzeppelin/contracts/access/Ownable.sol";
5+
import "./hts/HederaTokenService.sol";
6+
import "./hts/IHederaTokenService.sol";
7+
import "./hts/KeyHelper.sol";
8+
import "./HTSConnector.sol";
9+
10+
contract ExampleHTSConnector is Ownable, HTSConnector {
11+
constructor(
12+
string memory _name,
13+
string memory _symbol,
14+
address _lzEndpoint,
15+
address _delegate
16+
) payable HTSConnector(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {}
17+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.22;
3+
4+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5+
import {OFT} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFT.sol";
6+
7+
contract ExampleOFT is OFT {
8+
uint8 decimalsArg = 8;
9+
10+
constructor(
11+
string memory _name,
12+
string memory _symbol,
13+
address _lzEndpoint,
14+
address _delegate,
15+
uint256 _initialMint,
16+
uint8 _decimals
17+
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {
18+
_mint(msg.sender, _initialMint);
19+
decimalsArg = _decimals;
20+
}
21+
22+
function decimals() public view override returns (uint8) {
23+
return decimalsArg;
24+
}
25+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.20;
3+
4+
import {OFTCore} from "@layerzerolabs/lz-evm-oapp-v2/contracts/oft/OFTCore.sol";
5+
import "./hts/HederaTokenService.sol";
6+
import "./hts/IHederaTokenService.sol";
7+
import "./hts/KeyHelper.sol";
8+
9+
/**
10+
* @title HTS Connector
11+
* @dev HTS Connector is a HTS token that extends the functionality of the OFTCore contract.
12+
*/
13+
abstract contract HTSConnector is OFTCore, KeyHelper, HederaTokenService {
14+
address public htsTokenAddress;
15+
16+
event TokenCreated(address tokenAddress);
17+
18+
/**
19+
* @dev Constructor for the HTS Connector contract.
20+
* @param _name The name of HTS token
21+
* @param _symbol The symbol of HTS token
22+
* @param _lzEndpoint The LayerZero endpoint address.
23+
* @param _delegate The delegate capable of making OApp configurations inside of the endpoint.
24+
*/
25+
constructor(
26+
string memory _name,
27+
string memory _symbol,
28+
address _lzEndpoint,
29+
address _delegate
30+
) payable OFTCore(8, _lzEndpoint, _delegate) {
31+
IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1);
32+
keys[0] = getSingleKey(
33+
KeyType.SUPPLY,
34+
KeyValueType.INHERIT_ACCOUNT_KEY,
35+
bytes("")
36+
);
37+
38+
IHederaTokenService.Expiry memory expiry = IHederaTokenService.Expiry(0, address(this), 8000000);
39+
IHederaTokenService.HederaToken memory token = IHederaTokenService.HederaToken(
40+
_name, _symbol, address(this), "memo", true, 5000, false, keys, expiry
41+
);
42+
43+
(int responseCode, address tokenAddress) = HederaTokenService.createFungibleToken(
44+
token, 1000, int32(int256(uint256(8)))
45+
);
46+
require(responseCode == HederaTokenService.SUCCESS_CODE, "Failed to create HTS token");
47+
48+
int256 transferResponse = HederaTokenService.transferToken(tokenAddress, address(this), msg.sender, 1000);
49+
require(transferResponse == HederaTokenService.SUCCESS_CODE, "HTS: Transfer failed");
50+
51+
htsTokenAddress = tokenAddress;
52+
53+
emit TokenCreated(tokenAddress);
54+
}
55+
56+
/**
57+
* @dev Retrieves the address of the underlying HTS implementation.
58+
* @return The address of the HTS token.
59+
*/
60+
function token() public view returns (address) {
61+
return htsTokenAddress;
62+
}
63+
64+
/**
65+
* @notice Indicates whether the HTS Connector contract requires approval of the 'token()' to send.
66+
* @return requiresApproval Needs approval of the underlying token implementation.
67+
*/
68+
function approvalRequired() external pure virtual returns (bool) {
69+
return false;
70+
}
71+
72+
/**
73+
* @dev Burns tokens from the sender's specified balance.
74+
* @param _from The address to debit the tokens from.
75+
* @param _amountLD The amount of tokens to send in local decimals.
76+
* @param _minAmountLD The minimum amount to send in local decimals.
77+
* @param _dstEid The destination chain ID.
78+
* @return amountSentLD The amount sent in local decimals.
79+
* @return amountReceivedLD The amount received in local decimals on the remote.
80+
*/
81+
function _debit(
82+
address _from,
83+
uint256 _amountLD,
84+
uint256 _minAmountLD,
85+
uint32 _dstEid
86+
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
87+
require(_amountLD <= uint64(type(int64).max), "HTSConnector: amount exceeds int64 safe range");
88+
89+
(amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);
90+
91+
int256 transferResponse = HederaTokenService.transferToken(htsTokenAddress, _from, address(this), int64(uint64(_amountLD)));
92+
require(transferResponse == HederaTokenService.SUCCESS_CODE, "HTS: Transfer failed");
93+
94+
(int256 response,) = HederaTokenService.burnToken(htsTokenAddress, int64(uint64(amountSentLD)), new int64[](0));
95+
require(response == HederaTokenService.SUCCESS_CODE, "HTS: Burn failed");
96+
}
97+
98+
/**
99+
* @dev Credits tokens to the specified address.
100+
* @param _to The address to credit the tokens to.
101+
* @param _amountLD The amount of tokens to credit in local decimals.
102+
* @dev _srcEid The source chain ID.
103+
* @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals.
104+
*/
105+
function _credit(
106+
address _to,
107+
uint256 _amountLD,
108+
uint32 /*_srcEid*/
109+
) internal virtual override returns (uint256) {
110+
require(_amountLD <= uint64(type(int64).max), "HTSConnector: amount exceeds int64 safe range");
111+
112+
(int256 response, ,) = HederaTokenService.mintToken(htsTokenAddress, int64(uint64(_amountLD)), new bytes[](0));
113+
require(response == HederaTokenService.SUCCESS_CODE, "HTS: Mint failed");
114+
115+
int256 transferResponse = HederaTokenService.transferToken(htsTokenAddress, address(this), _to, int64(uint64(_amountLD)));
116+
require(transferResponse == HederaTokenService.SUCCESS_CODE, "HTS: Transfer failed");
117+
118+
return _amountLD;
119+
}
120+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity >=0.5.0 <0.9.0;
3+
pragma experimental ABIEncoderV2;
4+
5+
import "./IHederaTokenService.sol";
6+
7+
abstract contract HederaTokenService {
8+
// all response codes are defined here https://github.com/hashgraph/hedera-smart-contracts/blob/main/contracts/system-contracts/HederaResponseCodes.sol
9+
int32 constant UNKNOWN_CODE = 21;
10+
int32 constant SUCCESS_CODE = 22;
11+
12+
address constant precompileAddress = address(0x167);
13+
// 90 days in seconds
14+
int32 constant defaultAutoRenewPeriod = 7776000;
15+
16+
modifier nonEmptyExpiry(IHederaTokenService.HederaToken memory token) {
17+
if (token.expiry.second == 0 && token.expiry.autoRenewPeriod == 0) {
18+
token.expiry.autoRenewPeriod = defaultAutoRenewPeriod;
19+
}
20+
_;
21+
}
22+
23+
/// Generic event
24+
event CallResponseEvent(bool, bytes);
25+
26+
/// Mints an amount of the token to the defined treasury account
27+
/// @param token The token for which to mint tokens. If token does not exist, transaction results in
28+
/// INVALID_TOKEN_ID
29+
/// @param amount Applicable to tokens of type FUNGIBLE_COMMON. The amount to mint to the Treasury Account.
30+
/// Amount must be a positive non-zero number represented in the lowest denomination of the
31+
/// token. The new supply must be lower than 2^63.
32+
/// @param metadata Applicable to tokens of type NON_FUNGIBLE_UNIQUE. A list of metadata that are being created.
33+
/// Maximum allowed size of each metadata is 100 bytes
34+
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
35+
/// @return newTotalSupply The new supply of tokens. For NFTs it is the total count of NFTs
36+
/// @return serialNumbers If the token is an NFT the newly generate serial numbers, otherwise empty.
37+
function mintToken(
38+
address token,
39+
int64 amount,
40+
bytes[] memory metadata
41+
)
42+
internal
43+
returns (
44+
int responseCode,
45+
int64 newTotalSupply,
46+
int64[] memory serialNumbers
47+
)
48+
{
49+
(bool success, bytes memory result) = precompileAddress.call(
50+
abi.encodeWithSelector(
51+
IHederaTokenService.mintToken.selector,
52+
token,
53+
amount,
54+
metadata
55+
)
56+
);
57+
(responseCode, newTotalSupply, serialNumbers) = success
58+
? abi.decode(result, (int32, int64, int64[]))
59+
: (HederaTokenService.UNKNOWN_CODE, int64(0), new int64[](0));
60+
}
61+
62+
/// Burns an amount of the token from the defined treasury account
63+
/// @param token The token for which to burn tokens. If token does not exist, transaction results in
64+
/// INVALID_TOKEN_ID
65+
/// @param amount Applicable to tokens of type FUNGIBLE_COMMON. The amount to burn from the Treasury Account.
66+
/// Amount must be a positive non-zero number, not bigger than the token balance of the treasury
67+
/// account (0; balance], represented in the lowest denomination.
68+
/// @param serialNumbers Applicable to tokens of type NON_FUNGIBLE_UNIQUE. The list of serial numbers to be burned.
69+
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
70+
/// @return newTotalSupply The new supply of tokens. For NFTs it is the total count of NFTs
71+
function burnToken(
72+
address token,
73+
int64 amount,
74+
int64[] memory serialNumbers
75+
) internal returns (int responseCode, int64 newTotalSupply) {
76+
(bool success, bytes memory result) = precompileAddress.call(
77+
abi.encodeWithSelector(
78+
IHederaTokenService.burnToken.selector,
79+
token,
80+
amount,
81+
serialNumbers
82+
)
83+
);
84+
(responseCode, newTotalSupply) = success
85+
? abi.decode(result, (int32, int64))
86+
: (HederaTokenService.UNKNOWN_CODE, int64(0));
87+
}
88+
89+
/// Creates a Fungible Token with the specified properties
90+
/// @param token the basic properties of the token being created
91+
/// @param initialTotalSupply Specifies the initial supply of tokens to be put in circulation. The
92+
/// initial supply is sent to the Treasury Account. The supply is in the lowest denomination possible.
93+
/// @param decimals the number of decimal places a token is divisible by
94+
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
95+
/// @return tokenAddress the created token's address
96+
function createFungibleToken(
97+
IHederaTokenService.HederaToken memory token,
98+
int64 initialTotalSupply,
99+
int32 decimals
100+
)
101+
internal
102+
nonEmptyExpiry(token)
103+
returns (int responseCode, address tokenAddress)
104+
{
105+
(bool success, bytes memory result) = precompileAddress.call{
106+
value: msg.value
107+
}(
108+
abi.encodeWithSelector(
109+
IHederaTokenService.createFungibleToken.selector,
110+
token,
111+
initialTotalSupply,
112+
decimals
113+
)
114+
);
115+
116+
(responseCode, tokenAddress) = success
117+
? abi.decode(result, (int32, address))
118+
: (HederaTokenService.UNKNOWN_CODE, address(0));
119+
}
120+
121+
/// Transfers tokens where the calling account/contract is implicitly the first entry in the token transfer list,
122+
/// where the amount is the value needed to zero balance the transfers. Regular signing rules apply for sending
123+
/// (positive amount) or receiving (negative amount)
124+
/// @param token The token to transfer to/from
125+
/// @param sender The sender for the transaction
126+
/// @param receiver The receiver of the transaction
127+
/// @param amount Non-negative value to send. a negative value will result in a failure.
128+
function transferToken(
129+
address token,
130+
address sender,
131+
address receiver,
132+
int64 amount
133+
) internal returns (int responseCode) {
134+
(bool success, bytes memory result) = precompileAddress.call(
135+
abi.encodeWithSelector(
136+
IHederaTokenService.transferToken.selector,
137+
token,
138+
sender,
139+
receiver,
140+
amount
141+
)
142+
);
143+
responseCode = success
144+
? abi.decode(result, (int32))
145+
: HederaTokenService.UNKNOWN_CODE;
146+
}
147+
148+
/// Operation to update token keys
149+
/// @param token The token address
150+
/// @param keys The token keys
151+
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
152+
function updateTokenKeys(address token, IHederaTokenService.TokenKey[] memory keys)
153+
internal returns (int64 responseCode){
154+
(bool success, bytes memory result) = precompileAddress.call(
155+
abi.encodeWithSelector(IHederaTokenService.updateTokenKeys.selector, token, keys));
156+
(responseCode) = success ? abi.decode(result, (int32)) : HederaTokenService.UNKNOWN_CODE;
157+
}
158+
159+
}

0 commit comments

Comments
 (0)