diff --git a/.github/workflows/foundry_test.yml b/.github/workflows/foundry_test.yml new file mode 100644 index 000000000..39e7e2b18 --- /dev/null +++ b/.github/workflows/foundry_test.yml @@ -0,0 +1,46 @@ +name: Foundry Tests + +on: + workflow_dispatch: + pull_request: + branches: [main] + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Use Node.js [18.15] + uses: actions/setup-node@v3 + with: + node-version: 18.15 + cache: npm + + - name: Create .env file + run: cp local.env .env + + - name: Install dependencies + run: npm ci + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/.github/workflows/test-workflow.yml b/.github/workflows/test-workflow.yml index c3a025ec2..eda20a26a 100644 --- a/.github/workflows/test-workflow.yml +++ b/.github/workflows/test-workflow.yml @@ -40,6 +40,11 @@ jobs: - name: Install dependencies run: npm ci + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + - name: Start the local node run: npx hedera start -d --network local timeout-minutes: 5 diff --git a/.gitignore b/.gitignore index 2025916ae..dd21829bb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,24 @@ artifacts/contracts .env test-results.* +## --- Foundry Gitignore --- + +# Compiler files +forge-cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Coverage +lcov.info +coverage/ + +## --- Hardhat Gitignore for Foundry artifacts --- +artifacts/forge-std +artifacts/ds-test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..888d42dcd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 000000000..4af1a8571 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +grep -q "Signed-off-by" $1 || (echo "No Signed-off-by found. Run git commit --signoff" && false) diff --git a/FOUNDRY_TESTING.md b/FOUNDRY_TESTING.md new file mode 100644 index 000000000..9265b4558 --- /dev/null +++ b/FOUNDRY_TESTING.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/README.md b/README.md index 29c51a8a5..e234f3176 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,8 @@ For further details on methods, hashes and availability please refer to [PRNG Pr This project is set up using the Hardhat development environment. To get started, please follow this [test setup guide](./TEST_SETUP.md). +For using this project as a library in a Foundry project see [Foundry Testing](FOUNDRY_TESTING.md) + ## Support If you have a question on how to use the product, please see our @@ -63,4 +65,3 @@ to [oss@hedera.com](mailto:oss@hedera.com). ## Smart contracts - testing [Smart contracts tests - documentation](https://raw.githubusercontent.com/hashgraph/hedera-smart-contracts/main/test/README.md) - diff --git a/contracts/base/NoDelegateCall.sol b/contracts/base/NoDelegateCall.sol new file mode 100644 index 000000000..dd55600de --- /dev/null +++ b/contracts/base/NoDelegateCall.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import "../libraries/Constants.sol"; + +/// @title Prevents delegatecall to a contract +/// @notice Base contract that provides a modifier for preventing delegatecall to methods in a child contract +abstract contract NoDelegateCall is Constants { + /// @dev The original address of this contract + address private immutable original; + + /// @dev slightly modified as in context of constructor address(this) is the address of the deployed contract and not the etched contract address + /// hence _original allows passing the address to which a contract is etched to; for normal uses pass ADDRESS_ZERO + constructor(address _original) { + // Immutables are computed in the init code of the contract, and then inlined into the deployed bytecode. + // In other words, this variable won't change when it's checked at runtime. + original = _original == ADDRESS_ZERO ? address(this) : _original; + } + + /// @dev Private method is used instead of inlining into modifier because modifiers are copied into each method, + /// and the use of immutable means the address bytes are copied in every place the modifier is used. + function checkNotDelegateCall() private view { + require(address(this) == original, "NO_DELEGATECALL"); + } + + /// @notice Prevents delegatecall into the modified method + modifier noDelegateCall() { + checkNotDelegateCall(); + _; + } +} diff --git a/contracts/libraries/Constants.sol b/contracts/libraries/Constants.sol new file mode 100644 index 000000000..4c0b0a4a9 --- /dev/null +++ b/contracts/libraries/Constants.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +abstract contract Constants { + address internal constant HTS_PRECOMPILE = address(0x167); + address internal constant EXCHANGE_RATE_PRECOMPILE = address(0x168); + address internal constant UTIL_PRECOMPILE = address(0x168); + + address internal constant ADDRESS_ZERO = address(0); +} diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 000000000..c8274ea5b --- /dev/null +++ b/foundry.toml @@ -0,0 +1,12 @@ +[profile.default] +src = 'contracts' +out = 'out' +libs = ['node_modules', 'lib'] +test = 'test/foundry' +cache_path = 'forge-cache' +remappings = [ + '@openzeppelin/=node_modules/@openzeppelin/', + 'hardhat/=node_modules/hardhat/', +] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/hardhat.config.js b/hardhat.config.js index fe13e5bca..26a6c30f5 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,6 +1,7 @@ require('@nomicfoundation/hardhat-chai-matchers') require('@nomiclabs/hardhat-ethers') require('@openzeppelin/hardhat-upgrades') +require('@nomicfoundation/hardhat-foundry'); const { OPERATOR_ID_A, OPERATOR_KEY_A, diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 000000000..f73c73d20 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit f73c73d2018eb6a111f35e4dae7b4f27401e9421 diff --git a/package-lock.json b/package-lock.json index 95a3b18c3..d731af993 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,12 @@ "@hashgraph/hedera-local": "2.13.0", "@hashgraph/sdk": "^2.25.0", "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", + "@nomicfoundation/hardhat-foundry": "^1.1.1", "@openzeppelin/contracts": "^4.9.3", "@openzeppelin/contracts-upgradeable": "^4.9.3", "@openzeppelin/hardhat-upgrades": "^1.22.1", - "hardhat": "^2.14.0", + "hardhat": "^2.17.2", + "husky": "^8.0.0", "mocha-junit-reporter": "^2.2.0", "mocha-multi-reporters": "^1.5.1", "prettier": "3.0.0" @@ -2694,6 +2696,18 @@ "hardhat": "^2.9.4" } }, + "node_modules/@nomicfoundation/hardhat-foundry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-foundry/-/hardhat-foundry-1.1.1.tgz", + "integrity": "sha512-cXGCBHAiXas9Pg9MhMOpBVQCkWRYoRFG7GJJAph+sdQsfd22iRs5U5Vs9XmpGEQd1yEvYISQZMeE68Nxj65iUQ==", + "dev": true, + "dependencies": { + "chalk": "^2.4.2" + }, + "peerDependencies": { + "hardhat": "^2.17.2" + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", @@ -5792,6 +5806,21 @@ "node": ">= 6" } }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", diff --git a/package.json b/package.json index 25ed7af49..f7c7e32a5 100644 --- a/package.json +++ b/package.json @@ -16,14 +16,28 @@ "url": "https://github.com/hashgraph/hedera-smart-contracts/issues" }, "homepage": "https://github.com/hashgraph/hedera-smart-contracts#readme", + "scripts": { + "forge:build": "forge build", + "forge:test": "forge test", + "forge:coverage": "forge coverage", + "forge:coverage:report": "forge coverage --report lcov", + "forge:coverage:html": "forge coverage --report lcov && genhtml lcov.info --branch-coverage --output-dir coverage", + "hh:compile": "hardhat compile", + "hh:test": "hardhat test", + "hedera:start": "npx @hashgraph/hedera-local start --limits=false --dev=true --balance=10000000", + "hedera:stop": "npx @hashgraph/hedera-local stop", + "prepare": "husky install" + }, "devDependencies": { "@hashgraph/hedera-local": "2.13.0", "@hashgraph/sdk": "^2.25.0", "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", + "@nomicfoundation/hardhat-foundry": "^1.1.1", "@openzeppelin/contracts": "^4.9.3", "@openzeppelin/contracts-upgradeable": "^4.9.3", "@openzeppelin/hardhat-upgrades": "^1.22.1", - "hardhat": "^2.14.0", + "hardhat": "^2.17.2", + "husky": "^8.0.0", "mocha-junit-reporter": "^2.2.0", "mocha-multi-reporters": "^1.5.1", "prettier": "3.0.0" @@ -33,4 +47,3 @@ "dotenv": "^16.3.1" } } - diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 000000000..1076a4256 --- /dev/null +++ b/remappings.txt @@ -0,0 +1,2 @@ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ \ No newline at end of file diff --git a/test/foundry/ExchangeRatePrecompileMock.t.sol b/test/foundry/ExchangeRatePrecompileMock.t.sol new file mode 100644 index 000000000..ae5c5700e --- /dev/null +++ b/test/foundry/ExchangeRatePrecompileMock.t.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import './utils/ExchangeRateUtils.sol'; + +contract ExchangeRatePrecompileMockTest is ExchangeRateUtils { + + // setUp is executed before each and every test function + function setUp() public { + _setUpExchangeRatePrecompileMock(); + _setUpAccounts(); + } + + function test_CanCorrectlyConvertTinycentsToTinybars() public { + uint256 tinycents = 1e8; + uint256 tinybars = _doConvertTinycentsToTinybars(tinycents); + assertEq(tinybars, 1e7, "expected 1 cent to equal 1e7 tinybar(0.1 HBAR) at $0.1/HBAR"); + } + + function test_CanCorrectlyConvertTinybarsToTinyCents() public { + uint256 tinybars = 1e8; + uint256 tinycents = _doConvertTinybarsToTinycents(tinybars); + assertEq(tinycents, 1e9, "expected 1 HBAR to equal 10 cents(1e9 tinycents) at $0.1/HBAR"); + } + +} + +// forge test --match-contract ExchangeRatePrecompileMockTest -vv diff --git a/test/foundry/HederaFungibleToken.t.sol b/test/foundry/HederaFungibleToken.t.sol new file mode 100644 index 000000000..976589888 --- /dev/null +++ b/test/foundry/HederaFungibleToken.t.sol @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '../../contracts/hts-precompile/IHederaTokenService.sol'; +import '../../contracts/hts-precompile/KeyHelper.sol'; +import './utils/HederaTokenUtils.sol'; +import './utils/HederaFungibleTokenUtils.sol'; + +contract HederaFungibleTokenTest is HederaTokenUtils, HederaFungibleTokenUtils { + + // setUp is executed before each and every test function + function setUp() public { + _setUpHtsPrecompileMock(); + _setUpAccounts(); + } + + // positive cases + function test_CreateHederaFungibleTokenViaHtsPrecompile() public { + address sender = alice; + string memory name = 'Token A'; + string memory symbol = 'TA'; + address treasury = alice; + int64 initialTotalSupply = 1e16; + int32 decimals = 8; + + _doCreateHederaFungibleTokenViaHtsPrecompile(sender, name, symbol, treasury, initialTotalSupply, decimals); + } + + function test_CreateHederaFungibleTokenDirectly() public { + address sender = alice; + string memory name = 'Token A'; + string memory symbol = 'TA'; + address treasury = alice; + int64 initialTotalSupply = 1e16; + int32 decimals = 8; + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + + _doCreateHederaFungibleTokenDirectly(sender, name, symbol, treasury, initialTotalSupply, decimals, keys); + } + + function test_ApproveViaHtsPrecompile() public { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + uint allowance = 1e8; + _doApproveViaHtsPrecompile(alice, tokenAddress, bob, allowance); + } + + function test_ApproveDirectly() public { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + uint allowance = 1e8; + _doApproveDirectly(alice, tokenAddress, bob, allowance); + } + + function test_TransferViaHtsPrecompile() public { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + bool success; + uint256 amount = 1e8; + + TransferParams memory transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: amount + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected transfer to fail since recipient is not associated with token'); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, 'expected bob to associate with token'); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, true, 'expected transfer to succeed'); + } + + function test_TransferDirectly() public { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + bool success; + uint256 amount = 1e8; + + TransferParams memory transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: amount + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, false, 'expected transfer to fail since recipient is not associated with token'); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, 'expected bob to associate with token'); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, true, 'expected transfer to succeed'); + } + + function test_TransferUsingAllowanceViaHtsPrecompile() public { + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + bool success; + uint256 amount = 1e8; + + TransferParams memory transferParams = TransferParams({ + sender: bob, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: amount + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected transfer to fail since bob is not associated with token'); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, 'expected bob to associate with token'); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected transfer to fail since bob is not granted an allowance'); + + uint allowance = 1e8; + _doApproveViaHtsPrecompile(alice, tokenAddress, bob, allowance); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, true, 'expected transfer to succeed'); + } + + function test_TransferUsingAllowanceDirectly() public { + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + bool success; + uint256 amount = 1e8; + + TransferParams memory transferParams = TransferParams({ + sender: bob, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: amount + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, false, 'expected transfer to fail since bob is not associated with token'); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, 'expected bob to associate with token'); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, false, 'expected transfer to fail since bob is not granted an allowance'); + + uint allowance = 1e8; + _doApproveViaHtsPrecompile(alice, tokenAddress, bob, allowance); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, true, 'expected transfer to succeed'); + } + + /// @dev there is no test_CanMintDirectly as the ERC20 standard does not typically allow direct mints + function test_CanMintViaHtsPrecompile() public { + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + _doAssociateViaHtsPrecompile(bob, tokenAddress); + + bool success; + + int64 mintAmount = 1e8; + + MintResponse memory mintResponse; + MintParams memory mintParams; + + mintParams = MintParams({ + sender: bob, + token: tokenAddress, + mintAmount: mintAmount + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + assertEq(mintResponse.success, false, "expected mint to fail since bob is not supply key"); + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: mintAmount + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + assertEq(mintResponse.success, true, "expected mint to succeed"); + } + + /// @dev there is no test_CanBurnDirectly as the ERC20 standard does not typically allow direct burns + function test_CanBurnViaHtsPrecompile() public { + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + address tokenAddress = _createSimpleMockFungibleToken(alice, keys); + + bool success; + + int64 burnAmount = 1e8; + + BurnParams memory burnParams; + + burnParams = BurnParams({ + sender: bob, + token: tokenAddress, + amountOrSerialNumber: burnAmount + }); + + (success, ) = _doBurnViaHtsPrecompile(burnParams); + assertEq(success, false, "expected burn to fail since bob is not treasury"); + + burnParams = BurnParams({ + sender: alice, + token: tokenAddress, + amountOrSerialNumber: burnAmount + }); + + (success, ) = _doBurnViaHtsPrecompile(burnParams); + assertEq(success, true, "expected burn to succeed"); + } + + // negative cases + function test_CannotApproveIfSpenderNotAssociated() public { + /// @dev already demonstrated in some of the postive test cases + // cannot approve spender if spender is not associated with HederaFungibleToken BOTH directly and viaHtsPrecompile + } + + function test_CannotTransferIfRecipientNotAssociated() public { + /// @dev already demonstrated in some of the postive test cases + // cannot transfer to recipient if recipient is not associated with HederaFungibleToken BOTH directly and viaHtsPrecompile + } +} + +// forge test --match-contract HederaFungibleTokenTest --match-test test_CanBurnViaHtsPrecompile -vv +// forge test --match-contract HederaFungibleTokenTest -vv \ No newline at end of file diff --git a/test/foundry/HederaNonFungibleToken.t.sol b/test/foundry/HederaNonFungibleToken.t.sol new file mode 100644 index 000000000..6efa25d30 --- /dev/null +++ b/test/foundry/HederaNonFungibleToken.t.sol @@ -0,0 +1,531 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '../../contracts/hts-precompile/IHederaTokenService.sol'; +import '../../contracts/hts-precompile/HederaResponseCodes.sol'; +import '../../contracts/hts-precompile/KeyHelper.sol'; +import './mocks/hts-precompile/HederaNonFungibleToken.sol'; +import './mocks/hts-precompile/HtsSystemContractMock.sol'; + +import './utils/HederaNonFungibleTokenUtils.sol'; +import '../../contracts/libraries/Constants.sol'; + +contract HederaNonFungibleTokenTest is HederaNonFungibleTokenUtils { + + // setUp is executed before each and every test function + function setUp() public { + _setUpHtsPrecompileMock(); + _setUpAccounts(); + } + + // positive cases + function test_CreateHederaNonFungibleTokenViaHtsPrecompile() public { + + address sender = alice; + string memory name = 'NFT A'; + string memory symbol = 'NFT-A'; + address treasury = bob; + + bool success; + + (success, ) = _doCreateHederaNonFungibleTokenViaHtsPrecompile(sender, name, symbol, treasury); + assertEq(success, false, "expected failure since treasury is not sender"); + + treasury = alice; + + (success, ) = _doCreateHederaNonFungibleTokenViaHtsPrecompile(sender, name, symbol, treasury); + assertEq(success, true, "expected success since treasury is sender"); + + } + + function test_CreateHederaNonFungibleTokenDirectly() public { + + address sender = alice; + string memory name = 'NFT A'; + string memory symbol = 'NFT-A'; + address treasury = bob; + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](0); + + bool success; + + (success, ) = _doCreateHederaNonFungibleTokenDirectly(sender, name, symbol, treasury, keys); + assertEq(success, false, "expected failure since treasury is not sender"); + + treasury = alice; + + (success, ) = _doCreateHederaNonFungibleTokenDirectly(sender, name, symbol, treasury, keys); + assertEq(success, true, "expected success since treasury is sender"); + + } + + function test_ApproveViaHtsPrecompile() public { + + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + + MintResponse memory mintResponse; + MintParams memory mintParams; + + mintParams = MintParams({ + sender: bob, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + assertEq(mintResponse.success, false, "expected failure since bob is not supply key"); + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, "bob should have associated with token"); + + ApproveNftParams memory approveNftParams; + + approveNftParams = ApproveNftParams({ + sender: bob, + token: tokenAddress, + spender: carol, + serialId: mintResponse.serialId + }); + + success = _doApproveNftViaHtsPrecompile(approveNftParams); + assertEq(success, false, "should have failed as bob does not own NFT with serialId"); + + approveNftParams = ApproveNftParams({ + sender: alice, + token: tokenAddress, + spender: carol, + serialId: mintResponse.serialId + }); + + success = _doApproveNftViaHtsPrecompile(approveNftParams); + assertEq(success, true, "should have succeeded as alice does own NFT with serialId"); + } + + function test_ApproveDirectly() public { + + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + + MintResponse memory mintResponse; + MintParams memory mintParams; + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, "bob should have associated with token"); + + ApproveNftParams memory approveNftParams; + + approveNftParams = ApproveNftParams({ + sender: bob, + token: tokenAddress, + spender: carol, + serialId: mintResponse.serialId + }); + + success = _doApproveNftDirectly(approveNftParams); + assertEq(success, false, "should have failed as bob does not own NFT with serialId"); + + approveNftParams = ApproveNftParams({ + sender: alice, + token: tokenAddress, + spender: carol, + serialId: mintResponse.serialId + }); + + success = _doApproveNftDirectly(approveNftParams); + assertEq(success, true, "should have succeeded as alice does own NFT with serialId"); + } + + function test_TransferViaHtsPrecompile() public { + + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + uint256 serialIdU256; + + MintResponse memory mintResponse; + MintParams memory mintParams; + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + serialIdU256 = uint64(mintResponse.serialId); + + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, "bob should have associated with token"); + + TransferParams memory transferParams; + + transferParams = TransferParams({ + sender: bob, + token: tokenAddress, + from: alice, + to: carol, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected fail since bob does not own nft or have approval'); + + transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: carol, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected fail since carol is not associated with nft'); + + transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, true, 'expected success'); + } + + function test_TransferDirectly() public { + + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + uint256 serialIdU256; + + MintResponse memory mintResponse; + MintParams memory mintParams; + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + serialIdU256 = uint64(mintResponse.serialId); + + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, "bob should have associated with token"); + + TransferParams memory transferParams; + + transferParams = TransferParams({ + sender: bob, + token: tokenAddress, + from: alice, + to: carol, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, false, 'expected fail since bob does not own nft or have approval'); + + transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: carol, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, false, 'expected fail since carol is not associated with nft'); + + transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, true, 'expected success'); + } + + function test_TransferUsingAllowanceViaHtsPrecompile() public { + + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + uint256 serialIdU256; + + MintResponse memory mintResponse; + MintParams memory mintParams; + + TransferParams memory transferParams; + + ApproveNftParams memory approveNftParams; + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + serialIdU256 = uint64(mintResponse.serialId); + + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + transferParams = TransferParams({ + sender: carol, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected fail since carol is not approved'); + + approveNftParams = ApproveNftParams({ + sender: alice, + token: tokenAddress, + spender: carol, + serialId: mintResponse.serialId + }); + + _doApproveNftDirectly(approveNftParams); + + transferParams = TransferParams({ + sender: carol, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected fail since bob is not associated with nft'); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, "bob should have associated with token"); + + transferParams = TransferParams({ + sender: carol, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, true, 'expected success'); + } + + function test_TransferUsingAllowanceDirectly() public { + + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + uint256 serialIdU256; + + MintResponse memory mintResponse; + MintParams memory mintParams; + + TransferParams memory transferParams; + + ApproveNftParams memory approveNftParams; + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + serialIdU256 = uint64(mintResponse.serialId); + + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + transferParams = TransferParams({ + sender: carol, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferViaHtsPrecompile(transferParams); + assertEq(success, false, 'expected fail since carol is not approved'); + + approveNftParams = ApproveNftParams({ + sender: alice, + token: tokenAddress, + spender: carol, + serialId: mintResponse.serialId + }); + + _doApproveNftDirectly(approveNftParams); + + transferParams = TransferParams({ + sender: carol, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, false, 'expected fail since bob is not associated with nft'); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, "bob should have associated with token"); + + transferParams = TransferParams({ + sender: carol, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, true, 'expected success'); + } + + /// @dev there is no test_CanBurnDirectly as the ERC20 standard does not typically allow direct burns + function test_CanBurnViaHtsPrecompile() public { + + bytes[] memory NULL_BYTES = new bytes[](1); + + IHederaTokenService.TokenKey[] memory keys = new IHederaTokenService.TokenKey[](1); + keys[0] = KeyHelper.getSingleKey(KeyHelper.KeyType.SUPPLY, KeyHelper.KeyValueType.CONTRACT_ID, alice); + address tokenAddress = _createSimpleMockNonFungibleToken(alice, keys); + + bool success; + uint256 serialIdU256; + + MintResponse memory mintResponse; + MintParams memory mintParams; + BurnParams memory burnParams; + + mintParams = MintParams({ + sender: alice, + token: tokenAddress, + mintAmount: 0 + }); + + mintResponse = _doMintViaHtsPrecompile(mintParams); + serialIdU256 = uint64(mintResponse.serialId); + + assertEq(mintResponse.success, true, "expected success since alice is supply key"); + + success = _doAssociateViaHtsPrecompile(bob, tokenAddress); + assertEq(success, true, "bob should have associated with token"); + + TransferParams memory transferParams; + + transferParams = TransferParams({ + sender: alice, + token: tokenAddress, + from: alice, + to: bob, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, true, 'expected success'); + + burnParams = BurnParams({ + sender: alice, + token: tokenAddress, + amountOrSerialNumber: mintResponse.serialId + }); + + (success, ) = _doBurnViaHtsPrecompile(burnParams); + assertEq(success, false, "burn should fail, since treasury does not own nft"); + + transferParams = TransferParams({ + sender: bob, + token: tokenAddress, + from: bob, + to: alice, + amountOrSerialNumber: serialIdU256 + }); + + (success, ) = _doTransferDirectly(transferParams); + assertEq(success, true, 'expected success'); + + burnParams = BurnParams({ + sender: alice, + token: tokenAddress, + amountOrSerialNumber: mintResponse.serialId + }); + + (success, ) = _doBurnViaHtsPrecompile(burnParams); + assertEq(success, true, "burn should succeed"); + } + + // negative cases + function test_CannotApproveIfSpenderNotAssociated() public { + /// @dev already demonstrated in some of the postive test cases + // cannot approve spender if spender is not associated with HederaNonFungibleToken BOTH directly and viaHtsPrecompile + } + + function test_CannotTransferIfRecipientNotAssociated() public { + /// @dev already demonstrated in some of the postive test cases + // cannot transfer to recipient if recipient is not associated with HederaNonFungibleToken BOTH directly and viaHtsPrecompile + } +} + +// forge test --match-contract HederaNonFungibleTokenTest --match-test test_TransferUsingAllowanceDirectly -vv +// forge test --match-contract HederaNonFungibleTokenTest -vv \ No newline at end of file diff --git a/test/foundry/UtilPrecompileMock.t.sol b/test/foundry/UtilPrecompileMock.t.sol new file mode 100644 index 000000000..5f3ef9c76 --- /dev/null +++ b/test/foundry/UtilPrecompileMock.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import './utils/UtilUtils.sol'; + +contract UtilPrecompileMockTest is UtilUtils { + + mapping(bytes32 => bool) private seeds; // use mapping over list as it's much faster to index + + // setUp is executed before each and every test function + function setUp() public { + _setUpUtilPrecompileMock(); + _setUpAccounts(); + } + + function test_CallPseudoRandomSeed() public { + + uint256 iterations = 10000; + + address sender = alice; + bytes32 seed; + + for (uint256 i = 0; i < iterations; i++) { + seed = _doCallPseudorandomSeed(sender); + + if (seeds[seed]) { + revert("seed already exists"); + } + + seeds[seed] = true; + + sender = _getAccount(uint256(seed) % NUM_OF_ACCOUNTS); + } + } + +} + +// forge test --match-contract UtilPrecompileMockTest -vv diff --git a/test/foundry/mocks/exchange-rate-precompile/ExchangeRatePrecompileMock.sol b/test/foundry/mocks/exchange-rate-precompile/ExchangeRatePrecompileMock.sol new file mode 100644 index 000000000..3f1dd61ef --- /dev/null +++ b/test/foundry/mocks/exchange-rate-precompile/ExchangeRatePrecompileMock.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '../../../../contracts/exchange-rate-precompile/IExchangeRate.sol'; + +contract ExchangeRatePrecompileMock is IExchangeRate { + + // 1e8 tinybars = 1 HBAR + // 1e8 tinycents = 1 cent = 0.01 USD + + // HBAR/USD rate in tinybars/tinycents + uint256 private rate; // 1e8 / 10; // Initial rate of 1e8 tinybars/10 tinycents, equivalent to $0.10/1 HBAR + /// @dev it appears that contracts that are etched do NOT have any starting state i.e. all state is initialised to the default + /// hence "rate" is not initialised to 1e7 here, but updateRate is called after the ExchangeRatePrecompileMock is etched(using vm.etch) onto the EXCHANGE_RATE_PRECOMPILE address + + function tinycentsToTinybars(uint256 tinycents) external override returns (uint256) { + require(rate > 0, "Rate must be greater than 0"); + return (tinycents * rate) / 1e8; + } + + function tinybarsToTinycents(uint256 tinybars) external override returns (uint256) { + require(rate > 0, "Rate must be greater than 0"); + return (tinybars * 1e8) / rate; + // (1e8 * 1e8) / (1e8 / 12) = (12*1e8) tinycents + } + + function updateRate(uint256 newRate) external { + require(newRate > 0, "New rate must be greater than 0"); + rate = newRate; + } + + function getCurrentRate() external view returns (uint256) { + return rate; + } +} \ No newline at end of file diff --git a/test/foundry/mocks/hts-precompile/HederaFungibleToken.sol b/test/foundry/mocks/hts-precompile/HederaFungibleToken.sol new file mode 100644 index 000000000..3a5c97881 --- /dev/null +++ b/test/foundry/mocks/hts-precompile/HederaFungibleToken.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '@openzeppelin/contracts/token/ERC20/ERC20.sol'; + +import '../../../../contracts/hts-precompile/HederaResponseCodes.sol'; +import '../../../../contracts/hts-precompile/IHederaTokenService.sol'; +import './HtsSystemContractMock.sol'; +import '../../../../contracts/libraries/Constants.sol'; + +contract HederaFungibleToken is ERC20, Constants { + error HtsPrecompileError(int64 responseCode); + HtsSystemContractMock internal constant HtsPrecompile = HtsSystemContractMock(HTS_PRECOMPILE); + + bool public constant IS_FUNGIBLE = true; /// @dev if HederaNonFungibleToken then false + uint8 internal immutable _decimals; + + constructor( + IHederaTokenService.FungibleTokenInfo memory _fungibleTokenInfo + ) ERC20(_fungibleTokenInfo.tokenInfo.token.name, _fungibleTokenInfo.tokenInfo.token.symbol) { + HtsPrecompile.registerHederaFungibleToken(msg.sender, _fungibleTokenInfo); + _decimals = uint8(uint32(_fungibleTokenInfo.decimals)); + address treasury = _fungibleTokenInfo.tokenInfo.token.treasury; + _mint(treasury, uint(uint64(_fungibleTokenInfo.tokenInfo.totalSupply))); + } + + /// @dev the HtsSystemContractMock should do precheck validation before calling any function with this modifier + /// the HtsSystemContractMock has priveleged access to do certain operations + modifier onlyHtsPrecompile() { + require(msg.sender == HTS_PRECOMPILE, 'NOT_HTS_PRECOMPILE'); + _; + } + + // public/external state-changing functions: + // onlyHtsPrecompile functions: + /// @dev mints "amount" to treasury + function mintRequestFromHtsPrecompile(int64 amount) external onlyHtsPrecompile { + (, IHederaTokenService.FungibleTokenInfo memory fungibleTokenInfo) = HtsPrecompile.getFungibleTokenInfo( + address(this) + ); + address treasury = fungibleTokenInfo.tokenInfo.token.treasury; + _mint(treasury, uint64(amount)); + } + + /// @dev burns "amount" from treasury + function burnRequestFromHtsPrecompile(int64 amount) external onlyHtsPrecompile { + (, IHederaTokenService.FungibleTokenInfo memory fungibleTokenInfo) = HtsPrecompile.getFungibleTokenInfo( + address(this) + ); + address treasury = fungibleTokenInfo.tokenInfo.token.treasury; + _burn(treasury, uint64(amount)); + } + + function wipeRequestFromHtsPrecompile(address account, int64 amount) external onlyHtsPrecompile { + _burn(account, uint64(amount)); + } + + /// @dev transfers "amount" from "from" to "to" + function transferRequestFromHtsPrecompile(bool isRequestFromOwner, address spender, address from, address to, uint256 amount) external onlyHtsPrecompile returns (int64 responseCode) { + if (!isRequestFromOwner) { + _spendAllowance(from, spender, amount); + } + _transfer(from, to, amount); + + return HederaResponseCodes.SUCCESS; + } + + /// @dev gives "spender" an allowance of "amount" for "account" + function approveRequestFromHtsPrecompile( + address account, + address spender, + uint256 amount + ) external onlyHtsPrecompile { + _approve(account, spender, amount); + } + + // standard ERC20 functions overriden for HtsSystemContractMock prechecks: + function approve(address spender, uint256 amount) public override returns (bool) { + int64 responseCode = HtsPrecompile.preApprove(msg.sender, spender, amount); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + return super.approve(spender, amount); + } + + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + int64 responseCode = HtsPrecompile.preTransfer(msg.sender, from, to, amount); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + return super.transferFrom(from, to, amount); + } + + function transfer(address to, uint256 amount) public virtual override returns (bool) { + int64 responseCode = HtsPrecompile.preTransfer(ADDRESS_ZERO, msg.sender, to, amount); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + return super.transfer(to, amount); + } + + // standard ERC20 overriden functions + function decimals() public view override returns (uint8) { + return _decimals; + } +} diff --git a/test/foundry/mocks/hts-precompile/HederaNonFungibleToken.sol b/test/foundry/mocks/hts-precompile/HederaNonFungibleToken.sol new file mode 100644 index 000000000..0803dc519 --- /dev/null +++ b/test/foundry/mocks/hts-precompile/HederaNonFungibleToken.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; + +import '../../../../contracts/hts-precompile/HederaResponseCodes.sol'; +import '../../../../contracts/hts-precompile/IHederaTokenService.sol'; +import './HtsSystemContractMock.sol'; +import '../../../../contracts/libraries/Constants.sol'; + +contract HederaNonFungibleToken is ERC721, Constants { + error HtsPrecompileError(int64 responseCode); + + HtsSystemContractMock internal constant HtsPrecompile = HtsSystemContractMock(HTS_PRECOMPILE); + + bool public constant IS_FUNGIBLE = false; /// @dev if HederaFungibleToken then true + + struct NFTCounter { + int64 minted; + int64 burned; + } + + NFTCounter internal nftCount; + + /// @dev NonFungibleTokenInfo is for each NFT(with a unique serial number) that is minted; however TokenInfo covers the common token info across all instances + constructor( + IHederaTokenService.TokenInfo memory _nftTokenInfo + ) ERC721(_nftTokenInfo.token.name, _nftTokenInfo.token.symbol) { + address sender = msg.sender; + HtsPrecompile.registerHederaNonFungibleToken(sender, _nftTokenInfo); + } + + /// @dev the HtsSystemContractMock should do precheck validation before calling any function with this modifier + /// the HtsSystemContractMock has priveleged access to do certain operations + modifier onlyHtsPrecompile() { + require(msg.sender == HTS_PRECOMPILE, 'NOT_HTS_PRECOMPILE'); + _; + } + + // public/external state-changing functions: + // onlyHtsPrecompile functions: + function mintRequestFromHtsPrecompile( + bytes[] memory metadata + ) external onlyHtsPrecompile returns (int64 newTotalSupply, int64 serialNumber) { + (, IHederaTokenService.TokenInfo memory nftTokenInfo) = HtsPrecompile.getTokenInfo( + address(this) + ); + address treasury = nftTokenInfo.token.treasury; + + serialNumber = ++nftCount.minted; // the first nft that is minted has serialNumber: 1 + _mint(treasury, uint64(serialNumber)); + + newTotalSupply = int64(int256(totalSupply())); + } + + function burnRequestFromHtsPrecompile( + int64[] calldata tokenIds + ) public onlyHtsPrecompile returns (int64 newTotalSupply) { + int64 burnCount = int64(uint64(tokenIds.length)); + nftCount.burned = nftCount.burned + burnCount; + + for (uint256 i = 0; i < uint64(burnCount); i++) { + uint256 tokenId = uint64(tokenIds[i]); + _burn(tokenId); + } + + newTotalSupply = int64(int256(totalSupply())); + } + + function wipeRequestFromHtsPrecompile( + int64[] calldata tokenIds + ) external onlyHtsPrecompile { + burnRequestFromHtsPrecompile(tokenIds); // implementation happens to coincide with burnRequestFromHtsPrecompile unlike in HederaFungibleToken + } + + /// @dev transfers "amount" from "from" to "to" + function transferRequestFromHtsPrecompile( + bool isRequestFromOwner, + address spender, + address from, + address to, + uint256 tokenId + ) external onlyHtsPrecompile returns (int64 responseCode) { + bool isSpenderApproved = _isApprovedOrOwner(spender, tokenId); + if (!isSpenderApproved) { + return HederaResponseCodes.INSUFFICIENT_TOKEN_BALANCE; + } + + _transfer(from, to, tokenId); + responseCode = HederaResponseCodes.SUCCESS; + } + + /// @dev unlike fungible/ERC20 tokens this only allows for a single spender to be approved at any one time + function approveRequestFromHtsPrecompile(address spender, int64 tokenId) external onlyHtsPrecompile { + _approve(spender, uint64(tokenId)); + } + + function setApprovalForAllFromHtsPrecompile( + address owner, + address operator, + bool approved + ) external onlyHtsPrecompile { + _setApprovalForAll(owner, operator, approved); + } + + // standard ERC721 functions overriden for HtsSystemContractMock prechecks: + function approve(address to, uint256 tokenId) public override { + address sender = msg.sender; + address spender = to; + int64 responseCode = HtsPrecompile.preApprove(sender, spender, tokenId); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + + // TODO: do checks on approval prior to calling approval to avoid reverting with the OpenZeppelin error strings + // this checks can be done in the HtsPrecompile.pre{Action} functions and ultimately in the _precheck{Action} internal functions + return super.approve(to, tokenId); + } + + function setApprovalForAll(address operator, bool approved) public override { + address sender = msg.sender; + int64 responseCode = HtsPrecompile.preSetApprovalForAll(sender, operator, approved); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + return super.setApprovalForAll(operator, approved); + } + + function transferFrom(address from, address to, uint256 tokenId) public override { + address sender = msg.sender; + int64 responseCode = HtsPrecompile.preTransfer(sender, from, to, tokenId); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + return super.transferFrom(from, to, tokenId); + } + + function safeTransferFrom(address from, address to, uint256 tokenId) public override { + address sender = msg.sender; + int64 responseCode = HtsPrecompile.preTransfer(sender, from, to, tokenId); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + return super.safeTransferFrom(from, to, tokenId); + } + + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public override { + address sender = msg.sender; + int64 responseCode = HtsPrecompile.preTransfer(sender, from, to, tokenId); + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + return super.safeTransferFrom(from, to, tokenId, data); + } + + // Additional(not in IHederaTokenService or in IERC721) public/external view functions: + function totalSupply() public view returns (uint256) { + return uint64(nftCount.minted - nftCount.burned); + } + + function isApprovedOrOwner(address spender, uint256 tokenId) external view returns (bool) { + return _isApprovedOrOwner(spender, tokenId); + } + + function mintCount() external view returns (int64 minted) { + minted = nftCount.minted; + } + + function burnCount() external view returns (int64 burned) { + burned = nftCount.burned; + } + +} diff --git a/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol b/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol new file mode 100644 index 000000000..964057a11 --- /dev/null +++ b/test/foundry/mocks/hts-precompile/HtsSystemContractMock.sol @@ -0,0 +1,1839 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import 'forge-std/console.sol'; + +import '../../../../contracts/hts-precompile/HederaResponseCodes.sol'; +import '../../../../contracts/hts-precompile/KeyHelper.sol'; +import './HederaFungibleToken.sol'; +import './HederaNonFungibleToken.sol'; +import '../../../../contracts/base/NoDelegateCall.sol'; +import '../../../../contracts/libraries/Constants.sol'; + +import '../interfaces/IHtsPrecompileMock.sol'; +import '../libraries/HederaTokenValidation.sol'; + +contract HtsSystemContractMock is NoDelegateCall, KeyHelper, IHtsPrecompileMock { + + error HtsPrecompileError(int64 responseCode); + + /// @dev only for Fungible tokens + // Fungible token -> FungibleTokenInfo + mapping(address => FungibleTokenInfo) internal _fungibleTokenInfos; + // Fungible token -> _isFungible + mapping(address => bool) internal _isFungible; + + /// @dev only for NonFungibleToken + // NFT token -> TokenInfo; TokenInfo is used instead of NonFungibleTokenInfo as the former is common to all NFT instances whereas the latter is for a specific NFT instance(uniquely identified by its serialNumber) + mapping(address => TokenInfo) internal _nftTokenInfos; + // NFT token -> serialNumber -> PartialNonFungibleTokenInfo + mapping(address => mapping(int64 => PartialNonFungibleTokenInfo)) internal _partialNonFungibleTokenInfos; + // NFT token -> _isNonFungible + mapping(address => bool) internal _isNonFungible; + + /// @dev common to both NFT and Fungible HTS tokens + // HTS token -> account -> isAssociated + mapping(address => mapping(address => bool)) internal _association; + // HTS token -> account -> isKyced + mapping(address => mapping(address => TokenConfig)) internal _kyc; // is KYCed is the positive case(i.e. explicitly requires KYC approval); see defaultKycStatus + // HTS token -> account -> isFrozen + mapping(address => mapping(address => TokenConfig)) internal _unfrozen; // is unfrozen is positive case(i.e. explicitly requires being unfrozen); see freezeDefault + // HTS token -> keyType -> key address(contractId) e.g. tokenId -> 16 -> 0x123 means that the SUPPLY key for tokenId is account 0x123 + mapping(address => mapping(uint => address)) internal _tokenKeys; /// @dev faster access then getting keys via {FungibleTokenInfo|NonFungibleTokenInfo}#TokenInfo.HederaToken.tokenKeys[]; however only supports KeyValueType.CONTRACT_ID + // HTS token -> deleted + mapping(address => bool) internal _tokenDeleted; + // HTS token -> paused + mapping(address => TokenConfig) internal _tokenPaused; + + constructor() NoDelegateCall(HTS_PRECOMPILE) {} + + // peripheral internal helpers: + // Concatenate metadata bytes arrays + function _concatenate(bytes[] memory metadata) internal pure returns (bytes memory) { + // Calculate the total length of concatenated bytes + uint totalLength = 0; + for (uint i = 0; i < metadata.length; i++) { + totalLength += metadata[i].length; + } + + // Create a new bytes variable with the total length + bytes memory result = new bytes(totalLength); + + // Concatenate bytes from metadata array into result + uint currentIndex = 0; + for (uint i = 0; i < metadata.length; i++) { + for (uint j = 0; j < metadata[i].length; j++) { + result[currentIndex] = metadata[i][j]; + currentIndex++; + } + } + + return result; + } + + modifier onlyHederaToken() { + require(_isToken(msg.sender), 'NOT_HEDERA_TOKEN'); + _; + } + + // Check if the address is a token + function _isToken(address token) internal view returns (bool) { + return _isFungible[token] || _isNonFungible[token]; + } + + /// @dev Hedera appears to have phased out authorization from the EOA with https://github.com/hashgraph/hedera-services/releases/tag/v0.36.0 + function _isAccountOriginOrSender(address account) internal view returns (bool) { + return _isAccountOrigin(account) || _isAccountSender(account); + } + + function _isAccountOrigin(address account) internal view returns (bool) { + return account == tx.origin; + } + + function _isAccountSender(address account) internal view returns (bool) { + return account == msg.sender; + } + + // Get the treasury account for a token + function _getTreasuryAccount(address token) internal view returns (address treasury) { + if (_isFungible[token]) { + treasury = _fungibleTokenInfos[token].tokenInfo.token.treasury; + } else { + treasury = _nftTokenInfos[token].token.treasury; + } + } + + // Check if the treasury signature is valid + function _hasTreasurySig(address token) internal view returns (bool validKey, bool noKey) { + address key = _getTreasuryAccount(token); + noKey = key == ADDRESS_ZERO; + validKey = _isAccountSender(key); + } + + // Check if the admin key signature is valid + function _hasAdminKeySig(address token) internal view returns (bool validKey, bool noKey) { + address key = _getKey(token, KeyHelper.KeyType.ADMIN); + noKey = key == ADDRESS_ZERO; + validKey = _isAccountSender(key); + } + + // Check if the kyc key signature is valid + function _hasKycKeySig(address token) internal view returns (bool validKey, bool noKey) { + address key = _getKey(token, KeyHelper.KeyType.KYC); + noKey = key == ADDRESS_ZERO; + validKey = _isAccountSender(key); + } + + // Check if the freeze key signature is valid + function _hasFreezeKeySig(address token) internal view returns (bool validKey, bool noKey) { + address key = _getKey(token, KeyHelper.KeyType.FREEZE); + noKey = key == ADDRESS_ZERO; + validKey = _isAccountSender(key); + } + + // Check if the wipe key signature is valid + function _hasWipeKeySig(address token) internal view returns (bool validKey, bool noKey) { + address key = _getKey(token, KeyHelper.KeyType.WIPE); + noKey = key == ADDRESS_ZERO; + validKey = _isAccountSender(key); + } + + // Check if the supply key signature is valid + function _hasSupplyKeySig(address token) internal view returns (bool validKey, bool noKey) { + address key = _getKey(token, KeyHelper.KeyType.SUPPLY); + noKey = key == ADDRESS_ZERO; + validKey = _isAccountSender(key); + } + + // Check if the fee schedule key signature is valid + function _hasFeeScheduleKeySig(address token) internal view returns (bool validKey, bool noKey) { + address key = _getKey(token, KeyHelper.KeyType.FEE); + noKey = key == ADDRESS_ZERO; + validKey = _isAccountSender(key); + } + + // Check if the pause key signature is valid + function _hasPauseKeySig(address token) internal view returns (bool validKey, bool noKey) { + address key = _getKey(token, KeyHelper.KeyType.PAUSE); + noKey = key == ADDRESS_ZERO; + validKey = _isAccountSender(key); + } + + function _setFungibleTokenInfoToken(address token, HederaToken memory hederaToken) internal { + _fungibleTokenInfos[token].tokenInfo.token.name = hederaToken.name; + _fungibleTokenInfos[token].tokenInfo.token.symbol = hederaToken.symbol; + _fungibleTokenInfos[token].tokenInfo.token.treasury = hederaToken.treasury; + _fungibleTokenInfos[token].tokenInfo.token.memo = hederaToken.memo; + _fungibleTokenInfos[token].tokenInfo.token.tokenSupplyType = hederaToken.tokenSupplyType; + _fungibleTokenInfos[token].tokenInfo.token.maxSupply = hederaToken.maxSupply; + _fungibleTokenInfos[token].tokenInfo.token.freezeDefault = hederaToken.freezeDefault; + } + + function _setFungibleTokenExpiry(address token, Expiry memory expiryInfo) internal { + _fungibleTokenInfos[token].tokenInfo.token.expiry.second = expiryInfo.second; + _fungibleTokenInfos[token].tokenInfo.token.expiry.autoRenewAccount = expiryInfo.autoRenewAccount; + _fungibleTokenInfos[token].tokenInfo.token.expiry.autoRenewPeriod = expiryInfo.autoRenewPeriod; + } + + function _setFungibleTokenInfo(address token, TokenInfo memory tokenInfo) internal { + _fungibleTokenInfos[token].tokenInfo.totalSupply = tokenInfo.totalSupply; + _fungibleTokenInfos[token].tokenInfo.deleted = tokenInfo.deleted; + _fungibleTokenInfos[token].tokenInfo.defaultKycStatus = tokenInfo.defaultKycStatus; + _fungibleTokenInfos[token].tokenInfo.pauseStatus = tokenInfo.pauseStatus; + _fungibleTokenInfos[token].tokenInfo.ledgerId = tokenInfo.ledgerId; + + // TODO: Handle copying of other arrays (fixedFees, fractionalFees, and royaltyFees) if needed + } + + function _setFungibleTokenKeys(address token, TokenKey[] memory tokenKeys) internal { + + // Copy the tokenKeys array + uint256 length = tokenKeys.length; + for (uint256 i = 0; i < length; i++) { + TokenKey memory tokenKey = tokenKeys[i]; + _fungibleTokenInfos[token].tokenInfo.token.tokenKeys.push(tokenKey); + + /// @dev contractId can in fact be any address including an EOA address + /// The KeyHelper lists 5 types for KeyValueType; however only CONTRACT_ID is considered + _tokenKeys[token][tokenKey.keyType] = tokenKey.key.contractId; + } + + } + + function _setFungibleTokenInfo(FungibleTokenInfo memory fungibleTokenInfo) internal returns (address treasury) { + address tokenAddress = msg.sender; + treasury = fungibleTokenInfo.tokenInfo.token.treasury; + + _setFungibleTokenInfoToken(tokenAddress, fungibleTokenInfo.tokenInfo.token); + _setFungibleTokenExpiry(tokenAddress, fungibleTokenInfo.tokenInfo.token.expiry); + _setFungibleTokenKeys(tokenAddress, fungibleTokenInfo.tokenInfo.token.tokenKeys); + _setFungibleTokenInfo(tokenAddress, fungibleTokenInfo.tokenInfo); + + _fungibleTokenInfos[tokenAddress].decimals = fungibleTokenInfo.decimals; + } + + function _setNftTokenInfoToken(address token, HederaToken memory hederaToken) internal { + _nftTokenInfos[token].token.name = hederaToken.name; + _nftTokenInfos[token].token.symbol = hederaToken.symbol; + _nftTokenInfos[token].token.treasury = hederaToken.treasury; + _nftTokenInfos[token].token.memo = hederaToken.memo; + _nftTokenInfos[token].token.tokenSupplyType = hederaToken.tokenSupplyType; + _nftTokenInfos[token].token.maxSupply = hederaToken.maxSupply; + _nftTokenInfos[token].token.freezeDefault = hederaToken.freezeDefault; + } + + function _setNftTokenExpiry(address token, Expiry memory expiryInfo) internal { + _nftTokenInfos[token].token.expiry.second = expiryInfo.second; + _nftTokenInfos[token].token.expiry.autoRenewAccount = expiryInfo.autoRenewAccount; + _nftTokenInfos[token].token.expiry.autoRenewPeriod = expiryInfo.autoRenewPeriod; + } + + + function _setNftTokenInfo(address token, TokenInfo memory nftTokenInfo) internal { + _nftTokenInfos[token].totalSupply = nftTokenInfo.totalSupply; + _nftTokenInfos[token].deleted = nftTokenInfo.deleted; + _nftTokenInfos[token].defaultKycStatus = nftTokenInfo.defaultKycStatus; + _nftTokenInfos[token].pauseStatus = nftTokenInfo.pauseStatus; + _nftTokenInfos[token].ledgerId = nftTokenInfo.ledgerId; + + // TODO: Handle copying of other arrays (fixedFees, fractionalFees, and royaltyFees) if needed + } + + function _setNftTokenKeys(address token, TokenKey[] memory tokenKeys) internal { + // Copy the tokenKeys array + uint256 length = tokenKeys.length; + for (uint256 i = 0; i < length; i++) { + TokenKey memory tokenKey = tokenKeys[i]; + _nftTokenInfos[token].token.tokenKeys.push(tokenKey); + + /// @dev contractId can in fact be any address including an EOA address + /// The KeyHelper lists 5 types for KeyValueType; however only CONTRACT_ID is considered + _tokenKeys[token][tokenKey.keyType] = tokenKey.key.contractId; + } + } + + function _setNftTokenInfo(TokenInfo memory nftTokenInfo) internal returns (address treasury) { + address tokenAddress = msg.sender; + treasury = nftTokenInfo.token.treasury; + + _setNftTokenInfoToken(tokenAddress, nftTokenInfo.token); + _setNftTokenKeys(tokenAddress, nftTokenInfo.token.tokenKeys); + _setNftTokenExpiry(tokenAddress, nftTokenInfo.token.expiry); + _setNftTokenInfo(tokenAddress, nftTokenInfo); + } + + // TODO: implement _post{Action} "internal" functions called inside and at the end of the pre{Action} functions is success == true + // for getters implement _get{Data} "view internal" functions that have the exact same name as the HTS getter function name that is called after the precheck + + function _precheckCreateToken( + address sender, + HederaToken memory token, + int64 initialTotalSupply, + int32 decimals + ) internal view returns (int64 responseCode) { + bool validTreasurySig = sender == token.treasury; + + // if admin key is specified require admin sig + KeyValue memory key = _getTokenKey(token.tokenKeys, _getKeyTypeValue(KeyHelper.KeyType.ADMIN)); + + if (key.contractId != ADDRESS_ZERO) { + if (sender != key.contractId) { + return HederaResponseCodes.INVALID_ADMIN_KEY; + } + } + + for (uint256 i = 0; i < token.tokenKeys.length; i++) { + TokenKey memory tokenKey = token.tokenKeys[i]; + + if (tokenKey.key.contractId != ADDRESS_ZERO) { + bool accountExists = _doesAccountExist(tokenKey.key.contractId); + + if (!accountExists) { + + if (tokenKey.keyType == 1) { // KeyType.ADMIN + return HederaResponseCodes.INVALID_ADMIN_KEY; + } + + if (tokenKey.keyType == 2) { // KeyType.KYC + return HederaResponseCodes.INVALID_KYC_KEY; + } + + if (tokenKey.keyType == 4) { // KeyType.FREEZE + return HederaResponseCodes.INVALID_FREEZE_KEY; + } + + if (tokenKey.keyType == 8) { // KeyType.WIPE + return HederaResponseCodes.INVALID_WIPE_KEY; + } + + if (tokenKey.keyType == 16) { // KeyType.SUPPLY + return HederaResponseCodes.INVALID_SUPPLY_KEY; + } + + if (tokenKey.keyType == 32) { // KeyType.FEE + return HederaResponseCodes.INVALID_CUSTOM_FEE_SCHEDULE_KEY; + } + + if (tokenKey.keyType == 64) { // KeyType.PAUSE + return HederaResponseCodes.INVALID_PAUSE_KEY; + } + } + } + } + + // TODO: add additional validation on token; validation most likely required on only tokenKeys(if an address(contract/EOA) has a zero-balance then consider the tokenKey invalid since active accounts on Hedera must have a positive HBAR balance) + if (!validTreasurySig) { + return HederaResponseCodes.AUTHORIZATION_FAILED; + } + + if (decimals < 0 || decimals > 18) { + return HederaResponseCodes.INVALID_TOKEN_DECIMALS; + } + + if (initialTotalSupply < 0) { + return HederaResponseCodes.INVALID_TOKEN_INITIAL_SUPPLY; + } + + uint256 tokenNameLength = _getStringLength(token.name); + uint256 tokenSymbolLength = _getStringLength(token.symbol); + + if (tokenNameLength == 0) { + return HederaResponseCodes.MISSING_TOKEN_NAME; + } + + // TODO: investigate correctness of max length conditionals + // solidity strings use UTF-8 encoding, Hedera restricts the name and symbol to 100 bytes + // in ASCII that is 100 characters + // however in UTF-8 it is 100/4 = 25 UT-8 characters + if (tokenNameLength > 100) { + return HederaResponseCodes.TOKEN_NAME_TOO_LONG; + } + + if (tokenSymbolLength == 0) { + return HederaResponseCodes.MISSING_TOKEN_SYMBOL; + } + + if (tokenSymbolLength > 100) { + return HederaResponseCodes.TOKEN_SYMBOL_TOO_LONG; + } + + return HederaResponseCodes.SUCCESS; + } + + function _precheckDeleteToken(address sender, address token) internal view returns (bool success, int64 responseCode) { + + /// @dev success is initialised to true such that the sequence of any of the validation functions below can be easily rearranged + /// the rearrangement of the functions may be done to more closely align the response codes with the actual response codes returned by Hedera + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (bool validKey, bool noKey) = _hasAdminKeySig(token); + (success, responseCode) = success ? HederaTokenValidation._validateAdminKey(validKey, noKey) : (success, responseCode); + } + + /// @dev handles precheck logic for both freeze and unfreeze + function _precheckFreezeToken(address sender, address token, address account) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (bool validKey, bool noKey) = _hasFreezeKeySig(token); + (success, responseCode) = success ? HederaTokenValidation._validateFreezeKey(validKey, noKey) : (success, responseCode); + } + + /// @dev handles precheck logic for both pause and unpause + function _precheckPauseToken(address sender, address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (bool validKey, bool noKey) = _hasPauseKeySig(token); + (success, responseCode) = success ? HederaTokenValidation._validatePauseKey(validKey, noKey) : (success, responseCode); + } + + /// @dev handles precheck logic for both kyc grant and revoke + function _precheckKyc(address sender, address token, address account) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? _validateKycKey(token) : (success, responseCode); + } + + function _precheckUpdateTokenExpiryInfo(address sender, address token, Expiry memory expiryInfo) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? _validateAdminKey(token) : (success, responseCode); + // TODO: validate expiryInfo; move validation into common HederaTokenValidation contract that exposes validation functions + } + + function _precheckUpdateTokenInfo(address sender, address token, HederaToken memory tokenInfo) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? _validateAdminKey(token) : (success, responseCode); + // TODO: validate tokenInfo; move validation into common HederaTokenValidation contract that exposes validation functions + } + + function _precheckUpdateTokenKeys(address sender, address token, TokenKey[] memory keys) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? _validateAdminKey(token) : (success, responseCode); + // TODO: validate keys; move validation into common HederaTokenValidation contract that exposes validation functions + } + + function _validateAdminKey(address token) internal view returns (bool success, int64 responseCode) { + (bool validKey, bool noKey) = _hasAdminKeySig(token); + (success, responseCode) = HederaTokenValidation._validateAdminKey(validKey, noKey); + } + + function _validateKycKey(address token) internal view returns (bool success, int64 responseCode) { + (bool validKey, bool noKey) = _hasKycKeySig(token); + (success, responseCode) = HederaTokenValidation._validateKycKey(validKey, noKey); + } + + function _validateSupplyKey(address token) internal view returns (bool success, int64 responseCode) { + (bool validKey, bool noKey) = _hasSupplyKeySig(token); + (success, responseCode) = HederaTokenValidation._validateSupplyKey(validKey, noKey); + } + + function _validateFreezeKey(address token) internal view returns (bool success, int64 responseCode) { + (bool validKey, bool noKey) = _hasFreezeKeySig(token); + (success, responseCode) = HederaTokenValidation._validateFreezeKey(validKey, noKey); + } + + function _validateTreasuryKey(address token) internal view returns (bool success, int64 responseCode) { + (bool validKey, bool noKey) = _hasTreasurySig(token); + (success, responseCode) = HederaTokenValidation._validateTreasuryKey(validKey, noKey); + } + + function _validateWipeKey(address token) internal view returns (bool success, int64 responseCode) { + (bool validKey, bool noKey) = _hasWipeKeySig(token); + (success, responseCode) = HederaTokenValidation._validateWipeKey(validKey, noKey); + } + + function _validateAccountKyc(address token, address account) internal view returns (bool success, int64 responseCode) { + bool isKyced; + (responseCode, isKyced) = isKyc(token, account); + success = _doesAccountPassKyc(responseCode, isKyced); + (success, responseCode) = HederaTokenValidation._validateAccountKyc(success); + } + + function _validateAccountUnfrozen(address token, address account) internal view returns (bool success, int64 responseCode) { + bool isAccountFrozen; + (responseCode, isAccountFrozen) = isFrozen(token, account); + success = _doesAccountPassUnfrozen(responseCode, isAccountFrozen); + (success, responseCode) = success ? HederaTokenValidation._validateAccountFrozen(success) : (success, responseCode); + } + + /// @dev the following internal _precheck functions are called in either of the following 2 scenarios: + /// 1. before the HtsSystemContractMock calls any of the HederaFungibleToken or HederaNonFungibleToken functions that specify the onlyHtsPrecompile modifier + /// 2. in any of HtsSystemContractMock functions that specifies the onlyHederaToken modifier which is only callable by a HederaFungibleToken or HederaNonFungibleToken contract + + /// @dev for both Fungible and NonFungible + function _precheckApprove( + address token, + address sender, // sender should be owner in order to approve + address spender, + uint256 amountOrSerialNumber /// for Fungible is the amount and for NonFungible is the serialNumber + ) internal view returns (bool success, int64 responseCode) { + + success = true; + + /// @dev Hedera does not require an account to be associated with a token in be approved an allowance + // if (!_association[token][owner] || !_association[token][spender]) { + // return HederaResponseCodes.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; + // } + + (success, responseCode) = success ? _validateAccountKyc(token, sender) : (success, responseCode); + (success, responseCode) = success ? _validateAccountKyc(token, spender) : (success, responseCode); + + (success, responseCode) = success ? _validateAccountUnfrozen(token, sender) : (success, responseCode); + (success, responseCode) = success ? _validateAccountUnfrozen(token, spender) : (success, responseCode); + + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateNftOwnership(token, sender, amountOrSerialNumber, _isNonFungible, _partialNonFungibleTokenInfos) : (success, responseCode); + } + + function _precheckSetApprovalForAll( + address token, + address owner, + address operator, + bool approved + ) internal view returns (bool success, int64 responseCode) { + + success = true; + + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + + (success, responseCode) = success ? HederaTokenValidation._validateTokenAssociation(token, owner, _association) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateTokenAssociation(token, operator, _association) : (success, responseCode); + + (success, responseCode) = success ? _validateAccountKyc(token, owner) : (success, responseCode); + (success, responseCode) = success ? _validateAccountKyc(token, operator) : (success, responseCode); + + (success, responseCode) = success ? _validateAccountUnfrozen(token, owner) : (success, responseCode); + (success, responseCode) = success ? _validateAccountUnfrozen(token, operator) : (success, responseCode); + + (success, responseCode) = success ? HederaTokenValidation._validateIsNonFungible(token, _isNonFungible) : (success, responseCode); + } + + function _precheckMint( + address token, + int64 amount, + bytes[] memory metadata + ) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? _validateSupplyKey(token) : (success, responseCode); + } + + // TODO: implement multiple NFTs being burnt instead of just index 0 + function _precheckBurn( + address token, + int64 amount, + int64[] memory serialNumbers // since only 1 NFT can be burnt at a time; expect length to be 1 + ) internal view returns (bool success, int64 responseCode) { + success = true; + + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? _validateTreasuryKey(token) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateTokenSufficiency(token, _getTreasuryAccount(token), amount, serialNumbers[0], _isFungible, _isNonFungible, _partialNonFungibleTokenInfos) : (success, responseCode); + } + + // TODO: implement multiple NFTs being wiped, instead of just index 0 + function _precheckWipe( + address sender, + address token, + address account, + int64 amount, + int64[] memory serialNumbers // since only 1 NFT can be wiped at a time; expect length to be 1 + ) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validBurnInput(token, _isFungible, _isNonFungible, amount, serialNumbers) : (success, responseCode); + (success, responseCode) = success ? _validateWipeKey(token) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateTokenSufficiency(token, account, amount, serialNumbers[0], _isFungible, _isNonFungible, _partialNonFungibleTokenInfos) : (success, responseCode); + } + + function _precheckGetApproved( + address token, + uint256 serialNumber + ) internal view returns (bool success, int64 responseCode) { + // TODO: do additional validation that serialNumber exists and is not burnt + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckGetFungibleTokenInfo(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateIsFungible(token, _isFungible) : (success, responseCode); + } + + function _precheckGetNonFungibleTokenInfo(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateIsNonFungible(token, _isNonFungible) : (success, responseCode); + } + + function _precheckGetTokenCustomFees(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckGetTokenDefaultFreezeStatus(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckGetTokenDefaultKycStatus(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckGetTokenExpiryInfo(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckGetTokenInfo(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckGetTokenKey(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckGetTokenType(address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckIsFrozen(address token, address account) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? _validateFreezeKey(token) : (success, responseCode); + } + + function _precheckIsKyc(address token, address account) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? _validateKycKey(token) : (success, responseCode); + } + + function _precheckAllowance( + address token, + address owner, + address spender + ) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + } + + function _precheckAssociateToken(address account, address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + + // TODO: consider extending HederaTokenValidation#_validateTokenAssociation with TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT + if (success) { + if (_association[token][account]) { + return (false, HederaResponseCodes.TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT); + } + } + + } + + function _precheckDissociateToken(address account, address token) internal view returns (bool success, int64 responseCode) { + success = true; + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateTokenAssociation(token, account, _association) : (success, responseCode); + } + + /// @dev doesPassKyc if KYC is not enabled or if enabled then account is KYCed explicitly or by default + function _doesAccountPassKyc(int64 responseCode, bool isKyced) internal pure returns (bool doesPassKyc) { + doesPassKyc = responseCode == HederaResponseCodes.SUCCESS ? isKyced : true; + } + + /// @dev doesPassUnfrozen if freeze is not enabled or if enabled then account is unfrozen explicitly or by default + function _doesAccountPassUnfrozen(int64 responseCode, bool isFrozen) internal pure returns (bool doesPassUnfrozen) { + doesPassUnfrozen = responseCode == HederaResponseCodes.SUCCESS ? !isFrozen : true; + } + + function _precheckTransfer( + address token, + address spender, + address from, + address to, + uint256 amountOrSerialNumber + ) internal view returns (bool success, int64 responseCode, bool isRequestFromOwner) { + + success = true; + + (success, responseCode) = success ? HederaTokenValidation._validateToken(token, _tokenDeleted, _isFungible, _isNonFungible) : (success, responseCode); + + (success, responseCode) = success ? HederaTokenValidation._validateTokenAssociation(token, from, _association) : (success, responseCode); + (success, responseCode) = success ? HederaTokenValidation._validateTokenAssociation(token, to, _association) : (success, responseCode); + + (success, responseCode) = success ? _validateAccountKyc(token, spender) : (success, responseCode); + (success, responseCode) = success ? _validateAccountKyc(token, from) : (success, responseCode); + (success, responseCode) = success ? _validateAccountKyc(token, to) : (success, responseCode); + + (success, responseCode) = success ? _validateAccountUnfrozen(token, spender) : (success, responseCode); + (success, responseCode) = success ? _validateAccountUnfrozen(token, from) : (success, responseCode); + (success, responseCode) = success ? _validateAccountUnfrozen(token, to) : (success, responseCode); + + // If transfer request is not from owner then check allowance of msg.sender + bool shouldAssumeRequestFromOwner = spender == ADDRESS_ZERO; + isRequestFromOwner = _isAccountSender(from) || shouldAssumeRequestFromOwner; + + (success, responseCode) = success ? HederaTokenValidation._validateTokenSufficiency(token, from, amountOrSerialNumber, amountOrSerialNumber, _isFungible, _isNonFungible, _partialNonFungibleTokenInfos) : (success, responseCode); + + if (isRequestFromOwner || !success) { + return (success, responseCode, isRequestFromOwner); + } + + (success, responseCode) = success ? HederaTokenValidation._validateApprovalSufficiency(token, spender, from, amountOrSerialNumber, _isFungible, _isNonFungible) : (success, responseCode); + + return (success, responseCode, isRequestFromOwner); + } + + function _postTransfer( + address token, + address spender, + address from, + address to, + uint256 amountOrSerialNumber + ) internal { + if (_isNonFungible[token]) { + int64 serialNumber = int64(uint64(amountOrSerialNumber)); + _partialNonFungibleTokenInfos[token][serialNumber].ownerId = to; + delete _partialNonFungibleTokenInfos[token][serialNumber].spenderId; + } + } + + function _postApprove( + address token, + address sender, + address spender, + uint256 amountOrSerialNumber + ) internal { + if (_isNonFungible[token]) { + int64 serialNumber = int64(uint64(amountOrSerialNumber)); + _partialNonFungibleTokenInfos[token][serialNumber].spenderId = spender; + } + } + + function _postMint( + address token, + int64 amountOrSerialNumber, + bytes[] memory metadata + ) internal { + if (_isNonFungible[token]) { + _partialNonFungibleTokenInfos[token][amountOrSerialNumber] = PartialNonFungibleTokenInfo({ + ownerId: _getTreasuryAccount(token), + creationTime: int64(int(block.timestamp)), + metadata: _concatenate(metadata), + spenderId: ADDRESS_ZERO + }); + } + } + + function _postBurn( + address token, + int64 amount, + int64[] memory serialNumbers + ) internal { + if (_isNonFungible[token]) { + int64 serialNumber; + uint burnCount = serialNumbers.length; + for (uint256 i = 0; i < burnCount; i++) { + serialNumber = serialNumbers[i]; + delete _partialNonFungibleTokenInfos[token][serialNumber].ownerId; + delete _partialNonFungibleTokenInfos[token][serialNumber].spenderId; + + // TODO: remove the break statement below once multiple NFT burns are enabled in a single call + break; // only delete the info at index 0 since only 1 NFT is burnt at a time + } + } + } + + function preApprove( + address sender, // msg.sender in the context of the Hedera{Non|}FungibleToken; it should be owner for SUCCESS + address spender, + uint256 amountOrSerialNumber /// for Fungible is the amount and for NonFungible is the serialNumber + ) external onlyHederaToken returns (int64 responseCode) { + address token = msg.sender; + bool success; + (success, responseCode) = _precheckApprove(token, sender, spender, amountOrSerialNumber); + if (success) { + _postApprove(token, sender, spender, amountOrSerialNumber); + } + } + + function preSetApprovalForAll( + address sender, // msg.sender in the context of the Hedera{Non|}FungibleToken; it should be owner for SUCCESS + address operator, + bool approved + ) external onlyHederaToken returns (int64 responseCode) { + address token = msg.sender; + bool success; + (success, responseCode) = _precheckSetApprovalForAll(token, sender, operator, approved); + } + + /// @dev not currently called by Hedera{}Token + function preMint( + address token, + int64 amount, + bytes[] memory metadata + ) external onlyHederaToken returns (int64 responseCode) { + address token = msg.sender; + bool success; + (success, responseCode) = _precheckMint(token, amount, metadata); + + if (success) { + + int64 amountOrSerialNumber; + + if (_isFungible[token]) { + amountOrSerialNumber = amount; + } else { + amountOrSerialNumber = HederaNonFungibleToken(token).mintCount() + 1; + } + + _postMint(token, amountOrSerialNumber, metadata); + } + } + + /// @dev not currently called by Hedera{}Token + function preBurn(int64 amount, int64[] memory serialNumbers) external onlyHederaToken returns (int64 responseCode) { + address token = msg.sender; + bool success; + (success, responseCode) = _precheckBurn(token, amount, serialNumbers); + + if (success) { + _postBurn(token, amount, serialNumbers); + } + } + + function preTransfer( + address spender, /// @dev if spender == ADDRESS_ZERO then assume ERC20#transfer(i.e. msg.sender is attempting to spend their balance) otherwise ERC20#transferFrom(i.e. msg.sender is attempting to spend balance of "from" using allowance) + address from, + address to, + uint256 amountOrSerialNumber + ) external onlyHederaToken returns (int64 responseCode) { + address token = msg.sender; + bool success; + (success, responseCode, ) = _precheckTransfer(token, spender, from, to, amountOrSerialNumber); + if (success) { + _postTransfer(token, spender, from, to, amountOrSerialNumber); + } + } + + /// @dev register HederaFungibleToken; msg.sender is the HederaFungibleToken + /// can be called by any contract; however assumes msg.sender is a HederaFungibleToken + function registerHederaFungibleToken(address caller, FungibleTokenInfo memory fungibleTokenInfo) external { + + /// @dev if caller is this contract(i.e. the HtsSystemContractMock) then no need to call _precheckCreateToken since it was already called when the createFungibleToken or other relevant method was called + bool doPrecheck = caller != address(this); + + int64 responseCode = doPrecheck ? _precheckCreateToken(caller, fungibleTokenInfo.tokenInfo.token, fungibleTokenInfo.tokenInfo.totalSupply, fungibleTokenInfo.decimals) : HederaResponseCodes.SUCCESS; + + if (responseCode != HederaResponseCodes.SUCCESS) { + revert("PRECHECK_FAILED"); // TODO: revert with custom error that includes response code + } + + address tokenAddress = msg.sender; + _isFungible[tokenAddress] = true; + address treasury = _setFungibleTokenInfo(fungibleTokenInfo); + associateToken(treasury, tokenAddress); + } + + /// @dev register HederaNonFungibleToken; msg.sender is the HederaNonFungibleToken + /// can be called by any contract; however assumes msg.sender is a HederaNonFungibleToken + function registerHederaNonFungibleToken(address caller, TokenInfo memory nftTokenInfo) external { + + /// @dev if caller is this contract(i.e. the HtsSystemContractMock) then no need to call _precheckCreateToken since it was already called when the createNonFungibleToken or other relevant method was called + bool doPrecheck = caller != address(this); + + int64 responseCode = doPrecheck ? _precheckCreateToken(caller, nftTokenInfo.token, 0, 0) : HederaResponseCodes.SUCCESS; + + if (responseCode != HederaResponseCodes.SUCCESS) { + revert("PRECHECK_FAILED"); // TODO: revert with custom error that includes response code + } + + address tokenAddress = msg.sender; + _isNonFungible[tokenAddress] = true; + address treasury = _setNftTokenInfo(nftTokenInfo); + + associateToken(treasury, tokenAddress); + } + + // IHederaTokenService public/external view functions: + function getApproved( + address token, + uint256 serialNumber + ) external view returns (int64 responseCode, address approved) { + + bool success; + (success, responseCode) = _precheckGetApproved(token, serialNumber); + + if (!success) { + return (responseCode, approved); + } + + // TODO: abstract logic into _get{Data} function + approved = HederaNonFungibleToken(token).getApproved(serialNumber); + } + + function getFungibleTokenInfo( + address token + ) external view returns (int64 responseCode, FungibleTokenInfo memory fungibleTokenInfo) { + + bool success; + (success, responseCode) = _precheckGetFungibleTokenInfo(token); + + if (!success) { + return (responseCode, fungibleTokenInfo); + } + + // TODO: abstract logic into _get{Data} function + fungibleTokenInfo = _fungibleTokenInfos[token]; + } + + function getNonFungibleTokenInfo( + address token, + int64 serialNumber + ) external view returns (int64 responseCode, NonFungibleTokenInfo memory nonFungibleTokenInfo) { + + bool success; + (success, responseCode) = _precheckGetNonFungibleTokenInfo(token); + + if (!success) { + return (responseCode, nonFungibleTokenInfo); + } + + // TODO: abstract logic into _get{Data} function + TokenInfo memory nftTokenInfo = _nftTokenInfos[token]; + PartialNonFungibleTokenInfo memory partialNonFungibleTokenInfo = _partialNonFungibleTokenInfos[token][ + serialNumber + ]; + + nonFungibleTokenInfo.tokenInfo = nftTokenInfo; + + nonFungibleTokenInfo.serialNumber = serialNumber; + + nonFungibleTokenInfo.ownerId = partialNonFungibleTokenInfo.ownerId; + nonFungibleTokenInfo.creationTime = partialNonFungibleTokenInfo.creationTime; + nonFungibleTokenInfo.metadata = partialNonFungibleTokenInfo.metadata; + nonFungibleTokenInfo.spenderId = partialNonFungibleTokenInfo.spenderId; + } + + function getTokenCustomFees( + address token + ) + external + view + returns ( + int64 responseCode, + FixedFee[] memory fixedFees, + FractionalFee[] memory fractionalFees, + RoyaltyFee[] memory royaltyFees + ) + { + + bool success; + (success, responseCode) = _precheckGetTokenCustomFees(token); + + if (!success) { + return (responseCode, fixedFees, fractionalFees, royaltyFees); + } + + // TODO: abstract logic into _get{Data} function + if (_isFungible[token]) { + fixedFees = _fungibleTokenInfos[token].tokenInfo.fixedFees; + fractionalFees = _fungibleTokenInfos[token].tokenInfo.fractionalFees; + royaltyFees = _fungibleTokenInfos[token].tokenInfo.royaltyFees; + } else { + fixedFees = _nftTokenInfos[token].fixedFees; + fractionalFees = _nftTokenInfos[token].fractionalFees; + royaltyFees = _nftTokenInfos[token].royaltyFees; + } + } + + function getTokenDefaultFreezeStatus( + address token + ) external view returns (int64 responseCode, bool defaultFreezeStatus) { + + bool success; + (success, responseCode) = _precheckGetTokenDefaultFreezeStatus(token); + + if (!success) { + return (responseCode, defaultFreezeStatus); + } + + // TODO: abstract logic into _get{Data} function + if (_isFungible[token]) { + defaultFreezeStatus = _fungibleTokenInfos[token].tokenInfo.token.freezeDefault; + } else { + defaultFreezeStatus = _nftTokenInfos[token].token.freezeDefault; + } + } + + function getTokenDefaultKycStatus(address token) external view returns (int64 responseCode, bool defaultKycStatus) { + + bool success; + (success, responseCode) = _precheckGetTokenDefaultKycStatus(token); + + if (!success) { + return (responseCode, defaultKycStatus); + } + + // TODO: abstract logic into _get{Data} function + if (_isFungible[token]) { + defaultKycStatus = _fungibleTokenInfos[token].tokenInfo.defaultKycStatus; + } else { + defaultKycStatus = _nftTokenInfos[token].defaultKycStatus; + } + } + + function getTokenExpiryInfo(address token) external view returns (int64 responseCode, Expiry memory expiry) { + + bool success; + (success, responseCode) = _precheckGetTokenExpiryInfo(token); + + if (!success) { + return (responseCode, expiry); + } + + // TODO: abstract logic into _get{Data} function + if (_isFungible[token]) { + expiry = _fungibleTokenInfos[token].tokenInfo.token.expiry; + } else { + expiry = _nftTokenInfos[token].token.expiry; + } + } + + function getTokenInfo(address token) external view returns (int64 responseCode, TokenInfo memory tokenInfo) { + + bool success; + (success, responseCode) = _precheckGetTokenInfo(token); + + if (!success) { + return (responseCode, tokenInfo); + } + + // TODO: abstract logic into _get{Data} function + if (_isFungible[token]) { + tokenInfo = _fungibleTokenInfos[token].tokenInfo; + } else { + tokenInfo = _nftTokenInfos[token]; + } + } + + function getTokenKey(address token, uint keyType) external view returns (int64 responseCode, KeyValue memory key) { + + bool success; + (success, responseCode) = _precheckGetTokenKey(token); + + if (!success) { + return (responseCode, key); + } + + // TODO: abstract logic into _get{Data} function + /// @dev the key can be retrieved using either of the following methods + // method 1: gas inefficient + // key = _getTokenKey(_fungibleTokenInfos[token].tokenInfo.token.tokenKeys, keyType); + + // method 2: more gas efficient and works for BOTH token types; however currently only considers contractId + address keyValue = _tokenKeys[token][keyType]; + key.contractId = keyValue; + } + + function _getTokenKey(IHederaTokenService.TokenKey[] memory tokenKeys, uint keyType) internal view returns (KeyValue memory key) { + uint256 length = tokenKeys.length; + + for (uint256 i = 0; i < length; i++) { + IHederaTokenService.TokenKey memory tokenKey = tokenKeys[i]; + if (tokenKey.keyType == keyType) { + key = tokenKey.key; + break; + } + } + } + + function getTokenType(address token) external view returns (int64 responseCode, int32 tokenType) { + + bool success; + (success, responseCode) = _precheckGetTokenType(token); + + if (!success) { + return (responseCode, tokenType); + } + + // TODO: abstract logic into _get{Data} function + bool isFungibleToken = _isFungible[token]; + bool isNonFungibleToken = _isNonFungible[token]; + tokenType = isFungibleToken ? int32(0) : int32(1); + } + + function grantTokenKyc(address token, address account) external returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckKyc(msg.sender, token, account); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + _kyc[token][account].explicit = true; + _kyc[token][account].value = true; + } + + /// @dev Applicable ONLY to NFT Tokens; accessible via IERC721 + function isApprovedForAll( + address token, + address owner, + address operator + ) external view returns (int64 responseCode, bool approved) {} + + function isFrozen(address token, address account) public view returns (int64 responseCode, bool frozen) { + + bool success = true; + (success, responseCode) = _precheckIsFrozen(token, account); + + if (!success) { + return (responseCode, frozen); + } + + bool isFungible = _isFungible[token]; + bool isNonFungible = _isNonFungible[token]; + // TODO: abstract logic into _isFrozen function + bool freezeDefault; + if (isFungible) { + FungibleTokenInfo memory fungibleTokenInfo = _fungibleTokenInfos[token]; + freezeDefault = fungibleTokenInfo.tokenInfo.token.freezeDefault; + } else { + TokenInfo memory nftTokenInfo = _nftTokenInfos[token]; + freezeDefault = nftTokenInfo.token.freezeDefault; + } + + TokenConfig memory unfrozenConfig = _unfrozen[token][account]; + + /// @dev if unfrozenConfig.explicit is false && freezeDefault is true then an account must explicitly be unfrozen otherwise assume unfrozen + frozen = unfrozenConfig.explicit ? !(unfrozenConfig.value) : (freezeDefault ? !(unfrozenConfig.value) : false); + } + + function isKyc(address token, address account) public view returns (int64 responseCode, bool kycGranted) { + + bool success; + (success, responseCode) = _precheckIsKyc(token, account); + + if (!success) { + return (responseCode, kycGranted); + } + + // TODO: abstract logic into _isKyc function + bool isFungible = _isFungible[token]; + bool isNonFungible = _isNonFungible[token]; + bool defaultKycStatus; + if (isFungible) { + FungibleTokenInfo memory fungibleTokenInfo = _fungibleTokenInfos[token]; + defaultKycStatus = fungibleTokenInfo.tokenInfo.defaultKycStatus; + } else { + TokenInfo memory nftTokenInfo = _nftTokenInfos[token]; + defaultKycStatus = nftTokenInfo.defaultKycStatus; + } + + TokenConfig memory kycConfig = _kyc[token][account]; + + /// @dev if kycConfig.explicit is false && defaultKycStatus is true then an account must explicitly be KYCed otherwise assume KYCed + kycGranted = kycConfig.explicit ? kycConfig.value : (defaultKycStatus ? kycConfig.value : true); + } + + function isToken(address token) public view returns (int64 responseCode, bool isToken) { + isToken = _isToken(token); + responseCode = isToken ? HederaResponseCodes.SUCCESS : HederaResponseCodes.INVALID_TOKEN_ID; + } + + function allowance( + address token, + address owner, + address spender + ) public view returns (int64 responseCode, uint256 allowance) { + + bool success; + (success, responseCode) = _precheckAllowance(token, owner, spender); + + if (!success) { + return (responseCode, allowance); + } + + // TODO: abstract logic into _allowance function + allowance = HederaFungibleToken(token).allowance(owner, spender); + } + + // Additional(not in IHederaTokenService) public/external view functions: + /// @dev KeyHelper.KeyType is an enum; whereas KeyHelper.keyTypes is a mapping that maps the enum index to a uint256 + /// keyTypes[KeyType.ADMIN] = 1; + /// keyTypes[KeyType.KYC] = 2; + /// keyTypes[KeyType.FREEZE] = 4; + /// keyTypes[KeyType.WIPE] = 8; + /// keyTypes[KeyType.SUPPLY] = 16; + /// keyTypes[KeyType.FEE] = 32; + /// keyTypes[KeyType.PAUSE] = 64; + /// i.e. the relation is 2^(uint(KeyHelper.KeyType)) = keyType + function _getKey(address token, KeyHelper.KeyType keyType) internal view returns (address keyOwner) { + /// @dev the following relation is used due to the below described issue with KeyHelper.getKeyType + uint _keyType = _getKeyTypeValue(keyType); + /// @dev the following does not work since the KeyHelper has all of its storage/state cleared/defaulted once vm.etch is used + /// to fix this KeyHelper should expose a function that does what it's constructor does i.e. initialise the keyTypes mapping + // uint _keyType = getKeyType(keyType); + keyOwner = _tokenKeys[token][_keyType]; + } + + // TODO: move into a common util contract as it's used elsewhere + function _getKeyTypeValue(KeyHelper.KeyType keyType) internal pure returns (uint256 keyTypeValue) { + keyTypeValue = 2 ** uint(keyType); + } + + function _getBalance(address account) internal view returns (uint256 balance) { + balance = account.balance; + } + + // TODO: validate account exists wherever applicable; transfers, mints, burns, etc + // is account(either an EOA or contract) has a non-zero balance then assume it exists + function _doesAccountExist(address account) internal view returns (bool exists) { + exists = _getBalance(account) > 0; + } + + // IHederaTokenService public/external state-changing functions: + function createFungibleToken( + HederaToken memory token, + int64 initialTotalSupply, + int32 decimals + ) external payable noDelegateCall returns (int64 responseCode, address tokenAddress) { + responseCode = _precheckCreateToken(msg.sender, token, initialTotalSupply, decimals); + if (responseCode != HederaResponseCodes.SUCCESS) { + return (responseCode, ADDRESS_ZERO); + } + + FungibleTokenInfo memory fungibleTokenInfo; + TokenInfo memory tokenInfo; + + tokenInfo.token = token; + tokenInfo.totalSupply = initialTotalSupply; + + fungibleTokenInfo.decimals = decimals; + fungibleTokenInfo.tokenInfo = tokenInfo; + + /// @dev no need to register newly created HederaFungibleToken in this context as the constructor will call HtsSystemContractMock#registerHederaFungibleToken + HederaFungibleToken hederaFungibleToken = new HederaFungibleToken(fungibleTokenInfo); + return (HederaResponseCodes.SUCCESS, address(hederaFungibleToken)); + } + + function createNonFungibleToken( + HederaToken memory token + ) external payable noDelegateCall returns (int64 responseCode, address tokenAddress) { + responseCode = _precheckCreateToken(msg.sender, token, 0, 0); + if (responseCode != HederaResponseCodes.SUCCESS) { + return (responseCode, ADDRESS_ZERO); + } + + TokenInfo memory tokenInfo; + tokenInfo.token = token; + + /// @dev no need to register newly created HederaNonFungibleToken in this context as the constructor will call HtsSystemContractMock#registerHederaNonFungibleToken + HederaNonFungibleToken hederaNonFungibleToken = new HederaNonFungibleToken(tokenInfo); + return (HederaResponseCodes.SUCCESS, address(hederaNonFungibleToken)); + } + + // TODO: implement logic that considers fixedFees, fractionalFees where applicable such as on transfers + function createFungibleTokenWithCustomFees( + HederaToken memory token, + int64 initialTotalSupply, + int32 decimals, + FixedFee[] memory fixedFees, + FractionalFee[] memory fractionalFees + ) external payable noDelegateCall returns (int64 responseCode, address tokenAddress) { + responseCode = _precheckCreateToken(msg.sender, token, initialTotalSupply, decimals); + if (responseCode != HederaResponseCodes.SUCCESS) { + return (responseCode, ADDRESS_ZERO); + } + + FungibleTokenInfo memory fungibleTokenInfo; + TokenInfo memory tokenInfo; + + tokenInfo.token = token; + tokenInfo.totalSupply = initialTotalSupply; + tokenInfo.fixedFees = fixedFees; + tokenInfo.fractionalFees = fractionalFees; + + fungibleTokenInfo.decimals = decimals; + fungibleTokenInfo.tokenInfo = tokenInfo; + + /// @dev no need to register newly created HederaFungibleToken in this context as the constructor will call HtsSystemContractMock#registerHederaFungibleToken + HederaFungibleToken hederaFungibleToken = new HederaFungibleToken(fungibleTokenInfo); + return (HederaResponseCodes.SUCCESS, address(hederaFungibleToken)); + } + + // TODO: implement logic that considers fixedFees, royaltyFees where applicable such as on transfers + function createNonFungibleTokenWithCustomFees( + HederaToken memory token, + FixedFee[] memory fixedFees, + RoyaltyFee[] memory royaltyFees + ) external payable noDelegateCall returns (int64 responseCode, address tokenAddress) { + responseCode = _precheckCreateToken(msg.sender, token, 0, 0); + if (responseCode != HederaResponseCodes.SUCCESS) { + return (responseCode, ADDRESS_ZERO); + } + + TokenInfo memory tokenInfo; + tokenInfo.token = token; + tokenInfo.fixedFees = fixedFees; + tokenInfo.royaltyFees = royaltyFees; + + /// @dev no need to register newly created HederaNonFungibleToken in this context as the constructor will call HtsSystemContractMock#registerHederaNonFungibleToken + HederaNonFungibleToken hederaNonFungibleToken = new HederaNonFungibleToken(tokenInfo); + return (HederaResponseCodes.SUCCESS, address(hederaNonFungibleToken)); + } + + // TODO + function cryptoTransfer( + TransferList memory transferList, + TokenTransferList[] memory tokenTransfers + ) external noDelegateCall returns (int64 responseCode) {} + + function deleteToken(address token) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckDeleteToken(msg.sender, token); + + if (!success) { + return responseCode; + } + + _tokenDeleted[token] = true; + } + + function approve( + address token, + address spender, + uint256 amount + ) external noDelegateCall returns (int64 responseCode) { + address owner = msg.sender; + bool success; + (success, responseCode) = _precheckApprove(token, owner, spender, amount); // _precheckApprove works for BOTH token types + + if (!success) { + return responseCode; + } + + _postApprove(token, owner, spender, amount); + HederaFungibleToken(token).approveRequestFromHtsPrecompile(owner, spender, amount); + } + + function approveNFT( + address token, + address approved, + uint256 serialNumber + ) external noDelegateCall returns (int64 responseCode) { + address owner = msg.sender; + address spender = approved; + int64 _serialNumber = int64(int(serialNumber)); + bool success; + (success, responseCode) = _precheckApprove(token, owner, spender, serialNumber); // _precheckApprove works for BOTH token types + + if (!success) { + return responseCode; + } + + _postApprove(token, owner, spender, serialNumber); + HederaNonFungibleToken(token).approveRequestFromHtsPrecompile(spender, _serialNumber); + } + + function associateToken(address account, address token) public noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckAssociateToken(account, token); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + _association[token][account] = true; + } + + function associateTokens( + address account, + address[] memory tokens + ) external noDelegateCall returns (int64 responseCode) { + for (uint256 i = 0; i < tokens.length; i++) { + responseCode = associateToken(account, tokens[i]); + if (responseCode != HederaResponseCodes.SUCCESS) { + return responseCode; + } + } + } + + function dissociateTokens( + address account, + address[] memory tokens + ) external noDelegateCall returns (int64 responseCode) { + for (uint256 i = 0; i < tokens.length; i++) { + int64 responseCode = dissociateToken(account, tokens[i]); + if (responseCode != HederaResponseCodes.SUCCESS) { + return responseCode; + } + } + } + + function dissociateToken(address account, address token) public noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckDissociateToken(account, token); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + _association[token][account] = false; + } + + function freezeToken(address token, address account) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckFreezeToken(msg.sender, token, account); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + _unfrozen[token][account].explicit = true; + _unfrozen[token][account].value = false; + } + + function mintToken( + address token, + int64 amount, + bytes[] memory metadata + ) external noDelegateCall returns (int64 responseCode, int64 newTotalSupply, int64[] memory serialNumbers) { + bool success; + (success, responseCode) = _precheckMint(token, amount, metadata); + + if (!success) { + return (responseCode, 0, new int64[](0)); + } + + int64 amountOrSerialNumber; + + if (_isFungible[token]) { + amountOrSerialNumber = amount; + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(token); + hederaFungibleToken.mintRequestFromHtsPrecompile(amount); + newTotalSupply = int64(int(hederaFungibleToken.totalSupply())); + } + + if (_isNonFungible[token]) { + serialNumbers = new int64[](1); // since you can only mint 1 NFT at a time + int64 serialNumber; + (newTotalSupply, serialNumber) = HederaNonFungibleToken(token).mintRequestFromHtsPrecompile(metadata); + serialNumbers[0] = serialNumber; + amountOrSerialNumber = serialNumber; + } + + _postMint(token, amountOrSerialNumber, metadata); + return (responseCode, newTotalSupply, serialNumbers); + } + + function burnToken( + address token, + int64 amount, + int64[] memory serialNumbers + ) external noDelegateCall returns (int64 responseCode, int64 newTotalSupply) { + bool success; + (success, responseCode) = _precheckBurn(token, amount, serialNumbers); + + if (!success) { + return (responseCode, 0); + } + + // TODO: abstract logic into _post{Action} function + if (_isFungible[token]) { + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(token); + hederaFungibleToken.burnRequestFromHtsPrecompile(amount); + newTotalSupply = int64(int(hederaFungibleToken.totalSupply())); + } + + if (_isNonFungible[token]) { // this conditional is redundant but added for code readibility + newTotalSupply = HederaNonFungibleToken(token).burnRequestFromHtsPrecompile(serialNumbers); + } + + _postBurn(token, amount, serialNumbers); + } + + function pauseToken(address token) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckPauseToken(msg.sender, token); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + _tokenPaused[token].explicit = true; + _tokenPaused[token].value = true; + } + + function revokeTokenKyc(address token, address account) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckKyc(msg.sender, token, account); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + _kyc[token][account].explicit = true; + _kyc[token][account].value = false; + } + + function setApprovalForAll( + address token, + address operator, + bool approved + ) external noDelegateCall returns (int64 responseCode) { + address owner = msg.sender; + bool success; + (success, responseCode) = _precheckSetApprovalForAll(token, owner, operator, approved); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + HederaNonFungibleToken(token).setApprovalForAllFromHtsPrecompile(owner, operator, approved); + } + + function transferFrom( + address token, + address from, + address to, + uint256 amount + ) external noDelegateCall returns (int64 responseCode) { + /// @dev spender is set to non-zero address such that shouldAssumeRequestFromOwner always evaluates to false if HtsSystemContractMock#transferFrom is called + address spender = msg.sender; + bool isRequestFromOwner; + + bool success; + (success, responseCode, isRequestFromOwner) = _precheckTransfer(token, spender, from, to, amount); + + if (!success) { + return responseCode; + } + + _postTransfer(token, spender, from, to, amount); + responseCode = HederaFungibleToken(token).transferRequestFromHtsPrecompile( + isRequestFromOwner, + spender, + from, + to, + amount + ); + } + + function transferFromNFT( + address token, + address from, + address to, + uint256 serialNumber + ) external noDelegateCall returns (int64 responseCode) { + address spender = msg.sender; + bool isRequestFromOwner; + + bool success; + (success, responseCode, isRequestFromOwner) = _precheckTransfer(token, spender, from, to, serialNumber); + + if (!success) { + return responseCode; + } + + _postTransfer(token, spender, from, to, serialNumber); + HederaNonFungibleToken(token).transferRequestFromHtsPrecompile( + isRequestFromOwner, + spender, + from, + to, + serialNumber + ); + } + + /// TODO implementation is currently identical to transferFromNFT; investigate the differences between the 2 functions + function transferNFT( + address token, + address sender, + address recipient, + int64 serialNumber + ) public noDelegateCall returns (int64 responseCode) { + address spender = msg.sender; + uint256 _serialNumber = uint64(serialNumber); + bool isRequestFromOwner; + + bool success; + (success, responseCode, isRequestFromOwner) = _precheckTransfer(token, spender, sender, recipient, _serialNumber); + + if (!success) { + return responseCode; + } + + _postTransfer(token, spender, sender, recipient, _serialNumber); + responseCode = HederaNonFungibleToken(token).transferRequestFromHtsPrecompile( + isRequestFromOwner, + spender, + sender, + recipient, + _serialNumber + ); + } + + function transferNFTs( + address token, + address[] memory sender, + address[] memory receiver, + int64[] memory serialNumber + ) external noDelegateCall returns (int64 responseCode) { + uint length = sender.length; + uint receiverCount = receiver.length; + uint serialNumberCount = serialNumber.length; + + require(length == receiverCount && length == serialNumberCount, 'UNEQUAL_ARRAYS'); + + address _sender; + address _receiver; + int64 _serialNumber; + + for (uint256 i = 0; i < length; i++) { + _sender = sender[i]; + _receiver = receiver[i]; + _serialNumber = serialNumber[i]; + + responseCode = transferNFT(token, _sender, _receiver, _serialNumber); + + // TODO: instead of reverting return responseCode; this will require prechecks on each individual transfer before enacting the transfer of all NFTs + // alternatively consider reverting but catch error and extract responseCode from the error and return the responseCode + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + } + } + + /// TODO implementation is currently identical to transferFrom; investigate the differences between the 2 functions + function transferToken( + address token, + address sender, + address recipient, + int64 amount + ) public noDelegateCall returns (int64 responseCode) { + address spender = msg.sender; + bool isRequestFromOwner; + uint _amount = uint(int(amount)); + + bool success; + (success, responseCode, isRequestFromOwner) = _precheckTransfer(token, spender, sender, recipient, _amount); + + if (!success) { + return responseCode; + } + + _postTransfer(token, spender, sender, recipient, _amount); + responseCode = HederaFungibleToken(token).transferRequestFromHtsPrecompile( + isRequestFromOwner, + spender, + sender, + recipient, + _amount + ); + } + + function transferTokens( + address token, + address[] memory accountId, + int64[] memory amount + ) external noDelegateCall returns (int64 responseCode) { + uint length = accountId.length; + uint amountCount = amount.length; + + require(length == amountCount, 'UNEQUAL_ARRAYS'); + + address spender = msg.sender; + address receiver; + int64 _amount; + + for (uint256 i = 0; i < length; i++) { + receiver = accountId[i]; + _amount = amount[i]; + + responseCode = transferToken(token, spender, receiver, _amount); + + // TODO: instead of reverting return responseCode; this will require prechecks on each individual transfer before enacting the transfer of all NFTs + // alternatively consider reverting but catch error and extract responseCode from the error and return the responseCode + if (responseCode != HederaResponseCodes.SUCCESS) { + revert HtsPrecompileError(responseCode); + } + } + } + + function unfreezeToken(address token, address account) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckFreezeToken(msg.sender, token, account); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + _unfrozen[token][account].explicit = true; + _unfrozen[token][account].value = true; + } + + function unpauseToken(address token) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckPauseToken(msg.sender, token); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + _tokenPaused[token].explicit = true; + _tokenPaused[token].value = false; + } + + function updateTokenExpiryInfo( + address token, + Expiry memory expiryInfo + ) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckUpdateTokenExpiryInfo(msg.sender, token, expiryInfo); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + if (_isFungible[token]) { + _setFungibleTokenExpiry(token, expiryInfo); + } + + if (_isNonFungible[token]) { + _setNftTokenExpiry(token, expiryInfo); + } + } + + function updateTokenInfo( + address token, + HederaToken memory tokenInfo + ) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckUpdateTokenInfo(msg.sender, token, tokenInfo); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + if (_isFungible[token]) { + _setFungibleTokenInfoToken(token, tokenInfo); + } + + if (_isNonFungible[token]) { + _setNftTokenInfoToken(token, tokenInfo); + } + } + + function updateTokenKeys( + address token, + TokenKey[] memory keys + ) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckUpdateTokenKeys(msg.sender, token, keys); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + if (_isFungible[token]) { + _setFungibleTokenKeys(token, keys); + } + + if (_isNonFungible[token]) { + _setNftTokenKeys(token, keys); + } + + } + + function wipeTokenAccount( + address token, + address account, + int64 amount + ) external noDelegateCall returns (int64 responseCode) { + + int64[] memory nullArray; + + bool success; + (success, responseCode) = _precheckWipe(msg.sender, token, account, amount, nullArray); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(token); + hederaFungibleToken.wipeRequestFromHtsPrecompile(account, amount); + } + + function wipeTokenAccountNFT( + address token, + address account, + int64[] memory serialNumbers + ) external noDelegateCall returns (int64 responseCode) { + + bool success; + (success, responseCode) = _precheckWipe(msg.sender, token, account, 0, serialNumbers); + + if (!success) { + return responseCode; + } + + // TODO: abstract logic into _post{Action} function + int64 serialNumber; + uint burnCount = serialNumbers.length; + for (uint256 i = 0; i < burnCount; i++) { + serialNumber = serialNumbers[i]; + delete _partialNonFungibleTokenInfos[token][serialNumber].ownerId; + delete _partialNonFungibleTokenInfos[token][serialNumber].spenderId; + } + } + + // TODO + function redirectForToken(address token, bytes memory encodedFunctionSelector) external noDelegateCall override returns (int64 responseCode, bytes memory response) {} + + // Additional(not in IHederaTokenService) public/external state-changing functions: + function isAssociated(address account, address token) external view returns (bool associated) { + associated = _association[token][account]; + } + + function getTreasuryAccount(address token) external view returns (address treasury) { + return _getTreasuryAccount(token); + } + + function _getStringLength(string memory _string) internal pure returns (uint length) { + length = bytes(_string).length; + } +} diff --git a/test/foundry/mocks/interfaces/IHtsPrecompileMock.sol b/test/foundry/mocks/interfaces/IHtsPrecompileMock.sol new file mode 100644 index 000000000..a33bfa24f --- /dev/null +++ b/test/foundry/mocks/interfaces/IHtsPrecompileMock.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '../../../../contracts/hts-precompile/IHederaTokenService.sol'; + +interface IHtsPrecompileMock is IHederaTokenService { + + struct TokenConfig { + bool explicit; // true if it was explicitly set to value + bool value; + } + + // this struct avoids duplicating common NFT data, in particular IHederaTokenService.NonFungibleTokenInfo.tokenInfo + struct PartialNonFungibleTokenInfo { + address ownerId; + int64 creationTime; + bytes metadata; + address spenderId; + } +} diff --git a/test/foundry/mocks/libraries/HederaTokenValidation.sol b/test/foundry/mocks/libraries/HederaTokenValidation.sol new file mode 100644 index 000000000..bd5bc7674 --- /dev/null +++ b/test/foundry/mocks/libraries/HederaTokenValidation.sol @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '../../../../contracts/hts-precompile/HederaResponseCodes.sol'; +import '../hts-precompile/HederaFungibleToken.sol'; +import '../hts-precompile/HederaNonFungibleToken.sol'; +import '../interfaces/IHtsPrecompileMock.sol'; + +library HederaTokenValidation { + + /// checks if token exists and has not been deleted and returns appropriate response code + function _validateToken( + address token, + mapping(address => bool) storage _tokenDeleted, + mapping(address => bool) storage _isFungible, + mapping(address => bool) storage _isNonFungible + ) internal view returns (bool success, int64 responseCode) { + + if (_tokenDeleted[token]) { + return (false, HederaResponseCodes.TOKEN_WAS_DELETED); + } + + if (!_isFungible[token] && !_isNonFungible[token]) { + return (false, HederaResponseCodes.INVALID_TOKEN_ID); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateIsFungible( + address token, + mapping(address => bool) storage _isFungible + ) internal view returns (bool success, int64 responseCode) { + + if (!_isFungible[token]) { + return (false, HederaResponseCodes.INVALID_TOKEN_ID); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateIsNonFungible( + address token, + mapping(address => bool) storage _isNonFungible + ) internal view returns (bool success, int64 responseCode) { + if (!_isNonFungible[token]) { + return (false, HederaResponseCodes.INVALID_TOKEN_ID); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateAdminKey(bool validKey, bool noKey) internal pure returns (bool success, int64 responseCode) { + if (noKey) { + return (false, HederaResponseCodes.TOKEN_IS_IMMUTABLE); + } + + if (!validKey) { + return (false, HederaResponseCodes.INVALID_ADMIN_KEY); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateFreezeKey(bool validKey, bool noKey) internal pure returns (bool success, int64 responseCode) { + + if (noKey) { + return (false, HederaResponseCodes.TOKEN_HAS_NO_FREEZE_KEY); + } + + if (!validKey) { + return (false, HederaResponseCodes.INVALID_FREEZE_KEY); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validatePauseKey(bool validKey, bool noKey) internal pure returns (bool success, int64 responseCode) { + if (noKey) { + return (false, HederaResponseCodes.TOKEN_HAS_NO_PAUSE_KEY); + } + + if (!validKey) { + return (false, HederaResponseCodes.INVALID_PAUSE_KEY); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateKycKey(bool validKey, bool noKey) internal pure returns (bool success, int64 responseCode) { + if (noKey) { + return (false, HederaResponseCodes.TOKEN_HAS_NO_KYC_KEY); + } + + if (!validKey) { + return (false, HederaResponseCodes.INVALID_KYC_KEY); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateSupplyKey(bool validKey, bool noKey) internal pure returns (bool success, int64 responseCode) { + if (noKey) { + return (false, HederaResponseCodes.TOKEN_HAS_NO_SUPPLY_KEY); + } + + if (!validKey) { + return (false, HederaResponseCodes.INVALID_SUPPLY_KEY); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateTreasuryKey(bool validKey, bool noKey) internal pure returns (bool success, int64 responseCode) { + if (noKey) { + return (false, HederaResponseCodes.AUTHORIZATION_FAILED); + } + + if (!validKey) { + return (false, HederaResponseCodes.AUTHORIZATION_FAILED); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateWipeKey(bool validKey, bool noKey) internal pure returns (bool success, int64 responseCode) { + if (noKey) { + return (false, HederaResponseCodes.TOKEN_HAS_NO_WIPE_KEY); + } + + if (!validKey) { + return (false, HederaResponseCodes.INVALID_WIPE_KEY); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateAccountKyc(bool kycPass) internal pure returns (bool success, int64 responseCode) { + + if (!kycPass) { + return (false, HederaResponseCodes.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + + } + + function _validateAccountFrozen(bool frozenPass) internal pure returns (bool success, int64 responseCode) { + + if (!frozenPass) { + return (false, HederaResponseCodes.ACCOUNT_FROZEN_FOR_TOKEN); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + + } + + function _validateNftOwnership( + address token, + address expectedOwner, + uint serialNumber, + mapping(address => bool) storage _isNonFungible, + mapping(address => mapping(int64 => IHtsPrecompileMock.PartialNonFungibleTokenInfo)) storage _partialNonFungibleTokenInfos + ) internal view returns (bool success, int64 responseCode) { + if (_isNonFungible[token]) { + int64 _serialNumber = int64(uint64(serialNumber)); + IHtsPrecompileMock.PartialNonFungibleTokenInfo memory partialNonFungibleTokenInfo = _partialNonFungibleTokenInfos[token][_serialNumber]; + + if (partialNonFungibleTokenInfo.ownerId != expectedOwner) { + return (false, HederaResponseCodes.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO); + } + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateFungibleBalance( + address token, + address owner, + uint amount, + mapping(address => bool) storage _isFungible + ) internal view returns (bool success, int64 responseCode) { + if (_isFungible[token]) { + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(token); + + bool sufficientBalance = hederaFungibleToken.balanceOf(owner) >= uint64(amount); + + if (!sufficientBalance) { + return (false, HederaResponseCodes.INSUFFICIENT_TOKEN_BALANCE); + } + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateTokenSufficiency( + address token, + address owner, + int64 amount, + int64 serialNumber, + mapping(address => bool) storage _isFungible, + mapping(address => bool) storage _isNonFungible, + mapping(address => mapping(int64 => IHtsPrecompileMock.PartialNonFungibleTokenInfo)) storage _partialNonFungibleTokenInfos + ) internal view returns (bool success, int64 responseCode) { + + uint256 amountU256 = uint64(amount); + uint256 serialNumberU256 = uint64(serialNumber); + return _validateTokenSufficiency(token, owner, amountU256, serialNumberU256, _isFungible, _isNonFungible, _partialNonFungibleTokenInfos); + } + + function _validateTokenSufficiency( + address token, + address owner, + uint256 amount, + uint256 serialNumber, + mapping(address => bool) storage _isFungible, + mapping(address => bool) storage _isNonFungible, + mapping(address => mapping(int64 => IHtsPrecompileMock.PartialNonFungibleTokenInfo)) storage _partialNonFungibleTokenInfos + ) internal view returns (bool success, int64 responseCode) { + + if (_isFungible[token]) { + return _validateFungibleBalance(token, owner, amount, _isFungible); + } + + if (_isNonFungible[token]) { + return _validateNftOwnership(token, owner, serialNumber, _isNonFungible, _partialNonFungibleTokenInfos); + } + } + + function _validateFungibleApproval( + address token, + address spender, + address from, + uint256 amount, + mapping(address => bool) storage _isFungible + ) internal view returns (bool success, int64 responseCode) { + if (_isFungible[token]) { + + uint256 allowance = HederaFungibleToken(token).allowance(from, spender); + + // TODO: do validation for other allowance response codes such as SPENDER_DOES_NOT_HAVE_ALLOWANCE and MAX_ALLOWANCES_EXCEEDED + if (allowance < amount) { + return (false, HederaResponseCodes.AMOUNT_EXCEEDS_ALLOWANCE); + } + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateNftApproval( + address token, + address spender, + uint256 serialNumber, + mapping(address => bool) storage _isNonFungible + ) internal view returns (bool success, int64 responseCode) { + + if (_isNonFungible[token]) { + bool canSpendToken = HederaNonFungibleToken(token).isApprovedOrOwner(spender, serialNumber); + if (!canSpendToken) { + return (false, HederaResponseCodes.INSUFFICIENT_ACCOUNT_BALANCE); + } + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateApprovalSufficiency( + address token, + address spender, + address from, + uint256 amountOrSerialNumber, + mapping(address => bool) storage _isFungible, + mapping(address => bool) storage _isNonFungible + ) internal view returns (bool success, int64 responseCode) { + + if (_isFungible[token]) { + return _validateFungibleApproval(token, spender, from, amountOrSerialNumber, _isFungible); + } + + if (_isNonFungible[token]) { + return _validateNftApproval(token, spender, amountOrSerialNumber, _isNonFungible); + } + } + + function _validBurnInput( + address token, + mapping(address => bool) storage _isFungible, + mapping(address => bool) storage _isNonFungible, + int64 amount, + int64[] memory serialNumbers + ) internal view returns (bool success, int64 responseCode) { + + if (_isFungible[token] && serialNumbers.length > 0) { + return (false, HederaResponseCodes.INVALID_TOKEN_ID); + } + + if (_isNonFungible[token] && amount > 0) { + return (false, HederaResponseCodes.INVALID_TOKEN_ID); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } + + function _validateTokenAssociation( + address token, + address account, + mapping(address => mapping(address => bool)) storage _association + ) internal view returns (bool success, int64 responseCode) { + if (!_association[token][account]) { + return (false, HederaResponseCodes.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT); + } + + success = true; + responseCode = HederaResponseCodes.SUCCESS; + } +} diff --git a/test/foundry/mocks/util-precompile/UtilPrecompileMock.sol b/test/foundry/mocks/util-precompile/UtilPrecompileMock.sol new file mode 100644 index 000000000..0df2d4ab0 --- /dev/null +++ b/test/foundry/mocks/util-precompile/UtilPrecompileMock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '../../../../contracts/util-precompile/IPrngSystemContract.sol'; + +contract UtilPrecompileMock is IPrngSystemContract { + + address internal constant UTIL_PRECOMPILE = address(0x169); + + bytes32 internal lastSeed; // to increase pseudorandomness by feeding in the previous seed into latest seed + + function getPseudorandomSeed() external returns (bytes32) { + lastSeed = keccak256(abi.encodePacked(lastSeed, block.timestamp, block.number, msg.sender)); + return lastSeed; + } +} diff --git a/test/foundry/utils/CommonUtils.sol b/test/foundry/utils/CommonUtils.sol new file mode 100644 index 000000000..c65b586c4 --- /dev/null +++ b/test/foundry/utils/CommonUtils.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import 'forge-std/Test.sol'; + +import '../../../contracts/hts-precompile/KeyHelper.sol'; + +/// generic test utils +abstract contract CommonUtils is Test, KeyHelper { + + address internal alice = vm.addr(1); + address internal bob = vm.addr(2); + address internal carol = vm.addr(3); + address internal dave = vm.addr(4); + + uint256 public constant NUM_OF_ACCOUNTS = 4; + + modifier setPranker(address pranker) { + vm.startPrank(pranker); + _; + vm.stopPrank(); + } + + function _setUpAccounts() internal { + vm.deal(alice, 100 ether); + vm.deal(bob, 100 ether); + vm.deal(carol, 100 ether); + vm.deal(dave, 100 ether); + } + + function _getAccount(uint index) internal returns (address) { + if (index == 0) { + return alice; + } + if (index == 1) { + return bob; + } + if (index == 2) { + return carol; + } + + return dave; // return dave by default + } + + function _getKeyTypeValue(KeyHelper.KeyType keyType) internal pure returns (uint256 keyTypeValue) { + keyTypeValue = 2 ** uint(keyType); + } + +} diff --git a/test/foundry/utils/ExchangeRateUtils.sol b/test/foundry/utils/ExchangeRateUtils.sol new file mode 100644 index 000000000..9524cd868 --- /dev/null +++ b/test/foundry/utils/ExchangeRateUtils.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import 'forge-std/Test.sol'; + +import '../mocks/exchange-rate-precompile/ExchangeRatePrecompileMock.sol'; +import './CommonUtils.sol'; +import '../../../contracts/libraries/Constants.sol'; + +/// for testing actions of the exchange rate precompiled/system contract +abstract contract ExchangeRateUtils is Test, CommonUtils, Constants { + + ExchangeRatePrecompileMock exchangeRatePrecompile = ExchangeRatePrecompileMock(EXCHANGE_RATE_PRECOMPILE); + + function _setUpExchangeRatePrecompileMock() internal { + ExchangeRatePrecompileMock exchangeRatePrecompileMock = new ExchangeRatePrecompileMock(); + bytes memory code = address(exchangeRatePrecompileMock).code; + vm.etch(EXCHANGE_RATE_PRECOMPILE, code); + _doUpdateRate(1e7); + } + + function _doConvertTinycentsToTinybars(uint256 tinycents) internal returns (uint256 tinybars) { + + tinybars = exchangeRatePrecompile.tinycentsToTinybars(tinycents); + + } + + function _doConvertTinybarsToTinycents(uint256 tinybars) internal returns (uint256 tinycents) { + + tinycents = exchangeRatePrecompile.tinybarsToTinycents(tinybars); + + } + + function _doUpdateRate(uint256 newRate) internal { + + exchangeRatePrecompile.updateRate(newRate); + + } + +} diff --git a/test/foundry/utils/HederaFungibleTokenUtils.sol b/test/foundry/utils/HederaFungibleTokenUtils.sol new file mode 100644 index 000000000..95e721fa2 --- /dev/null +++ b/test/foundry/utils/HederaFungibleTokenUtils.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '../../../contracts/hts-precompile/IHederaTokenService.sol'; +import '../../../contracts/hts-precompile/HederaResponseCodes.sol'; + +import '../mocks/hts-precompile/HederaFungibleToken.sol'; + +import "./CommonUtils.sol"; +import "./HederaTokenUtils.sol"; + +contract HederaFungibleTokenUtils is CommonUtils, HederaTokenUtils { + + function _getSimpleHederaFungibleTokenInfo( + string memory name, + string memory symbol, + address treasury, + int64 initialTotalSupply, + int32 decimals + ) internal returns (IHederaTokenService.FungibleTokenInfo memory fungibleTokenInfo) { + IHederaTokenService.TokenInfo memory tokenInfo; + + IHederaTokenService.HederaToken memory token = _getSimpleHederaToken(name, symbol, treasury); + + tokenInfo.token = token; + tokenInfo.totalSupply = initialTotalSupply; + + fungibleTokenInfo.decimals = decimals; + fungibleTokenInfo.tokenInfo = tokenInfo; + } + + function _doCreateHederaFungibleTokenViaHtsPrecompile( + address sender, + string memory name, + string memory symbol, + address treasury, + int64 initialTotalSupply, + int32 decimals + ) internal setPranker(sender) returns (address tokenAddress) { + bool isToken; + assertTrue(isToken == false); + IHederaTokenService.HederaToken memory token = _getSimpleHederaToken(name, symbol, treasury); + + int64 responseCode; + (responseCode, tokenAddress) = htsPrecompile.createFungibleToken(token, initialTotalSupply, decimals); + + int32 tokenType; + (, isToken) = htsPrecompile.isToken(tokenAddress); + (responseCode, tokenType) = htsPrecompile.getTokenType(tokenAddress); + + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(tokenAddress); + + assertEq(responseCode, HederaResponseCodes.SUCCESS, 'Failed to createFungibleToken'); + + assertEq(responseCode, HederaResponseCodes.SUCCESS, 'Did not set is{}Token correctly'); + assertEq(tokenType, 0, 'Did not set isFungible correctly'); + + assertEq(uint64(initialTotalSupply), hederaFungibleToken.totalSupply(), 'Did not set initial supply correctly'); + assertEq(token.name, hederaFungibleToken.name(), 'Did not set name correctly'); + assertEq(token.symbol, hederaFungibleToken.symbol(), 'Did not set symbol correctly'); + assertEq( + hederaFungibleToken.totalSupply(), + hederaFungibleToken.balanceOf(token.treasury), + 'Did not mint initial supply to treasury' + ); + } + + function _doCreateHederaFungibleTokenDirectly( + address sender, + string memory name, + string memory symbol, + address treasury, + int64 initialTotalSupply, + int32 decimals, + IHederaTokenService.TokenKey[] memory keys + ) internal setPranker(sender) returns (address tokenAddress) { + IHederaTokenService.FungibleTokenInfo memory fungibleTokenInfo = _getSimpleHederaFungibleTokenInfo( + name, + symbol, + sender, + initialTotalSupply, + decimals + ); + + fungibleTokenInfo.tokenInfo.token.tokenKeys = keys; + + IHederaTokenService.HederaToken memory token = fungibleTokenInfo.tokenInfo.token; + + /// @dev no need to register newly created HederaFungibleToken in this context as the constructor will call HtsSystemContractMock#registerHederaFungibleToken + HederaFungibleToken hederaFungibleToken = new HederaFungibleToken(fungibleTokenInfo); + tokenAddress = address(hederaFungibleToken); + + (int64 responseCode, int32 tokenType) = htsPrecompile.getTokenType(tokenAddress); + + assertEq(responseCode, HederaResponseCodes.SUCCESS, 'Did not set is{}Token correctly'); + assertEq(tokenType, 0, 'Did not set isFungible correctly'); + + assertEq(uint64(initialTotalSupply), hederaFungibleToken.totalSupply(), 'Did not set initial supply correctly'); + assertEq(token.name, hederaFungibleToken.name(), 'Did not set name correctly'); + assertEq(token.symbol, hederaFungibleToken.symbol(), 'Did not set symbol correctly'); + assertEq( + hederaFungibleToken.totalSupply(), + hederaFungibleToken.balanceOf(token.treasury), + 'Did not mint initial supply to treasury' + ); + } + + function _createSimpleMockFungibleToken( + address sender, + IHederaTokenService.TokenKey[] memory keys + ) internal returns (address tokenAddress) { + string memory name = 'Token A'; + string memory symbol = 'TA'; + address treasury = sender; + int64 initialTotalSupply = 1e16; + int32 decimals = 8; + + tokenAddress = _doCreateHederaFungibleTokenDirectly( + sender, + name, + symbol, + treasury, + initialTotalSupply, + decimals, + keys + ); + } + + function _doApproveViaHtsPrecompile( + address sender, + address token, + address spender, + uint allowance + ) internal setPranker(sender) returns (bool success) { + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(token); + uint spenderStartingAllowance = hederaFungibleToken.allowance(sender, spender); + int64 responseCode = htsPrecompile.approve(token, spender, allowance); + assertEq( + responseCode, + HederaResponseCodes.SUCCESS, + "expected spender to be given token allowance to sender's account" + ); + + uint spenderFinalAllowance = hederaFungibleToken.allowance(sender, spender); + + assertEq(spenderFinalAllowance, allowance, "spender's expected allowance not set correctly"); + } + + function _doApproveDirectly( + address sender, + address token, + address spender, + uint allowance + ) internal setPranker(sender) returns (bool success) { + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(token); + uint spenderStartingAllowance = hederaFungibleToken.allowance(sender, spender); + success = hederaFungibleToken.approve(spender, allowance); + assertEq(success, true, 'expected successful approval'); + uint spenderFinalAllowance = hederaFungibleToken.allowance(sender, spender); + assertEq(spenderFinalAllowance, allowance, "spender's expected allowance not set correctly"); + } +} diff --git a/test/foundry/utils/HederaNonFungibleTokenUtils.sol b/test/foundry/utils/HederaNonFungibleTokenUtils.sol new file mode 100644 index 000000000..a47d91f96 --- /dev/null +++ b/test/foundry/utils/HederaNonFungibleTokenUtils.sol @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import '../../../contracts/hts-precompile/IHederaTokenService.sol'; +import '../../../contracts/hts-precompile/HederaResponseCodes.sol'; + +import '../mocks/hts-precompile/HederaFungibleToken.sol'; + +import "./CommonUtils.sol"; +import "./HederaTokenUtils.sol"; + +contract HederaNonFungibleTokenUtils is CommonUtils, HederaTokenUtils { + + function _getSimpleHederaNftTokenInfo( + string memory name, + string memory symbol, + address treasury + ) internal returns (IHederaTokenService.TokenInfo memory tokenInfo) { + IHederaTokenService.HederaToken memory token = _getSimpleHederaToken(name, symbol, treasury); + tokenInfo.token = token; + } + + function _doCreateHederaNonFungibleTokenViaHtsPrecompile( + address sender, + string memory name, + string memory symbol, + address treasury + ) internal setPranker(sender) returns (bool success, address tokenAddress) { + + int64 expectedResponseCode = HederaResponseCodes.SUCCESS; + int64 responseCode; + + if (sender != treasury) { + expectedResponseCode = HederaResponseCodes.AUTHORIZATION_FAILED; + } + + IHederaTokenService.HederaToken memory token = _getSimpleHederaToken(name, symbol, treasury); + (responseCode, tokenAddress) = htsPrecompile.createNonFungibleToken(token); + + assertEq(expectedResponseCode, responseCode, "response code does not equal expected response code"); + + success = responseCode == HederaResponseCodes.SUCCESS; + + if (success) { + int32 tokenType; + bool isToken; + (, isToken) = htsPrecompile.isToken(tokenAddress); + (responseCode, tokenType) = htsPrecompile.getTokenType(tokenAddress); + + HederaNonFungibleToken hederaNonFungibleToken = HederaNonFungibleToken(tokenAddress); + + assertEq(responseCode, HederaResponseCodes.SUCCESS, 'Failed to createNonFungibleToken'); + + assertEq(responseCode, HederaResponseCodes.SUCCESS, 'Did not set is{}Token correctly'); + assertEq(tokenType, 1, 'Did not set isNonFungible correctly'); + + assertEq(token.name, hederaNonFungibleToken.name(), 'Did not set name correctly'); + assertEq(token.symbol, hederaNonFungibleToken.symbol(), 'Did not set symbol correctly'); + assertEq( + hederaNonFungibleToken.totalSupply(), + hederaNonFungibleToken.balanceOf(token.treasury), + 'Did not mint initial supply to treasury' + ); + } + + } + + function _doCreateHederaNonFungibleTokenDirectly( + address sender, + string memory name, + string memory symbol, + address treasury, + IHederaTokenService.TokenKey[] memory keys + ) internal setPranker(sender) returns (bool success, address tokenAddress) { + + int64 expectedResponseCode = HederaResponseCodes.SUCCESS; + int64 responseCode; + + IHederaTokenService.TokenInfo memory nftTokenInfo = _getSimpleHederaNftTokenInfo( + name, + symbol, + treasury + ); + + nftTokenInfo.token.tokenKeys = keys; + + IHederaTokenService.HederaToken memory token = nftTokenInfo.token; + + if (sender != treasury) { + expectedResponseCode = HederaResponseCodes.AUTHORIZATION_FAILED; + } + + if (expectedResponseCode != HederaResponseCodes.SUCCESS) { + vm.expectRevert(bytes("PRECHECK_FAILED")); + } + + /// @dev no need to register newly created HederaNonFungibleToken in this context as the constructor will call HtsSystemContractMock#registerHederaNonFungibleToken + HederaNonFungibleToken hederaNonFungibleToken = new HederaNonFungibleToken(nftTokenInfo); + + if (expectedResponseCode == HederaResponseCodes.SUCCESS) { + success = true; + } + + if (success) { + + tokenAddress = address(hederaNonFungibleToken); + + (int64 responseCode, int32 tokenType) = htsPrecompile.getTokenType(tokenAddress); + + assertEq(responseCode, HederaResponseCodes.SUCCESS, 'Did not set is{}Token correctly'); + assertEq(tokenType, 1, 'Did not set isNonFungible correctly'); + + assertEq(token.name, hederaNonFungibleToken.name(), 'Did not set name correctly'); + assertEq(token.symbol, hederaNonFungibleToken.symbol(), 'Did not set symbol correctly'); + assertEq( + hederaNonFungibleToken.totalSupply(), + hederaNonFungibleToken.balanceOf(token.treasury), + 'Did not mint initial supply to treasury' + ); + + } + + } + + function _createSimpleMockNonFungibleToken( + address sender, + IHederaTokenService.TokenKey[] memory keys + ) internal returns (address tokenAddress) { + + string memory name = 'NFT A'; + string memory symbol = 'NFT-A'; + address treasury = sender; + + (, tokenAddress) = _doCreateHederaNonFungibleTokenDirectly(sender, name, symbol, treasury, keys); + } + + struct ApproveNftParams { + address sender; + address token; + address spender; + int64 serialId; + } + + struct ApproveNftInfo { + address owner; + address spender; + uint256 serialIdU256; + } + + function _doApproveNftViaHtsPrecompile(ApproveNftParams memory approveNftParams) internal setPranker(approveNftParams.sender) returns (bool success) { + + int64 expectedResponseCode = HederaResponseCodes.SUCCESS; + int64 responseCode; + + ApproveNftInfo memory approveNftInfo; + + HederaNonFungibleToken hederaNonFungibleToken = HederaNonFungibleToken(approveNftParams.token); + + approveNftInfo.serialIdU256 = uint64(approveNftParams.serialId); + approveNftInfo.owner = hederaNonFungibleToken.ownerOf(approveNftInfo.serialIdU256); + + if (approveNftParams.sender != approveNftInfo.owner) { + expectedResponseCode = HederaResponseCodes.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO; + } + + responseCode = htsPrecompile.approveNFT(approveNftParams.token, approveNftParams.spender, approveNftInfo.serialIdU256); + + assertEq(responseCode, expectedResponseCode, "expected response code does not equal actual response code"); + + success = responseCode == HederaResponseCodes.SUCCESS; + + approveNftInfo.spender = hederaNonFungibleToken.getApproved(approveNftInfo.serialIdU256); + + if (success) { + assertEq(approveNftInfo.spender, approveNftParams.spender, "spender was not correctly updated"); + } + + } + + function _doApproveNftDirectly(ApproveNftParams memory approveNftParams) internal setPranker(approveNftParams.sender) returns (bool success) { + + int64 expectedResponseCode = HederaResponseCodes.SUCCESS; + int64 responseCode; + + ApproveNftInfo memory approveNftInfo; + + HederaNonFungibleToken hederaNonFungibleToken = HederaNonFungibleToken(approveNftParams.token); + + approveNftInfo.serialIdU256 = uint64(approveNftParams.serialId); + approveNftInfo.owner = hederaNonFungibleToken.ownerOf(approveNftInfo.serialIdU256); + + if (approveNftParams.sender != approveNftInfo.owner) { + expectedResponseCode = HederaResponseCodes.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO; + } + + if (expectedResponseCode != HederaResponseCodes.SUCCESS) { + vm.expectRevert( + abi.encodeWithSelector( + HederaFungibleToken.HtsPrecompileError.selector, + expectedResponseCode + ) + ); + } + + hederaNonFungibleToken.approve(approveNftParams.spender, approveNftInfo.serialIdU256); + + if (expectedResponseCode == HederaResponseCodes.SUCCESS) { + success = true; + } + + approveNftInfo.spender = hederaNonFungibleToken.getApproved(approveNftInfo.serialIdU256); + + if (success) { + assertEq(approveNftInfo.spender, approveNftParams.spender, "spender was not correctly updated"); + } + + } + +} diff --git a/test/foundry/utils/HederaTokenUtils.sol b/test/foundry/utils/HederaTokenUtils.sol new file mode 100644 index 000000000..31d26e2cb --- /dev/null +++ b/test/foundry/utils/HederaTokenUtils.sol @@ -0,0 +1,667 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import 'forge-std/Test.sol'; + +import '../mocks/hts-precompile/HtsSystemContractMock.sol'; +import '../../../contracts/hts-precompile/IHederaTokenService.sol'; +import './CommonUtils.sol'; + +/// for testing actions common to both HTS token types i.e FUNGIBLE and NON_FUNGIBLE +/// also has common constants for both HTS token types +abstract contract HederaTokenUtils is Test, CommonUtils, Constants { + + HtsSystemContractMock htsPrecompile = HtsSystemContractMock(HTS_PRECOMPILE); + + function _setUpHtsPrecompileMock() internal { + HtsSystemContractMock htsPrecompileMock = new HtsSystemContractMock(); + bytes memory code = address(htsPrecompileMock).code; + vm.etch(HTS_PRECOMPILE, code); + } + + function _getSimpleHederaToken( + string memory name, + string memory symbol, + address treasury + ) internal returns (IHederaTokenService.HederaToken memory token) { + token.name = name; + token.symbol = symbol; + token.treasury = treasury; + } + + function _doAssociateViaHtsPrecompile( + address sender, + address token + ) internal setPranker(sender) returns (bool success) { + bool isInitiallyAssociated = htsPrecompile.isAssociated(sender, token); + int64 responseCode = htsPrecompile.associateToken(sender, token); + success = responseCode == HederaResponseCodes.SUCCESS; + + int64 expectedResponseCode; + + if (isInitiallyAssociated) { + expectedResponseCode = HederaResponseCodes.TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT; + } + + if (!isInitiallyAssociated) { + expectedResponseCode = HederaResponseCodes.SUCCESS; + } + + bool isFinallyAssociated = htsPrecompile.isAssociated(sender, token); + + assertEq(responseCode, expectedResponseCode, 'expected response code does not match actual response code'); + assertEq(isFinallyAssociated, true, 'expected account to always be finally associated'); + } + + struct MintKeys { + address supplyKey; + address treasury; + } + + struct MintInfo { + uint256 totalSupply; + uint256 treasuryBalance; + bool isFungible; + bool isNonFungible; + uint256 mintAmountU256; + int64 mintCount; + } + + struct MintParams { + address sender; + address token; + int64 mintAmount; + } + + struct MintResponse { + bool success; + int64 responseCode; + int64 serialId; + } + + function _doMintViaHtsPrecompile(MintParams memory mintParams) internal setPranker(mintParams.sender) returns (MintResponse memory mintResponse) { + + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(mintParams.token); + HederaNonFungibleToken hederaNonFungibleToken = HederaNonFungibleToken(mintParams.token); + + bytes[] memory NULL_BYTES = new bytes[](1); + + int64 newTotalSupply; + int64[] memory serialNumbers; + int32 tokenType; + + int64 expectedResponseCode = HederaResponseCodes.SUCCESS; // assume SUCCESS initially and later overwrite error code accordingly + + IHederaTokenService.KeyValue memory supplyKey; + + (, supplyKey) = htsPrecompile.getTokenKey(mintParams.token, _getKeyTypeValue(KeyHelper.KeyType.SUPPLY)); + + MintKeys memory mintKeys = MintKeys({ + supplyKey: supplyKey.contractId, + treasury: htsPrecompile.getTreasuryAccount(mintParams.token) + }); + + (mintResponse.responseCode, tokenType) = htsPrecompile.getTokenType(mintParams.token); + + mintResponse.success = mintResponse.responseCode == HederaResponseCodes.SUCCESS; + + if (tokenType == 1) { + /// @dev since you can only mint one NFT at a time; also mintAmount is ONLY applicable to type FUNGIBLE + mintParams.mintAmount = 1; + } + + MintInfo memory preMintInfo = MintInfo({ + totalSupply: mintResponse.success ? (tokenType == 0 ? hederaFungibleToken.totalSupply() : hederaNonFungibleToken.totalSupply()) : 0, + treasuryBalance: mintResponse.success ? (tokenType == 0 ? hederaFungibleToken.balanceOf(mintKeys.treasury) : hederaNonFungibleToken.totalSupply()) : 0, + isFungible: tokenType == 0 ? true : false, + isNonFungible: tokenType == 1 ? true : false, + mintAmountU256: uint64(mintParams.mintAmount), + mintCount: tokenType == 1 ? hederaNonFungibleToken.mintCount() : int64(0) + }); + + if (mintKeys.supplyKey != mintParams.sender) { + expectedResponseCode = HederaResponseCodes.INVALID_SUPPLY_KEY; + } + + if (mintKeys.supplyKey == ADDRESS_ZERO) { + expectedResponseCode = HederaResponseCodes.TOKEN_HAS_NO_SUPPLY_KEY; + } + + (mintResponse.responseCode, newTotalSupply, serialNumbers) = htsPrecompile.mintToken(mintParams.token, mintParams.mintAmount, NULL_BYTES); + + assertEq(expectedResponseCode, mintResponse.responseCode, 'expected response code does not equal actual response code'); + + mintResponse.success = mintResponse.responseCode == HederaResponseCodes.SUCCESS; + + MintInfo memory postMintInfo = MintInfo({ + totalSupply: tokenType == 0 ? hederaFungibleToken.totalSupply() : hederaNonFungibleToken.totalSupply(), + treasuryBalance: tokenType == 0 ? hederaFungibleToken.balanceOf(mintKeys.treasury) : hederaNonFungibleToken.totalSupply(), + isFungible: tokenType == 0 ? true : false, + isNonFungible: tokenType == 1 ? true : false, + mintAmountU256: uint64(mintParams.mintAmount), + mintCount: tokenType == 1 ? hederaNonFungibleToken.mintCount() : int64(0) + }); + + if (mintResponse.success) { + + assertEq( + postMintInfo.totalSupply, + uint64(newTotalSupply), + 'expected newTotalSupply to equal post mint totalSupply' + ); + + if (preMintInfo.isFungible) { + + assertEq( + preMintInfo.totalSupply + preMintInfo.mintAmountU256, + postMintInfo.totalSupply, + 'expected total supply to increase by mint amount' + ); + assertEq( + preMintInfo.treasuryBalance + preMintInfo.mintAmountU256, + postMintInfo.treasuryBalance, + 'expected treasury balance to increase by mint amount' + ); + } + + if (preMintInfo.isNonFungible) { + assertEq( + preMintInfo.totalSupply + 1, + postMintInfo.totalSupply, + 'expected total supply to increase by mint amount' + ); + assertEq( + preMintInfo.treasuryBalance + 1, + postMintInfo.treasuryBalance, + 'expected treasury balance to increase by mint amount' + ); + + assertEq(preMintInfo.mintCount + 1, postMintInfo.mintCount, "expected mintCount to increase by 1"); + assertEq(serialNumbers[0], postMintInfo.mintCount, "expected minted serialNumber to equal mintCount"); + + mintResponse.serialId = serialNumbers[0]; + } + } + + if (!mintResponse.success) { + assertEq( + preMintInfo.totalSupply, + postMintInfo.totalSupply, + 'expected total supply to not change if failed' + ); + assertEq( + preMintInfo.treasuryBalance, + postMintInfo.treasuryBalance, + 'expected treasury balance to not change if failed' + ); + } + } + + struct TransferParams { + address sender; + address token; + address from; + address to; + uint256 amountOrSerialNumber; // amount for FUNGIBLE serialNumber for NON_FUNGIBLE + } + + struct TransferInfo { + // applicable to FUNGIBLE + uint256 spenderAllowance; + uint256 fromBalance; + uint256 toBalance; + // applicable to NON_FUNGIBLE + address owner; + address approvedId; + bool isSenderOperator; + } + + struct TransferChecks { + bool isRecipientAssociated; + bool isRequestFromOwner; + int64 expectedResponseCode; + bool isToken; + int32 tokenType; + bool isFungible; + bool isNonFungible; + } + + function _doTransferViaHtsPrecompile( + TransferParams memory transferParams + ) internal setPranker(transferParams.sender) returns (bool success, int64 responseCode) { + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(transferParams.token); + HederaNonFungibleToken hederaNonFungibleToken = HederaNonFungibleToken(transferParams.token); + + TransferChecks memory transferChecks; + TransferInfo memory preTransferInfo; + + transferChecks.expectedResponseCode = HederaResponseCodes.SUCCESS; // assume SUCCESS and overwrite with !SUCCESS where applicable + + (transferChecks.expectedResponseCode, transferChecks.tokenType) = htsPrecompile.getTokenType(transferParams.token); + + if (transferChecks.expectedResponseCode == HederaResponseCodes.SUCCESS) { + transferChecks.isFungible = transferChecks.tokenType == 0 ? true : false; + transferChecks.isNonFungible = transferChecks.tokenType == 1 ? true : false; + } + + transferChecks.isRecipientAssociated = htsPrecompile.isAssociated(transferParams.to, transferParams.token); + transferChecks.isRequestFromOwner = transferParams.sender == transferParams.from; + + if (transferChecks.isFungible) { + preTransferInfo.spenderAllowance = hederaFungibleToken.allowance(transferParams.from, transferParams.sender); + preTransferInfo.fromBalance = hederaFungibleToken.balanceOf(transferParams.from); + preTransferInfo.toBalance = hederaFungibleToken.balanceOf(transferParams.to); + } + + if (transferChecks.isNonFungible) { + preTransferInfo.owner = hederaNonFungibleToken.ownerOf(transferParams.amountOrSerialNumber); + preTransferInfo.approvedId = hederaNonFungibleToken.getApproved(transferParams.amountOrSerialNumber); + preTransferInfo.isSenderOperator = hederaNonFungibleToken.isApprovedForAll(transferParams.from, transferParams.sender); + } + + if (transferChecks.isRequestFromOwner) { + if (transferChecks.isFungible) { + if (preTransferInfo.fromBalance < transferParams.amountOrSerialNumber) { + transferChecks.expectedResponseCode = HederaResponseCodes.INSUFFICIENT_TOKEN_BALANCE; + } + } + + if (transferChecks.isNonFungible) { + if (preTransferInfo.owner != transferParams.sender) { + transferChecks.expectedResponseCode = HederaResponseCodes.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO; + } + } + } + + if (!transferChecks.isRequestFromOwner) { + if (transferChecks.isFungible) { + if (preTransferInfo.spenderAllowance < transferParams.amountOrSerialNumber) { + transferChecks.expectedResponseCode = HederaResponseCodes.AMOUNT_EXCEEDS_ALLOWANCE; + } + } + + if (transferChecks.isNonFungible) { + + if (preTransferInfo.owner != transferParams.from) { + transferChecks.expectedResponseCode = HederaResponseCodes.INVALID_ALLOWANCE_OWNER_ID; + } + + if (preTransferInfo.approvedId != transferParams.sender && !preTransferInfo.isSenderOperator) { + transferChecks.expectedResponseCode = HederaResponseCodes.SPENDER_DOES_NOT_HAVE_ALLOWANCE; + } + } + } + + if (!transferChecks.isRecipientAssociated) { + transferChecks.expectedResponseCode = HederaResponseCodes.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; + } + + responseCode = htsPrecompile.transferFrom( + transferParams.token, + transferParams.from, + transferParams.to, + transferParams.amountOrSerialNumber + ); + + assertEq( + transferChecks.expectedResponseCode, + responseCode, + 'expected response code does not equal actual response code' + ); + + success = responseCode == HederaResponseCodes.SUCCESS; + + TransferInfo memory postTransferInfo; + + if (transferChecks.isFungible) { + postTransferInfo.spenderAllowance = hederaFungibleToken.allowance(transferParams.from, transferParams.sender); + postTransferInfo.fromBalance = hederaFungibleToken.balanceOf(transferParams.from); + postTransferInfo.toBalance = hederaFungibleToken.balanceOf(transferParams.to); + } + + if (transferChecks.isNonFungible) { + postTransferInfo.owner = hederaNonFungibleToken.ownerOf(transferParams.amountOrSerialNumber); + postTransferInfo.approvedId = hederaNonFungibleToken.getApproved(transferParams.amountOrSerialNumber); + postTransferInfo.isSenderOperator = hederaNonFungibleToken.isApprovedForAll(transferParams.from, transferParams.sender); + } + + if (success) { + + if (transferChecks.isFungible) { + assertEq( + preTransferInfo.toBalance + transferParams.amountOrSerialNumber, + postTransferInfo.toBalance, + 'to balance did not update correctly' + ); + assertEq( + preTransferInfo.fromBalance - transferParams.amountOrSerialNumber, + postTransferInfo.fromBalance, + 'from balance did not update correctly' + ); + + if (!transferChecks.isRequestFromOwner) { + assertEq( + preTransferInfo.spenderAllowance - transferParams.amountOrSerialNumber, + postTransferInfo.spenderAllowance, + 'spender allowance did not update correctly' + ); + } + } + + if (transferChecks.isNonFungible) { + assertEq(postTransferInfo.owner, transferParams.to, "expected to to be new owner"); + assertEq(postTransferInfo.approvedId, ADDRESS_ZERO, "expected approvedId to be reset"); + assertEq(postTransferInfo.isSenderOperator, preTransferInfo.isSenderOperator, "operator should not have changed"); + } + } + + if (!success) { + + if (transferChecks.isFungible) { + assertEq(preTransferInfo.toBalance, postTransferInfo.toBalance, 'to balance changed unexpectedly'); + assertEq(preTransferInfo.fromBalance, postTransferInfo.fromBalance, 'from balance changed unexpectedly'); + + if (!transferChecks.isRequestFromOwner) { + assertEq( + preTransferInfo.spenderAllowance, + postTransferInfo.spenderAllowance, + 'spender allowance changed unexpectedly' + ); + } + } + + if (transferChecks.isNonFungible) { + assertEq(preTransferInfo.owner, postTransferInfo.owner, 'owner should not have changed on failure'); + assertEq(preTransferInfo.approvedId, postTransferInfo.approvedId, 'approvedId should not have changed on failure'); + assertEq(preTransferInfo.isSenderOperator, postTransferInfo.isSenderOperator, 'isSenderOperator should not have changed on failure'); + } + } + } + + function _doTransferDirectly( + TransferParams memory transferParams + ) internal setPranker(transferParams.sender) returns (bool success, int64 responseCode) { + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(transferParams.token); + HederaNonFungibleToken hederaNonFungibleToken = HederaNonFungibleToken(transferParams.token); + + TransferChecks memory transferChecks; + TransferInfo memory preTransferInfo; + + transferChecks.expectedResponseCode = HederaResponseCodes.SUCCESS; // assume SUCCESS and overwrite with !SUCCESS where applicable + + (transferChecks.expectedResponseCode, transferChecks.tokenType) = htsPrecompile.getTokenType(transferParams.token); + + if (transferChecks.expectedResponseCode == HederaResponseCodes.SUCCESS) { + transferChecks.isFungible = transferChecks.tokenType == 0 ? true : false; + transferChecks.isNonFungible = transferChecks.tokenType == 1 ? true : false; + } + + transferChecks.isRecipientAssociated = htsPrecompile.isAssociated(transferParams.to, transferParams.token); + transferChecks.isRequestFromOwner = transferParams.sender == transferParams.from; + + if (transferChecks.isFungible) { + preTransferInfo.spenderAllowance = hederaFungibleToken.allowance(transferParams.from, transferParams.sender); + preTransferInfo.fromBalance = hederaFungibleToken.balanceOf(transferParams.from); + preTransferInfo.toBalance = hederaFungibleToken.balanceOf(transferParams.to); + } + + if (transferChecks.isNonFungible) { + preTransferInfo.owner = hederaNonFungibleToken.ownerOf(transferParams.amountOrSerialNumber); + preTransferInfo.approvedId = hederaNonFungibleToken.getApproved(transferParams.amountOrSerialNumber); + preTransferInfo.isSenderOperator = hederaNonFungibleToken.isApprovedForAll(transferParams.from, transferParams.sender); + } + + if (transferChecks.isRequestFromOwner) { + if (transferChecks.isFungible) { + if (preTransferInfo.fromBalance < transferParams.amountOrSerialNumber) { + transferChecks.expectedResponseCode = HederaResponseCodes.INSUFFICIENT_TOKEN_BALANCE; + } + } + + if (transferChecks.isNonFungible) { + if (preTransferInfo.owner != transferParams.sender) { + transferChecks.expectedResponseCode = HederaResponseCodes.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO; + } + } + } + + if (!transferChecks.isRequestFromOwner) { + if (transferChecks.isFungible) { + if (preTransferInfo.spenderAllowance < transferParams.amountOrSerialNumber) { + transferChecks.expectedResponseCode = HederaResponseCodes.AMOUNT_EXCEEDS_ALLOWANCE; + } + } + + if (transferChecks.isNonFungible) { + + if (preTransferInfo.owner != transferParams.from) { + transferChecks.expectedResponseCode = HederaResponseCodes.INVALID_ALLOWANCE_OWNER_ID; + } + + if (preTransferInfo.approvedId != transferParams.sender && !preTransferInfo.isSenderOperator) { + transferChecks.expectedResponseCode = HederaResponseCodes.SPENDER_DOES_NOT_HAVE_ALLOWANCE; + } + } + } + + if (!transferChecks.isRecipientAssociated) { + transferChecks.expectedResponseCode = HederaResponseCodes.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; + } + + if (transferChecks.expectedResponseCode != HederaResponseCodes.SUCCESS) { + vm.expectRevert( + abi.encodeWithSelector( + HederaFungibleToken.HtsPrecompileError.selector, + transferChecks.expectedResponseCode + ) + ); + } + + if (transferChecks.isRequestFromOwner) { + if (transferChecks.isFungible) { + hederaFungibleToken.transfer(transferParams.to, transferParams.amountOrSerialNumber); + } + if (transferChecks.isNonFungible) { + hederaNonFungibleToken.transferFrom(transferParams.from, transferParams.to, transferParams.amountOrSerialNumber); + } + } + + if (!transferChecks.isRequestFromOwner) { + if (transferChecks.isFungible) { + hederaFungibleToken.transferFrom(transferParams.from, transferParams.to, transferParams.amountOrSerialNumber); + } + if (transferChecks.isNonFungible) { + hederaNonFungibleToken.transferFrom(transferParams.from, transferParams.to, transferParams.amountOrSerialNumber); + } + } + + if (transferChecks.expectedResponseCode == HederaResponseCodes.SUCCESS) { + success = true; + } + + TransferInfo memory postTransferInfo; + + if (transferChecks.isFungible) { + postTransferInfo.spenderAllowance = hederaFungibleToken.allowance(transferParams.from, transferParams.sender); + postTransferInfo.fromBalance = hederaFungibleToken.balanceOf(transferParams.from); + postTransferInfo.toBalance = hederaFungibleToken.balanceOf(transferParams.to); + } + + if (transferChecks.isNonFungible) { + postTransferInfo.owner = hederaNonFungibleToken.ownerOf(transferParams.amountOrSerialNumber); + postTransferInfo.approvedId = hederaNonFungibleToken.getApproved(transferParams.amountOrSerialNumber); + postTransferInfo.isSenderOperator = hederaNonFungibleToken.isApprovedForAll(transferParams.from, transferParams.sender); + } + + if (success) { + if (transferChecks.isFungible) { + assertEq( + preTransferInfo.toBalance + transferParams.amountOrSerialNumber, + postTransferInfo.toBalance, + 'to balance did not update correctly' + ); + assertEq( + preTransferInfo.fromBalance - transferParams.amountOrSerialNumber, + postTransferInfo.fromBalance, + 'from balance did not update correctly' + ); + + if (!transferChecks.isRequestFromOwner) { + assertEq( + preTransferInfo.spenderAllowance - transferParams.amountOrSerialNumber, + postTransferInfo.spenderAllowance, + 'spender allowance did not update correctly' + ); + } + } + + if (transferChecks.isNonFungible) { + assertEq(postTransferInfo.owner, transferParams.to, "expected to to be new owner"); + assertEq(postTransferInfo.approvedId, ADDRESS_ZERO, "expected approvedId to be reset"); + assertEq(postTransferInfo.isSenderOperator, preTransferInfo.isSenderOperator, "operator should not have changed"); + } + } + + if (!success) { + if (transferChecks.isFungible) { + assertEq(preTransferInfo.toBalance, postTransferInfo.toBalance, 'to balance changed unexpectedly'); + assertEq(preTransferInfo.fromBalance, postTransferInfo.fromBalance, 'from balance changed unexpectedly'); + + if (!transferChecks.isRequestFromOwner) { + assertEq( + preTransferInfo.spenderAllowance, + postTransferInfo.spenderAllowance, + 'spender allowance changed unexpectedly' + ); + } + } + + if (transferChecks.isNonFungible) { + assertEq(preTransferInfo.owner, postTransferInfo.owner, 'owner should not have changed on failure'); + assertEq(preTransferInfo.approvedId, postTransferInfo.approvedId, 'approvedId should not have changed on failure'); + assertEq(preTransferInfo.isSenderOperator, postTransferInfo.isSenderOperator, 'isSenderOperator should not have changed on failure'); + } + } + } + + struct BurnParams { + address sender; + address token; + int64 amountOrSerialNumber; + } + + struct BurnChecks { + bool isToken; + int32 tokenType; + bool isFungible; + bool isNonFungible; + uint256 amountOrSerialNumberU256; + int64 expectedResponseCode; + } + + struct BurnInfo { + address owner; + uint256 totalSupply; + uint256 treasuryBalance; + } + + function _doBurnViaHtsPrecompile(BurnParams memory burnParams) internal setPranker(burnParams.sender) returns (bool success, int64 responseCode) { + + HederaFungibleToken hederaFungibleToken = HederaFungibleToken(burnParams.token); + HederaNonFungibleToken hederaNonFungibleToken = HederaNonFungibleToken(burnParams.token); + + BurnChecks memory burnChecks; + + bytes[] memory NULL_BYTES = new bytes[](1); + + int64 newTotalSupply; + int64[] memory serialNumbers = new int64[](1); // this test function currently only supports 1 NFT being burnt at a time + + burnChecks.amountOrSerialNumberU256 = uint64(burnParams.amountOrSerialNumber); + burnChecks.expectedResponseCode = HederaResponseCodes.SUCCESS; // assume SUCCESS initially and later overwrite error code accordingly + + burnChecks.expectedResponseCode = HederaResponseCodes.SUCCESS; // assume SUCCESS and overwrite with !SUCCESS where applicable + + (burnChecks.expectedResponseCode, burnChecks.tokenType) = htsPrecompile.getTokenType(burnParams.token); + + if (burnChecks.expectedResponseCode == HederaResponseCodes.SUCCESS) { + burnChecks.isFungible = burnChecks.tokenType == 0 ? true : false; + burnChecks.isNonFungible = burnChecks.tokenType == 1 ? true : false; + } + + address treasury = htsPrecompile.getTreasuryAccount(burnParams.token); + + BurnInfo memory preBurnInfo; + + preBurnInfo.totalSupply = hederaFungibleToken.totalSupply(); + preBurnInfo.treasuryBalance = hederaFungibleToken.balanceOf(treasury); + + if (burnChecks.isNonFungible) { + // amount is only applicable to type FUNGIBLE + serialNumbers[0] = burnParams.amountOrSerialNumber; // only burn 1 NFT at a time + preBurnInfo.owner = hederaNonFungibleToken.ownerOf(burnChecks.amountOrSerialNumberU256); + burnParams.amountOrSerialNumber = 0; + + if (burnParams.sender != preBurnInfo.owner) { + burnChecks.expectedResponseCode = HederaResponseCodes.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO; + } + } + + if (treasury != burnParams.sender) { + burnChecks.expectedResponseCode = HederaResponseCodes.AUTHORIZATION_FAILED; + } + + (responseCode, newTotalSupply) = htsPrecompile.burnToken(burnParams.token, burnParams.amountOrSerialNumber, serialNumbers); + + assertEq(burnChecks.expectedResponseCode, responseCode, 'expected response code does not equal actual response code'); + + success = responseCode == HederaResponseCodes.SUCCESS; + + BurnInfo memory postBurnInfo; + + postBurnInfo.totalSupply = hederaFungibleToken.totalSupply(); + postBurnInfo.treasuryBalance = hederaFungibleToken.balanceOf(treasury); + + if (success) { + if (burnChecks.isFungible) { + assertEq( + preBurnInfo.totalSupply - burnChecks.amountOrSerialNumberU256, + postBurnInfo.totalSupply, + 'expected total supply to decrease by burn amount' + ); + assertEq( + preBurnInfo.treasuryBalance - burnChecks.amountOrSerialNumberU256, + postBurnInfo.treasuryBalance, + 'expected treasury balance to decrease by burn amount' + ); + } + + if (burnChecks.isNonFungible) { + assertEq( + preBurnInfo.totalSupply - 1, + postBurnInfo.totalSupply, + 'expected total supply to decrease by burn amount' + ); + assertEq( + preBurnInfo.treasuryBalance - 1, + postBurnInfo.treasuryBalance, + 'expected treasury balance to decrease by burn amount' + ); + } + } + + if (!success) { + assertEq( + preBurnInfo.totalSupply, + postBurnInfo.totalSupply, + 'expected total supply to not change if failed' + ); + assertEq( + preBurnInfo.treasuryBalance, + postBurnInfo.treasuryBalance, + 'expected treasury balance to not change if failed' + ); + } + } + +} diff --git a/test/foundry/utils/UtilUtils.sol b/test/foundry/utils/UtilUtils.sol new file mode 100644 index 000000000..c97df7bc7 --- /dev/null +++ b/test/foundry/utils/UtilUtils.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.9; + +import 'forge-std/Test.sol'; + +import '../mocks/util-precompile/UtilPrecompileMock.sol'; +import './CommonUtils.sol'; +import '../../../contracts/libraries/Constants.sol'; + +/// for testing actions of the util precompiled/system contract +abstract contract UtilUtils is Test, CommonUtils, Constants { + + UtilPrecompileMock utilPrecompile = UtilPrecompileMock(UTIL_PRECOMPILE); + + function _setUpUtilPrecompileMock() internal { + UtilPrecompileMock utilPrecompileMock = new UtilPrecompileMock(); + bytes memory code = address(utilPrecompileMock).code; + vm.etch(UTIL_PRECOMPILE, code); + } + + function _doCallPseudorandomSeed(address sender) internal setPranker(sender) returns (bytes32 seed) { + seed = utilPrecompile.getPseudorandomSeed(); + } +}