Skip to content

Commit

Permalink
feat: token freeze, kyc and pause (#197)
Browse files Browse the repository at this point in the history
Signed-off-by: Mariusz Jasuwienas <[email protected]>
  • Loading branch information
arianejasuwienas committed Feb 10, 2025
1 parent 0850f4c commit 3bb1932
Show file tree
Hide file tree
Showing 22 changed files with 1,565 additions and 82 deletions.
9 changes: 9 additions & 0 deletions contracts/HederaResponseCodes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +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;
}
453 changes: 430 additions & 23 deletions contracts/HtsSystemContract.sol

Large diffs are not rendered by default.

32 changes: 29 additions & 3 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 {HtsSystemContract, HTS_ADDRESS} from "./HtsSystemContract.sol";
import {IERC20} from "./IERC20.sol";
import {MirrorNode} from "./MirrorNode.sol";
Expand Down Expand Up @@ -265,7 +266,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 @@ -320,16 +321,18 @@ 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);
bytes memory ed25519 = keccak256(bytes(key._type)) == keccak256(bytes("ED25519"))
? vm.parseBytes(key.key)
: new bytes(0);
bytes memory ECDSA_secp256k1 = keccak256(bytes(key._type)) == keccak256(bytes("ECDSA_SECP256K1"))
? vm.parseBytes(key.key)
: new bytes(0);
address contractId = ed25519.length + ECDSA_secp256k1.length > 0
? mirrorNode().getAccountAddressByPublicKey(key.key)
: address(0);
return TokenKey(
keyType,
KeyValue(
Expand Down Expand Up @@ -449,6 +452,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 @@ -517,4 +538,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
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
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
45 changes: 45 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,55 @@ 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);
if (token === null) return ret(ZERO_HEX_32_BYTE, `Token \`${tokenId}\` not found`);

for (const key of ['admin', 'kyc', 'freeze', 'wipe', 'supply', 'fee_schedule', 'pause']) {
const value = /**@type{{contractId: string}}*/ (token[`${key}_key`]);
if (!value) continue;
assert(typeof value === 'object' && 'key' in value && typeof value.key === 'string');
const result = await mirrorNodeClient.getAccountsByPublicKey(value.key);
if (result === undefined || !result || result.accounts.length === 0) continue;
value.contractId = result.accounts[0].evm_address;
}

unresolvedValues = slotMapOf(token).load(nrequestedSlot);

if (unresolvedValues === undefined)
Expand Down
13 changes: 9 additions & 4 deletions src/slotmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,15 @@ const _types = {
t_int64: str => [toIntHex256(str ?? 0)],
t_address: str => [
str
? (mirrorNode, blockNumber) =>
mirrorNode
.getAccount(str, blockNumber)
.then(acc => toIntHex256(acc?.evm_address ?? str?.replace('0.0.', '') ?? 0))
? async (mirrorNode, blockNumber) => {
return str.startsWith('0x')
? str.substring(2).padStart(64, '0')
: mirrorNode
.getAccount(str, blockNumber)
.then(acc =>
toIntHex256(acc?.evm_address ?? str?.replace('0.0.', '') ?? 0)
);
}
: toIntHex256(0),
],
t_bool: value => [toIntHex256(value ? 1 : 0)],
Expand Down
Loading

0 comments on commit 3bb1932

Please sign in to comment.