Skip to content
Draft
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
15 changes: 15 additions & 0 deletions contracts/HederaResponseCodes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
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 ACCOUNT_FROZEN_FOR_TOKEN = 165;
int32 internal constant INVALID_TOKEN_ID = 167;
int32 internal constant TOKEN_HAS_NO_FREEZE_KEY = 172;
int32 internal constant ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN = 176;
int32 internal constant TOKEN_HAS_NO_KYC_KEY = 177;
int32 internal constant INSUFFICIENT_TOKEN_BALANCE = 178;
int32 internal constant SENDER_DOES_NOT_OWN_NFT_SERIAL_NO = 237;
int32 internal constant TOKEN_IS_PAUSED = 265;
int32 internal constant TOKEN_HAS_NO_PAUSE_KEY = 266;
int32 internal constant SPENDER_DOES_NOT_HAVE_ALLOWANCE = 292;
int32 internal constant MAX_ALLOWANCES_EXCEEDED = 294;
}
191 changes: 186 additions & 5 deletions contracts/HtsSystemContract.sol

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions contracts/HtsSystemContractJson.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
import {Vm} from "forge-std/Vm.sol";
import {decode} from './Base64.sol';
import {IHederaTokenService} from "./IHederaTokenService.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 @@ -477,6 +478,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
40 changes: 20 additions & 20 deletions contracts/IHederaTokenService.sol
Original file line number Diff line number Diff line change
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
26 changes: 26 additions & 0 deletions contracts/MirrorNode.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,32 @@ 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 getAccountAddress(string memory accountId) public returns (address) {
if (bytes(accountId).length == 0
|| keccak256(bytes(accountId)) == keccak256(bytes("null"))
Expand Down
2 changes: 2 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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
35 changes: 35 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,41 @@ 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);
Expand Down
63 changes: 63 additions & 0 deletions test/Freeze.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {TestSetup} from "./lib/TestSetup.sol";
import {HtsSystemContract, HTS_ADDRESS} from "../contracts/HtsSystemContract.sol";
import {IHederaTokenService} from "../contracts/IHederaTokenService.sol";
import {HederaResponseCodes} from "../contracts/HederaResponseCodes.sol";
import {IERC20} from "../contracts/IERC20.sol";
import {IERC721} from "../contracts/IERC721.sol";

contract FreezeTest is Test, TestSetup {

address private token;
address private owner;
address private to;
uint256 private amount = 4_000000;

function setUp() external {
setUpMockStorageForNonFork();
IHederaTokenService.HederaToken memory hederaToken;
hederaToken.name = "Token name";
hederaToken.symbol = "Token symbol";
hederaToken.treasury = makeAddr("Token treasury");
hederaToken.tokenKeys = new IHederaTokenService.TokenKey[](1);
owner = makeAddr("freeze");
hederaToken.tokenKeys[0].keyType = 0x4;
hederaToken.tokenKeys[0].key.ed25519 = hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc";
(, token) = IHederaTokenService(HTS_ADDRESS).createFungibleToken{value: 1000}(hederaToken, 1000000, 4);
vm.assertNotEq(token, address(0));
to = makeAddr("bob");
}

function test_HTS_transferToken_success_when_not_frozen() public {
address from = makeAddr("from");
deal(token, from, amount);
vm.prank(from);
IHederaTokenService(HTS_ADDRESS).transferToken(token, from, to, int64(int256(amount)));
assertEq(IERC20(token).balanceOf(to), amount);
}

function test_HTS_freezing_success_with_correct_key() public {
address from = makeAddr("bob");
vm.startPrank(owner);
int64 freezeCode = IHederaTokenService(HTS_ADDRESS).freezeToken(token, from);
assertEq(freezeCode, HederaResponseCodes.SUCCESS);
int64 unfreezeCode = IHederaTokenService(HTS_ADDRESS).unfreezeToken(token, from);
assertEq(unfreezeCode, HederaResponseCodes.SUCCESS);
vm.stopPrank();
}

function test_HTS_transferToken_failure_when_frozen() public {
address from = makeAddr("bob");
deal(token, from, amount);
vm.prank(owner);
IHederaTokenService(HTS_ADDRESS).freezeToken(token, from);

vm.prank(from);
(int64 code) = IHederaTokenService(HTS_ADDRESS).transferToken(token, from, to, int64(int256(amount)));
assertEq(code, HederaResponseCodes.ACCOUNT_FROZEN_FOR_TOKEN);
}
}
4 changes: 2 additions & 2 deletions test/HTS.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -793,8 +793,8 @@ contract HTSTest is Test, TestSetup {
int64[] memory amounts = new int64[](1);
amounts[0] = 4_000000;
vm.prank(owner);
vm.expectRevert("transferTokens: invalid token");
IHederaTokenService(HTS_ADDRESS).transferTokens(address(0), to, amounts);
(int64 code) = IHederaTokenService(HTS_ADDRESS).transferTokens(address(0), to, amounts);
assertEq(code, HederaResponseCodes.INVALID_TOKEN_ID);
}

function test_HTS_transferTokens_inconsistent_input() public {
Expand Down
76 changes: 76 additions & 0 deletions test/KYC.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.0;

import {Test} from "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import {TestSetup} from "./lib/TestSetup.sol";
import {HtsSystemContract, HTS_ADDRESS} from "../contracts/HtsSystemContract.sol";
import {IHederaTokenService} from "../contracts/IHederaTokenService.sol";
import {HederaResponseCodes} from "../contracts/HederaResponseCodes.sol";
import {IERC20} from "../contracts/IERC20.sol";
import {IERC721} from "../contracts/IERC721.sol";

contract KYCTest is Test, TestSetup {

address private token;
address private owner;
address private to;
uint256 private amount = 4_000000;

function setUp() external {
setUpMockStorageForNonFork();
IHederaTokenService.HederaToken memory hederaToken;
hederaToken.name = "Token name";
hederaToken.symbol = "Token symbol";
hederaToken.treasury = makeAddr("Token treasury");
hederaToken.tokenKeys = new IHederaTokenService.TokenKey[](1);
owner = makeAddr("kyc");
hederaToken.tokenKeys[0].keyType = 0x2;
hederaToken.tokenKeys[0].key.ed25519 = hex"5db29fb3f19f8618cc4689cf13e78a935621845d67547719faf49f65d5c367cc";
(, token) = IHederaTokenService(HTS_ADDRESS).createFungibleToken{value: 1000}(hederaToken, 1000000, 4);
vm.assertNotEq(token, address(0));
to = makeAddr("bob");
}

function test_HTS_transferToken_success_with_kyc() public {
deal(token, owner, amount);
uint256 balanceOfOwner = IERC20(token).balanceOf(owner);
IHederaTokenService(HTS_ADDRESS).grantTokenKyc(token, owner);
vm.prank(owner);
IHederaTokenService(HTS_ADDRESS).transferToken(token, owner, to, int64(int256(amount)));
assertEq(IERC20(token).balanceOf(owner), balanceOfOwner - amount);
assertEq(IERC20(token).balanceOf(to), amount);
}

function test_ERC20_transferToken_success_with_kyc() public {
deal(token, owner, amount);
uint256 balanceOfOwner = IERC20(token).balanceOf(owner);
IHederaTokenService(HTS_ADDRESS).grantTokenKyc(token, owner);
vm.prank(owner);
IERC20(token).transfer(to, amount);
assertEq(IERC20(token).balanceOf(owner), balanceOfOwner - amount);
assertEq(IERC20(token).balanceOf(to), amount);
}

function test_HTS_transferToken_failure_without_kyc() public {
address from = makeAddr("from");
deal(token, from, amount);
vm.prank(from);
(int64 code) = IHederaTokenService(HTS_ADDRESS).transferToken(token, from, to, int64(int256(amount)));
assertEq(code, HederaResponseCodes.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN);
}

function test_HTS_transferToken_success_with_kyc_granted() public {
address from = makeAddr("from");
deal(token, from, amount);
uint256 balanceOfFrom = IERC20(token).balanceOf(from);
vm.prank(owner);

IHederaTokenService(HTS_ADDRESS).grantTokenKyc(token, from);
vm.prank(from);
IHederaTokenService(HTS_ADDRESS).transferToken(token, from, to, int64(int256(amount)));

assertEq(IERC20(token).balanceOf(from), balanceOfFrom - amount);
assertEq(IERC20(token).balanceOf(to), amount);
}
}
Loading