Skip to content

Commit

Permalink
feat: Implement mint and burn (#106)
Browse files Browse the repository at this point in the history
Signed-off-by: Victor Yanev <[email protected]>
Signed-off-by: Victor Yanev <[email protected]>
  • Loading branch information
victor-yanev authored Nov 27, 2024
1 parent fa66ff6 commit e178113
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 29 deletions.
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();
_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 }
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
);
}
Loading

0 comments on commit e178113

Please sign in to comment.