From 4b6d9d32720fefc0ab1f873d7f44ceb7b64ae579 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 07:17:09 +0100 Subject: [PATCH 01/26] chore: fix lint issues --- src/libraries/hts/LibHederaTokenService.sol | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/libraries/hts/LibHederaTokenService.sol b/src/libraries/hts/LibHederaTokenService.sol index 11584eb..2f0b793 100644 --- a/src/libraries/hts/LibHederaTokenService.sol +++ b/src/libraries/hts/LibHederaTokenService.sol @@ -11,9 +11,7 @@ library LibHederaTokenService { int32 private constant DEFAULT_AUTO_RENEW_PERIOD = 7776000; modifier nonEmptyExpiry(IHederaTokenService.HederaToken memory token) { - if (token.expiry.second == 0 && token.expiry.autoRenewPeriod == 0) { - token.expiry.autoRenewPeriod = DEFAULT_AUTO_RENEW_PERIOD; - } + _nonEmptyExpiry(token); _; } @@ -347,7 +345,7 @@ library LibHederaTokenService { /// @param to The account address of the receiver of `serialNumber` /// @param serialNumber The NFT serial number to transfer /// @return responseCode The response code for the status of the request. SUCCESS is 22. - function transferFromNFT(address token, address from, address to, uint256 serialNumber) + function transferFromNft(address token, address from, address to, uint256 serialNumber) external returns (int64 responseCode) { @@ -379,7 +377,7 @@ library LibHederaTokenService { /// @param approved The new approved NFT controller. To revoke approvals pass in the zero address. /// @param serialNumber The NFT serial number to approve /// @return responseCode The response code for the status of the request. SUCCESS is 22. - function approveNFT(address token, address approved, uint256 serialNumber) internal returns (int256 responseCode) { + function approveNft(address token, address approved, uint256 serialNumber) internal returns (int256 responseCode) { (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( abi.encodeWithSelector(IHederaTokenService.approveNFT.selector, token, approved, serialNumber) ); @@ -551,7 +549,7 @@ library LibHederaTokenService { /// @param sender the sender of an nft /// @param receiver the receiver of the nft sent by the same index at sender /// @param serialNumber the serial number of the nft sent by the same index at sender - function transferNFTs( + function transferNfts( address token, address[] memory sender, address[] memory receiver, @@ -587,7 +585,7 @@ library LibHederaTokenService { /// @param sender The sender for the transaction /// @param receiver The receiver of the transaction /// @param serialNumber The serial number of the NFT to transfer. - function transferNFT(address token, address sender, address receiver, int64 serialNumber) + function transferNft(address token, address sender, address receiver, int64 serialNumber) internal returns (int256 responseCode) { @@ -632,7 +630,7 @@ library LibHederaTokenService { /// @param account The account address to revoke kyc /// @param serialNumbers The serial numbers of token to wipe /// @return responseCode The response code for the status of the request. SUCCESS is 22. - function wipeTokenAccountNFT(address token, address account, int64[] memory serialNumbers) + function wipeTokenAccountNft(address token, address account, int64[] memory serialNumbers) internal returns (int256 responseCode) { @@ -801,4 +799,10 @@ library LibHederaTokenService { ); responseCode = success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN; } + + function _nonEmptyExpiry(IHederaTokenService.HederaToken memory token) internal pure { + if (token.expiry.second == 0 && token.expiry.autoRenewPeriod == 0) { + token.expiry.autoRenewPeriod = DEFAULT_AUTO_RENEW_PERIOD; + } + } } From ac36312f6ed414f1c4a21963df2c65759307a120 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 07:17:33 +0100 Subject: [PATCH 02/26] chore: import hts fee helper as lib --- src/libraries/hts/LibFeeHelper.sol | 401 +++++++++++++++++++++++++++++ 1 file changed, 401 insertions(+) create mode 100644 src/libraries/hts/LibFeeHelper.sol diff --git a/src/libraries/hts/LibFeeHelper.sol b/src/libraries/hts/LibFeeHelper.sol new file mode 100644 index 0000000..0531367 --- /dev/null +++ b/src/libraries/hts/LibFeeHelper.sol @@ -0,0 +1,401 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; +pragma experimental ABIEncoderV2; + +import {IHederaTokenService} from "hedera-token-service/IHederaTokenService.sol"; + +library LibFeeHelper { + function createFixedHbarFee(int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee memory fixedFee) + { + fixedFee.amount = amount; + fixedFee.useHbarsForPayment = true; + fixedFee.feeCollector = feeCollector; + } + + function createFixedTokenFee(int64 amount, address tokenId, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee memory fixedFee) + { + fixedFee.amount = amount; + fixedFee.tokenId = tokenId; + fixedFee.feeCollector = feeCollector; + } + + function createFixedSelfDenominatedFee(int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee memory fixedFee) + { + fixedFee.amount = amount; + fixedFee.useCurrentTokenForPayment = true; + fixedFee.feeCollector = feeCollector; + } + + function createFractionalFee(int64 numerator, int64 denominator, bool netOfTransfers, address feeCollector) + internal + pure + returns (IHederaTokenService.FractionalFee memory fractionalFee) + { + fractionalFee.numerator = numerator; + fractionalFee.denominator = denominator; + fractionalFee.netOfTransfers = netOfTransfers; + fractionalFee.feeCollector = feeCollector; + } + + function createFractionalFeeWithMinAndMax( + int64 numerator, + int64 denominator, + int64 minimumAmount, + int64 maximumAmount, + bool netOfTransfers, + address feeCollector + ) internal pure returns (IHederaTokenService.FractionalFee memory fractionalFee) { + fractionalFee.numerator = numerator; + fractionalFee.denominator = denominator; + fractionalFee.minimumAmount = minimumAmount; + fractionalFee.maximumAmount = maximumAmount; + fractionalFee.netOfTransfers = netOfTransfers; + fractionalFee.feeCollector = feeCollector; + } + + function createFractionalFeeWithLimits( + int64 numerator, + int64 denominator, + int64 minimumAmount, + int64 maximumAmount, + bool netOfTransfers, + address feeCollector + ) internal pure returns (IHederaTokenService.FractionalFee memory fractionalFee) { + fractionalFee.numerator = numerator; + fractionalFee.denominator = denominator; + fractionalFee.minimumAmount = minimumAmount; + fractionalFee.maximumAmount = maximumAmount; + fractionalFee.netOfTransfers = netOfTransfers; + fractionalFee.feeCollector = feeCollector; + } + + function createRoyaltyFeeWithoutFallback(int64 numerator, int64 denominator, address feeCollector) + internal + pure + returns (IHederaTokenService.RoyaltyFee memory royaltyFee) + { + royaltyFee.numerator = numerator; + royaltyFee.denominator = denominator; + royaltyFee.feeCollector = feeCollector; + } + + function createRoyaltyFeeWithHbarFallbackFee(int64 numerator, int64 denominator, int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.RoyaltyFee memory royaltyFee) + { + royaltyFee.numerator = numerator; + royaltyFee.denominator = denominator; + royaltyFee.amount = amount; + royaltyFee.useHbarsForPayment = true; + royaltyFee.feeCollector = feeCollector; + } + + function createRoyaltyFeeWithTokenDenominatedFallbackFee( + int64 numerator, + int64 denominator, + int64 amount, + address tokenId, + address feeCollector + ) internal pure returns (IHederaTokenService.RoyaltyFee memory royaltyFee) { + royaltyFee.numerator = numerator; + royaltyFee.denominator = denominator; + royaltyFee.amount = amount; + royaltyFee.tokenId = tokenId; + royaltyFee.feeCollector = feeCollector; + } + + function createNAmountFixedFeesForHbars(uint8 numberOfFees, int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee[] memory fixedFees) + { + fixedFees = new IHederaTokenService.FixedFee[](numberOfFees); + + for (uint8 i = 0; i < numberOfFees; i++) { + IHederaTokenService.FixedFee memory fixedFee = createFixedFeeForHbars(amount, feeCollector); + fixedFees[i] = fixedFee; + } + } + + function createSingleFixedFeeForToken(int64 amount, address tokenId, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee[] memory fixedFees) + { + fixedFees = new IHederaTokenService.FixedFee[](1); + IHederaTokenService.FixedFee memory fixedFee = createFixedFeeForToken(amount, tokenId, feeCollector); + fixedFees[0] = fixedFee; + } + + function createFixedFeesForToken( + int64 amount, + address tokenId, + address firstFeeCollector, + address secondFeeCollector + ) internal pure returns (IHederaTokenService.FixedFee[] memory fixedFees) { + fixedFees = new IHederaTokenService.FixedFee[](1); + IHederaTokenService.FixedFee memory fixedFee1 = createFixedFeeForToken(amount, tokenId, firstFeeCollector); + IHederaTokenService.FixedFee memory fixedFee2 = createFixedFeeForToken(2 * amount, tokenId, secondFeeCollector); + fixedFees[0] = fixedFee1; + fixedFees[0] = fixedFee2; + } + + function createSingleFixedFeeForHbars(int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee[] memory fixedFees) + { + fixedFees = new IHederaTokenService.FixedFee[](1); + IHederaTokenService.FixedFee memory fixedFee = createFixedFeeForHbars(amount, feeCollector); + fixedFees[0] = fixedFee; + } + + function createSingleFixedFeeForCurrentToken(int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee[] memory fixedFees) + { + fixedFees = new IHederaTokenService.FixedFee[](1); + IHederaTokenService.FixedFee memory fixedFee = createFixedFeeForCurrentToken(amount, feeCollector); + fixedFees[0] = fixedFee; + } + + function createSingleFixedFeeWithInvalidFlags(int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee[] memory fixedFees) + { + fixedFees = new IHederaTokenService.FixedFee[](1); + IHederaTokenService.FixedFee memory fixedFee = createFixedFeeWithInvalidFlags(amount, feeCollector); + fixedFees[0] = fixedFee; + } + + function createSingleFixedFeeWithTokenIdAndHbars(int64 amount, address tokenId, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee[] memory fixedFees) + { + fixedFees = new IHederaTokenService.FixedFee[](1); + IHederaTokenService.FixedFee memory fixedFee = createFixedFeeWithTokenIdAndHbars(amount, tokenId, feeCollector); + fixedFees[0] = fixedFee; + } + + function createFixedFeesWithAllTypes(int64 amount, address tokenId, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee[] memory fixedFees) + { + fixedFees = new IHederaTokenService.FixedFee[](3); + IHederaTokenService.FixedFee memory fixedFeeForToken = createFixedFeeForToken(amount, tokenId, feeCollector); + IHederaTokenService.FixedFee memory fixedFeeForHbars = createFixedFeeForHbars(amount * 2, feeCollector); + IHederaTokenService.FixedFee memory fixedFeeForCurrentToken = + createFixedFeeForCurrentToken(amount * 4, feeCollector); + fixedFees[0] = fixedFeeForToken; + fixedFees[1] = fixedFeeForHbars; + fixedFees[2] = fixedFeeForCurrentToken; + } + + function createFixedFeeForToken(int64 amount, address tokenId, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee memory fixedFee) + { + fixedFee.amount = amount; + fixedFee.tokenId = tokenId; + fixedFee.feeCollector = feeCollector; + } + + function createFixedFeeForHbars(int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee memory fixedFee) + { + fixedFee.amount = amount; + fixedFee.useHbarsForPayment = true; + fixedFee.feeCollector = feeCollector; + } + + function createFixedFeeForCurrentToken(int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee memory fixedFee) + { + fixedFee.amount = amount; + fixedFee.useCurrentTokenForPayment = true; + fixedFee.feeCollector = feeCollector; + } + + //Used for negative scenarios + function createFixedFeeWithInvalidFlags(int64 amount, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee memory fixedFee) + { + fixedFee.amount = amount; + fixedFee.useHbarsForPayment = true; + fixedFee.useCurrentTokenForPayment = true; + fixedFee.feeCollector = feeCollector; + } + + //Used for negative scenarios + function createFixedFeeWithTokenIdAndHbars(int64 amount, address tokenId, address feeCollector) + internal + pure + returns (IHederaTokenService.FixedFee memory fixedFee) + { + fixedFee.amount = amount; + fixedFee.tokenId = tokenId; + fixedFee.useHbarsForPayment = true; + fixedFee.feeCollector = feeCollector; + } + + function getEmptyFixedFees() internal pure returns (IHederaTokenService.FixedFee[] memory fixedFees) {} + + function createNAmountFractionalFees( + uint8 numberOfFees, + int64 numerator, + int64 denominator, + bool netOfTransfers, + address feeCollector + ) internal pure returns (IHederaTokenService.FractionalFee[] memory fractionalFees) { + fractionalFees = new IHederaTokenService.FractionalFee[](numberOfFees); + + for (uint8 i = 0; i < numberOfFees; i++) { + IHederaTokenService.FractionalFee memory fractionalFee = + createFractionalFee(numerator, denominator, netOfTransfers, feeCollector); + fractionalFees[i] = fractionalFee; + } + } + + function createSingleFractionalFee(int64 numerator, int64 denominator, bool netOfTransfers, address feeCollector) + internal + pure + returns (IHederaTokenService.FractionalFee[] memory fractionalFees) + { + fractionalFees = new IHederaTokenService.FractionalFee[](1); + IHederaTokenService.FractionalFee memory fractionalFee = + createFractionalFee(numerator, denominator, netOfTransfers, feeCollector); + fractionalFees[0] = fractionalFee; + } + + function createSingleFractionalFeeWithLimits( + int64 numerator, + int64 denominator, + int64 minimumAmount, + int64 maximumAmount, + bool netOfTransfers, + address feeCollector + ) internal pure returns (IHederaTokenService.FractionalFee[] memory fractionalFees) { + fractionalFees = new IHederaTokenService.FractionalFee[](1); + IHederaTokenService.FractionalFee memory fractionalFee = createFractionalFeeWithLimits( + numerator, denominator, minimumAmount, maximumAmount, netOfTransfers, feeCollector + ); + fractionalFees[0] = fractionalFee; + } + + function getEmptyFractionalFees() + internal + pure + returns (IHederaTokenService.FractionalFee[] memory fractionalFees) + { + fractionalFees = new IHederaTokenService.FractionalFee[](0); + } + + function createNAmountRoyaltyFees(uint8 numberOfFees, int64 numerator, int64 denominator, address feeCollector) + internal + pure + returns (IHederaTokenService.RoyaltyFee[] memory royaltyFees) + { + royaltyFees = new IHederaTokenService.RoyaltyFee[](numberOfFees); + + for (uint8 i = 0; i < numberOfFees; i++) { + IHederaTokenService.RoyaltyFee memory royaltyFee = createRoyaltyFee(numerator, denominator, feeCollector); + royaltyFees[i] = royaltyFee; + } + } + + function getEmptyRoyaltyFees() internal pure returns (IHederaTokenService.RoyaltyFee[] memory royaltyFees) { + royaltyFees = new IHederaTokenService.RoyaltyFee[](0); + } + + function createSingleRoyaltyFee(int64 numerator, int64 denominator, address feeCollector) + internal + pure + returns (IHederaTokenService.RoyaltyFee[] memory royaltyFees) + { + royaltyFees = new IHederaTokenService.RoyaltyFee[](1); + + IHederaTokenService.RoyaltyFee memory royaltyFee = createRoyaltyFee(numerator, denominator, feeCollector); + royaltyFees[0] = royaltyFee; + } + + function createSingleRoyaltyFeeWithFallbackFee( + int64 numerator, + int64 denominator, + int64 amount, + address tokenId, + bool useHbarsForPayment, + address feeCollector + ) internal pure returns (IHederaTokenService.RoyaltyFee[] memory royaltyFees) { + royaltyFees = new IHederaTokenService.RoyaltyFee[](1); + + IHederaTokenService.RoyaltyFee memory royaltyFee = + createRoyaltyFeeWithFallbackFee(numerator, denominator, amount, tokenId, useHbarsForPayment, feeCollector); + royaltyFees[0] = royaltyFee; + } + + function createRoyaltyFeesWithAllTypes( + int64 numerator, + int64 denominator, + int64 amount, + address tokenId, + address feeCollector + ) internal pure returns (IHederaTokenService.RoyaltyFee[] memory royaltyFees) { + royaltyFees = new IHederaTokenService.RoyaltyFee[](3); + IHederaTokenService.RoyaltyFee memory royaltyFeeWithoutFallback = + createRoyaltyFee(numerator, denominator, feeCollector); + IHederaTokenService.RoyaltyFee memory royaltyFeeWithFallbackHbar = + createRoyaltyFeeWithFallbackFee(numerator, denominator, amount, address(0x0), true, feeCollector); + IHederaTokenService.RoyaltyFee memory royaltyFeeWithFallbackToken = + createRoyaltyFeeWithFallbackFee(numerator, denominator, amount, tokenId, false, feeCollector); + royaltyFees[0] = royaltyFeeWithoutFallback; + royaltyFees[1] = royaltyFeeWithFallbackHbar; + royaltyFees[2] = royaltyFeeWithFallbackToken; + } + + function createRoyaltyFee(int64 numerator, int64 denominator, address feeCollector) + internal + pure + returns (IHederaTokenService.RoyaltyFee memory royaltyFee) + { + royaltyFee.numerator = numerator; + royaltyFee.denominator = denominator; + royaltyFee.feeCollector = feeCollector; + } + + function createRoyaltyFeeWithFallbackFee( + int64 numerator, + int64 denominator, + int64 amount, + address tokenId, + bool useHbarsForPayment, + address feeCollector + ) internal pure returns (IHederaTokenService.RoyaltyFee memory royaltyFee) { + royaltyFee.numerator = numerator; + royaltyFee.denominator = denominator; + royaltyFee.amount = amount; + royaltyFee.tokenId = tokenId; + royaltyFee.useHbarsForPayment = useHbarsForPayment; + royaltyFee.feeCollector = feeCollector; + } +} From 10111514139201a36772eea28d38dae711dbb041 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 07:18:13 +0100 Subject: [PATCH 03/26] chore: remove unused imports --- src/libraries/LibProduct.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libraries/LibProduct.sol b/src/libraries/LibProduct.sol index 4261bf5..2f936d6 100644 --- a/src/libraries/LibProduct.sol +++ b/src/libraries/LibProduct.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.30; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IHederaTokenService} from "hedera-token-service/IHederaTokenService.sol"; -import {HederaTokenService} from "hedera-token-service/HederaTokenService.sol"; -import {KeyHelper} from "hedera-token-service/KeyHelper.sol"; import {HederaResponseCodes} from "hedera-system-contracts/HederaResponseCodes.sol"; import {LibHederaTokenService} from "@chronicle/libraries/hts/LibHederaTokenService.sol"; import {LibContext} from "@chronicle/libraries/LibContext.sol"; From 991106ac35789514993847220eed1f010dfb9333 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 07:19:00 +0100 Subject: [PATCH 04/26] chore: import and use fee helper lib to calculate fees --- src/libraries/LibProduct.sol | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/libraries/LibProduct.sol b/src/libraries/LibProduct.sol index 2f936d6..9ba79ac 100644 --- a/src/libraries/LibProduct.sol +++ b/src/libraries/LibProduct.sol @@ -11,11 +11,13 @@ import {KeyType, KeyValueType} from "@chronicle-types/KeyHelperStorage.sol"; import {Status, Product, ProductStorage, PRODUCT_STORAGE_SLOT} from "@chronicle-types/ProductStorage.sol"; import {LibParty} from "@chronicle/libraries/LibParty.sol"; import {LibKeyHelper} from "@chronicle/libraries/hts/LibKeyHelper.sol"; +import {LibFeeHelper} from "@chronicle/libraries/hts/LibFeeHelper.sol"; import "@chronicle-logs/ProductLogs.sol"; library LibProduct { using LibParty for address; using LibKeyHelper for KeyType; + using LibFeeHelper for int64; using LibHederaTokenService for IHederaTokenService.HederaToken; using LibHederaTokenService for address; using EnumerableSet for EnumerableSet.AddressSet; @@ -205,11 +207,16 @@ library LibProduct { } function _mintProductToken(address _tokenAddress, int64 _initialSupply) + function _getProductFees(int64 _price) private returns (int256 mintResponseCode_, int64 newTotalSupply_, int64[] memory serialNumbers_) + view + returns (IHederaTokenService.FixedFee[] memory fixedFees_, IHederaTokenService.RoyaltyFee[] memory royaltyFees_) { bytes[] memory metadata = new bytes[](0); (mintResponseCode_, newTotalSupply_, serialNumbers_) = _tokenAddress.mintToken(_initialSupply, metadata); + fixedFees_ = _price.createSingleFixedFeeForHbars(address(this)); + royaltyFees_ = LibFeeHelper.createRoyaltyFeesWithAllTypes(1, 1000, _price, USDC_ADDRESS, address(this)); } function _getProductToken(string calldata _name, string calldata _memo) @@ -234,29 +241,4 @@ library LibProduct { tokenKeys_[5] = KeyType.FEE.getSingleKey(KeyValueType.CONTRACT_ID, address(this)); tokenKeys_[6] = KeyType.PAUSE.getSingleKey(KeyValueType.CONTRACT_ID, address(this)); } - - function _getProductFees(int64 _price) - private - view - returns (IHederaTokenService.FixedFee[] memory fixedFees_, IHederaTokenService.RoyaltyFee[] memory royaltyFees_) - { - fixedFees_ = new IHederaTokenService.FixedFee[](1); - fixedFees_[0] = IHederaTokenService.FixedFee({ - amount: _price, - tokenId: address(0), - useHbarsForPayment: true, - useCurrentTokenForPayment: false, - feeCollector: address(this) - }); - - royaltyFees_ = new IHederaTokenService.RoyaltyFee[](1); - royaltyFees_[0] = IHederaTokenService.RoyaltyFee({ - numerator: 1, - denominator: 1000, - amount: _price, - tokenId: address(0), - useHbarsForPayment: true, - feeCollector: address(this) - }); - } } From 57aa62240aedb1167dabf998f4503aa1d0a3f8d0 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 07:22:29 +0100 Subject: [PATCH 05/26] chore: import usdc address for royalty fee fallback --- src/libraries/LibProduct.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/LibProduct.sol b/src/libraries/LibProduct.sol index 9ba79ac..de2ca64 100644 --- a/src/libraries/LibProduct.sol +++ b/src/libraries/LibProduct.sol @@ -8,7 +8,9 @@ import {LibHederaTokenService} from "@chronicle/libraries/hts/LibHederaTokenServ import {LibContext} from "@chronicle/libraries/LibContext.sol"; import {Role} from "@chronicle-types/PartyStorage.sol"; import {KeyType, KeyValueType} from "@chronicle-types/KeyHelperStorage.sol"; -import {Status, Product, ProductStorage, PRODUCT_STORAGE_SLOT} from "@chronicle-types/ProductStorage.sol"; +import { + Status, Product, ProductStorage, PRODUCT_STORAGE_SLOT, USDC_ADDRESS +} from "@chronicle-types/ProductStorage.sol"; import {LibParty} from "@chronicle/libraries/LibParty.sol"; import {LibKeyHelper} from "@chronicle/libraries/hts/LibKeyHelper.sol"; import {LibFeeHelper} from "@chronicle/libraries/hts/LibFeeHelper.sol"; From c867b49e8f4e13296183a6301e396f582c6f6e30 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 07:24:54 +0100 Subject: [PATCH 06/26] feat: associate token with protocol and user after minting --- src/libraries/LibProduct.sol | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libraries/LibProduct.sol b/src/libraries/LibProduct.sol index de2ca64..b8bb061 100644 --- a/src/libraries/LibProduct.sol +++ b/src/libraries/LibProduct.sol @@ -47,6 +47,8 @@ library LibProduct { (int256 mintResponseCode, int64 newTotalSupply, int64[] memory serialNumbers) = _mintProductToken(tokenAddress, _initialSupply); if (mintResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to mint product"); + _associateProductToken(address(this), tokenAddress); + _associateProductToken(sender, tokenAddress); ProductStorage storage $ = _productStorage(); $.activeProducts.add(tokenAddress); @@ -206,6 +208,15 @@ library LibProduct { _getProductFees(_price); (createResponseCode_, tokenAddress_) = _getProductToken(_name, _memo).createNonFungibleTokenWithCustomFees(fixedFees, royaltyFees); + + function _associateProductToken(address _party, address _tokenAddress) private { + (int256 associateResponseCode) = _party.associateToken(_tokenAddress); + if ( + associateResponseCode != HederaResponseCodes.SUCCESS + || associateResponseCode != HederaResponseCodes.TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT + ) revert("Failed to associate token"); + } + } function _mintProductToken(address _tokenAddress, int64 _initialSupply) From 186f071d9488b780c54e79f6852b4f276987b03d Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 07:26:45 +0100 Subject: [PATCH 07/26] chore: check for errors in private functions --- src/libraries/LibProduct.sol | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/libraries/LibProduct.sol b/src/libraries/LibProduct.sol index b8bb061..bb04e8c 100644 --- a/src/libraries/LibProduct.sol +++ b/src/libraries/LibProduct.sol @@ -40,16 +40,14 @@ library LibProduct { address sender = LibContext._msgSender(); if (!sender._hasActiveRole(Role.Supplier)) revert("Not a Supplier"); - (int256 createResponseCode, address tokenAddress) = _createProductToken(_name, _memo, _price); - if (createResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to create product"); + address tokenAddress = _createProductToken(_name, _memo, _price); if (tokenAddress == address(0)) revert("Invalid token address"); - (int256 mintResponseCode, int64 newTotalSupply, int64[] memory serialNumbers) = - _mintProductToken(tokenAddress, _initialSupply); - if (mintResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to mint product"); _associateProductToken(address(this), tokenAddress); _associateProductToken(sender, tokenAddress); + (int64 newTotalSupply, int64[] memory serialNumbers) = _mintProductToken(tokenAddress, _initialSupply); + ProductStorage storage $ = _productStorage(); $.activeProducts.add(tokenAddress); Product memory product = Product({ @@ -98,9 +96,7 @@ library LibProduct { if ($.tokenToProduct[_tokenAddress].owner != LibContext._msgSender()) revert("Not the owner"); if ($.tokenToProduct[_tokenAddress].status != Status.Created) revert("Product sold"); - (int256 mintResponseCode, int64 newTotalSupply, int64[] memory serialNumbers) = - _mintProductToken(_tokenAddress, _quantity); - if (mintResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to mint product"); + (int64 newTotalSupply, int64[] memory serialNumbers) = _mintProductToken(_tokenAddress, _quantity); Product memory product = $.tokenToProduct[_tokenAddress]; product.totalSupply = newTotalSupply; @@ -202,12 +198,15 @@ library LibProduct { function _createProductToken(string calldata _name, string calldata _memo, int64 _price) private - returns (int256 createResponseCode_, address tokenAddress_) + returns (address) { (IHederaTokenService.FixedFee[] memory fixedFees, IHederaTokenService.RoyaltyFee[] memory royaltyFees) = _getProductFees(_price); - (createResponseCode_, tokenAddress_) = + (int256 createResponseCode, address tokenAddress) = _getProductToken(_name, _memo).createNonFungibleTokenWithCustomFees(fixedFees, royaltyFees); + if (createResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to create product"); + return tokenAddress; + } function _associateProductToken(address _party, address _tokenAddress) private { (int256 associateResponseCode) = _party.associateToken(_tokenAddress); @@ -217,17 +216,19 @@ library LibProduct { ) revert("Failed to associate token"); } + function _mintProductToken(address _tokenAddress, int64 _initialSupply) private returns (int64, int64[] memory) { + bytes[] memory metadata; + (int256 mintResponseCode, int64 newTotalSupply, int64[] memory serialNumbers) = + _tokenAddress.mintToken(_initialSupply, metadata); + if (mintResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to mint product"); + return (newTotalSupply, serialNumbers); } - function _mintProductToken(address _tokenAddress, int64 _initialSupply) function _getProductFees(int64 _price) private - returns (int256 mintResponseCode_, int64 newTotalSupply_, int64[] memory serialNumbers_) view returns (IHederaTokenService.FixedFee[] memory fixedFees_, IHederaTokenService.RoyaltyFee[] memory royaltyFees_) { - bytes[] memory metadata = new bytes[](0); - (mintResponseCode_, newTotalSupply_, serialNumbers_) = _tokenAddress.mintToken(_initialSupply, metadata); fixedFees_ = _price.createSingleFixedFeeForHbars(address(this)); royaltyFees_ = LibFeeHelper.createRoyaltyFeesWithAllTypes(1, 1000, _price, USDC_ADDRESS, address(this)); } From 263eb8913c2220c39a776e66da064b616b6e033c Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 08:14:12 +0100 Subject: [PATCH 08/26] chore: remove created status --- src/libraries/types/ProductStorage.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/types/ProductStorage.sol b/src/libraries/types/ProductStorage.sol index 39cd7d1..2607de4 100644 --- a/src/libraries/types/ProductStorage.sol +++ b/src/libraries/types/ProductStorage.sol @@ -14,7 +14,6 @@ struct ProductStorage { } enum Status { - Created, ForSale, Sold, Shipped, From 6f319d9bf0b27b98df81d6c422707ea05a3d5f37 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 08:14:52 +0100 Subject: [PATCH 09/26] chore: set for sale as the default status --- src/libraries/LibProduct.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libraries/LibProduct.sol b/src/libraries/LibProduct.sol index bb04e8c..7caa50a 100644 --- a/src/libraries/LibProduct.sol +++ b/src/libraries/LibProduct.sol @@ -58,7 +58,7 @@ library LibProduct { price: _price, totalSupply: newTotalSupply, owner: sender, - status: Status.Created, + status: Status.ForSale, timestamp: uint40(block.timestamp) }); $.tokenToProduct[tokenAddress] = product; @@ -73,10 +73,7 @@ library LibProduct { ProductStorage storage $ = _productStorage(); if (!$.activeProducts.contains(_tokenAddress)) revert("Invalid token address"); if ($.tokenToProduct[_tokenAddress].owner != LibContext._msgSender()) revert("Not the owner"); - if ( - $.tokenToProduct[_tokenAddress].status != Status.Created - || $.tokenToProduct[_tokenAddress].status != Status.ForSale - ) revert("Product sold"); + if ($.tokenToProduct[_tokenAddress].status != Status.ForSale) revert("Product sold"); _updateProductTokenInfo(_tokenAddress, _getProductToken(_name, _memo)); _updateProductTokenFees(_tokenAddress, _price); @@ -94,7 +91,7 @@ library LibProduct { ProductStorage storage $ = _productStorage(); if (!$.activeProducts.contains(_tokenAddress)) revert("Invalid token address"); if ($.tokenToProduct[_tokenAddress].owner != LibContext._msgSender()) revert("Not the owner"); - if ($.tokenToProduct[_tokenAddress].status != Status.Created) revert("Product sold"); + if ($.tokenToProduct[_tokenAddress].status != Status.ForSale) revert("Product sold"); (int64 newTotalSupply, int64[] memory serialNumbers) = _mintProductToken(_tokenAddress, _quantity); @@ -109,7 +106,7 @@ library LibProduct { ProductStorage storage $ = _productStorage(); if (!$.activeProducts.contains(_tokenAddress)) revert("Invalid token address"); if ($.tokenToProduct[_tokenAddress].owner != LibContext._msgSender()) revert("Not the owner"); - if ($.tokenToProduct[_tokenAddress].status != Status.Created) revert("Product sold"); + if ($.tokenToProduct[_tokenAddress].status != Status.ForSale) revert("Product sold"); (int256 burnResponseCode, int64 newTotalSupply) = _tokenAddress.burnToken(_quantity, _serialNumbers); if (burnResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to burn product"); From 66f6528bd93dbfa471f0bc00367c2be136cc344f Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 08:16:25 +0100 Subject: [PATCH 10/26] feat: start supply chain facet --- src/facets/SupplyChainFacet.sol | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/facets/SupplyChainFacet.sol diff --git a/src/facets/SupplyChainFacet.sol b/src/facets/SupplyChainFacet.sol new file mode 100644 index 0000000..71eb099 --- /dev/null +++ b/src/facets/SupplyChainFacet.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +contract SupplyChainFacet {} From 79eb30c047c28f3d30dc362739efa601d88169cf Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 08:16:45 +0100 Subject: [PATCH 11/26] feat: start supply chain lib --- src/libraries/LibSupplyChain.sol | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/libraries/LibSupplyChain.sol diff --git a/src/libraries/LibSupplyChain.sol b/src/libraries/LibSupplyChain.sol new file mode 100644 index 0000000..c832a2a --- /dev/null +++ b/src/libraries/LibSupplyChain.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {Role} from "@chronicle-types/PartyStorage.sol"; +import {Status} from "@chronicle-types/ProductStorage.sol"; +import {LibParty} from "@chronicle/libraries/LibParty.sol"; +import {LibProduct} from "@chronicle/libraries/LibProduct.sol"; +import {LibContext} from "@chronicle/libraries/LibContext.sol"; + +library LibSupplyChain { + using LibParty for address; + using LibProduct for address; + + function _buyProduct(address _tokenAddress, uint256 _quantity) internal { + address sender = LibContext._msgSender(); + if (!sender._hasActiveRole(Role.Retailer)) revert("Not a Retailer"); + if (_tokenAddress._getProductByTokenAddress().status != Status.ForSale) revert("Product not available"); + } +} From 65252db21a0a08ff82772cfeb6e3d2eaee277bbf Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 16:49:16 +0100 Subject: [PATCH 12/26] chore: SafeHTS Library --- src/libraries/hts/safe-hts/LibSafeHTS.sol | 325 ++++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 src/libraries/hts/safe-hts/LibSafeHTS.sol diff --git a/src/libraries/hts/safe-hts/LibSafeHTS.sol b/src/libraries/hts/safe-hts/LibSafeHTS.sol new file mode 100644 index 0000000..b9af36c --- /dev/null +++ b/src/libraries/hts/safe-hts/LibSafeHTS.sol @@ -0,0 +1,325 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; +pragma experimental ABIEncoderV2; + +import {HederaResponseCodes} from "hedera-system-contracts/HederaResponseCodes.sol"; +import {IHederaTokenService} from "hedera-token-service/IHederaTokenService.sol"; + +library LibSafeHTS { + address private constant PRECOMPILE_ADDRESS = address(0x167); + // 90 days in seconds + int32 private constant DEFAULT_AUTO_RENEW_PERIOD = 7776000; + + error CryptoTransferFailed(); + error MintFailed(); + error BurnFailed(); + error MultipleAssociationsFailed(); + error SingleAssociationFailed(); + error MultipleDissociationsFailed(); + error SingleDissociationFailed(); + error TokensTransferFailed(); + error NFTsTransferFailed(); + error TokenTransferFailed(); + error NFTTransferFailed(); + error CreateFungibleTokenFailed(); + error CreateFungibleTokenWithCustomFeesFailed(); + error CreateNonFungibleTokenFailed(); + error CreateNonFungibleTokenWithCustomFeesFailed(); + error ApproveFailed(); + error NFTApproveFailed(); + error SetTokenApprovalForAllFailed(); + error TokenDeleteFailed(); + error FreezeTokenFailed(); + error UnfreezeTokenFailed(); + error GrantTokenKYCFailed(); + error RevokeTokenKYCFailed(); + error PauseTokenFailed(); + error UnpauseTokenFailed(); + error WipeTokenAccountFailed(); + error WipeTokenAccountNFTFailed(); + error UpdateTokenInfoFailed(); + error UpdateTokenExpiryInfoFailed(); + error UpdateTokenKeysFailed(); + + function safeCryptoTransfer( + IHederaTokenService.TransferList memory transferList, + IHederaTokenService.TokenTransferList[] memory tokenTransfers + ) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.cryptoTransfer.selector, transferList, tokenTransfers) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert CryptoTransferFailed(); + } + + function safeMintToken(address token, int64 amount, bytes[] memory metadata) + internal + returns (int64 newTotalSupply, int64[] memory serialNumbers) + { + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.mintToken.selector, token, amount, metadata) + ); + (responseCode, newTotalSupply, serialNumbers) = success + ? abi.decode(result, (int32, int64, int64[])) + : (HederaResponseCodes.UNKNOWN, int64(0), new int64[](0)); + if (responseCode != HederaResponseCodes.SUCCESS) revert MintFailed(); + } + + function safeBurnToken(address token, int64 amount, int64[] memory serialNumbers) + internal + returns (int64 newTotalSupply) + { + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.burnToken.selector, token, amount, serialNumbers) + ); + (responseCode, newTotalSupply) = + success ? abi.decode(result, (int32, int64)) : (HederaResponseCodes.UNKNOWN, int64(0)); + if (responseCode != HederaResponseCodes.SUCCESS) revert BurnFailed(); + } + + function safeAssociateTokens(address account, address[] memory tokens) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.associateTokens.selector, account, tokens) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert MultipleAssociationsFailed(); + } + + function safeAssociateToken(address token, address account) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.associateToken.selector, account, token)); + if (!tryDecodeSuccessResponseCode(success, result)) revert SingleAssociationFailed(); + } + + function safeDissociateTokens(address account, address[] memory tokens) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.dissociateTokens.selector, account, tokens) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert MultipleDissociationsFailed(); + } + + function safeDissociateToken(address token, address account) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.dissociateToken.selector, account, token) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert SingleDissociationFailed(); + } + + function safeTransferTokens(address token, address[] memory accountIds, int64[] memory amounts) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.transferTokens.selector, token, accountIds, amounts) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert TokensTransferFailed(); + } + + /// forge-lint: disable-next-line(mixed-case-function) + function safeTransferNFTs( + address token, + address[] memory sender, + address[] memory receiver, + int64[] memory serialNumber + ) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.transferNFTs.selector, token, sender, receiver, serialNumber) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert NFTsTransferFailed(); + } + + function safeTransferToken(address token, address sender, address receiver, int64 amount) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.transferToken.selector, token, sender, receiver, amount) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert TokenTransferFailed(); + } + + /// forge-lint: disable-next-line(mixed-case-function) + function safeTransferNFT(address token, address sender, address receiver, int64 serialNumber) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.transferNFT.selector, token, sender, receiver, serialNumber) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert NFTTransferFailed(); + } + + function safeCreateFungibleToken( + IHederaTokenService.HederaToken memory token, + int64 initialTotalSupply, + int32 decimals + ) internal returns (address tokenAddress) { + nonEmptyExpiry(token); + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call{value: msg.value}( + abi.encodeWithSelector( + IHederaTokenService.createFungibleToken.selector, token, initialTotalSupply, decimals + ) + ); + (responseCode, tokenAddress) = + success ? abi.decode(result, (int32, address)) : (HederaResponseCodes.UNKNOWN, address(0)); + if (responseCode != HederaResponseCodes.SUCCESS) revert CreateFungibleTokenFailed(); + } + + function safeCreateFungibleTokenWithCustomFees( + IHederaTokenService.HederaToken memory token, + int64 initialTotalSupply, + int32 decimals, + IHederaTokenService.FixedFee[] memory fixedFees, + IHederaTokenService.FractionalFee[] memory fractionalFees + ) internal returns (address tokenAddress) { + nonEmptyExpiry(token); + int256 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call{value: msg.value}( + abi.encodeWithSelector( + IHederaTokenService.createFungibleTokenWithCustomFees.selector, + token, + initialTotalSupply, + decimals, + fixedFees, + fractionalFees + ) + ); + (responseCode, tokenAddress) = + success ? abi.decode(result, (int32, address)) : (HederaResponseCodes.UNKNOWN, address(0)); + if (responseCode != HederaResponseCodes.SUCCESS) revert CreateFungibleTokenWithCustomFeesFailed(); + } + + function safeCreateNonFungibleToken(IHederaTokenService.HederaToken memory token) + internal + returns (address tokenAddress) + { + nonEmptyExpiry(token); + int256 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call{value: msg.value}( + abi.encodeWithSelector(IHederaTokenService.createNonFungibleToken.selector, token) + ); + (responseCode, tokenAddress) = + success ? abi.decode(result, (int32, address)) : (HederaResponseCodes.UNKNOWN, address(0)); + if (responseCode != HederaResponseCodes.SUCCESS) revert CreateNonFungibleTokenFailed(); + } + + function safeCreateNonFungibleTokenWithCustomFees( + IHederaTokenService.HederaToken memory token, + IHederaTokenService.FixedFee[] memory fixedFees, + IHederaTokenService.RoyaltyFee[] memory royaltyFees + ) internal returns (address tokenAddress) { + nonEmptyExpiry(token); + int256 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call{value: msg.value}( + abi.encodeWithSelector( + IHederaTokenService.createNonFungibleTokenWithCustomFees.selector, token, fixedFees, royaltyFees + ) + ); + (responseCode, tokenAddress) = + success ? abi.decode(result, (int32, address)) : (HederaResponseCodes.UNKNOWN, address(0)); + if (responseCode != HederaResponseCodes.SUCCESS) revert CreateNonFungibleTokenWithCustomFeesFailed(); + } + + function safeApprove(address token, address spender, uint256 amount) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.approve.selector, token, spender, amount) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert ApproveFailed(); + } + + /// forge-lint: disable-next-line(mixed-case-function) + function safeApproveNFT(address token, address approved, int64 serialNumber) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.approveNFT.selector, token, approved, serialNumber) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert NFTApproveFailed(); + } + + function safeSetApprovalForAll(address token, address operator, bool approved) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.setApprovalForAll.selector, token, operator, approved) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert SetTokenApprovalForAllFailed(); + } + + function safeDeleteToken(address token) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.deleteToken.selector, token)); + if (!tryDecodeSuccessResponseCode(success, result)) revert TokenDeleteFailed(); + } + + function safeFreezeToken(address token, address account) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.freezeToken.selector, token, account)); + if (!tryDecodeSuccessResponseCode(success, result)) revert FreezeTokenFailed(); + } + + function safeUnfreezeToken(address token, address account) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.unfreezeToken.selector, token, account)); + if (!tryDecodeSuccessResponseCode(success, result)) revert UnfreezeTokenFailed(); + } + + function safeGrantTokenKyc(address token, address account) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.grantTokenKyc.selector, token, account)); + if (!tryDecodeSuccessResponseCode(success, result)) revert GrantTokenKYCFailed(); + } + + function safeRevokeTokenKyc(address token, address account) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.revokeTokenKyc.selector, token, account)); + if (!tryDecodeSuccessResponseCode(success, result)) revert RevokeTokenKYCFailed(); + } + + function safePauseToken(address token) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.pauseToken.selector, token)); + if (!tryDecodeSuccessResponseCode(success, result)) revert PauseTokenFailed(); + } + + function safeUnpauseToken(address token) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.unpauseToken.selector, token)); + if (!tryDecodeSuccessResponseCode(success, result)) revert UnpauseTokenFailed(); + } + + function safeWipeTokenAccount(address token, address account, int64 amount) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.wipeTokenAccount.selector, token, account, amount) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert WipeTokenAccountFailed(); + } + + /// forge-lint: disable-next-line(mixed-case-function) + function safeWipeTokenAccountNFT(address token, address account, int64[] memory serialNumbers) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.wipeTokenAccountNFT.selector, token, account, serialNumbers) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert WipeTokenAccountNFTFailed(); + } + + function safeUpdateTokenInfo(address token, IHederaTokenService.HederaToken memory tokenInfo) internal { + nonEmptyExpiry(tokenInfo); + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.updateTokenInfo.selector, token, tokenInfo) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert UpdateTokenInfoFailed(); + } + + function safeUpdateTokenExpiryInfo(address token, IHederaTokenService.Expiry memory expiryInfo) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.updateTokenExpiryInfo.selector, token, expiryInfo) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert UpdateTokenExpiryInfoFailed(); + } + + function safeUpdateTokenKeys(address token, IHederaTokenService.TokenKey[] memory keys) internal { + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.updateTokenKeys.selector, token, keys)); + if (!tryDecodeSuccessResponseCode(success, result)) revert UpdateTokenKeysFailed(); + } + + function tryDecodeSuccessResponseCode(bool success, bytes memory result) private pure returns (bool) { + return (success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN) == HederaResponseCodes.SUCCESS; + } + + function nonEmptyExpiry(IHederaTokenService.HederaToken memory token) private view { + if (token.expiry.second == 0 && token.expiry.autoRenewPeriod == 0) { + token.expiry.autoRenewPeriod = DEFAULT_AUTO_RENEW_PERIOD; + token.expiry.autoRenewAccount = address(this); + } + } +} From e7f2bf81c80dae2238ba27e3efc4dccbda3ebb25 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 16:50:45 +0100 Subject: [PATCH 13/26] chore(forge-lint): ignore mixed case function --- src/libraries/hts/LibHederaTokenService.sol | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libraries/hts/LibHederaTokenService.sol b/src/libraries/hts/LibHederaTokenService.sol index 2f0b793..90347be 100644 --- a/src/libraries/hts/LibHederaTokenService.sol +++ b/src/libraries/hts/LibHederaTokenService.sol @@ -345,7 +345,8 @@ library LibHederaTokenService { /// @param to The account address of the receiver of `serialNumber` /// @param serialNumber The NFT serial number to transfer /// @return responseCode The response code for the status of the request. SUCCESS is 22. - function transferFromNft(address token, address from, address to, uint256 serialNumber) + /// forge-lint: disable-next-line(mixed-case-function) + function transferFromNFT(address token, address from, address to, uint256 serialNumber) external returns (int64 responseCode) { @@ -377,7 +378,8 @@ library LibHederaTokenService { /// @param approved The new approved NFT controller. To revoke approvals pass in the zero address. /// @param serialNumber The NFT serial number to approve /// @return responseCode The response code for the status of the request. SUCCESS is 22. - function approveNft(address token, address approved, uint256 serialNumber) internal returns (int256 responseCode) { + /// forge-lint: disable-next-line(mixed-case-function) + function approveNFT(address token, address approved, uint256 serialNumber) internal returns (int256 responseCode) { (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( abi.encodeWithSelector(IHederaTokenService.approveNFT.selector, token, approved, serialNumber) ); @@ -549,7 +551,8 @@ library LibHederaTokenService { /// @param sender the sender of an nft /// @param receiver the receiver of the nft sent by the same index at sender /// @param serialNumber the serial number of the nft sent by the same index at sender - function transferNfts( + /// forge-lint: disable-next-line(mixed-case-function) + function transferNFTs( address token, address[] memory sender, address[] memory receiver, @@ -585,7 +588,8 @@ library LibHederaTokenService { /// @param sender The sender for the transaction /// @param receiver The receiver of the transaction /// @param serialNumber The serial number of the NFT to transfer. - function transferNft(address token, address sender, address receiver, int64 serialNumber) + /// forge-lint: disable-next-line(mixed-case-function) + function transferNFT(address token, address sender, address receiver, int64 serialNumber) internal returns (int256 responseCode) { @@ -630,7 +634,8 @@ library LibHederaTokenService { /// @param account The account address to revoke kyc /// @param serialNumbers The serial numbers of token to wipe /// @return responseCode The response code for the status of the request. SUCCESS is 22. - function wipeTokenAccountNft(address token, address account, int64[] memory serialNumbers) + /// forge-lint: disable-next-line(mixed-case-function) + function wipeTokenAccountNFT(address token, address account, int64[] memory serialNumbers) internal returns (int256 responseCode) { From 72af20602c1e5a22d70b60064f8dd7e24ca0199d Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 16:53:06 +0100 Subject: [PATCH 14/26] docs: hts diamond libs description --- HTS-LIBS.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 HTS-LIBS.md diff --git a/HTS-LIBS.md b/HTS-LIBS.md new file mode 100644 index 0000000..41336ff --- /dev/null +++ b/HTS-LIBS.md @@ -0,0 +1,113 @@ +# hedera-diamond-hts-lib + +_Seamless Hedera Token Service (HTS) integration for EIP-2535 Diamond Standard smart contracts._ + +--- + +## Overview + +**hedera-diamond-hts-lib** solves the challenge of using Hedera's non-EVM HTS precompiles and abstract contracts (like `IHederaTokenService`) within modular, upgradeable Diamond (EIP-2535) architectures. Instead of inheriting abstract contracts (which is incompatible with delegatecall-based facets), this library provides gas-optimized, composable `library` wrappers for all core HTS operations. + +These libraries enable facets in a Diamond proxy to perform mint, burn, transfer, associate, dissociate, freeze, KYC, and other token operations directly, using delegatecall-compatible stateless functions. This unlocks full HTS utility in modular, upgradable dApps. + +--- + +## Features + +- **Modular HTS Operations:** Each HTS function (mint, burn, transfer, etc.) is available as a standalone library function. +- **Diamond Facet Compatible:** Designed for use inside EIP-2535 facets via delegatecall, with no stateful logic. +- **Full HTS Coverage:** Supports association, dissociation, KYC, freeze, custom fees, NFT and fungible operations, and more. +- **Lightweight & Composable:** No inheritance, minimal overhead, easily composed with other libraries and storage patterns. + +--- + +## Installation + +### Prerequisites +- Foundry or hardhat project + +### Add to Your Project + +#### Option 1: Manual Copy +Copy the `.sol` files from `src/libraries/hts/` into your project's libraries directory. + +#### Option 2: forge install +```sh +forge install dadadave80/chronicle +``` + +--- + +## Usage Example + +Import and use in a facet contract: + +```solidity +import {LibHederaTokenService} from "@chronicle/libraries/hts/LibHederaTokenService.sol"; + +contract ProductsFacet { + function mintProduct(address token, int64 amount) external { + // Calls the Hedera Token Service mint via precompile + (int256 code, int64 newSupply, int64[] memory serials) = LibHederaTokenService.mintToken(token, amount, new bytes[](0)); + require(code == 22, "Mint failed"); // 22 = SUCCESS + } +} +``` + +**Best Practices:** +- Use with [LibDiamond](https://eips.ethereum.org/EIPS/eip-2535) storage patterns—never store state in libraries. +- Use `using LibHederaTokenService for address;` for more ergonomic syntax. +- Combine with access control and event logging as needed. + +--- + +## Project Structure + +- `LibHederaTokenService.sol` — Core stateless wrappers for all HTS precompile operations (mint, burn, transfer, associate, etc.) +- `LibSafeHTS.sol` — Revert-on-failure safe wrappers for all HTS calls (throws on non-SUCCESS response). +- `LibFeeHelper.sol` — Utilities for constructing custom fee structs for HTS tokens. +- `LibKeyHelper.sol` — Utilities for managing HTS key types and key assignment. + +--- + +## Limitations / Considerations + +- **Delegatecall Overhead:** Calls from facets via delegatecall are slightly more expensive than direct contract calls. +- **No Library State:** All logic must be stateless; use Diamond storage patterns for persistent data. +- **HTS Compatibility:** Only works on Hedera-compatible EVM chains with HTS precompiles available at `0x167`. +- **Error Handling:** Use `LibSafeHTS` for automatic revert on failure, or handle response codes manually with `LibHederaTokenService`. + +--- + +## Testing + +- **Unit Tests:** + - Hardhat: `npx hardhat test` + - Foundry: `forge test` +- **Coverage:** + - Hardhat: `npx hardhat coverage` + - Foundry: `forge coverage` + +Tests cover all core HTS operations, including edge cases and error handling. + +--- + +## Contributing + +- Fork and submit PRs for new HTS methods, optimizations, or bug fixes. +- Adhere to Solidity style guidelines and include unit tests for new features. +- Open issues for feature requests or HTS compatibility questions. + +--- + +## License + +MIT + +--- + +## References + +- [Hedera Token Service Documentation](https://docs.hedera.com/hedera/smart-contracts/hedera-token-service) +- [EIP-2535 Diamond Standard](https://eips.ethereum.org/EIPS/eip-2535) +- [Hedera Token Service Solidity Interfaces](https://github.com/hashgraph/hedera-smart-contracts) From 7f70119fed9ba70ce97a91d761295fc33e91c327 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:43:31 +0100 Subject: [PATCH 15/26] chore: add update token and transfer from functions --- src/libraries/hts/safe-hts/LibSafeHTS.sol | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/libraries/hts/safe-hts/LibSafeHTS.sol b/src/libraries/hts/safe-hts/LibSafeHTS.sol index b9af36c..f5df6fb 100644 --- a/src/libraries/hts/safe-hts/LibSafeHTS.sol +++ b/src/libraries/hts/safe-hts/LibSafeHTS.sol @@ -18,6 +18,7 @@ library LibSafeHTS { error MultipleDissociationsFailed(); error SingleDissociationFailed(); error TokensTransferFailed(); + error TokensTransferFromFailed(); error NFTsTransferFailed(); error TokenTransferFailed(); error NFTTransferFailed(); @@ -40,6 +41,7 @@ library LibSafeHTS { error UpdateTokenInfoFailed(); error UpdateTokenExpiryInfoFailed(); error UpdateTokenKeysFailed(); + error UpdateTokenCustomFeesFailed(); function safeCryptoTransfer( IHederaTokenService.TransferList memory transferList, @@ -132,6 +134,13 @@ library LibSafeHTS { if (!tryDecodeSuccessResponseCode(success, result)) revert TokenTransferFailed(); } + function safeTransferFromToken(address token, address from, address to, int64 amount) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.transferFrom.selector, token, from, to, amount) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert TokensTransferFromFailed(); + } + /// forge-lint: disable-next-line(mixed-case-function) function safeTransferNFT(address token, address sender, address receiver, int64 serialNumber) internal { (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( @@ -312,6 +321,19 @@ library LibSafeHTS { if (!tryDecodeSuccessResponseCode(success, result)) revert UpdateTokenKeysFailed(); } + function safeUpdateFungibleTokenCustomFees( + address token, + IHederaTokenService.FixedFee[] memory fixedFees, + IHederaTokenService.FractionalFee[] memory fractionalFees + ) internal { + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector( + IHederaTokenService.updateFungibleTokenCustomFees.selector, token, fixedFees, fractionalFees + ) + ); + if (!tryDecodeSuccessResponseCode(success, result)) revert UpdateTokenCustomFeesFailed(); + } + function tryDecodeSuccessResponseCode(bool success, bytes memory result) private pure returns (bool) { return (success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN) == HederaResponseCodes.SUCCESS; } From 65b29d2e2016c8c0c30dbf24a463b526640e9c96 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:44:10 +0100 Subject: [PATCH 16/26] chore: rename Product status and update product struct --- src/libraries/types/ProductStorage.sol | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libraries/types/ProductStorage.sol b/src/libraries/types/ProductStorage.sol index 2607de4..b500bb9 100644 --- a/src/libraries/types/ProductStorage.sol +++ b/src/libraries/types/ProductStorage.sol @@ -13,11 +13,12 @@ struct ProductStorage { EnumerableSet.AddressSet activeProducts; } -enum Status { +enum ProductStatus { + None, ForSale, - Sold, - Shipped, - Received + Ordered, + Assigned, + Sold } struct Product { @@ -26,8 +27,10 @@ struct Product { string name; string memo; int64 price; + int64 transportFee; int64 totalSupply; - address owner; - Status status; - uint40 timestamp; + address supplier; + ProductStatus status; + uint40 created; + uint40 updated; } From c825f815629ef618e40d45d5b573c13b54c05569 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:44:53 +0100 Subject: [PATCH 17/26] chore: use custom error --- src/libraries/LibParty.sol | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libraries/LibParty.sol b/src/libraries/LibParty.sol index 6bb0da2..231ccf8 100644 --- a/src/libraries/LibParty.sol +++ b/src/libraries/LibParty.sol @@ -7,7 +7,10 @@ import { } from "@chronicle-types/PartyStorage.sol"; import {LibContext} from "@chronicle/libraries/LibContext.sol"; import {LibOwnableRoles} from "@diamond/libraries/LibOwnableRoles.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) import "@chronicle-logs/PartyLogs.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@chronicle/libraries/errors/PartyErrors.sol"; library LibParty { using EnumerableSet for EnumerableSet.AddressSet; @@ -27,9 +30,9 @@ library LibParty { function _registerParty(string calldata _name, Role _role) internal { PartyStorage storage $ = _partyStorage(); address sender = LibContext._msgSender(); - if ($.parties[sender].frozen) revert("Party frozen"); - if (!$.roles[_role].add(sender)) revert("Role already exists"); - if (!$.activeParties.add(sender)) revert("Party already exists"); + if ($.parties[sender].frozen) revert PartyFrozen(sender); + if (!$.roles[_role].add(sender)) revert RoleAlreadyExists(_role); + if (!$.activeParties.add(sender)) revert PartyAlreadyExists(sender); Party memory party = Party({name: _name, addr: sender, role: _role, active: true, frozen: false, rating: Rating.Zero}); $.parties[sender] = party; @@ -39,8 +42,8 @@ library LibParty { function _deactivateParty(Role _role) internal { PartyStorage storage $ = _partyStorage(); address sender = LibContext._msgSender(); - if (!$.activeParties.remove(sender)) revert("Party not active"); - if (!$.roles[_role].remove(sender)) revert("Role not found"); + if (!$.activeParties.remove(sender)) revert PartyNotActive(sender); + if (!$.roles[_role].remove(sender)) revert RoleNotFound(_role); delete $.parties[sender].active; emit PartyDeactivated($.parties[sender]); } From f29a8eb6f394d33b979a151643ffb7fcea7eaada Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:45:09 +0100 Subject: [PATCH 18/26] chore: add custom errors --- src/libraries/errors/PartyErrors.sol | 15 +++++++++++++++ src/libraries/errors/ProductErrors.sol | 6 ++++++ src/libraries/errors/SupplyChainErrors.sol | 12 ++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 src/libraries/errors/PartyErrors.sol create mode 100644 src/libraries/errors/ProductErrors.sol create mode 100644 src/libraries/errors/SupplyChainErrors.sol diff --git a/src/libraries/errors/PartyErrors.sol b/src/libraries/errors/PartyErrors.sol new file mode 100644 index 0000000..893d7c0 --- /dev/null +++ b/src/libraries/errors/PartyErrors.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {Role} from "@chronicle-types/PartyStorage.sol"; + +error PartyInactive(address party); +error PartyAlreadyExists(address party); +error RoleNotFound(Role role); +error RoleAlreadyExists(Role role); +error PartyNotActive(address party); +error PartyFrozen(address party); +error PartyNotFrozen(address party); +error PartyNotSupplier(address party); +error PartyNotTransporter(address party); +error PartyNotRetailer(address party); diff --git a/src/libraries/errors/ProductErrors.sol b/src/libraries/errors/ProductErrors.sol new file mode 100644 index 0000000..2f702fb --- /dev/null +++ b/src/libraries/errors/ProductErrors.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +error ProductAlreadyExists(address productToken); +error ProductInactive(address productToken); +error ProductSold(address productToken); diff --git a/src/libraries/errors/SupplyChainErrors.sol b/src/libraries/errors/SupplyChainErrors.sol new file mode 100644 index 0000000..9be42bb --- /dev/null +++ b/src/libraries/errors/SupplyChainErrors.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +error SupplyChainInactive(address supplyChain); +error NotSupplier(address supplier); +error InvalidTokenAddress(); +error NotRetailer(address retailer); +error NotTransporter(address transporter); +error ProductNotOrdered(address productToken); +error ProductNotAssigned(address productToken); +error ProductNotAvailable(address productToken); +error ProductNotBought(address productToken); From 514f210f8063fd83aff54af0f62925d6609e1806 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:45:32 +0100 Subject: [PATCH 19/26] chore: SafeViewHTS lib --- src/libraries/hts/safe-hts/LibSafeViewHTS.sol | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 src/libraries/hts/safe-hts/LibSafeViewHTS.sol diff --git a/src/libraries/hts/safe-hts/LibSafeViewHTS.sol b/src/libraries/hts/safe-hts/LibSafeViewHTS.sol new file mode 100644 index 0000000..943b3bc --- /dev/null +++ b/src/libraries/hts/safe-hts/LibSafeViewHTS.sol @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.5.0 <0.9.0; +pragma experimental ABIEncoderV2; + +import {IHederaTokenService} from "hedera-token-service/IHederaTokenService.sol"; +import {HederaResponseCodes} from "hedera-system-contracts/HederaResponseCodes.sol"; + +library LibSafeViewHTS { + address private constant PRECOMPILE_ADDRESS = address(0x167); + // 90 days in seconds + int32 private constant DEFAULT_AUTO_RENEW_PERIOD = 7776000; + + error AllowanceFailed(); + error GetApprovedFailed(); + error IsApprovedForAllFailed(); + error IsFrozenFailed(); + error IsKYCGrantedFailed(); + error GetTokenCustomFeesFailed(); + error GetTokenDefaultFreezeStatusFailed(); + error GetTokenDefaultKYCStatusFailed(); + error GetTokenExpiryInfoFailed(); + error GetFungibleTokenInfoFailed(); + error GetTokenInfoFailed(); + error GetTokenKeyFailed(); + error GetNonFungibleTokenInfoFailed(); + error IsTokenFailed(); + error GetTokenTypeFailed(); + + function safeAllowance(address token, address owner, address spender) internal returns (uint256 allowance) { + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.allowance.selector, token, owner, spender) + ); + (responseCode, allowance) = success ? abi.decode(result, (int32, uint256)) : (HederaResponseCodes.UNKNOWN, 0); + if (responseCode != HederaResponseCodes.SUCCESS) revert AllowanceFailed(); + } + + function safeGetApproved(address token, int64 serialNumber) internal returns (address approved) { + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.getApproved.selector, token, serialNumber) + ); + (responseCode, approved) = + success ? abi.decode(result, (int32, address)) : (HederaResponseCodes.UNKNOWN, address(0)); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetApprovedFailed(); + } + + function safeIsApprovedForAll(address token, address owner, address operator) internal returns (bool approved) { + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.isApprovedForAll.selector, token, owner, operator) + ); + (responseCode, approved) = success ? abi.decode(result, (int32, bool)) : (HederaResponseCodes.UNKNOWN, false); + if (responseCode != HederaResponseCodes.SUCCESS) revert IsApprovedForAllFailed(); + } + + function safeIsFrozen(address token, address account) internal returns (bool frozen) { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.isFrozen.selector, token, account)); + (responseCode, frozen) = success ? abi.decode(result, (int32, bool)) : (HederaResponseCodes.UNKNOWN, false); + if (responseCode != HederaResponseCodes.SUCCESS) revert IsFrozenFailed(); + } + + function safeIsKyc(address token, address account) internal returns (bool kycGranted) { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.isKyc.selector, token, account)); + (responseCode, kycGranted) = success ? abi.decode(result, (int32, bool)) : (HederaResponseCodes.UNKNOWN, false); + if (responseCode != HederaResponseCodes.SUCCESS) revert IsKYCGrantedFailed(); + } + + function safeGetTokenCustomFees(address token) + internal + returns ( + IHederaTokenService.FixedFee[] memory fixedFees, + IHederaTokenService.FractionalFee[] memory fractionalFees, + IHederaTokenService.RoyaltyFee[] memory royaltyFees + ) + { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.getTokenCustomFees.selector, token)); + (responseCode, fixedFees, fractionalFees, royaltyFees) = success + ? abi.decode( + result, + ( + int32, + IHederaTokenService.FixedFee[], + IHederaTokenService.FractionalFee[], + IHederaTokenService.RoyaltyFee[] + ) + ) + : (HederaResponseCodes.UNKNOWN, fixedFees, fractionalFees, royaltyFees); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetTokenCustomFeesFailed(); + } + + function safeGetTokenDefaultFreezeStatus(address token) internal returns (bool defaultFreezeStatus) { + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.getTokenDefaultFreezeStatus.selector, token) + ); + (responseCode, defaultFreezeStatus) = + success ? abi.decode(result, (int32, bool)) : (HederaResponseCodes.UNKNOWN, false); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetTokenDefaultFreezeStatusFailed(); + } + + function safeGetTokenDefaultKycStatus(address token) internal returns (bool defaultKycStatus) { + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.getTokenDefaultKycStatus.selector, token) + ); + (responseCode, defaultKycStatus) = + success ? abi.decode(result, (int32, bool)) : (HederaResponseCodes.UNKNOWN, false); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetTokenDefaultKYCStatusFailed(); + } + + function safeGetTokenExpiryInfo(address token) internal returns (IHederaTokenService.Expiry memory expiry) { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.getTokenExpiryInfo.selector, token)); + (responseCode, expiry) = + success ? abi.decode(result, (int32, IHederaTokenService.Expiry)) : (HederaResponseCodes.UNKNOWN, expiry); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetTokenExpiryInfoFailed(); + } + + function safeGetFungibleTokenInfo(address token) + internal + returns (IHederaTokenService.FungibleTokenInfo memory fungibleTokenInfo) + { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.getFungibleTokenInfo.selector, token)); + (responseCode, fungibleTokenInfo) = success + ? abi.decode(result, (int32, IHederaTokenService.FungibleTokenInfo)) + : (HederaResponseCodes.UNKNOWN, fungibleTokenInfo); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetFungibleTokenInfoFailed(); + } + + function safeGetTokenInfo(address token) internal returns (IHederaTokenService.TokenInfo memory tokenInfo) { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.getTokenInfo.selector, token)); + (responseCode, tokenInfo) = success + ? abi.decode(result, (int32, IHederaTokenService.TokenInfo)) + : (HederaResponseCodes.UNKNOWN, tokenInfo); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetTokenInfoFailed(); + } + + function safeGetTokenKey(address token, uint256 keyType) + internal + returns (IHederaTokenService.KeyValue memory key) + { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.getTokenKey.selector, token, keyType)); + (responseCode, key) = + success ? abi.decode(result, (int32, IHederaTokenService.KeyValue)) : (HederaResponseCodes.UNKNOWN, key); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetTokenKeyFailed(); + } + + function safeGetNonFungibleTokenInfo(address token, int64 serialNumber) + internal + returns (IHederaTokenService.NonFungibleTokenInfo memory nonFungibleTokenInfo) + { + int32 responseCode; + (bool success, bytes memory result) = PRECOMPILE_ADDRESS.call( + abi.encodeWithSelector(IHederaTokenService.getNonFungibleTokenInfo.selector, token, serialNumber) + ); + (responseCode, nonFungibleTokenInfo) = success + ? abi.decode(result, (int32, IHederaTokenService.NonFungibleTokenInfo)) + : (HederaResponseCodes.UNKNOWN, nonFungibleTokenInfo); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetNonFungibleTokenInfoFailed(); + } + + function safeIsToken(address token) internal returns (bool isToken) { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.isToken.selector, token)); + (responseCode, isToken) = success ? abi.decode(result, (int32, bool)) : (HederaResponseCodes.UNKNOWN, false); + if (responseCode != HederaResponseCodes.SUCCESS) revert IsTokenFailed(); + } + + function safeGetTokenType(address token) internal returns (int32 tokenType) { + int32 responseCode; + (bool success, bytes memory result) = + PRECOMPILE_ADDRESS.call(abi.encodeWithSelector(IHederaTokenService.getTokenType.selector, token)); + (responseCode, tokenType) = + success ? abi.decode(result, (int32, int32)) : (HederaResponseCodes.UNKNOWN, int32(0)); + if (responseCode != HederaResponseCodes.SUCCESS) revert GetTokenTypeFailed(); + } + + function tryDecodeSuccessResponseCode(bool success, bytes memory result) private pure returns (bool) { + return (success ? abi.decode(result, (int32)) : HederaResponseCodes.UNKNOWN) == HederaResponseCodes.SUCCESS; + } + + function nonEmptyExpiry(IHederaTokenService.HederaToken memory token) private view { + if (token.expiry.second == 0 && token.expiry.autoRenewPeriod == 0) { + token.expiry.autoRenewPeriod = DEFAULT_AUTO_RENEW_PERIOD; + token.expiry.autoRenewAccount = address(this); + } + } +} From e6417526a4a55227dceb68d2a09e384f953785cd Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:45:51 +0100 Subject: [PATCH 20/26] chore: update product logs --- src/libraries/logs/ProductLogs.sol | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/libraries/logs/ProductLogs.sol b/src/libraries/logs/ProductLogs.sol index 0f60532..62540b6 100644 --- a/src/libraries/logs/ProductLogs.sol +++ b/src/libraries/logs/ProductLogs.sol @@ -2,13 +2,24 @@ pragma solidity 0.8.30; import {Product} from "@chronicle-types/ProductStorage.sol"; +import {SupplyChainStatus} from "@chronicle-types/SupplyChainStorage.sol"; -event ProductCreated(Product indexed product, int64[] serialNumbers); +event ProductCreated(Product indexed product); event ProductUpdated(Product indexed product); event ProductTransferred(address indexed from, address indexed to, Product indexed product); -event ProductQuantityIncreased(Product indexed product, int64[] serialNumbers); +event ProductQuantityIncreased(Product indexed product); -event ProductQuantityDecreased(Product indexed product, int64[] serialNumbers); +event ProductQuantityDecreased(Product indexed product); + +event ProductOrdered(address indexed retailer, address indexed product, int64 quantity); + +event ProductAssigned(address indexed transporter, address indexed product, int64 quantity); + +event ProductStatusUpdated(address indexed transporter, address indexed product, SupplyChainStatus status); + +event ProductFulfilled(address indexed transporter, address indexed product, int64 quantity); + +event ProductBought(address indexed retailer, address indexed product, int64 quantity); From 0c9a2d0fa0f36f75ddf7d4fc34153f4c4fcee658 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:46:08 +0100 Subject: [PATCH 21/26] chore: default to none --- src/libraries/types/PartyStorage.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/types/PartyStorage.sol b/src/libraries/types/PartyStorage.sol index cb34c8d..80f7595 100644 --- a/src/libraries/types/PartyStorage.sol +++ b/src/libraries/types/PartyStorage.sol @@ -15,6 +15,7 @@ struct PartyStorage { } enum Role { + None, Supplier, Transporter, Retailer From 90c7b063bbfac5ace0bfb399bae5fecca76231cc Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:46:40 +0100 Subject: [PATCH 22/26] feat: supply chain lib --- src/libraries/LibSupplyChain.sol | 184 ++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 5 deletions(-) diff --git a/src/libraries/LibSupplyChain.sol b/src/libraries/LibSupplyChain.sol index c832a2a..bf27a4f 100644 --- a/src/libraries/LibSupplyChain.sol +++ b/src/libraries/LibSupplyChain.sol @@ -2,18 +2,192 @@ pragma solidity 0.8.30; import {Role} from "@chronicle-types/PartyStorage.sol"; -import {Status} from "@chronicle-types/ProductStorage.sol"; +import {ProductStatus} from "@chronicle-types/ProductStorage.sol"; import {LibParty} from "@chronicle/libraries/LibParty.sol"; import {LibProduct} from "@chronicle/libraries/LibProduct.sol"; +import { + SupplyChainStatus, SupplyChainStorage, SUPPLYCHAIN_STORAGE_SLOT +} from "@chronicle-types/SupplyChainStorage.sol"; import {LibContext} from "@chronicle/libraries/LibContext.sol"; +import {LibSafeHTS} from "@chronicle/libraries/hts/safe-hts/LibSafeHTS.sol"; +import {LibSafeViewHTS} from "@chronicle/libraries/hts/safe-hts/LibSafeViewHTS.sol"; +import {IHederaTokenService} from "hedera-token-service/IHederaTokenService.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@chronicle-logs/ProductLogs.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@chronicle/libraries/errors/SupplyChainErrors.sol"; library LibSupplyChain { using LibParty for address; using LibProduct for address; + using LibSafeHTS for address; + using LibSafeViewHTS for address; + using Address for address payable; + using EnumerableSet for EnumerableSet.AddressSet; - function _buyProduct(address _tokenAddress, uint256 _quantity) internal { - address sender = LibContext._msgSender(); - if (!sender._hasActiveRole(Role.Retailer)) revert("Not a Retailer"); - if (_tokenAddress._getProductByTokenAddress().status != Status.ForSale) revert("Product not available"); + function _supplyChainStorage() internal pure returns (SupplyChainStorage storage scs_) { + assembly { + scs_.slot := SUPPLYCHAIN_STORAGE_SLOT + } + } + + function _retailerOrderProduct(address _productToken, int64 _quantity) internal { + address retailer = LibContext._msgSender(); + if (!retailer._hasActiveRole(Role.Retailer)) revert NotRetailer(retailer); + if (_productToken._getProductByTokenAddress().status != ProductStatus.ForSale) { + revert ProductNotAvailable(_productToken); + } + + (,,, uint256 totalFee) = _calculateFees(_productToken, _quantity); + payable(address(this)).sendValue(totalFee); + + _productToken.safeAssociateToken(retailer); + _productToken.safeApprove(retailer, uint256(int256(_quantity))); + + SupplyChainStorage storage $ = _supplyChainStorage(); + $.allOrderedProducts.add(_productToken); + $.retailerToProducts[retailer].add(_productToken); + $.productToRetailers[_productToken].add(retailer); + LibProduct._productStorage().tokenToProduct[_productToken].status = ProductStatus.Ordered; + + emit ProductOrdered(retailer, _productToken, _quantity); + } + + function _transporterSelectOrder(address _productToken, int64 _quantity) internal { + address transporter = LibContext._msgSender(); + if (!transporter._hasActiveRole(Role.Transporter)) revert NotTransporter(transporter); + + Product memory product = _productToken._getProductByTokenAddress(); + if (product.status != ProductStatus.Ordered) { + revert ProductNotOrdered(_productToken); + } + + _takeCollateralFromTransporter(_productToken, _quantity); + + SupplyChainStorage storage $ = _supplyChainStorage(); + $.transporterToProducts[transporter].add(_productToken); + $.productToTransporter[_productToken] = transporter; + $.transporterToProductSupplyChainStatus[transporter][_productToken] = SupplyChainStatus.Assigned; + LibProduct._productStorage().tokenToProduct[_productToken].status = ProductStatus.Assigned; + + emit ProductFulfilled(transporter, _productToken, _quantity); + } + + function _transporterUpdateStatus(address _productToken, SupplyChainStatus _status) internal { + address transporter = LibContext._msgSender(); + if (!transporter._hasActiveRole(Role.Transporter)) revert NotTransporter(transporter); + if (_productToken._getProductByTokenAddress().status != ProductStatus.Assigned) { + revert ProductNotAssigned(_productToken); + } + + SupplyChainStorage storage $ = _supplyChainStorage(); + $.productSupplyStatus[_productToken] = _status; + $.transporterToProductSupplyChainStatus[transporter][_productToken] = _status; + + emit ProductStatusUpdated(transporter, _productToken, _status); + } + + function _retailerReceiveProduct(address _productToken, int64 _quantity) internal { + address retailer = LibContext._msgSender(); + if (!retailer._hasActiveRole(Role.Retailer)) revert NotRetailer(retailer); + + Product memory product = _productToken._getProductByTokenAddress(); + if (product.status != ProductStatus.ForSale) revert ProductNotAvailable(_productToken); + + _productToken.safeTransferFromToken(address(this), retailer, _quantity); + + SupplyChainStorage storage $ = _supplyChainStorage(); + $.retailerToProducts[retailer].add(_productToken); + $.productToRetailers[_productToken].add(retailer); + + _paySupplier(_productToken, product.supplier, _quantity); + _payTransporter(_productToken, $.productToTransporter[_productToken], _quantity); + + emit ProductBought(retailer, _productToken, _quantity); + } + + function _calculateFees(address _productToken, int64 _quantity) + private + returns (uint256 platformFee_, uint256 supplierPay_, uint256 transporterPay_, uint256 totalFee_) + { + (IHederaTokenService.FixedFee[] memory fixedFees, IHederaTokenService.FractionalFee[] memory fractionalFees,) = + _productToken.safeGetTokenCustomFees(); + int64 platformUnitFee = (fixedFees[0].amount * fractionalFees[0].numerator) / fractionalFees[0].denominator; + platformFee_ = uint256(int256(platformUnitFee * _quantity)); + int64 supplierUnitPay = fixedFees[0].amount - platformUnitFee; + supplierPay_ = uint256(int256(supplierUnitPay * _quantity)); + int64 transporterUnitPay = fixedFees[1].amount; + transporterPay_ = uint256(int256(transporterUnitPay * _quantity)); + totalFee_ = platformFee_ + supplierPay_ + transporterPay_; + } + + function _paySupplier(address _productToken, address _supplier, int64 _quantity) private { + (uint256 supplierPay,,,) = _calculateFees(_productToken, _quantity); + payable(_supplier).sendValue(supplierPay); + } + + function _takeCollateralFromTransporter(address _productToken, int64 _quantity) private { + (,, uint256 transporterPay,) = _calculateFees(_productToken, _quantity); + payable(address(this)).sendValue(transporterPay); + } + + function _payTransporter(address _productToken, address _transporter, int64 _quantity) private { + (,, uint256 transporterPay,) = _calculateFees(_productToken, _quantity); + payable(_transporter).sendValue(transporterPay * 2); + } + + function _refundRetailer(address _productToken, address _retailer, int64 _quantity) private { + (uint256 platformFee,,, uint256 totalFee) = _calculateFees(_productToken, _quantity); + payable(_retailer).sendValue(totalFee - platformFee); + } + + // TODO: implement dutch auction + // function _getTransportFee(address _productToken) internal view returns (uint256) { + // Product memory product = _productToken._getProductByTokenAddress(); + // uint256 timeElapsed = block.timestamp - product.updated; + // return uint256(int256(product.transportFee)) - (DISCOUNT_RATE * timeElapsed); + // } + + function _getAllActiveDeliveriesAddress() internal view returns (address[] memory) { + return _supplyChainStorage().activeProductDeliveries.values(); + } + + function _getAllActiveDeliveries() internal view returns (Product[] memory products_) { + address[] memory addresses = _getAllActiveDeliveriesAddress(); + uint256 addressesLength = addresses.length; + products_ = new Product[](addressesLength); + for (uint256 i; i < addressesLength; ++i) { + products_[i] = LibProduct._getProductByTokenAddress(addresses[i]); + } + } + + function _getProductSupplyChainStatus(address _productToken) internal view returns (SupplyChainStatus) { + return _supplyChainStorage().productSupplyStatus[_productToken]; + } + + function _getRetailerOrders(address _retailer) internal view returns (address[] memory) { + return _supplyChainStorage().retailerToProducts[_retailer].values(); + } + + function _getTransporterOrders(address _transporter) internal view returns (address[] memory) { + return _supplyChainStorage().transporterToProducts[_transporter].values(); + } + + function _getSupplierOrders(address _supplier) internal view returns (address[] memory) { + return _supplyChainStorage().supplierToProducts[_supplier].values(); + } + + function _getProductRetailers(address _productToken) internal view returns (address[] memory) { + return _supplyChainStorage().productToRetailers[_productToken].values(); + } + + function _getProductTransporter(address _productToken) internal view returns (address) { + return _supplyChainStorage().productToTransporter[_productToken]; + } + + function _getProductSuppliers(address _productToken) internal view returns (address[] memory) { + return _supplyChainStorage().productToSuppliers[_productToken].values(); } } From 2c775a10a4f270f7a62e13f8a396a12585cddeed Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:47:10 +0100 Subject: [PATCH 23/26] chore use safe hts lib and update logic --- src/libraries/LibProduct.sol | 212 ++++++++++++++++++++--------------- 1 file changed, 119 insertions(+), 93 deletions(-) diff --git a/src/libraries/LibProduct.sol b/src/libraries/LibProduct.sol index 7caa50a..9803c90 100644 --- a/src/libraries/LibProduct.sol +++ b/src/libraries/LibProduct.sol @@ -3,25 +3,29 @@ pragma solidity 0.8.30; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import {IHederaTokenService} from "hedera-token-service/IHederaTokenService.sol"; -import {HederaResponseCodes} from "hedera-system-contracts/HederaResponseCodes.sol"; -import {LibHederaTokenService} from "@chronicle/libraries/hts/LibHederaTokenService.sol"; +import {LibSafeHTS} from "@chronicle/libraries/hts/safe-hts/LibSafeHTS.sol"; import {LibContext} from "@chronicle/libraries/LibContext.sol"; import {Role} from "@chronicle-types/PartyStorage.sol"; import {KeyType, KeyValueType} from "@chronicle-types/KeyHelperStorage.sol"; -import { - Status, Product, ProductStorage, PRODUCT_STORAGE_SLOT, USDC_ADDRESS -} from "@chronicle-types/ProductStorage.sol"; +import {ProductStatus, Product, ProductStorage, PRODUCT_STORAGE_SLOT} from "@chronicle-types/ProductStorage.sol"; import {LibParty} from "@chronicle/libraries/LibParty.sol"; import {LibKeyHelper} from "@chronicle/libraries/hts/LibKeyHelper.sol"; import {LibFeeHelper} from "@chronicle/libraries/hts/LibFeeHelper.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) import "@chronicle-logs/ProductLogs.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@chronicle/libraries/errors/ProductErrors.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@chronicle/libraries/errors/PartyErrors.sol"; +/// forge-lint: disable-next-line(unaliased-plain-import) +import "@chronicle/libraries/errors/SupplyChainErrors.sol"; library LibProduct { using LibParty for address; using LibKeyHelper for KeyType; using LibFeeHelper for int64; - using LibHederaTokenService for IHederaTokenService.HederaToken; - using LibHederaTokenService for address; + using LibSafeHTS for address; + using LibSafeHTS for IHederaTokenService.HederaToken; using EnumerableSet for EnumerableSet.AddressSet; //*////////////////////////////////////////////////////////////////////////// @@ -36,93 +40,119 @@ library LibProduct { //*////////////////////////////////////////////////////////////////////////// // INTERNAL FUNCTIONS //////////////////////////////////////////////////////////////////////////*// - function _addProduct(string calldata _name, string calldata _memo, int64 _price, int64 _initialSupply) internal { - address sender = LibContext._msgSender(); - if (!sender._hasActiveRole(Role.Supplier)) revert("Not a Supplier"); + function _addProduct( + string calldata _name, + string calldata _memo, + int64 _price, + int64 _transporterFee, + int64 _initialSupply + ) internal { + address supplier = LibContext._msgSender(); + if (!supplier._hasActiveRole(Role.Supplier)) revert NotSupplier(supplier); - address tokenAddress = _createProductToken(_name, _memo, _price); - if (tokenAddress == address(0)) revert("Invalid token address"); + address productToken = _createProductToken(_name, _memo, _price, _transporterFee, _initialSupply); + if (productToken == address(0)) revert InvalidTokenAddress(); - _associateProductToken(address(this), tokenAddress); - _associateProductToken(sender, tokenAddress); + productToken.safeAssociateToken(supplier); - (int64 newTotalSupply, int64[] memory serialNumbers) = _mintProductToken(tokenAddress, _initialSupply); + int64 newTotalSupply = _mintProductToken(productToken, _initialSupply); ProductStorage storage $ = _productStorage(); - $.activeProducts.add(tokenAddress); + $.activeProducts.add(productToken); Product memory product = Product({ id: uint32($.activeProducts.length()), - tokenAddress: tokenAddress, + tokenAddress: productToken, name: _name, memo: _memo, price: _price, + transportFee: _transporterFee, totalSupply: newTotalSupply, - owner: sender, - status: Status.ForSale, - timestamp: uint40(block.timestamp) + supplier: supplier, + status: ProductStatus.ForSale, + created: uint40(block.timestamp), + updated: uint40(block.timestamp) }); - $.tokenToProduct[tokenAddress] = product; - $.supplierToProducts[sender].add(tokenAddress); + $.tokenToProduct[productToken] = product; + $.supplierToProducts[supplier].add(productToken); - emit ProductCreated(product, serialNumbers); + emit ProductCreated(product); } - function _updateProduct(address _tokenAddress, string calldata _name, string calldata _memo, int64 _price) - internal - { + function _updateProduct( + address _productToken, + string calldata _name, + string calldata _memo, + int64 _price, + int64 _transporterFee + ) internal { + if (_productToken == address(0)) revert InvalidTokenAddress(); + ProductStorage storage $ = _productStorage(); - if (!$.activeProducts.contains(_tokenAddress)) revert("Invalid token address"); - if ($.tokenToProduct[_tokenAddress].owner != LibContext._msgSender()) revert("Not the owner"); - if ($.tokenToProduct[_tokenAddress].status != Status.ForSale) revert("Product sold"); + if (!$.activeProducts.contains(_productToken)) revert ProductInactive(_productToken); + if ($.tokenToProduct[_productToken].supplier != LibContext._msgSender()) { + revert NotSupplier($.tokenToProduct[_productToken].supplier); + } + if ($.tokenToProduct[_productToken].status != ProductStatus.ForSale) revert ProductSold(_productToken); - _updateProductTokenInfo(_tokenAddress, _getProductToken(_name, _memo)); - _updateProductTokenFees(_tokenAddress, _price); + _productToken.safeUpdateTokenInfo(_getProductToken(_name, _memo)); + _productToken.safeUpdateTokenKeys(_getProductTokenKeys()); + _updateProductToken(_productToken, _price, _transporterFee); - Product memory product = $.tokenToProduct[_tokenAddress]; + Product memory product = $.tokenToProduct[_productToken]; product.name = _name; product.memo = _memo; product.price = _price; - $.tokenToProduct[_tokenAddress] = product; + product.transportFee = _transporterFee; + product.updated = uint40(block.timestamp); + $.tokenToProduct[_productToken] = product; emit ProductUpdated(product); } - function _increaseProductQuantity(address _tokenAddress, int64 _quantity) internal { + function _increaseProductQuantity(address _productToken, int64 _quantity) internal { + if (_productToken == address(0)) revert InvalidTokenAddress(); + ProductStorage storage $ = _productStorage(); - if (!$.activeProducts.contains(_tokenAddress)) revert("Invalid token address"); - if ($.tokenToProduct[_tokenAddress].owner != LibContext._msgSender()) revert("Not the owner"); - if ($.tokenToProduct[_tokenAddress].status != Status.ForSale) revert("Product sold"); + if (!$.activeProducts.contains(_productToken)) revert ProductInactive(_productToken); + if ($.tokenToProduct[_productToken].supplier != LibContext._msgSender()) { + revert NotSupplier($.tokenToProduct[_productToken].supplier); + } + if ($.tokenToProduct[_productToken].status != ProductStatus.ForSale) revert ProductSold(_productToken); - (int64 newTotalSupply, int64[] memory serialNumbers) = _mintProductToken(_tokenAddress, _quantity); + int64 newTotalSupply = _mintProductToken(_productToken, _quantity); - Product memory product = $.tokenToProduct[_tokenAddress]; + Product memory product = $.tokenToProduct[_productToken]; product.totalSupply = newTotalSupply; - $.tokenToProduct[_tokenAddress] = product; + $.tokenToProduct[_productToken] = product; - emit ProductQuantityIncreased(product, serialNumbers); + emit ProductQuantityIncreased(product); } - function _decreaseProductQuantity(address _tokenAddress, int64 _quantity, int64[] memory _serialNumbers) internal { + function _decreaseProductQuantity(address _productToken, int64 _quantity) internal { + if (_productToken == address(0)) revert InvalidTokenAddress(); + ProductStorage storage $ = _productStorage(); - if (!$.activeProducts.contains(_tokenAddress)) revert("Invalid token address"); - if ($.tokenToProduct[_tokenAddress].owner != LibContext._msgSender()) revert("Not the owner"); - if ($.tokenToProduct[_tokenAddress].status != Status.ForSale) revert("Product sold"); + if (!$.activeProducts.contains(_productToken)) revert ProductInactive(_productToken); + if ($.tokenToProduct[_productToken].supplier != LibContext._msgSender()) { + revert NotSupplier($.tokenToProduct[_productToken].supplier); + } + if ($.tokenToProduct[_productToken].status != ProductStatus.ForSale) revert ProductSold(_productToken); - (int256 burnResponseCode, int64 newTotalSupply) = _tokenAddress.burnToken(_quantity, _serialNumbers); - if (burnResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to burn product"); + int64[] memory serialNumbers; + int64 newTotalSupply = _productToken.safeBurnToken(_quantity, serialNumbers); - Product memory product = $.tokenToProduct[_tokenAddress]; + Product memory product = $.tokenToProduct[_productToken]; product.totalSupply = newTotalSupply; - $.tokenToProduct[_tokenAddress] = product; + $.tokenToProduct[_productToken] = product; - emit ProductQuantityDecreased(product, _serialNumbers); + emit ProductQuantityDecreased(product); } //*////////////////////////////////////////////////////////////////////////// // VIEW FUNCTIONS //////////////////////////////////////////////////////////////////////////*// - function _getProductByTokenAddress(address _tokenAddress) internal view returns (Product memory) { - return _productStorage().tokenToProduct[_tokenAddress]; + function _getProductByTokenAddress(address _productToken) internal view returns (Product memory) { + return _productStorage().tokenToProduct[_productToken]; } function _getAllProductsTokenAddress() internal view returns (address[] memory) { @@ -166,12 +196,12 @@ library LibProduct { } } - function _getSupplierProductTokenAddresses(address _owner) internal view returns (address[] memory) { - return _productStorage().supplierToProducts[_owner].values(); + function _getSupplierProductTokenAddresses(address _supplier) internal view returns (address[] memory) { + return _productStorage().supplierToProducts[_supplier].values(); } - function _getSupplierProducts(address _owner) internal view returns (Product[] memory products_) { - address[] memory tokenAddresses = _getSupplierProductTokenAddresses(_owner); + function _getSupplierProducts(address _supplier) internal view returns (Product[] memory products_) { + address[] memory tokenAddresses = _getSupplierProductTokenAddresses(_supplier); products_ = new Product[](tokenAddresses.length); for (uint256 i; i < tokenAddresses.length; ++i) { products_[i] = _getProductByTokenAddress(tokenAddresses[i]); @@ -181,53 +211,49 @@ library LibProduct { //*////////////////////////////////////////////////////////////////////////// // PRIVATE FUNCTIONS //////////////////////////////////////////////////////////////////////////*// - function _updateProductTokenInfo(address _tokenAddress, IHederaTokenService.HederaToken memory tokenInfo) private { - int256 responseCode = _tokenAddress.updateTokenInfo(tokenInfo); - if (responseCode != HederaResponseCodes.SUCCESS) revert("Failed to update product token info"); + function _createProductToken( + string calldata _name, + string calldata _memo, + int64 _price, + int64 _transporterFee, + int64 _initialSupply + ) private returns (address) { + (IHederaTokenService.FixedFee[] memory fixedFees, IHederaTokenService.FractionalFee[] memory fractionalFee) = + _getProductFees(_price, _transporterFee); + address productToken = _getProductToken(_name, _memo).safeCreateFungibleTokenWithCustomFees( + _initialSupply, 0, fixedFees, fractionalFee + ); + return productToken; } - function _updateProductTokenFees(address _tokenAddress, int64 _price) private { - (IHederaTokenService.FixedFee[] memory fixedFees, IHederaTokenService.RoyaltyFee[] memory royaltyFees) = - _getProductFees(_price); - int256 responseCode = _tokenAddress.updateNonFungibleTokenCustomFees(fixedFees, royaltyFees); - if (responseCode != HederaResponseCodes.SUCCESS) revert("Failed to update product token fees"); - } - - function _createProductToken(string calldata _name, string calldata _memo, int64 _price) - private - returns (address) - { - (IHederaTokenService.FixedFee[] memory fixedFees, IHederaTokenService.RoyaltyFee[] memory royaltyFees) = - _getProductFees(_price); - (int256 createResponseCode, address tokenAddress) = - _getProductToken(_name, _memo).createNonFungibleTokenWithCustomFees(fixedFees, royaltyFees); - if (createResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to create product"); - return tokenAddress; + function _mintProductToken(address _productToken, int64 _initialSupply) private returns (int64) { + bytes[] memory metadata; + (int64 newTotalSupply,) = _productToken.safeMintToken(_initialSupply, metadata); + return newTotalSupply; } - function _associateProductToken(address _party, address _tokenAddress) private { - (int256 associateResponseCode) = _party.associateToken(_tokenAddress); - if ( - associateResponseCode != HederaResponseCodes.SUCCESS - || associateResponseCode != HederaResponseCodes.TOKEN_ALREADY_ASSOCIATED_TO_ACCOUNT - ) revert("Failed to associate token"); - } + function _updateProductToken(address _productToken, int64 _price, int64 _transporterFee) private { + (IHederaTokenService.FixedFee[] memory fixedFees, IHederaTokenService.FractionalFee[] memory fractionalFees) = + _getProductFees(_price, _transporterFee); - function _mintProductToken(address _tokenAddress, int64 _initialSupply) private returns (int64, int64[] memory) { - bytes[] memory metadata; - (int256 mintResponseCode, int64 newTotalSupply, int64[] memory serialNumbers) = - _tokenAddress.mintToken(_initialSupply, metadata); - if (mintResponseCode != HederaResponseCodes.SUCCESS) revert("Failed to mint product"); - return (newTotalSupply, serialNumbers); + _productToken.safeUpdateFungibleTokenCustomFees(fixedFees, fractionalFees); } - function _getProductFees(int64 _price) + function _getProductFees(int64 _price, int64 _transporterFee) private view - returns (IHederaTokenService.FixedFee[] memory fixedFees_, IHederaTokenService.RoyaltyFee[] memory royaltyFees_) + returns ( + IHederaTokenService.FixedFee[] memory fixedFees_, + IHederaTokenService.FractionalFee[] memory fractionalFee_ + ) { - fixedFees_ = _price.createSingleFixedFeeForHbars(address(this)); - royaltyFees_ = LibFeeHelper.createRoyaltyFeesWithAllTypes(1, 1000, _price, USDC_ADDRESS, address(this)); + fixedFees_ = new IHederaTokenService.FixedFee[](2); + // supplier fee + fixedFees_[0] = _price.createFixedFeeForHbars(LibContext._msgSender()); + // transporter fee + fixedFees_[1] = _transporterFee.createFixedFeeForHbars(LibContext._msgSender()); + // 1% platform fee + fractionalFee_ = LibFeeHelper.createSingleFractionalFee(100, 10000, false, address(this)); } function _getProductToken(string calldata _name, string calldata _memo) From 282ad2ee59cfc111dc7673b8bc72b8457d201612 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:47:52 +0100 Subject: [PATCH 24/26] chore: update functions to match lib --- src/facets/ProductsFacet.sol | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/facets/ProductsFacet.sol b/src/facets/ProductsFacet.sol index f91e24e..a49e448 100644 --- a/src/facets/ProductsFacet.sol +++ b/src/facets/ProductsFacet.sol @@ -7,22 +7,32 @@ import {Product} from "@chronicle-types/ProductStorage.sol"; contract ProductsFacet { using LibProduct for *; - function addProduct(string calldata _name, string calldata _memo, int64 _price, int64 _initialSupply) external { - _name._addProduct(_memo, _price, _initialSupply); + function addProduct( + string calldata _name, + string calldata _memo, + int64 _price, + int64 _transporterFee, + int64 _initialSupply + ) external { + _name._addProduct(_memo, _price, _transporterFee, _initialSupply); } - function updateProduct(address _tokenAddress, string calldata _name, string calldata _memo, int64 _price) - external - { - _tokenAddress._updateProduct(_name, _memo, _price); + function updateProduct( + address _tokenAddress, + string calldata _name, + string calldata _memo, + int64 _price, + int64 _transporterFee + ) external { + _tokenAddress._updateProduct(_name, _memo, _price, _transporterFee); } function increaseProductQuantity(address _tokenAddress, int64 _quantity) external { _tokenAddress._increaseProductQuantity(_quantity); } - function decreaseProductQuantity(address _tokenAddress, int64 _quantity, int64[] memory _serialNumbers) external { - _tokenAddress._decreaseProductQuantity(_quantity, _serialNumbers); + function decreaseProductQuantity(address _tokenAddress, int64 _quantity) external { + _tokenAddress._decreaseProductQuantity(_quantity); } function getProductByTokenAddress(address _tokenAddress) public view returns (Product memory) { @@ -45,11 +55,11 @@ contract ProductsFacet { return LibProduct._getProductsByRange(_start, _end); } - function getSupplierProductTokenAddresses(address _owner) public view returns (address[] memory) { - return _owner._getSupplierProductTokenAddresses(); + function getSupplierProductTokenAddresses(address _supplier) public view returns (address[] memory) { + return _supplier._getSupplierProductTokenAddresses(); } - function getSupplierProducts(address _owner) public view returns (Product[] memory) { - return _owner._getSupplierProducts(); + function getSupplierProducts(address _supplier) public view returns (Product[] memory) { + return _supplier._getSupplierProducts(); } } From 0640be324993d65b5161a977c37ef5f5dab2e3b7 Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:49:09 +0100 Subject: [PATCH 25/26] feat: supply chain facet --- src/facets/SupplyChainFacet.sol | 60 ++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/src/facets/SupplyChainFacet.sol b/src/facets/SupplyChainFacet.sol index 71eb099..e2381ac 100644 --- a/src/facets/SupplyChainFacet.sol +++ b/src/facets/SupplyChainFacet.sol @@ -1,4 +1,62 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.30; -contract SupplyChainFacet {} +import {LibSupplyChain} from "@chronicle/libraries/LibSupplyChain.sol"; +import {SupplyChainStatus} from "@chronicle-types/SupplyChainStorage.sol"; +import {Product} from "@chronicle-types/ProductStorage.sol"; + +contract SupplyChainFacet { + using LibSupplyChain for address; + + function retailerOrderProduct(address _productToken, int64 _quantity) external payable { + _productToken._retailerOrderProduct(_quantity); + } + + function transporterSelectOrder(address _productToken, int64 _quantity) external payable { + _productToken._transporterSelectOrder(_quantity); + } + + function transporterUpdateStatus(address _productToken, SupplyChainStatus _status) external { + _productToken._transporterUpdateStatus(_status); + } + + function retailerReceiveProduct(address _productToken, int64 _quantity) external payable { + _productToken._retailerReceiveProduct(_quantity); + } + + function getActiveDeliveries() external view returns (address[] memory) { + return LibSupplyChain._getAllActiveDeliveriesAddress(); + } + + function getAllActiveDeliveries() external view returns (Product[] memory) { + return LibSupplyChain._getAllActiveDeliveries(); + } + + function getRetailerOrders(address _retailer) external view returns (address[] memory) { + return LibSupplyChain._getRetailerOrders(_retailer); + } + + function getTransporterOrders(address _transporter) external view returns (address[] memory) { + return LibSupplyChain._getTransporterOrders(_transporter); + } + + function getSupplierOrders(address _supplier) external view returns (address[] memory) { + return LibSupplyChain._getSupplierOrders(_supplier); + } + + function getProductRetailers(address _productToken) external view returns (address[] memory) { + return LibSupplyChain._getProductRetailers(_productToken); + } + + function getProductTransporter(address _productToken) external view returns (address) { + return LibSupplyChain._getProductTransporter(_productToken); + } + + function getProductSuppliers(address _productToken) external view returns (address[] memory) { + return LibSupplyChain._getProductSuppliers(_productToken); + } + + function getProductSupplyChainStatus(address _productToken) external view returns (SupplyChainStatus) { + return LibSupplyChain._getProductSupplyChainStatus(_productToken); + } +} From 3d63a9895be620fb3ed623a2e53ba21307e8d0af Mon Sep 17 00:00:00 2001 From: David Dada Date: Fri, 8 Aug 2025 20:49:34 +0100 Subject: [PATCH 26/26] feat: supply chain storage --- src/libraries/types/SupplyChainStorage.sol | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/libraries/types/SupplyChainStorage.sol diff --git a/src/libraries/types/SupplyChainStorage.sol b/src/libraries/types/SupplyChainStorage.sol new file mode 100644 index 0000000..b828b53 --- /dev/null +++ b/src/libraries/types/SupplyChainStorage.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +// keccak256(abi.encode(uint256(keccak256("chronicle.supplychain.storage")) - 1)) & ~bytes32(uint256(0xff)); +bytes32 constant SUPPLYCHAIN_STORAGE_SLOT = 0xd78b51da6bf3447d06fc134310e80d3fca28d8b4ce3b67d10493eb303d209800; +// uint256(keccak256("chronicle.supplychain.admin")); +uint256 constant SUPPLYCHAIN_ADMIN_ROLE = 56670119067705456134706792050423419419426626061535994049731000446794313470214; +uint256 constant DELIVERY_DURATION = 7 days; +uint256 constant DISCOUNT_RATE = 100 wei; + +struct SupplyChainStorage { + mapping(address => EnumerableSet.AddressSet) retailerToProducts; + mapping(address => EnumerableSet.AddressSet) transporterToProducts; + mapping(address => EnumerableSet.AddressSet) supplierToProducts; + mapping(address => EnumerableSet.AddressSet) productToRetailers; + mapping(address => address) productToTransporter; + mapping(address => EnumerableSet.AddressSet) productToSuppliers; + mapping(address => SupplyChainStatus) productSupplyStatus; + mapping(address => mapping(address => SupplyChainStatus)) transporterToProductSupplyChainStatus; + EnumerableSet.AddressSet allOrderedProducts; + EnumerableSet.AddressSet activeProductDeliveries; +} + +enum SupplyChainStatus { + None, + Assigned, + EnRouteToPickup, + AtPickupLocation, + Loading, + PickedUp, + InTransit, + AtDeliveryLocation, + Unloading, + Delivered, + Delayed, + IssueReported +}