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: Implement mint and burn #106

Merged
merged 46 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c32669b
feat: Implement `mint` and `burn`
victor-yanev Nov 4, 2024
df73476
test: add tests
victor-yanev Nov 5, 2024
c8ec02b
chore: fix tests
victor-yanev Nov 5, 2024
7e5ad86
Merge branch 'main' into 78-implement-burn-and-mint-of-HTS-tokens
victor-yanev Nov 5, 2024
db1514b
chore: reformat HtsSystemContract.json
victor-yanev Nov 5, 2024
7867e38
chore: refactor interfaces
victor-yanev Nov 5, 2024
7bbe8e7
chore: reformat HtsSystemContract.json
victor-yanev Nov 5, 2024
607c4a9
chore: refactor interfaces
victor-yanev Nov 5, 2024
7992b96
fix: message in require
victor-yanev Nov 5, 2024
e59b0d9
fix: tests
victor-yanev Nov 5, 2024
69b6c25
chore: remove unused function
victor-yanev Nov 5, 2024
9213eb7
refactor: clean up code
victor-yanev Nov 5, 2024
354c1d4
Merge branch 'main' into 78-implement-burn-and-mint-of-HTS-tokens
victor-yanev Nov 6, 2024
52b8deb
feat: implement `mintToken` and `burnToken` from `IHederaTokenService`
victor-yanev Nov 6, 2024
6c3be95
Merge branch 'origin/main' into '78-implement-burn-and-mint-of-HTS-to…
victor-yanev Nov 6, 2024
ec1ec4c
refactor: remove `IERC20Mintable` and `IERC20Burnable`
victor-yanev Nov 6, 2024
929f8a7
test: address comments + fix tests
victor-yanev Nov 7, 2024
2bbb742
test: remove console logs
victor-yanev Nov 7, 2024
da4b20c
chore: resolve comments
victor-yanev Nov 7, 2024
b13734e
Merge branch 'refs/heads/main' into 78-implement-burn-and-mint-of-HTS…
victor-yanev Nov 13, 2024
916845d
feat: Implement `getTokenInfo` (foundry solution)
victor-yanev Nov 14, 2024
d9f98f2
Merge branch 'main' into 108-Implement-getTokenInfo-v2
victor-yanev Nov 15, 2024
cc4b42e
fix: tests
victor-yanev Nov 18, 2024
90e9a7b
chore: address comments
victor-yanev Nov 19, 2024
2a8233b
chore: address comments
victor-yanev Nov 19, 2024
5f8a24a
chore: reorder methods
victor-yanev Nov 19, 2024
042b112
chore: address comments
victor-yanev Nov 20, 2024
fb01870
Merge branch 'main' into 108-Implement-getTokenInfo-v2
victor-yanev Nov 20, 2024
3d3ea5d
chore: remove leftover logic for calculating offset
victor-yanev Nov 20, 2024
5b0c828
chore: small touches
victor-yanev Nov 20, 2024
3036738
chore: fix gaps
victor-yanev Nov 20, 2024
9893c58
chore: extend unit tests in StrStore.t.sol
victor-yanev Nov 21, 2024
f60362b
Merge branch '108-Implement-getTokenInfo-v2' into 78-implement-burn-a…
victor-yanev Nov 21, 2024
71e1ae0
chore: address comments
victor-yanev Nov 22, 2024
28c8d7a
Merge branch '108-Implement-getTokenInfo-v2' into 78-implement-burn-a…
victor-yanev Nov 22, 2024
3eb2a90
Merge branch '108-Implement-getTokenInfo-v2' into 78-implement-burn-a…
victor-yanev Nov 22, 2024
9b519f0
chore: fix failing tests
victor-yanev Nov 22, 2024
9bedceb
Merge branch '108-Implement-getTokenInfo-v2' into 78-implement-burn-a…
victor-yanev Nov 22, 2024
f1445c7
chore: simplify some changes + remove console logs
victor-yanev Nov 22, 2024
c8cbabf
chore: fix failing tests
victor-yanev Nov 22, 2024
2e50f2c
chore: remove unnecessary data from getAccount_0.0.2669675.json
victor-yanev Nov 22, 2024
4353e71
Merge branch '108-Implement-getTokenInfo-v2' into 78-implement-burn-a…
victor-yanev Nov 22, 2024
95acff0
Merge branch 'main' into 78-implement-burn-and-mint-of-HTS-tokens
victor-yanev Nov 22, 2024
56be6a0
chore: address comments
victor-yanev Nov 25, 2024
808971b
Merge remote-tracking branch 'origin/78-implement-burn-and-mint-of-HT…
victor-yanev Nov 25, 2024
4583b4c
chore: revert removal of htsSetup() method
victor-yanev Nov 25, 2024
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
13 changes: 13 additions & 0 deletions @hts-forking/test/data/MFCT/getBalanceOfToken_0.0.2669675.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"timestamp": "1727813254.582754397",
"balances": [
{
"account": "0.0.2669675",
"balance": 5000,
"decimals": 0
}
],
"links": {
"next": null
}
}
13 changes: 13 additions & 0 deletions @hts-forking/test/data/USDC/getBalanceOfToken_0.0.5176.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"timestamp": "1724799522.972495003",
"balances": [
{
"account": "0.0.5176",
"balance": 5000000,
"decimals": 6
}
],
"links": {
"next": null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"account": "0.0.2669675",
"alias": "HIQQEAMJCQN7IZLRPU6I53Q7VFOINWOXBZM365CNKLKTBQ5RXPYUKLWH",
"auto_renew_period": 7776000,
"balance": {
"balance": 99981012856,
"timestamp": "1727813254.582754397",
"tokens": [
{
"token_id": "0.0.4730999",
"balance": 5000
}
]
},
"created_timestamp": "1707161970.469792002",
"decline_reward": false,
"deleted": false,
"ethereum_nonce": 802,
"evm_address": "0xa3612a87022a4706fc9452c50abd2703ac4fd7d9",
"expiry_timestamp": "1714937970.469792002",
"key": {
"_type": "ECDSA_SECP256K1",
"key": "020189141bf465717d3c8eee1fa95c86d9d70e59bf744d52d530c3b1bbf1452ec7"
},
"max_automatic_token_associations": 0,
"memo": "auto-created account",
"pending_reward": 0,
"receiver_sig_required": false,
"staked_account_id": null,
"staked_node_id": null,
"stake_period_start": null,
"transactions": [],
"links": {
"next": "/api/v1/accounts/0.0.2669675?timestamp=lt:1726314195.875000001"
}
}
20 changes: 15 additions & 5 deletions scripts/curl
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,23 @@ for (const [re, fn] of /** @type {const} */([

return require(`../@hts-forking/test/data/${token.symbol}/getBalanceOfToken_${accountId}.json`);
}],
[/^accounts\/(0x[0-9a-fA-F]{40})$/, address => {
assert(typeof address === 'string');
[/^accounts\/((0x[0-9a-fA-F]{40})|(0\.0\.\d+))$/, idOrAliasOrEvmAddress => {
assert(typeof idOrAliasOrEvmAddress === 'string');
try {
return require(`../@hts-forking/test/data/getAccount_${address.toLowerCase()}.json`);
return require(`../@hts-forking/test/data/getAccount_${idOrAliasOrEvmAddress.toLowerCase()}.json`);
} catch {
if (address.startsWith(LONG_ZERO_PREFIX)) {
return { account: '0.0.' + parseInt(address, 16) };
if (idOrAliasOrEvmAddress.startsWith(LONG_ZERO_PREFIX)) {
return {
account: '0.0.' + parseInt(idOrAliasOrEvmAddress, 16),
evm_address: idOrAliasOrEvmAddress,
};
}
if (idOrAliasOrEvmAddress.startsWith('0.0.')) {
const accountNumber = parseInt(idOrAliasOrEvmAddress.slice(4));
return {
account: idOrAliasOrEvmAddress,
evm_address: `0x${accountNumber.toString(16).padStart(40, '0')}`
};
}
return undefined;
}
Expand Down
95 changes: 75 additions & 20 deletions src/HtsSystemContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,54 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events {
assembly { accountId := sload(slot) }
}

/**
* Query token info
* @param token - The token address to check
* @return responseCode - the response code for the status of the request. SUCCESS is `22`.
* @return tokenInfo - token info for `token`
*/
function getTokenInfo(address token) htsCall external returns (int64 responseCode, TokenInfo memory tokenInfo) {
require(token != address(0), "getTokenInfo: invalid token");

(responseCode, tokenInfo) = IHederaTokenService(token).getTokenInfo(token);
}

function mintToken(address token, int64 amount, bytes[] memory) htsCall external returns (
int64 responseCode,
int64 newTotalSupply,
int64[] memory serialNumbers
) {
require(token != address(0), "mintToken: invalid token");
require(amount > 0, "mintToken: invalid amount");

(int64 tokenInfoResponseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token);
require(tokenInfoResponseCode == 22, "mintToken: failed to get token info");

address treasuryAccount = tokenInfo.token.treasury;
require(treasuryAccount != address(0), "mintToken: invalid account");

HtsSystemContract(token)._update(address(0), treasuryAccount, uint256(uint64(amount)));

responseCode = 22; // HederaResponseCodes.SUCCESS
newTotalSupply = int64(uint64(IERC20(token).totalSupply()));
serialNumbers = new int64[](0);
require(newTotalSupply >= 0, "mintToken: invalid total supply");
}

function burnToken(address token, int64 amount, int64[] memory) htsCall external returns (
int64 responseCode,
int64 newTotalSupply
) {
require(token != address(0), "burnToken: invalid token");
require(amount > 0, "burnToken: invalid amount");

(int64 tokenInfoResponseCode, TokenInfo memory tokenInfo) = IHederaTokenService(token).getTokenInfo(token);
require(tokenInfoResponseCode == 22, "burnToken: failed to get token info");

address treasuryAccount = tokenInfo.token.treasury;
require(treasuryAccount != address(0), "burnToken: invalid account");

HtsSystemContract(token)._update(treasuryAccount, address(0), uint256(uint64(amount)));

responseCode = 22; // HederaResponseCodes.SUCCESS
newTotalSupply = int64(uint64(IERC20(token).totalSupply()));
require(newTotalSupply >= 0, "burnToken: invalid total supply");
}

/**
* @dev Validates `redirectForToken(address,bytes)` dispatcher arguments.
*
Expand Down Expand Up @@ -194,6 +230,15 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events {
require(msg.sender == HTS_ADDRESS, "getTokenInfo: unauthorized");
_initTokenData();
return abi.encode(22, _tokenInfo);
} else if (selector == this._update.selector) {
require(msg.data.length >= 124, "update: Not enough calldata");
require(msg.sender == HTS_ADDRESS, "update: unauthorized");
address from = address(bytes20(msg.data[40:60]));
address to = address(bytes20(msg.data[72:92]));
uint256 amount = uint256(bytes32(msg.data[92:124]));
_initTokenData();
acuarica marked this conversation as resolved.
Show resolved Hide resolved
_update(from, to, amount);
return abi.encode(true);
}
revert ("redirectForToken: not supported");
}
Expand Down Expand Up @@ -236,22 +281,32 @@ contract HtsSystemContract is IHederaTokenService, IERC20Events {
function _transfer(address from, address to, uint256 amount) private {
require(from != address(0), "hts: invalid sender");
require(to != address(0), "hts: invalid receiver");
_update(from, to, amount);
emit Transfer(from, to, amount);
}

bytes32 fromSlot = _balanceOfSlot(from);
uint256 fromBalance;
assembly { fromBalance := sload(fromSlot) }
require(fromBalance >= amount, "_transfer: insufficient balance");
assembly { sstore(fromSlot, sub(fromBalance, amount)) }

bytes32 toSlot = _balanceOfSlot(to);
uint256 toBalance;
assembly { toBalance := sload(toSlot) }
// Solidity's checked arithmetic will revert if this overflows
// https://soliditylang.org/blog/2020/12/16/solidity-v0.8.0-release-announcement
uint256 newToBalance = toBalance + amount;
assembly { sstore(toSlot, newToBalance) }
function _update(address from, address to, uint256 amount) public {
if (from == address(0)) {
totalSupply += amount;
} else {
bytes32 fromSlot = _balanceOfSlot(from);
uint256 fromBalance;
assembly { fromBalance := sload(fromSlot) }
require(fromBalance >= amount, "_transfer: insufficient balance");
assembly { sstore(fromSlot, sub(fromBalance, amount)) }
}

emit Transfer(from, to, amount);
if (to == address(0)) {
totalSupply -= amount;
} else {
bytes32 toSlot = _balanceOfSlot(to);
uint256 toBalance;
assembly { toBalance := sload(toSlot) }
// Solidity's checked arithmetic will revert if this overflows
// https://soliditylang.org/blog/2020/12/16/solidity-v0.8.0-release-announcement
uint256 newToBalance = toBalance + amount;
assembly { sstore(toSlot, newToBalance) }
}
}

function _approve(address owner, address spender, uint256 amount) private {
Expand Down
14 changes: 12 additions & 2 deletions src/HtsSystemContractJson.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,21 @@ contract HtsSystemContractJson is HtsSystemContract {

/**
* @dev Reading Smart Contract's data into it's storage directly from the MirrorNode.
* @dev Both `initialized` and `_mirrorNode` are stored in the same slot (with different offsets).
* Given how `initialized` is written to (see at the end of the method), it would seem that the
* instance variable `_mirrorNode` is overwritten.
* However, this is not the case because the slot space for each access is different:
* - `_mirrorNode` is accessed through the `0x167` address.
* - `initialized` is accessed through the address of the token.
*/
function _initTokenData() internal override {
if (initialized) return;

bytes32 slot;
assembly { slot := initialized.slot }
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved
if (vm.load(address(this), slot) == bytes32(uint256(1))) {
// Already initialized
return;
}

string memory json = mirrorNode().fetchTokenData(address(this));

assembly { slot := name.slot }
Expand Down
31 changes: 31 additions & 0 deletions src/IHederaTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,35 @@ interface IHederaTokenService {
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
/// @return tokenInfo TokenInfo info for `token`
function getTokenInfo(address token) external returns (int64 responseCode, TokenInfo memory tokenInfo);

/// 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
);
}
victor-yanev marked this conversation as resolved.
Show resolved Hide resolved
Loading