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: token freeze, kyc and pause (#197) #236

Open
wants to merge 14 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
8 changes: 8 additions & 0 deletions contracts/HederaResponseCodes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
pragma solidity ^0.8.0;

library HederaResponseCodes {
int32 internal constant NOT_SUPPORTED = 13;
int32 internal constant SUCCESS = 22; // The transaction succeeded
int32 internal constant TOKEN_HAS_NO_SUPPLY_KEY = 180;
int32 internal constant INSUFFICIENT_ACCOUNT_BALANCE = 28;
int32 internal constant INVALID_ACCOUNT_AMOUNTS = 48;
int32 internal constant ACCOUNT_REPEATED_IN_ACCOUNT_AMOUNTS = 74;
int32 internal constant INSUFFICIENT_TOKEN_BALANCE = 178;
int32 internal constant SENDER_DOES_NOT_OWN_NFT_SERIAL_NO = 237;
int32 internal constant SPENDER_DOES_NOT_HAVE_ALLOWANCE = 292;
int32 internal constant MAX_ALLOWANCES_EXCEEDED = 294;
}
442 changes: 419 additions & 23 deletions contracts/HtsSystemContract.sol

Large diffs are not rendered by default.

33 changes: 31 additions & 2 deletions contracts/HtsSystemContractJson.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {decode} from './Base64.sol';
import {HederaResponseCodes} from "./HederaResponseCodes.sol";
import {IHederaTokenService} from "./IHederaTokenService.sol";
import {HtsSystemContract, HTS_ADDRESS} from "./HtsSystemContract.sol";
import {IERC20} from "./IERC20.sol";
Expand Down Expand Up @@ -271,7 +272,7 @@ contract HtsSystemContractJson is HtsSystemContract {
return token;
}

function _getTokenKeys(string memory json) private pure returns (TokenKey[] memory tokenKeys) {
function _getTokenKeys(string memory json) private returns (TokenKey[] memory tokenKeys) {
tokenKeys = new TokenKey[](7);

try vm.parseJson(json, ".admin_key") returns (bytes memory keyBytes) {
Expand Down Expand Up @@ -326,7 +327,7 @@ contract HtsSystemContractJson is HtsSystemContract {
return tokenKeys;
}

function _getTokenKey(IMirrorNodeResponses.Key memory key, uint8 keyType) internal pure returns (TokenKey memory) {
function _getTokenKey(IMirrorNodeResponses.Key memory key, uint8 keyType) internal returns (TokenKey memory) {
bool inheritAccountKey = false;
address contractId = address(0);
address delegatableContractId = address(0);
Expand All @@ -336,6 +337,11 @@ contract HtsSystemContractJson is HtsSystemContract {
bytes memory ECDSA_secp256k1 = keccak256(bytes(key._type)) == keccak256(bytes("ECDSA_SECP256K1"))
? vm.parseBytes(key.key)
: new bytes(0);
address owner = ed25519.length + ECDSA_secp256k1.length > 0
? mirrorNode().getAccountAddressByPublicKey(key.key)
: address(0);
_setValue(super._keyOwnerSlot(keyType), bytes32(uint256(uint160(owner))));

return TokenKey(
keyType,
KeyValue(
Expand Down Expand Up @@ -455,6 +461,24 @@ contract HtsSystemContractJson is HtsSystemContract {
return slot;
}

function _isFrozenSlot(address account) internal override returns (bytes32) {
bytes32 slot = super._isFrozenSlot(account);
if (_shouldFetch(slot)) {
string memory freezeStatus = mirrorNode().getFreezeStatus(address(this), account);
_setValue(slot, bytes32(keccak256(bytes(freezeStatus)) == keccak256("FROZEN") ? uint256(1) : uint256(0)));
}
return slot;
}

function _hasKycGrantedSlot(address account) internal override returns (bytes32) {
bytes32 slot = super._hasKycGrantedSlot(account);
if (_shouldFetch(slot)) {
string memory kycStatus = mirrorNode().getKycStatus(address(this), account);
_setValue(slot, bytes32(keccak256(bytes(kycStatus)) == keccak256("GRANTED") ? uint256(1) : uint256(0)));
}
return slot;
}

function _allowanceSlot(address owner, address spender) internal override returns (bytes32) {
bytes32 slot = super._allowanceSlot(owner, spender);
if (_shouldFetch(slot)) {
Expand Down Expand Up @@ -523,4 +547,9 @@ contract HtsSystemContractJson is HtsSystemContract {
function _scratchAddr() private view returns (address) {
return address(bytes20(keccak256(abi.encode(address(this)))));
}

function _updateHbarBalanceOnAccount(address account, uint256 newBalance) internal override returns (int64) {
vm.deal(account, newBalance);
return HederaResponseCodes.SUCCESS;
}
}
46 changes: 23 additions & 23 deletions contracts/IHederaTokenService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,9 @@ interface IHederaTokenService {
/// @param transferList the list of hbar transfers to do
/// @param tokenTransfers the list of token transfers to do
/// @custom:version 0.3.0 the signature of the previous version was cryptoTransfer(TokenTransferList[] memory tokenTransfers)
// function cryptoTransfer(TransferList memory transferList, TokenTransferList[] memory tokenTransfers)
// external
// returns (int64 responseCode);
function cryptoTransfer(TransferList memory transferList, TokenTransferList[] memory tokenTransfers)
external
returns (int64 responseCode);

/// 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
Expand Down Expand Up @@ -605,18 +605,18 @@ interface IHederaTokenService {
/// @param account The account address associated with the token
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
/// @return frozen True if `account` is frozen for `token`
// function isFrozen(address token, address account)
// external
// returns (int64 responseCode, bool frozen);
function isFrozen(address token, address account)
external view
returns (int64 responseCode, bool frozen);

/// Query if token account has kyc granted
/// @param token The token address to check
/// @param account The account address associated with the token
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
/// @return kycGranted True if `account` has kyc granted for `token`
// function isKyc(address token, address account)
// external
// returns (int64 responseCode, bool kycGranted);
function isKyc(address token, address account)
external view
returns (int64 responseCode, bool kycGranted);

/// Operation to delete token
/// @param token The token address to be deleted
Expand Down Expand Up @@ -695,43 +695,43 @@ interface IHederaTokenService {
/// @param token The token address
/// @param account The account address to be frozen
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
// function freezeToken(address token, address account)
// external
// returns (int64 responseCode);
function freezeToken(address token, address account)
external
returns (int64 responseCode);

/// Operation to unfreeze token account
/// @param token The token address
/// @param account The account address to be unfrozen
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
// function unfreezeToken(address token, address account)
// external
// returns (int64 responseCode);
function unfreezeToken(address token, address account)
external
returns (int64 responseCode);

/// Operation to grant kyc to token account
/// @param token The token address
/// @param account The account address to grant kyc
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
// function grantTokenKyc(address token, address account)
// external
// returns (int64 responseCode);
function grantTokenKyc(address token, address account)
external
returns (int64 responseCode);

/// Operation to revoke kyc to token account
/// @param token The token address
/// @param account The account address to revoke kyc
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
// function revokeTokenKyc(address token, address account)
// external
// returns (int64 responseCode);
function revokeTokenKyc(address token, address account)
external
returns (int64 responseCode);

/// Operation to pause token
/// @param token The token address to be paused
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
// function pauseToken(address token) external returns (int64 responseCode);
function pauseToken(address token) external returns (int64 responseCode);

/// Operation to unpause token
/// @param token The token address to be unpaused
/// @return responseCode The response code for the status of the request. SUCCESS is 22.
// function unpauseToken(address token) external returns (int64 responseCode);
function unpauseToken(address token) external returns (int64 responseCode);

/// Operation to wipe fungible tokens from account
/// @param token The token address
Expand Down
40 changes: 40 additions & 0 deletions contracts/MirrorNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ abstract contract MirrorNode {

function fetchAccount(string memory account) external virtual returns (string memory json);

function fetchAccountByPublicKey(string memory publicKey) external virtual returns (string memory json);

function fetchTokenRelationshipOfAccount(string memory account, address token) external virtual returns (string memory json);

function fetchNonFungibleToken(address token, uint32 serial) external virtual returns (string memory json);
Expand Down Expand Up @@ -101,6 +103,44 @@ abstract contract MirrorNode {
return false;
}

function getFreezeStatus(address token, address account) external returns (string memory) {
try this.fetchTokenRelationshipOfAccount(vm.toString(account), token) returns (string memory json) {
if (vm.keyExistsJson(json, ".tokens")) {
bytes memory tokens = vm.parseJson(json, ".tokens");
IMirrorNodeResponses.TokenRelationship[] memory relationships = abi.decode(tokens, (IMirrorNodeResponses.TokenRelationship[]));
if (relationships.length > 0) {
return relationships[0].freeze_status;
}
}
} catch {}
return "NOT_APPLICABLE";
}

function getKycStatus(address token, address account) external returns (string memory) {
try this.fetchTokenRelationshipOfAccount(vm.toString(account), token) returns (string memory json) {
if (vm.keyExistsJson(json, ".tokens")) {
bytes memory tokens = vm.parseJson(json, ".tokens");
IMirrorNodeResponses.TokenRelationship[] memory relationships = abi.decode(tokens, (IMirrorNodeResponses.TokenRelationship[]));
if (relationships.length > 0) {
return relationships[0].kyc_status;
}
}
} catch {}
return "NOT_APPLICABLE";
}

function getAccountAddressByPublicKey(string memory publicKey) public returns (address) {
try this.fetchAccountByPublicKey(publicKey) returns (string memory json) {
if (vm.keyExistsJson(json, ".accounts[0].evm_address")) {
return vm.parseJsonAddress(json, ".accounts[0].evm_address");
}
} catch {
// Do nothing
}
// generate a deterministic address based on the account public key as a fallback
return address(uint160(uint256(keccak256(bytes(publicKey)))));
}

function getAccountAddress(string memory accountId) public returns (address) {
if (bytes(accountId).length == 0
|| keccak256(bytes(accountId)) == keccak256(bytes("null"))
Expand Down
4 changes: 4 additions & 0 deletions contracts/MirrorNodeFFI.sol
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ contract MirrorNodeFFI is MirrorNode {
));
}

function fetchAccountByPublicKey(string memory publicKey) external override returns (string memory) {
return _get(string.concat("accounts?limit=1&account.publickey=", publicKey));
}

function fetchTokenRelationshipOfAccount(string memory idOrAliasOrEvmAddress, address token) external override returns (string memory) {
return _get(string.concat(
"accounts/",
Expand Down
10 changes: 10 additions & 0 deletions src/forwarder/mirror-node-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ class MirrorNodeClient {
return this._get(`accounts/${idOrAliasOrEvmAddress}?transactions=false&${timestamp}`);
}

/**
* Fetches accounts information by account public key.
*
* @param {string} publicKey The account public key to fetch.
* @returns {Promise<{ accounts: {evm_address: string}[] } | null>} A `Promise` resolving to the account information or `null` if not found.
*/
async getAccountsByPublicKey(publicKey) {
return this._get(`accounts?limit=1&account.publickey=${publicKey}`);
}

/**
* @param {number} blockNumber
* @returns {Promise<{timestamp: {to: string}} | null>}
Expand Down
13 changes: 13 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ interface IMirrorNodeClient {
tokens: {
token_id: string;
automatic_association: boolean;
kyc_status: 'NOT_APPLICABLE' | 'GRANTED' | 'REVOKED';
frozen_status: 'NOT_APPLICABLE' | 'FROZEN' | 'UNFROZEN';
}[];
} | null>;

Expand Down Expand Up @@ -149,6 +151,17 @@ interface IMirrorNodeClient {
account: string;
evm_address: string;
} | null>;

/**
* Get accounts by public key.
*
* This method should call the Mirror Node API endpoint `GET /api/v1/accounts?limit=1&account.publickey={publicKey}`.
*
* @param publicKey
*/
getAccountsByPublicKey(publicKey: string): Promise<{
accounts: { evm_address: string }[];
} | null>;
}

/**
Expand Down
76 changes: 75 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const debug = require('util').debuglog('hts-forking');
const { ZERO_HEX_32_BYTE, toIntHex256 } = require('./utils');
const { slotMapOf, packValues, PersistentStorageMap } = require('./slotmap');
const { deployedBytecode } = require('../out/HtsSystemContract.sol/HtsSystemContract.json');
const { keccak256 } = require('ethers');

const HTSAddress = '0x0000000000000000000000000000000000000167';

Expand Down Expand Up @@ -262,11 +263,84 @@ async function getHtsStorageAt(address, requestedSlot, blockNumber, mirrorNodeCl
);
persistentStorage.store(tokenId, blockNumber, nrequestedSlot, atob(metadata));
}

// Encoded `address(tokenId).isKyc(tokenId, accountId)` slot
// slot(256) = `isKyc`selector(32) + padding(192) + accountId(32)
if (
nrequestedSlot >> 32n ===
0xf2c31ff4_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000n
) {
const accountId = `0.0.${parseInt(requestedSlot.slice(-8), 16)}`;
const { tokens } = (await mirrorNodeClient.getTokenRelationship(accountId, tokenId)) ?? {
tokens: [],
};
const kycGranted = tokens.length > 0 && tokens[0].kyc_status === 'GRANTED';
return ret(
`0x${toIntHex256(kycGranted ? 1 : 0)}`,
`Token ${tokenId} kyc for ${accountId} is ${kycGranted ? 'granted' : 'not granted'}`
);
}

// Encoded `address(tokenId).isFrozen(tokenId, accountId)` slot
// slot(256) = `isFrozen`selector(32) + padding(192) + accountId(32)
if (
nrequestedSlot >> 32n ===
0x46de0fb1_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000n
) {
const accountId = `0.0.${parseInt(requestedSlot.slice(-8), 16)}`;
const { tokens } = (await mirrorNodeClient.getTokenRelationship(accountId, tokenId)) ?? {
tokens: [],
};
const isFrozen = tokens.length > 0 && tokens[0].frozen_status === 'FROZEN';
return ret(
`0x${toIntHex256(isFrozen ? 1 : 0)}`,
`Token ${tokenId} is ${isFrozen ? 'frozen' : 'not frozen'} for account ${accountId}`
);
}

let unresolvedValues = persistentStorage.load(tokenId, blockNumber, nrequestedSlot);
if (unresolvedValues === undefined) {
const token = await mirrorNodeClient.getTokenById(tokenId, blockNumber);
const token =
/**@type{{token_keys: {key_type: string}[]}}*/ await mirrorNodeClient.getTokenById(
tokenId,
blockNumber
);
if (token === null) return ret(ZERO_HEX_32_BYTE, `Token \`${tokenId}\` not found`);
unresolvedValues = slotMapOf(token).load(nrequestedSlot);
// Encoded `address(tokenId).getTokenKey(tokenId, keyType)` slot
// slot(256) = `getTokenKey`selector(32) + padding(192) + keyType(32)
if (
nrequestedSlot >> 32n ===
0x3c4dd32e_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000n
) {
const keyType = parseInt(requestedSlot.slice(-2), 16);
const keys =
/**@type{Array<{key_type: number, key: string, _e_c_d_s_a_secp256k1: string, ed25519: string}>}*/ (
token['token_keys']
);
const noKeyRes = ret(
ZERO_HEX_32_BYTE,
`Token ${tokenId} has not set the owner of the key ${keyType}`
);
const keyFound = keys['find'](
(
/**@type{{key_type: number, key: string, _e_c_d_s_a_secp256k1: string, ed25519: string}}*/ key
) => key.key_type === keyType
);
if (keyFound === undefined) return noKeyRes;
const keyString = keyFound['_e_c_d_s_a_secp256k1'] || keyFound['ed25519'];
if (!keyString) return noKeyRes;
const result = await mirrorNodeClient.getAccountsByPublicKey(keyString);
if (result === undefined || !result || result.accounts.length === 0)
return ret(
`0x${keccak256(Buffer.from(keyString, 'utf-8')).slice(-40).padStart(64, '0')}`,
`Fallback value returned for ${tokenId} key ${keyType}`
);
return ret(
result.accounts[0].evm_address,
`Token ${tokenId} has set the owner of the key ${keyType}`
);
}

if (unresolvedValues === undefined)
return ret(ZERO_HEX_32_BYTE, `Requested slot does not match any field slots`);
Expand Down
Loading