feat: ERC-1155 同质化券系统 + denom统一为agnx (18位EVM标准)

新增ERC-1155双模式券系统(Utility Track专用):
- ICouponBatch接口 + CouponBatch核心合约(FACTORY/BURNER角色)
- Redemption1155兑付合约(余额/过期/门店/黑名单验证)
- CouponFactory新增mintBatch1155路由(上限100,000张O(1)铸造)
- Deploy.s.sol部署CouponBatch+Redemption1155+角色授权
- 46个新测试(CouponBatch 16 + Redemption1155 9 + Factory 11 + Integration 5 + fuzz)

Denom统一: ugnx(6位)→agnx(18位EVM标准), 金额×10^12

全量测试143/143通过,编译零错误。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-15 22:34:30 -08:00
parent 0ea869ac46
commit 4c1d907df6
12 changed files with 1322 additions and 22 deletions

View File

@ -4,7 +4,7 @@
# ======================== # ========================
# 基本配置 # 基本配置
# ======================== # ========================
minimum-gas-prices = "0ugnx" # 平台全额补贴,用户零 Gas minimum-gas-prices = "0agnx" # 平台全额补贴,用户零 Gasagnx = atto GNX, 18位精度
pruning = "default" pruning = "default"
pruning-keep-recent = "100" pruning-keep-recent = "100"
pruning-keep-every = "0" pruning-keep-every = "0"

View File

@ -39,8 +39,8 @@
}, },
"supply": [ "supply": [
{ {
"denom": "ugnx", "denom": "agnx",
"amount": "1000000000000000" "amount": "1000000000000000000000000000"
} }
], ],
"balances": [ "balances": [
@ -48,35 +48,35 @@
"_comment": "平台运营/Gas补贴池 (40%)", "_comment": "平台运营/Gas补贴池 (40%)",
"address": "genex1platform_operations_address", "address": "genex1platform_operations_address",
"coins": [ "coins": [
{ "denom": "ugnx", "amount": "400000000000000" } { "denom": "agnx", "amount": "400000000000000000000000000" }
] ]
}, },
{ {
"_comment": "团队与顾问 (20%) — 4年线性释放1年锁定", "_comment": "团队与顾问 (20%) — 4年线性释放1年锁定",
"address": "genex1team_vesting_address", "address": "genex1team_vesting_address",
"coins": [ "coins": [
{ "denom": "ugnx", "amount": "200000000000000" } { "denom": "agnx", "amount": "200000000000000000000000000" }
] ]
}, },
{ {
"_comment": "生态基金 (15%)", "_comment": "生态基金 (15%)",
"address": "genex1ecosystem_fund_address", "address": "genex1ecosystem_fund_address",
"coins": [ "coins": [
{ "denom": "ugnx", "amount": "150000000000000" } { "denom": "agnx", "amount": "150000000000000000000000000" }
] ]
}, },
{ {
"_comment": "未来融资预留 (15%)", "_comment": "未来融资预留 (15%)",
"address": "genex1future_financing_address", "address": "genex1future_financing_address",
"coins": [ "coins": [
{ "denom": "ugnx", "amount": "150000000000000" } { "denom": "agnx", "amount": "150000000000000000000000000" }
] ]
}, },
{ {
"_comment": "社区治理 DAO (10%)", "_comment": "社区治理 DAO (10%)",
"address": "genex1community_dao_address", "address": "genex1community_dao_address",
"coins": [ "coins": [
{ "denom": "ugnx", "amount": "100000000000000" } { "denom": "agnx", "amount": "100000000000000000000000000" }
] ]
} }
], ],
@ -84,11 +84,11 @@
{ {
"description": "Genex Chain 原生代币", "description": "Genex Chain 原生代币",
"denom_units": [ "denom_units": [
{ "denom": "ugnx", "exponent": 0, "aliases": ["micrognx"] }, { "denom": "agnx", "exponent": 0, "aliases": ["attognx"] },
{ "denom": "mgnx", "exponent": 3, "aliases": ["millignx"] }, { "denom": "ngnx", "exponent": 9, "aliases": ["nanognx"] },
{ "denom": "gnx", "exponent": 6, "aliases": ["GNX"] } { "denom": "gnx", "exponent": 18, "aliases": ["GNX"] }
], ],
"base": "ugnx", "base": "agnx",
"display": "gnx", "display": "gnx",
"name": "Genex Token", "name": "Genex Token",
"symbol": "GNX" "symbol": "GNX"
@ -101,7 +101,7 @@
"max_validators": 100, "max_validators": 100,
"max_entries": 7, "max_entries": 7,
"historical_entries": 10000, "historical_entries": 10000,
"bond_denom": "ugnx", "bond_denom": "agnx",
"min_commission_rate": "0.050000000000000000" "min_commission_rate": "0.050000000000000000"
}, },
"validators": [] "validators": []
@ -118,7 +118,7 @@
"gov": { "gov": {
"params": { "params": {
"min_deposit": [ "min_deposit": [
{ "denom": "ugnx", "amount": "10000000" } { "denom": "agnx", "amount": "10000000000000000000" }
], ],
"max_deposit_period": "172800s", "max_deposit_period": "172800s",
"voting_period": "172800s", "voting_period": "172800s",
@ -129,7 +129,7 @@
}, },
"evm": { "evm": {
"params": { "params": {
"evm_denom": "ugnx", "evm_denom": "agnx",
"enable_create": true, "enable_create": true,
"enable_call": true, "enable_call": true,
"extra_eips": [], "extra_eips": [],
@ -171,7 +171,7 @@
"_comment": "Genex 自定义合规模块创世状态", "_comment": "Genex 自定义合规模块创世状态",
"params": { "params": {
"enabled": true, "enabled": true,
"travel_rule_threshold": "3000000000", "travel_rule_threshold": "3000000000000000000000",
"structuring_window": "86400", "structuring_window": "86400",
"ofac_update_interval": "3600" "ofac_update_interval": "3600"
}, },

View File

@ -14,6 +14,8 @@ import "../src/Treasury.sol";
import "../src/Governance.sol"; import "../src/Governance.sol";
import "../src/ExchangeRateOracle.sol"; import "../src/ExchangeRateOracle.sol";
import "../src/CouponBackedSecurity.sol"; import "../src/CouponBackedSecurity.sol";
import "../src/CouponBatch.sol";
import "../src/Redemption1155.sol";
/// @title Deploy Genex /// @title Deploy Genex
/// @notice Transparent Proxy + + /// @notice Transparent Proxy + +
@ -29,6 +31,8 @@ contract Deploy is Script {
address public governance; address public governance;
address public oracle; address public oracle;
address public cbs; address public cbs;
address public couponBatch;
address public redemption1155;
function run() external { function run() external {
uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); uint256 deployerKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
@ -133,7 +137,27 @@ contract Deploy is Script {
console.log("ExchangeRateOracle:", oracle); console.log("ExchangeRateOracle:", oracle);
// ========================================== // ==========================================
// 10. CouponBackedSecurity // 10. CouponBatch (ERC-1155)
// ==========================================
CouponBatch couponBatchImpl = new CouponBatch();
couponBatch = _deployProxy(
address(couponBatchImpl),
abi.encodeCall(CouponBatch.initialize, ("https://genex.io/api/coupon-batch/{id}.json", deployer))
);
console.log("CouponBatch:", couponBatch);
// ==========================================
// 11. Redemption1155
// ==========================================
Redemption1155 redemption1155Impl = new Redemption1155();
redemption1155 = _deployProxy(
address(redemption1155Impl),
abi.encodeCall(Redemption1155.initialize, (couponBatch, compliance, deployer))
);
console.log("Redemption1155:", redemption1155);
// ==========================================
// 12. CouponBackedSecurity
// ========================================== // ==========================================
CouponBackedSecurity cbsImpl = new CouponBackedSecurity(); CouponBackedSecurity cbsImpl = new CouponBackedSecurity();
cbs = _deployProxy( cbs = _deployProxy(
@ -143,7 +167,12 @@ contract Deploy is Script {
console.log("CouponBackedSecurity:", cbs); console.log("CouponBackedSecurity:", cbs);
// ========================================== // ==========================================
// 11. // 13. CouponFactory CouponBatch
// ==========================================
CouponFactory(couponFactory).setCouponBatchContract(couponBatch);
// ==========================================
// 14.
// ========================================== // ==========================================
_grantRoles(deployer); _grantRoles(deployer);
@ -160,6 +189,8 @@ contract Deploy is Script {
console.log("Treasury: ", treasury); console.log("Treasury: ", treasury);
console.log("Governance: ", governance); console.log("Governance: ", governance);
console.log("ExchangeRateOracle: ", oracle); console.log("ExchangeRateOracle: ", oracle);
console.log("CouponBatch: ", couponBatch);
console.log("Redemption1155: ", redemption1155);
console.log("CouponBackedSecurity:", cbs); console.log("CouponBackedSecurity:", cbs);
console.log("========================================="); console.log("=========================================");
} }
@ -191,6 +222,18 @@ contract Deploy is Script {
// Redemption Coupon.burn owner approved // Redemption Coupon.burn owner approved
// Redemption burn ownerOf // Redemption burn ownerOf
// CouponFactory CouponBatch FACTORY_ROLE
CouponBatch(couponBatch).grantRole(
keccak256("FACTORY_ROLE"),
couponFactory
);
// Redemption1155 CouponBatch BURNER_ROLE
CouponBatch(couponBatch).grantRole(
keccak256("BURNER_ROLE"),
redemption1155
);
// Governance MULTISIG_ROLE initialize // Governance MULTISIG_ROLE initialize
console.log("Roles granted successfully"); console.log("Roles granted successfully");

View File

@ -0,0 +1,161 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155SupplyUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "./interfaces/ICouponBatch.sol";
/// @title CouponBatch ERC-1155
/// @notice
/// @dev Utility Track Transparent Proxy
contract CouponBatch is
Initializable,
ERC1155Upgradeable,
ERC1155SupplyUpgradeable,
AccessControlUpgradeable
{
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
bytes32 public constant FACTORY_ROLE = keccak256("FACTORY_ROLE");
uint256 private _nextTypeId;
/// @dev typeId
mapping(uint256 => ICouponBatch.BatchCouponConfig) private _configs;
/// @dev typeId
mapping(uint256 => uint256) private _mintedQuantities;
// --- Events ---
event BatchCouponMinted(
uint256 indexed typeId,
address indexed issuer,
uint256 faceValue,
uint256 quantity
);
event BatchCouponBurned(
uint256 indexed typeId,
address indexed account,
uint256 amount
);
function initialize(string memory uri_, address admin) external initializer {
__ERC1155_init(uri_);
__ERC1155Supply_init();
__AccessControl_init();
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(MINTER_ROLE, admin);
_nextTypeId = 1;
}
/// @notice `quantity` `to`
/// @param to
/// @param faceValue
/// @param quantity
/// @param config
/// @return typeId token type ID
function mint(
address to,
uint256 faceValue,
uint256 quantity,
ICouponBatch.BatchCouponConfig calldata config
) external onlyRole(FACTORY_ROLE) returns (uint256 typeId) {
require(to != address(0), "CouponBatch: zero address");
require(faceValue > 0, "CouponBatch: zero face value");
require(quantity > 0, "CouponBatch: zero quantity");
typeId = _nextTypeId++;
// faceValue issuer
_configs[typeId] = ICouponBatch.BatchCouponConfig({
transferable: config.transferable,
maxPrice: config.maxPrice,
expiryDate: config.expiryDate,
minPurchase: config.minPurchase,
stackable: config.stackable,
allowedStores: config.allowedStores,
issuer: config.issuer,
faceValue: faceValue
});
_mintedQuantities[typeId] = quantity;
// ERC-1155 mint O(1)
_mint(to, typeId, quantity, "");
emit BatchCouponMinted(typeId, config.issuer, faceValue, quantity);
}
/// @notice `amount` typeId
/// @param account
/// @param typeId ID
/// @param amount
function burn(
address account,
uint256 typeId,
uint256 amount
) external {
require(
account == _msgSender()
|| isApprovedForAll(account, _msgSender())
|| hasRole(BURNER_ROLE, _msgSender()),
"CouponBatch: not authorized to burn"
);
_burn(account, typeId, amount);
emit BatchCouponBurned(typeId, account, amount);
}
// --- ---
/// @notice
function getConfig(uint256 typeId) external view returns (ICouponBatch.BatchCouponConfig memory) {
require(exists(typeId), "CouponBatch: nonexistent type");
return _configs[typeId];
}
/// @notice
function getFaceValue(uint256 typeId) external view returns (uint256) {
require(exists(typeId), "CouponBatch: nonexistent type");
return _configs[typeId].faceValue;
}
/// @notice
function getMintedQuantity(uint256 typeId) external view returns (uint256) {
return _mintedQuantities[typeId];
}
// --- ---
/// @dev _beforeTokenTransfer
function _beforeTokenTransfer(
address operator,
address from,
address to,
uint256[] memory ids,
uint256[] memory amounts,
bytes memory data
) internal virtual override(ERC1155Upgradeable, ERC1155SupplyUpgradeable) {
super._beforeTokenTransfer(operator, from, to, ids, amounts, data);
// (from=0) (to=0)
if (from == address(0) || to == address(0)) return;
// token type
for (uint256 i = 0; i < ids.length; i++) {
ICouponBatch.BatchCouponConfig memory config = _configs[ids[i]];
require(config.transferable, "CouponBatch: non-transferable");
}
}
// --- ERC165 ---
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC1155Upgradeable, AccessControlUpgradeable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}

View File

@ -5,16 +5,18 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "./interfaces/ICoupon.sol"; import "./interfaces/ICoupon.sol";
import "./interfaces/ICouponBatch.sol";
import "./interfaces/ICompliance.sol"; import "./interfaces/ICompliance.sol";
/// @title CouponFactory /// @title CouponFactory
/// @notice NFT Utility/Security /// @notice ERC-721 ERC-1155
/// @dev 使 Transparent Proxy /// @dev 使 Transparent Proxy
contract CouponFactory is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable { contract CouponFactory is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
ICoupon public couponContract; ICoupon public couponContract;
ICouponBatch public couponBatchContract;
ICompliance public compliance; ICompliance public compliance;
uint256 public nextBatchId; uint256 public nextBatchId;
@ -40,7 +42,15 @@ contract CouponFactory is Initializable, AccessControlUpgradeable, ReentrancyGua
ICoupon.CouponType couponType ICoupon.CouponType couponType
); );
event CouponContractUpdated(address indexed newCouponContract); event CouponContractUpdated(address indexed newCouponContract);
event CouponBatchContractUpdated(address indexed newCouponBatchContract);
event ComplianceContractUpdated(address indexed newCompliance); event ComplianceContractUpdated(address indexed newCompliance);
event CouponBatch1155Minted(
address indexed issuer,
uint256 indexed batchId,
uint256 typeId,
uint256 faceValue,
uint256 quantity
);
function initialize( function initialize(
address _couponContract, address _couponContract,
@ -142,4 +152,60 @@ contract CouponFactory is Initializable, AccessControlUpgradeable, ReentrancyGua
compliance = ICompliance(_compliance); compliance = ICompliance(_compliance);
emit ComplianceContractUpdated(_compliance); emit ComplianceContractUpdated(_compliance);
} }
// =====================================================
// ERC-1155 Utility Track
// =====================================================
/// @notice ERC-1155 Utility Track
/// @param issuer
/// @param faceValue USDC 6
/// @param quantity 100,000ERC-1155 O(1)
/// @param config
/// @return typeId ERC-1155 token type ID
function mintBatch1155(
address issuer,
uint256 faceValue,
uint256 quantity,
ICouponBatch.BatchCouponConfig calldata config
) external onlyRole(MINTER_ROLE) nonReentrant returns (uint256 typeId) {
require(issuer != address(0), "CouponFactory: zero address issuer");
require(faceValue > 0, "CouponFactory: zero face value");
require(quantity > 0 && quantity <= 100000, "CouponFactory: invalid quantity");
require(
address(couponBatchContract) != address(0),
"CouponFactory: batch contract not set"
);
// Utility Track ERC-1155 Utility
require(config.maxPrice <= faceValue, "Utility: maxPrice <= faceValue");
require(
config.expiryDate <= block.timestamp + 365 days,
"Utility: max 12 months"
);
require(config.expiryDate > block.timestamp, "Utility: expiry must be future");
// CouponBatch
typeId = couponBatchContract.mint(issuer, faceValue, quantity, config);
// tokenIds ERC-1155 使 typeId + quantity
uint256 batchId = nextBatchId++;
batches[batchId] = BatchInfo({
issuer: issuer,
faceValue: faceValue,
quantity: quantity,
couponType: ICoupon.CouponType.Utility,
createdAt: block.timestamp,
tokenIds: new uint256[](0)
});
emit CouponBatch1155Minted(issuer, batchId, typeId, faceValue, quantity);
}
/// @notice CouponBatch (ERC-1155)
function setCouponBatchContract(address _couponBatchContract) external onlyRole(ADMIN_ROLE) {
require(_couponBatchContract != address(0), "CouponFactory: zero address");
couponBatchContract = ICouponBatch(_couponBatchContract);
emit CouponBatchContractUpdated(_couponBatchContract);
}
} }

View File

@ -0,0 +1,123 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import "./interfaces/ICouponBatch.sol";
import "./interfaces/ICompliance.sol";
/// @title Redemption1155 ERC-1155
/// @notice burn
/// @dev Transparent Proxy
contract Redemption1155 is Initializable, AccessControlUpgradeable, ReentrancyGuardUpgradeable {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
ICouponBatch public couponBatchContract;
ICompliance public compliance;
struct RedemptionRecord {
uint256 typeId;
uint256 amount; //
address consumer;
address issuer;
bytes32 storeId;
uint256 redeemedAt;
}
uint256 public totalRedemptions;
mapping(uint256 => RedemptionRecord) public redemptions;
// --- Events ---
event BatchCouponRedeemed(
uint256 indexed typeId,
uint256 amount,
address indexed consumer,
address indexed issuer,
bytes32 storeId,
uint256 redemptionId
);
function initialize(
address _couponBatchContract,
address _compliance,
address admin
) external initializer {
__AccessControl_init();
__ReentrancyGuard_init();
couponBatchContract = ICouponBatch(_couponBatchContract);
compliance = ICompliance(_compliance);
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ADMIN_ROLE, admin);
}
/// @notice `amount` typeId
/// @param typeId ERC-1155 token type ID
/// @param amount
/// @param storeId ID
function redeem(
uint256 typeId,
uint256 amount,
bytes32 storeId
) external nonReentrant {
require(amount > 0, "Redemption1155: zero amount");
//
require(
couponBatchContract.balanceOf(msg.sender, typeId) >= amount,
"Redemption1155: insufficient balance"
);
//
require(!compliance.isBlacklisted(msg.sender), "Redemption1155: blacklisted");
ICouponBatch.BatchCouponConfig memory config = couponBatchContract.getConfig(typeId);
//
require(block.timestamp <= config.expiryDate, "Redemption1155: coupon expired");
//
if (config.allowedStores.length > 0) {
require(
_isAllowedStore(storeId, config.allowedStores),
"Redemption1155: store not allowed"
);
}
//
couponBatchContract.burn(msg.sender, typeId, amount);
//
uint256 redemptionId = totalRedemptions++;
redemptions[redemptionId] = RedemptionRecord({
typeId: typeId,
amount: amount,
consumer: msg.sender,
issuer: config.issuer,
storeId: storeId,
redeemedAt: block.timestamp
});
emit BatchCouponRedeemed(
typeId, amount, msg.sender, config.issuer, storeId, redemptionId
);
}
/// @notice
function getRedemption(uint256 redemptionId) external view returns (RedemptionRecord memory) {
return redemptions[redemptionId];
}
/// @dev
function _isAllowedStore(
bytes32 storeId,
bytes32[] memory allowedStores
) internal pure returns (bool) {
for (uint256 i = 0; i < allowedStores.length; i++) {
if (allowedStores[i] == storeId) return true;
}
return false;
}
}

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title ICouponBatch ERC-1155
/// @notice CouponBatch ERC-1155
/// @dev ERC-1155 Utility Track
interface ICouponBatch {
struct BatchCouponConfig {
bool transferable; //
uint256 maxPrice; // Utility =
uint256 expiryDate; // Unix timestamp
uint256 minPurchase; //
bool stackable; // 使
bytes32[] allowedStores; // =
address issuer; //
uint256 faceValue; // USDC 6
}
function getConfig(uint256 typeId) external view returns (BatchCouponConfig memory);
function getFaceValue(uint256 typeId) external view returns (uint256);
function totalSupply(uint256 typeId) external view returns (uint256);
function balanceOf(address account, uint256 typeId) external view returns (uint256);
/// @notice
/// @return typeId ERC-1155 token type ID
function mint(
address to,
uint256 faceValue,
uint256 quantity,
BatchCouponConfig calldata config
) external returns (uint256 typeId);
/// @notice
function burn(address account, uint256 typeId, uint256 amount) external;
/// @notice ERC-1155
function safeTransferFrom(
address from,
address to,
uint256 typeId,
uint256 amount,
bytes calldata data
) external;
}

View File

@ -0,0 +1,256 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/CouponBatch.sol";
import "../src/interfaces/ICouponBatch.sol";
contract CouponBatchTest is Test {
CouponBatch batch;
address admin = address(1);
address factory = address(2);
address burner = address(3);
address issuer = address(4);
address user1 = address(5);
address user2 = address(6);
bytes32 storeA = keccak256("STORE_A");
bytes32 storeB = keccak256("STORE_B");
function setUp() public {
vm.startPrank(admin);
batch = new CouponBatch();
batch.initialize("https://genex.io/api/coupon-batch/{id}.json", admin);
batch.grantRole(keccak256("FACTORY_ROLE"), factory);
batch.grantRole(keccak256("BURNER_ROLE"), burner);
vm.stopPrank();
}
// ==========================================
//
// ==========================================
function test_MintCreatesNewType() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 100e6, 1000, config);
assertEq(typeId, 1);
assertEq(batch.balanceOf(issuer, typeId), 1000);
assertEq(batch.totalSupply(typeId), 1000);
assertEq(batch.getMintedQuantity(typeId), 1000);
}
function test_MintMultipleTypes() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.startPrank(factory);
uint256 typeId1 = batch.mint(issuer, 100e6, 500, config);
uint256 typeId2 = batch.mint(issuer, 50e6, 2000, config);
vm.stopPrank();
assertEq(typeId1, 1);
assertEq(typeId2, 2);
assertEq(batch.balanceOf(issuer, typeId1), 500);
assertEq(batch.balanceOf(issuer, typeId2), 2000);
}
function test_MintStoresConfig() public {
bytes32[] memory stores = new bytes32[](2);
stores[0] = storeA;
stores[1] = storeB;
ICouponBatch.BatchCouponConfig memory config = ICouponBatch.BatchCouponConfig({
transferable: false,
maxPrice: 50e6,
expiryDate: block.timestamp + 90 days,
minPurchase: 10,
stackable: true,
allowedStores: stores,
issuer: issuer,
faceValue: 50e6
});
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 50e6, 100, config);
ICouponBatch.BatchCouponConfig memory stored = batch.getConfig(typeId);
assertEq(stored.transferable, false);
assertEq(stored.maxPrice, 50e6);
assertEq(stored.minPurchase, 10);
assertEq(stored.stackable, true);
assertEq(stored.allowedStores.length, 2);
assertEq(stored.issuer, issuer);
assertEq(stored.faceValue, 50e6);
}
function test_MintZeroAddressReverts() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
vm.expectRevert("CouponBatch: zero address");
batch.mint(address(0), 100e6, 1000, config);
}
function test_MintZeroFaceValueReverts() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
vm.expectRevert("CouponBatch: zero face value");
batch.mint(issuer, 0, 1000, config);
}
function test_MintZeroQuantityReverts() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
vm.expectRevert("CouponBatch: zero quantity");
batch.mint(issuer, 100e6, 0, config);
}
function test_MintOnlyFactoryRole() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(user1);
vm.expectRevert();
batch.mint(issuer, 100e6, 1000, config);
}
function test_GetFaceValue() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 250e6, 100, config);
assertEq(batch.getFaceValue(typeId), 250e6);
}
function test_GetConfigNonexistentReverts() public {
vm.expectRevert("CouponBatch: nonexistent type");
batch.getConfig(999);
}
// ==========================================
//
// ==========================================
function test_BurnByOwner() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 100e6, 1000, config);
vm.prank(issuer);
batch.burn(issuer, typeId, 300);
assertEq(batch.balanceOf(issuer, typeId), 700);
assertEq(batch.totalSupply(typeId), 700);
}
function test_BurnByBurnerRole() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 100e6, 1000, config);
vm.prank(burner);
batch.burn(issuer, typeId, 500);
assertEq(batch.balanceOf(issuer, typeId), 500);
}
function test_BurnByApproved() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 100e6, 1000, config);
vm.prank(issuer);
batch.setApprovalForAll(user1, true);
vm.prank(user1);
batch.burn(issuer, typeId, 200);
assertEq(batch.balanceOf(issuer, typeId), 800);
}
function test_BurnUnauthorizedReverts() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 100e6, 1000, config);
vm.prank(user1);
vm.expectRevert("CouponBatch: not authorized to burn");
batch.burn(issuer, typeId, 100);
}
// ==========================================
//
// ==========================================
function test_TransferableTokenCanTransfer() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
config.transferable = true;
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 100e6, 100, config);
vm.prank(issuer);
batch.safeTransferFrom(issuer, user1, typeId, 50, "");
assertEq(batch.balanceOf(issuer, typeId), 50);
assertEq(batch.balanceOf(user1, typeId), 50);
}
function test_NonTransferableTokenReverts() public {
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
config.transferable = false;
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 100e6, 100, config);
vm.prank(issuer);
vm.expectRevert("CouponBatch: non-transferable");
batch.safeTransferFrom(issuer, user1, typeId, 10, "");
}
// ==========================================
// Fuzz
// ==========================================
function testFuzz_MintQuantity(uint256 quantity) public {
quantity = bound(quantity, 1, 100_000);
ICouponBatch.BatchCouponConfig memory config = _defaultConfig();
vm.prank(factory);
uint256 typeId = batch.mint(issuer, 100e6, quantity, config);
assertEq(batch.balanceOf(issuer, typeId), quantity);
assertEq(batch.totalSupply(typeId), quantity);
assertEq(batch.getMintedQuantity(typeId), quantity);
}
// ==========================================
// Helpers
// ==========================================
function _defaultConfig() internal view returns (ICouponBatch.BatchCouponConfig memory) {
bytes32[] memory stores = new bytes32[](0);
return ICouponBatch.BatchCouponConfig({
transferable: true,
maxPrice: 100e6,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: stores,
issuer: issuer,
faceValue: 100e6
});
}
}

View File

@ -6,11 +6,14 @@ import "../src/CouponFactory.sol";
import "../src/Coupon.sol"; import "../src/Coupon.sol";
import "../src/Compliance.sol"; import "../src/Compliance.sol";
import "../src/interfaces/ICoupon.sol"; import "../src/interfaces/ICoupon.sol";
import "../src/CouponBatch.sol";
import "../src/interfaces/ICouponBatch.sol";
contract CouponFactoryTest is Test { contract CouponFactoryTest is Test {
CouponFactory factory; CouponFactory factory;
Coupon coupon; Coupon coupon;
Compliance compliance; Compliance compliance;
CouponBatch couponBatch;
address admin = address(1); address admin = address(1);
address issuer = address(2); address issuer = address(2);
@ -31,9 +34,17 @@ contract CouponFactoryTest is Test {
factory = new CouponFactory(); factory = new CouponFactory();
factory.initialize(address(coupon), address(compliance), admin); factory.initialize(address(coupon), address(compliance), admin);
// CouponBatch
couponBatch = new CouponBatch();
couponBatch.initialize("https://genex.io/api/coupon-batch/{id}.json", admin);
// //
coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory)); coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory));
factory.grantRole(keccak256("MINTER_ROLE"), minter); factory.grantRole(keccak256("MINTER_ROLE"), minter);
couponBatch.grantRole(keccak256("FACTORY_ROLE"), address(factory));
// CouponBatch
factory.setCouponBatchContract(address(couponBatch));
// KYC // KYC
compliance.setKycLevel(issuer, 3); compliance.setKycLevel(issuer, 3);
@ -177,4 +188,120 @@ contract CouponFactoryTest is Test {
faceValue: 1000e6 faceValue: 1000e6
}); });
} }
function _batchConfig() internal view returns (ICouponBatch.BatchCouponConfig memory) {
bytes32[] memory stores = new bytes32[](0);
return ICouponBatch.BatchCouponConfig({
transferable: true,
maxPrice: 100e6,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: stores,
issuer: issuer,
faceValue: 100e6
});
}
// ==========================================
// ERC-1155 (mintBatch1155)
// ==========================================
function test_MintBatch1155Success() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
uint256 typeId = factory.mintBatch1155(issuer, 100e6, 10000, config);
assertEq(typeId, 1);
assertEq(couponBatch.balanceOf(issuer, typeId), 10000);
}
function test_MintBatch1155LargeQuantity() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
uint256 typeId = factory.mintBatch1155(issuer, 100e6, 100000, config);
assertEq(couponBatch.balanceOf(issuer, typeId), 100000);
}
function test_MintBatch1155RecordsBatchInfo() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
factory.mintBatch1155(issuer, 100e6, 5000, config);
CouponFactory.BatchInfo memory info = factory.getBatchInfo(0);
assertEq(info.issuer, issuer);
assertEq(info.faceValue, 100e6);
assertEq(info.quantity, 5000);
assertEq(info.tokenIds.length, 0); // ERC-1155 uses typeId, not tokenIds
}
function test_MintBatch1155ZeroIssuerReverts() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
vm.expectRevert("CouponFactory: zero address issuer");
factory.mintBatch1155(address(0), 100e6, 1000, config);
}
function test_MintBatch1155ZeroFaceValueReverts() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
vm.expectRevert("CouponFactory: zero face value");
factory.mintBatch1155(issuer, 0, 1000, config);
}
function test_MintBatch1155ExcessiveQuantityReverts() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
vm.expectRevert("CouponFactory: invalid quantity");
factory.mintBatch1155(issuer, 100e6, 100001, config);
}
function test_MintBatch1155MaxPriceExceedsFaceValueReverts() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
config.maxPrice = 200e6; //
vm.expectRevert("Utility: maxPrice <= faceValue");
factory.mintBatch1155(issuer, 100e6, 1000, config);
}
function test_MintBatch1155ExpiryTooFarReverts() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
config.expiryDate = block.timestamp + 400 days;
vm.expectRevert("Utility: max 12 months");
factory.mintBatch1155(issuer, 100e6, 1000, config);
}
function test_MintBatch1155ExpiredReverts() public {
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
config.expiryDate = block.timestamp - 1;
vm.expectRevert("Utility: expiry must be future");
factory.mintBatch1155(issuer, 100e6, 1000, config);
}
function test_MintBatch1155BatchContractNotSetReverts() public {
// CouponBatch
vm.startPrank(admin);
CouponFactory factory2 = new CouponFactory();
factory2.initialize(address(coupon), address(compliance), admin);
factory2.grantRole(keccak256("MINTER_ROLE"), minter);
vm.stopPrank();
vm.prank(minter);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
vm.expectRevert("CouponFactory: batch contract not set");
factory2.mintBatch1155(issuer, 100e6, 1000, config);
}
function test_MintBatch1155OnlyMinterRole() public {
vm.prank(issuer);
ICouponBatch.BatchCouponConfig memory config = _batchConfig();
vm.expectRevert();
factory.mintBatch1155(issuer, 100e6, 1000, config);
}
} }

View File

@ -0,0 +1,289 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/CouponFactory.sol";
import "../src/Coupon.sol";
import "../src/CouponBatch.sol";
import "../src/Redemption.sol";
import "../src/Redemption1155.sol";
import "../src/Settlement.sol";
import "../src/Compliance.sol";
import "../src/interfaces/ICoupon.sol";
import "../src/interfaces/ICouponBatch.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockUSDC1155 is ERC20 {
constructor() ERC20("USD Coin", "USDC") {}
function decimals() public pure override returns (uint8) { return 6; }
function mint(address to, uint256 amount) external { _mint(to, amount); }
}
/// @title ERC-721 + ERC-1155
contract Integration1155Test is Test {
CouponFactory factory;
Coupon coupon;
CouponBatch couponBatch;
Settlement settlement;
Redemption redemption;
Redemption1155 redemption1155;
Compliance compliance;
MockUSDC1155 usdc;
address admin = address(1);
address issuer = address(2);
address consumer1 = address(3);
address consumer2 = address(4);
bytes32 storeA = keccak256("STORE_A");
function setUp() public {
vm.startPrank(admin);
usdc = new MockUSDC1155();
//
compliance = new Compliance();
compliance.initialize(admin);
coupon = new Coupon();
coupon.initialize("Genex Coupon", "GXC", admin);
couponBatch = new CouponBatch();
couponBatch.initialize("https://genex.io/api/coupon-batch/{id}.json", admin);
factory = new CouponFactory();
factory.initialize(address(coupon), address(compliance), admin);
settlement = new Settlement();
settlement.initialize(address(coupon), address(compliance), address(usdc), admin);
redemption = new Redemption();
redemption.initialize(address(coupon), address(compliance), admin);
redemption1155 = new Redemption1155();
redemption1155.initialize(address(couponBatch), address(compliance), admin);
//
coupon.grantRole(keccak256("FACTORY_ROLE"), address(factory));
coupon.grantRole(keccak256("SETTLER_ROLE"), address(settlement));
coupon.grantRole(keccak256("SETTLER_ROLE"), address(redemption));
couponBatch.grantRole(keccak256("FACTORY_ROLE"), address(factory));
couponBatch.grantRole(keccak256("BURNER_ROLE"), address(redemption1155));
// CouponBatch
factory.setCouponBatchContract(address(couponBatch));
// KYC
compliance.setKycLevel(issuer, 3);
compliance.setKycLevel(consumer1, 1);
compliance.setKycLevel(consumer2, 1);
//
usdc.mint(consumer1, 500_000e6);
usdc.mint(consumer2, 500_000e6);
vm.stopPrank();
// Approvals
vm.prank(consumer1);
usdc.approve(address(settlement), type(uint256).max);
vm.prank(consumer1);
coupon.setApprovalForAll(address(settlement), true);
vm.prank(issuer);
coupon.setApprovalForAll(address(settlement), true);
}
/// @notice ERC-1155 : Factory铸造
function test_ERC1155FullLifecycle() public {
// Step 1: Factory ERC-1155
bytes32[] memory stores = new bytes32[](0);
ICouponBatch.BatchCouponConfig memory batchConfig = ICouponBatch.BatchCouponConfig({
transferable: true,
maxPrice: 50e6,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: stores,
issuer: issuer,
faceValue: 50e6
});
vm.prank(admin);
uint256 typeId = factory.mintBatch1155(issuer, 50e6, 10000, batchConfig);
assertEq(couponBatch.balanceOf(issuer, typeId), 10000);
assertEq(couponBatch.totalSupply(typeId), 10000);
// Step 2:
vm.prank(issuer);
couponBatch.safeTransferFrom(issuer, consumer1, typeId, 100, "");
assertEq(couponBatch.balanceOf(consumer1, typeId), 100);
assertEq(couponBatch.balanceOf(issuer, typeId), 9900);
// Step 3:
vm.prank(consumer1);
redemption1155.redeem(typeId, 5, storeA);
assertEq(couponBatch.balanceOf(consumer1, typeId), 95);
assertEq(couponBatch.totalSupply(typeId), 9995); // 5
Redemption1155.RedemptionRecord memory record = redemption1155.getRedemption(0);
assertEq(record.consumer, consumer1);
assertEq(record.amount, 5);
}
/// @notice Factory ERC-721 ERC-1155
function test_MixedERC721AndERC1155() public {
// ERC-721
ICoupon.CouponConfig memory config721 = ICoupon.CouponConfig({
couponType: ICoupon.CouponType.Utility,
transferable: true,
maxResaleCount: 3,
maxPrice: 100e6,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: new bytes32[](0),
issuer: issuer,
faceValue: 100e6
});
vm.prank(admin);
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 3, config721);
// ERC-1155
ICouponBatch.BatchCouponConfig memory config1155 = ICouponBatch.BatchCouponConfig({
transferable: true,
maxPrice: 50e6,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: new bytes32[](0),
issuer: issuer,
faceValue: 50e6
});
vm.prank(admin);
uint256 typeId = factory.mintBatch1155(issuer, 50e6, 5000, config1155);
//
assertEq(coupon.ownerOf(tokenIds[0]), issuer);
assertEq(couponBatch.balanceOf(issuer, typeId), 5000);
// BatchInfo nextBatchId
CouponFactory.BatchInfo memory batch0 = factory.getBatchInfo(0);
assertEq(batch0.tokenIds.length, 3); // ERC-721
CouponFactory.BatchInfo memory batch1 = factory.getBatchInfo(1);
assertEq(batch1.tokenIds.length, 0); // ERC-1155
assertEq(batch1.quantity, 5000);
}
/// @notice ERC-721 Settlement ERC-1155 Redemption1155
function test_DualTrackSettlementAndRedemption() public {
// ERC-721: + +
ICoupon.CouponConfig memory config721 = ICoupon.CouponConfig({
couponType: ICoupon.CouponType.Utility,
transferable: true,
maxResaleCount: 3,
maxPrice: 100e6,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: new bytes32[](0),
issuer: issuer,
faceValue: 100e6
});
vm.prank(admin);
uint256[] memory tokenIds = factory.mintBatch(issuer, 100e6, 1, config721);
vm.prank(admin);
settlement.executeSwap(tokenIds[0], consumer1, issuer, 100e6, address(usdc));
assertEq(coupon.ownerOf(tokenIds[0]), consumer1);
vm.prank(consumer1);
redemption.redeem(tokenIds[0], storeA);
// ERC-1155: + +
ICouponBatch.BatchCouponConfig memory config1155 = ICouponBatch.BatchCouponConfig({
transferable: true,
maxPrice: 20e6,
expiryDate: block.timestamp + 90 days,
minPurchase: 0,
stackable: true,
allowedStores: new bytes32[](0),
issuer: issuer,
faceValue: 20e6
});
vm.prank(admin);
uint256 typeId = factory.mintBatch1155(issuer, 20e6, 50000, config1155);
vm.prank(issuer);
couponBatch.safeTransferFrom(issuer, consumer1, typeId, 200, "");
vm.prank(consumer1);
redemption1155.redeem(typeId, 10, storeA);
assertEq(couponBatch.balanceOf(consumer1, typeId), 190);
}
/// @notice
function test_ComplianceAffectsBothStandards() public {
// ERC-1155
ICouponBatch.BatchCouponConfig memory config1155 = ICouponBatch.BatchCouponConfig({
transferable: true,
maxPrice: 100e6,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: new bytes32[](0),
issuer: issuer,
faceValue: 100e6
});
vm.prank(admin);
uint256 typeId = factory.mintBatch1155(issuer, 100e6, 1000, config1155);
vm.prank(issuer);
couponBatch.safeTransferFrom(issuer, consumer1, typeId, 50, "");
// consumer1
vm.prank(admin);
compliance.addToBlacklist(consumer1, "OFAC");
// ERC-1155
vm.prank(consumer1);
vm.expectRevert("Redemption1155: blacklisted");
redemption1155.redeem(typeId, 1, storeA);
}
/// @notice ERC-1155
function test_NonTransferableERC1155() public {
ICouponBatch.BatchCouponConfig memory config = ICouponBatch.BatchCouponConfig({
transferable: false,
maxPrice: 30e6,
expiryDate: block.timestamp + 90 days,
minPurchase: 0,
stackable: false,
allowedStores: new bytes32[](0),
issuer: issuer,
faceValue: 30e6
});
vm.prank(admin);
uint256 typeId = factory.mintBatch1155(issuer, 30e6, 1000, config);
//
vm.prank(issuer);
vm.expectRevert("CouponBatch: non-transferable");
couponBatch.safeTransferFrom(issuer, consumer1, typeId, 10, "");
// BURNER_ROLE
// Redemption1155 msg.sender balance
assertEq(couponBatch.balanceOf(issuer, typeId), 1000);
}
}

View File

@ -0,0 +1,187 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Redemption1155.sol";
import "../src/CouponBatch.sol";
import "../src/Compliance.sol";
import "../src/interfaces/ICouponBatch.sol";
contract Redemption1155Test is Test {
Redemption1155 redemption;
CouponBatch batch;
Compliance compliance;
address admin = address(1);
address factory = address(2);
address consumer = address(4);
address issuer = address(5);
bytes32 storeA = keccak256("STORE_A");
bytes32 storeB = keccak256("STORE_B");
function setUp() public {
vm.startPrank(admin);
// Compliance
compliance = new Compliance();
compliance.initialize(admin);
// CouponBatch
batch = new CouponBatch();
batch.initialize("https://genex.io/api/coupon-batch/{id}.json", admin);
// Redemption1155
redemption = new Redemption1155();
redemption.initialize(address(batch), address(compliance), admin);
//
batch.grantRole(keccak256("FACTORY_ROLE"), factory);
batch.grantRole(keccak256("BURNER_ROLE"), address(redemption));
compliance.setKycLevel(consumer, 1);
vm.stopPrank();
}
function test_RedeemSuccess() public {
uint256 typeId = _mintBatch(consumer, 100e6, 10, false);
vm.prank(consumer);
redemption.redeem(typeId, 3, storeA);
//
assertEq(batch.balanceOf(consumer, typeId), 7);
//
Redemption1155.RedemptionRecord memory record = redemption.getRedemption(0);
assertEq(record.typeId, typeId);
assertEq(record.amount, 3);
assertEq(record.consumer, consumer);
assertEq(record.issuer, issuer);
assertEq(record.storeId, storeA);
}
function test_RedeemAllUnits() public {
uint256 typeId = _mintBatch(consumer, 50e6, 5, false);
vm.prank(consumer);
redemption.redeem(typeId, 5, storeA);
assertEq(batch.balanceOf(consumer, typeId), 0);
assertEq(redemption.totalRedemptions(), 1);
}
function test_RedeemMultipleTimes() public {
uint256 typeId = _mintBatch(consumer, 100e6, 100, false);
vm.startPrank(consumer);
redemption.redeem(typeId, 30, storeA);
redemption.redeem(typeId, 50, storeA);
vm.stopPrank();
assertEq(batch.balanceOf(consumer, typeId), 20);
assertEq(redemption.totalRedemptions(), 2);
Redemption1155.RedemptionRecord memory r0 = redemption.getRedemption(0);
assertEq(r0.amount, 30);
Redemption1155.RedemptionRecord memory r1 = redemption.getRedemption(1);
assertEq(r1.amount, 50);
}
function test_RedeemZeroAmountReverts() public {
uint256 typeId = _mintBatch(consumer, 100e6, 10, false);
vm.prank(consumer);
vm.expectRevert("Redemption1155: zero amount");
redemption.redeem(typeId, 0, storeA);
}
function test_RedeemInsufficientBalanceReverts() public {
uint256 typeId = _mintBatch(consumer, 100e6, 5, false);
vm.prank(consumer);
vm.expectRevert("Redemption1155: insufficient balance");
redemption.redeem(typeId, 10, storeA);
}
function test_RedeemBlacklistedReverts() public {
uint256 typeId = _mintBatch(consumer, 100e6, 10, false);
vm.prank(admin);
compliance.addToBlacklist(consumer, "OFAC");
vm.prank(consumer);
vm.expectRevert("Redemption1155: blacklisted");
redemption.redeem(typeId, 1, storeA);
}
function test_RedeemExpiredReverts() public {
uint256 typeId = _mintBatch(consumer, 100e6, 10, false);
//
vm.warp(block.timestamp + 200 days);
vm.prank(consumer);
vm.expectRevert("Redemption1155: coupon expired");
redemption.redeem(typeId, 1, storeA);
}
function test_RedeemStoreRestricted() public {
uint256 typeId = _mintBatch(consumer, 100e6, 10, true);
//
bytes32 storeC = keccak256("STORE_C");
vm.prank(consumer);
vm.expectRevert("Redemption1155: store not allowed");
redemption.redeem(typeId, 1, storeC);
// 使
vm.prank(consumer);
redemption.redeem(typeId, 1, storeA);
assertEq(batch.balanceOf(consumer, typeId), 9);
}
function test_RedeemNoBalanceReverts() public {
uint256 typeId = _mintBatch(issuer, 100e6, 10, false);
// consumer typeId
vm.prank(consumer);
vm.expectRevert("Redemption1155: insufficient balance");
redemption.redeem(typeId, 1, storeA);
}
// ==========================================
// Helpers
// ==========================================
function _mintBatch(
address to,
uint256 faceValue,
uint256 quantity,
bool withStores
) internal returns (uint256) {
bytes32[] memory stores;
if (withStores) {
stores = new bytes32[](2);
stores[0] = storeA;
stores[1] = storeB;
} else {
stores = new bytes32[](0);
}
ICouponBatch.BatchCouponConfig memory config = ICouponBatch.BatchCouponConfig({
transferable: true,
maxPrice: faceValue,
expiryDate: block.timestamp + 180 days,
minPurchase: 0,
stackable: false,
allowedStores: stores,
issuer: issuer,
faceValue: faceValue
});
vm.prank(factory);
return batch.mint(to, faceValue, quantity, config);
}
}

View File

@ -34,7 +34,7 @@
| 出块时间 | **≤ 1秒**CometBFT即时终结性 | | 出块时间 | **≤ 1秒**CometBFT即时终结性 |
| TPS | ≥ 5,000Block-STM并行执行 | | TPS | ≥ 5,000Block-STM并行执行 |
| Gas策略 | 平台前期全额补贴用户零Gas | | Gas策略 | 平台前期全额补贴用户零Gas |
| 原生代币 | GNXGas + 治理前期Gas补贴用户不接触 | | 原生代币 | GNXGas + 治理前期Gas补贴用户不接触;基础单位 agnxatto GNX, 18位精度EVM标准 |
| 节点运营 | 平台运营验证节点 + 合格机构验证节点 | | 节点运营 | 平台运营验证节点 + 合格机构验证节点 |
| 跨链 | IBC连接Cosmos生态 + Axelar桥连接Ethereum | | 跨链 | IBC连接Cosmos生态 + Axelar桥连接Ethereum |
@ -85,7 +85,7 @@ make build
# 创建创世账户 # 创建创世账户
./genexd keys add validator ./genexd keys add validator
./genexd genesis add-genesis-account validator 1000000000ugnx ./genexd genesis add-genesis-account validator 1000000000000000000000000000agnx
# 启动本地节点 # 启动本地节点
./genexd start ./genexd start
@ -618,7 +618,11 @@ forge verify-contract \
### 13.2 代币分配 ### 13.2 代币分配
``` ```
总供应量: 1,000,000,000 GNX 总供应量: 1,000,000,000 GNX= 10^27 agnx
基础单位: agnxatto GNX, 18位精度与EVM wei对齐
中间单位: ngnxnano GNX, 10^9 agnx类似gwei
显示单位: GNX10^18 agnx
├── 平台运营/Gas补贴: 40%4亿— 前期Gas支出从此池扣除 ├── 平台运营/Gas补贴: 40%4亿— 前期Gas支出从此池扣除
├── 团队与顾问: 20%2亿— 4年线性释放1年锁定期 ├── 团队与顾问: 20%2亿— 4年线性释放1年锁定期
├── 生态基金: 15%1.5亿)— 开发者激励、做市商激励 ├── 生态基金: 15%1.5亿)— 开发者激励、做市商激励