From fb6c839eeecfb7159458f79c9ad3e9191de326b5 Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Sun, 25 Jan 2026 12:33:23 +0100 Subject: [PATCH 01/12] Chore: Adding latest rewards --- scripts/deployAllBadges.ts | 160 +++++++++++++++++ scripts/setupAllRewards.ts | 348 +++++++++++++++++++++++++++++++++++++ scripts/setupUSDCReward.ts | 89 ++++++++++ 3 files changed, 597 insertions(+) create mode 100644 scripts/deployAllBadges.ts create mode 100644 scripts/setupAllRewards.ts create mode 100644 scripts/setupUSDCReward.ts diff --git a/scripts/deployAllBadges.ts b/scripts/deployAllBadges.ts new file mode 100644 index 0000000..671249f --- /dev/null +++ b/scripts/deployAllBadges.ts @@ -0,0 +1,160 @@ +import { ethers } from 'hardhat'; + +/** + * Script to deploy multiple ERC1155Soulbound badge contracts and MockUSDC + * + * Deploys: + * 1. KPOP Badges (soulbound) + * 2. F1 Grand Prix VIP Ticket (soulbound) + * 3. New Jeans New Album (soulbound) + * 4. Quince Discount (soulbound) + * 5. MockUSDC for testing rewards + */ + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Deploying contracts with account:', deployer.address); + console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); + + // Get Rewards contract address from env or use the deployed one + const REWARDS_CONTRACT = process.env.REWARDS_CONTRACT || '0x2E028B97F8E72b8FD934953Ee676feBdfb420C4f'; + + // Badge configurations + const badges = [ + { + name: 'KPOP Badges', + symbol: 'KPOP', + baseURI: 'https://summon.xyz/kpop/badges/', + contractURI: 'https://summon.xyz/kpop/contract/', + }, + { + name: 'F1 Grand Prix VIP Ticket', + symbol: 'F1VIP', + baseURI: 'https://summon.xyz/rewards/badges/', + contractURI: 'https://summon.xyz/rewards/contract/', + }, + { + name: 'New Jeans New Album', + symbol: 'NJALBUM', + baseURI: 'https://summon.xyz/rewards/badges/', + contractURI: 'https://summon.xyz/rewards/contract/', + }, + { + name: 'Quince Discount', + symbol: 'QUINCE', + baseURI: 'https://summon.xyz/rewards/badges/', + contractURI: 'https://summon.xyz/rewards/contract/', + }, + ]; + + const deployedBadges: { name: string; address: string }[] = []; + + // Deploy ERC1155Soulbound contracts + const ERC1155Soulbound = await ethers.getContractFactory('ERC1155Soulbound'); + + for (const badge of badges) { + console.log(`\n========================================`); + console.log(`Deploying ${badge.name}...`); + console.log(`========================================`); + + const contract = await ERC1155Soulbound.deploy( + badge.name, + badge.symbol, + badge.baseURI, + badge.contractURI, + 100, // maxPerMint + false, // isPaused + deployer.address // devWallet + ); + await contract.waitForDeployment(); + const address = await contract.getAddress(); + + console.log(` Deployed to: ${address}`); + + // Add initial token + console.log(' Adding initial badge token...'); + const addTokenTx = await contract.addNewToken({ + tokenId: 1, + tokenUri: `${badge.baseURI}1/metadata.json`, + receiver: ethers.ZeroAddress, + feeBasisPoints: 0, + }); + await addTokenTx.wait(); + console.log(' Badge #1 added'); + + // Whitelist Rewards contract + console.log(' Whitelisting Rewards contract...'); + const whitelistTx = await contract.updateWhitelistAddress(REWARDS_CONTRACT, true); + await whitelistTx.wait(); + console.log(' Rewards contract whitelisted'); + + deployedBadges.push({ name: badge.name, address }); + } + + // Deploy MockUSDC + console.log(`\n========================================`); + console.log(`Deploying MockUSDC...`); + console.log(`========================================`); + + const MockUSDC = await ethers.getContractFactory('MockUSDC'); + const mockUSDC = await MockUSDC.deploy('USDC', 'USDC', 6); + await mockUSDC.waitForDeployment(); + const usdcAddress = await mockUSDC.getAddress(); + console.log(` Deployed to: ${usdcAddress}`); + + // Mint some USDC to deployer for testing + console.log(' Minting 1,000,000 USDC to deployer...'); + const mintTx = await mockUSDC.mint(deployer.address, ethers.parseUnits('1000000', 6)); + await mintTx.wait(); + console.log(' USDC minted'); + + // Summary + console.log('\n========================================'); + console.log('Deployment Summary'); + console.log('========================================'); + console.log('\nBadge Contracts:'); + for (const badge of deployedBadges) { + console.log(` ${badge.name}: ${badge.address}`); + } + console.log(`\nMockUSDC: ${usdcAddress}`); + console.log(`\nRewards Contract: ${REWARDS_CONTRACT}`); + console.log(`Dev Wallet: ${deployer.address}`); + + // Export for use in setup script + console.log('\n========================================'); + console.log('Environment Variables for Setup Script'); + console.log('========================================'); + console.log(`KPOP_BADGES_ADDRESS=${deployedBadges[0].address}`); + console.log(`F1_BADGES_ADDRESS=${deployedBadges[1].address}`); + console.log(`NEWJEANS_BADGES_ADDRESS=${deployedBadges[2].address}`); + console.log(`QUINCE_BADGES_ADDRESS=${deployedBadges[3].address}`); + console.log(`MOCK_USDC_ADDRESS=${usdcAddress}`); + console.log(`REWARDS_ADDRESS=${REWARDS_CONTRACT}`); + + // Verification commands + console.log('\n========================================'); + console.log('Verification Commands'); + console.log('========================================'); + for (const [i, badge] of deployedBadges.entries()) { + const config = badges[i]; + console.log(`\n# ${badge.name}`); + console.log( + `npx hardhat verify --network sepolia ${badge.address} "${config.name}" "${config.symbol}" "${config.baseURI}" "${config.contractURI}" 100 false ${deployer.address}` + ); + } + console.log(`\n# MockUSDC`); + console.log(`npx hardhat verify --network sepolia ${usdcAddress} "USDC" "USDC" 6`); + + return { + badges: deployedBadges, + usdc: usdcAddress, + rewards: REWARDS_CONTRACT, + }; +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/setupAllRewards.ts b/scripts/setupAllRewards.ts new file mode 100644 index 0000000..0061738 --- /dev/null +++ b/scripts/setupAllRewards.ts @@ -0,0 +1,348 @@ +import { ethers } from 'hardhat'; + +/** + * Script to setup multiple badge contracts with Rewards contract + * + * This script: + * 1. For each badge contract: + * - Whitelists Rewards contract + * - Whitelists Manager wallet + * - Mints soulbound badges to Manager + * - Approves Rewards contract + * - Creates reward token + * 2. For USDC: + * - Whitelists token on Rewards (if treasury system enabled) + * - Approves Rewards to spend USDC + * - Creates USDC reward token + */ + +// Contract addresses - UPDATE THESE after deployment +const KPOP_BADGES_ADDRESS = process.env.KPOP_BADGES_ADDRESS || '0x049d3CC16a5521E1dE1922059d09FCDd719DC81c'; +const F1_BADGES_ADDRESS = process.env.F1_BADGES_ADDRESS || '0x1a7a1879bE0C3fD48e033B2eEF40063bFE551731'; +const NEWJEANS_BADGES_ADDRESS = process.env.NEWJEANS_BADGES_ADDRESS || '0x4afF7E3F1191b4dEE2a0358417a750C1c6fF9b62'; +const QUINCE_BADGES_ADDRESS = process.env.QUINCE_BADGES_ADDRESS || '0x40813d715Ed741C0bA6848763c93aaF75fEA7F55'; +const MOCK_USDC_ADDRESS = process.env.MOCK_USDC_ADDRESS || '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; +const REWARDS_ADDRESS = process.env.REWARDS_ADDRESS || '0x2E028B97F8E72b8FD934953Ee676feBdfb420C4f'; + +// Configuration +const BADGES_TO_MINT = 100000; // How many badges to mint per contract +const REWARD_MAX_SUPPLY = 10000; // How many rewards can be claimed (10k as requested) +const USDC_REWARD_AMOUNT = ethers.parseUnits('10', 6); // 10 USDC per claim + +// Reward token IDs +const REWARD_TOKEN_IDS = { + KPOP: 1001, + F1: 2001, + NEWJEANS: 2002, + QUINCE: 2003, + USDC: 2004, +}; + +interface BadgeConfig { + name: string; + address: string; + tokenId: number; + rewardTokenId: number; +} + +async function setupBadgeReward( + badgeConfig: BadgeConfig, + rewards: any, + deployer: any, + accessToken: any, + DEV_CONFIG_ROLE: string +) { + console.log(`\n========================================`); + console.log(`Setting up ${badgeConfig.name}`); + console.log(`========================================`); + + if (!badgeConfig.address || badgeConfig.address === '') { + console.log(' SKIPPED: Address not provided'); + return; + } + + const badge = await ethers.getContractAt('ERC1155Soulbound', badgeConfig.address); + + // Step 1: Whitelist Rewards contract + console.log('Step 1: Whitelisting Rewards contract...'); + try { + const isWhitelisted = await badge.whitelistAddresses(REWARDS_ADDRESS); + if (isWhitelisted) { + console.log(' Already whitelisted'); + } else { + const tx = await badge.updateWhitelistAddress(REWARDS_ADDRESS, true); + await tx.wait(); + console.log(' Rewards contract whitelisted'); + } + } catch (error: any) { + console.log(' Error:', error.message); + } + + // Step 2: Whitelist Manager wallet + console.log('\nStep 2: Whitelisting Manager wallet...'); + try { + const isWhitelisted = await badge.whitelistAddresses(deployer.address); + if (isWhitelisted) { + console.log(' Already whitelisted'); + } else { + const tx = await badge.updateWhitelistAddress(deployer.address, true); + await tx.wait(); + console.log(' Manager wallet whitelisted'); + } + } catch (error: any) { + console.log(' Error:', error.message); + } + + // Step 3: Mint badges to Manager + console.log(`\nStep 3: Minting ${BADGES_TO_MINT} badges...`); + try { + const balanceBefore = await badge.balanceOf(deployer.address, badgeConfig.tokenId); + console.log(` Current balance: ${balanceBefore}`); + + if (balanceBefore >= BigInt(BADGES_TO_MINT)) { + console.log(' Already have enough badges'); + } else { + const tx = await badge.adminMintId(deployer.address, badgeConfig.tokenId, BADGES_TO_MINT, true); + await tx.wait(); + const balanceAfter = await badge.balanceOf(deployer.address, badgeConfig.tokenId); + console.log(` Minted! New balance: ${balanceAfter}`); + } + } catch (error: any) { + console.log(' Error:', error.message); + } + + // Step 4: Approve Rewards contract + console.log('\nStep 4: Approving Rewards contract...'); + try { + const isApproved = await badge.isApprovedForAll(deployer.address, REWARDS_ADDRESS); + if (isApproved) { + console.log(' Already approved'); + } else { + const tx = await badge.setApprovalForAll(REWARDS_ADDRESS, true); + await tx.wait(); + console.log(' Approved'); + } + } catch (error: any) { + console.log(' Error:', error.message); + } + + // Step 5: Create reward token + console.log(`\nStep 5: Creating reward token #${badgeConfig.rewardTokenId}...`); + try { + const exists = await rewards.isTokenExist(badgeConfig.rewardTokenId).catch(() => false); + if (exists) { + console.log(' Reward token already exists'); + } else { + const rewardToken = { + tokenId: badgeConfig.rewardTokenId, + maxSupply: REWARD_MAX_SUPPLY, + tokenUri: `https://storage.summon.xyz/default/rewards/${badgeConfig.rewardTokenId}/metadata.json`, + rewards: [ + { + rewardType: 3, // ERC1155 + rewardAmount: 1, // 1 badge per claim + rewardTokenAddress: badgeConfig.address, + rewardTokenIds: [], + rewardTokenId: badgeConfig.tokenId, + }, + ], + }; + + console.log(' Creating reward...'); + const tx = await rewards.createTokenAndDepositRewards(rewardToken); + await tx.wait(); + console.log(' Reward token created'); + + // Verify badges transferred + const rewardsBalance = await badge.balanceOf(REWARDS_ADDRESS, badgeConfig.tokenId); + console.log(` Rewards contract now holds ${rewardsBalance} badges`); + } + } catch (error: any) { + console.log(' Error:', error.message); + if (error.reason) console.log(' Reason:', error.reason); + } +} + +async function setupUSDCReward(rewards: any, deployer: any) { + console.log(`\n========================================`); + console.log(`Setting up USDC Reward`); + console.log(`========================================`); + + if (!MOCK_USDC_ADDRESS || MOCK_USDC_ADDRESS === '') { + console.log(' SKIPPED: USDC address not provided'); + return; + } + + const usdc = await ethers.getContractAt('MockUSDC', MOCK_USDC_ADDRESS); + + // Step 1: Whitelist USDC token on Rewards (if function exists) + console.log('Step 1: Checking if token whitelist is required...'); + try { + // Try to whitelist if function exists + const isWhitelisted = await rewards.isWhitelistedToken(MOCK_USDC_ADDRESS).catch(() => null); + if (isWhitelisted === false) { + const tx = await rewards.whitelistToken(MOCK_USDC_ADDRESS); + await tx.wait(); + console.log(' USDC whitelisted on Rewards'); + } else if (isWhitelisted === true) { + console.log(' USDC already whitelisted'); + } else { + console.log(' Token whitelist not required (function not found)'); + } + } catch (error: any) { + console.log(' Whitelist not required or function not available'); + } + + // Step 2: Approve Rewards to spend USDC + console.log('\nStep 2: Approving Rewards to spend USDC...'); + try { + const totalNeeded = USDC_REWARD_AMOUNT * BigInt(REWARD_MAX_SUPPLY); + const tx = await usdc.approve(REWARDS_ADDRESS, totalNeeded); + await tx.wait(); + console.log(` Approved ${ethers.formatUnits(totalNeeded, 6)} USDC`); + } catch (error: any) { + console.log(' Error:', error.message); + } + + // Step 3: Create USDC reward token + console.log(`\nStep 3: Creating USDC reward token #${REWARD_TOKEN_IDS.USDC}...`); + try { + const exists = await rewards.isTokenExist(REWARD_TOKEN_IDS.USDC).catch(() => false); + if (exists) { + console.log(' Reward token already exists'); + } else { + const rewardToken = { + tokenId: REWARD_TOKEN_IDS.USDC, + maxSupply: REWARD_MAX_SUPPLY, + tokenUri: `https://storage.summon.xyz/default/rewards/${REWARD_TOKEN_IDS.USDC}/metadata.json`, + rewards: [ + { + rewardType: 1, // ERC20 + rewardAmount: USDC_REWARD_AMOUNT, + rewardTokenAddress: MOCK_USDC_ADDRESS, + rewardTokenIds: [], + rewardTokenId: 0, + }, + ], + }; + + console.log(' Creating reward...'); + const tx = await rewards.createTokenAndDepositRewards(rewardToken); + await tx.wait(); + console.log(' USDC reward token created'); + + // Verify USDC transferred + const rewardsBalance = await usdc.balanceOf(REWARDS_ADDRESS); + console.log(` Rewards contract now holds ${ethers.formatUnits(rewardsBalance, 6)} USDC`); + } + } catch (error: any) { + console.log(' Error:', error.message); + if (error.reason) console.log(' Reason:', error.reason); + } +} + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Setting up Rewards with account:', deployer.address); + console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); + + // Get contract instances + const rewards = await ethers.getContractAt('Rewards', REWARDS_ADDRESS); + + // Get AccessToken and grant DEV_CONFIG_ROLE if needed + console.log('\n========================================'); + console.log('Checking DEV_CONFIG_ROLE on AccessToken'); + console.log('========================================'); + + const accessTokenAddress = await rewards.getRewardTokenContract(); + const accessToken = await ethers.getContractAt('AccessToken', accessTokenAddress); + const DEV_CONFIG_ROLE = await accessToken.DEV_CONFIG_ROLE(); + + const hasRole = await accessToken.hasRole(DEV_CONFIG_ROLE, REWARDS_ADDRESS); + if (!hasRole) { + console.log('Granting DEV_CONFIG_ROLE to Rewards...'); + const tx = await accessToken.grantRole(DEV_CONFIG_ROLE, REWARDS_ADDRESS); + await tx.wait(); + console.log(' DEV_CONFIG_ROLE granted'); + } else { + console.log(' Rewards already has DEV_CONFIG_ROLE'); + } + + // Badge configurations + const badgeConfigs: BadgeConfig[] = [ + { + name: 'KPOP Badges', + address: KPOP_BADGES_ADDRESS, + tokenId: 1, + rewardTokenId: REWARD_TOKEN_IDS.KPOP, + }, + { + name: 'F1 Grand Prix VIP Ticket', + address: F1_BADGES_ADDRESS, + tokenId: 1, + rewardTokenId: REWARD_TOKEN_IDS.F1, + }, + { + name: 'New Jeans New Album', + address: NEWJEANS_BADGES_ADDRESS, + tokenId: 1, + rewardTokenId: REWARD_TOKEN_IDS.NEWJEANS, + }, + { + name: 'Quince Discount', + address: QUINCE_BADGES_ADDRESS, + tokenId: 1, + rewardTokenId: REWARD_TOKEN_IDS.QUINCE, + }, + ]; + + // Setup each badge + for (const config of badgeConfigs) { + await setupBadgeReward(config, rewards, deployer, accessToken, DEV_CONFIG_ROLE); + } + + // Setup USDC reward + await setupUSDCReward(rewards, deployer); + + // Summary + console.log('\n========================================'); + console.log('Setup Complete!'); + console.log('========================================'); + + console.log('\nReward Tokens Created:'); + for (const config of badgeConfigs) { + if (config.address) { + console.log(` ${config.name}: Token ID ${config.rewardTokenId}`); + } + } + if (MOCK_USDC_ADDRESS) { + console.log(` USDC Reward: Token ID ${REWARD_TOKEN_IDS.USDC}`); + } + + console.log('\nContract Addresses:'); + console.log(' Rewards:', REWARDS_ADDRESS); + console.log(' KPOP Badges:', KPOP_BADGES_ADDRESS || 'Not deployed'); + console.log(' F1 Badges:', F1_BADGES_ADDRESS || 'Not deployed'); + console.log(' New Jeans Badges:', NEWJEANS_BADGES_ADDRESS || 'Not deployed'); + console.log(' Quince Badges:', QUINCE_BADGES_ADDRESS || 'Not deployed'); + console.log(' Mock USDC:', MOCK_USDC_ADDRESS || 'Not deployed'); + + console.log('\n========================================'); + console.log('Usage Examples'); + console.log('========================================'); + console.log('\n1. Mint reward tokens to users:'); + console.log(` rewards.adminMintById(userAddress, ${REWARD_TOKEN_IDS.F1}, 1, true)`); + + console.log('\n2. Users claim their rewards:'); + console.log(` rewards.claimReward(${REWARD_TOKEN_IDS.F1})`); + + console.log('\n3. Check if user can claim:'); + console.log(` rewardToken.balanceOf(userAddress, ${REWARD_TOKEN_IDS.F1})`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/setupUSDCReward.ts b/scripts/setupUSDCReward.ts new file mode 100644 index 0000000..91297ac --- /dev/null +++ b/scripts/setupUSDCReward.ts @@ -0,0 +1,89 @@ +import { ethers } from 'hardhat'; + +/** + * Script to setup USDC reward with treasury deposit + */ + +const MOCK_USDC_ADDRESS = '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; +const REWARDS_ADDRESS = '0x2E028B97F8E72b8FD934953Ee676feBdfb420C4f'; + +const REWARD_MAX_SUPPLY = 10000; +const USDC_REWARD_AMOUNT = ethers.parseUnits('10', 6); // 10 USDC per claim +const REWARD_TOKEN_ID = 2004; + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Setting up USDC Reward with account:', deployer.address); + + const usdc = await ethers.getContractAt('MockUSDC', MOCK_USDC_ADDRESS); + const rewards = await ethers.getContractAt('Rewards', REWARDS_ADDRESS); + + const totalNeeded = USDC_REWARD_AMOUNT * BigInt(REWARD_MAX_SUPPLY); + console.log(`Total USDC needed: ${ethers.formatUnits(totalNeeded, 6)}`); + + // Step 1: Check if already whitelisted + console.log('\nStep 1: Checking whitelist...'); + const isWhitelisted = await rewards.isWhitelistedToken(MOCK_USDC_ADDRESS); + console.log(` USDC whitelisted: ${isWhitelisted}`); + + // Step 2: Approve Rewards to spend USDC + console.log('\nStep 2: Approving Rewards to spend USDC...'); + const approveTx = await usdc.approve(REWARDS_ADDRESS, totalNeeded); + await approveTx.wait(); + console.log(` Approved ${ethers.formatUnits(totalNeeded, 6)} USDC`); + + // Step 3: Deposit USDC to treasury + console.log('\nStep 3: Depositing USDC to treasury...'); + const depositTx = await rewards.depositToTreasury(MOCK_USDC_ADDRESS, totalNeeded); + await depositTx.wait(); + console.log(` Deposited ${ethers.formatUnits(totalNeeded, 6)} USDC to treasury`); + + // Verify deposit + const treasuryBalance = await rewards.getTreasuryBalance(MOCK_USDC_ADDRESS); + console.log(` Treasury balance: ${ethers.formatUnits(treasuryBalance, 6)} USDC`); + + // Step 4: Create USDC reward token + console.log(`\nStep 4: Creating USDC reward token #${REWARD_TOKEN_ID}...`); + const exists = await rewards.isTokenExist(REWARD_TOKEN_ID).catch(() => false); + if (exists) { + console.log(' Reward token already exists'); + } else { + const rewardToken = { + tokenId: REWARD_TOKEN_ID, + maxSupply: REWARD_MAX_SUPPLY, + tokenUri: `https://storage.summon.xyz/default/rewards/${REWARD_TOKEN_ID}/metadata.json`, + rewards: [ + { + rewardType: 1, // ERC20 + rewardAmount: USDC_REWARD_AMOUNT, + rewardTokenAddress: MOCK_USDC_ADDRESS, + rewardTokenIds: [], + rewardTokenId: 0, + }, + ], + }; + + console.log(' Creating reward...'); + const tx = await rewards.createTokenAndDepositRewards(rewardToken); + await tx.wait(); + console.log(' USDC reward token created!'); + + // Check reserved amount + const reserved = await rewards.getReservedAmount(MOCK_USDC_ADDRESS); + console.log(` Reserved amount: ${ethers.formatUnits(reserved, 6)} USDC`); + } + + console.log('\n========================================'); + console.log('USDC Reward Setup Complete!'); + console.log('========================================'); + console.log(`Reward Token ID: ${REWARD_TOKEN_ID}`); + console.log(`Max Supply: ${REWARD_MAX_SUPPLY}`); + console.log(`USDC per claim: ${ethers.formatUnits(USDC_REWARD_AMOUNT, 6)}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); From 3c4bebd391971e2daa1fac01bbcd527fd814cd80 Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Wed, 4 Feb 2026 15:04:55 +0100 Subject: [PATCH 02/12] Chore: Add scripts --- scripts/checkApprovals.ts | 40 ++++ scripts/deployAllBadges.ts | 39 ++-- scripts/deployRewardsWithRoles.ts | 143 +++++++++++++ scripts/depositExtraUSDC.ts | 84 ++++++++ scripts/setupAllRewards.ts | 53 +++-- scripts/setupWalletForRewards.ts | 115 ++++++++++ scripts/verifyRewards.ts | 94 ++++++++ test/rewardsSoulbound.test.ts | 345 ++++++++++++++++++++++++++++++ 8 files changed, 868 insertions(+), 45 deletions(-) create mode 100644 scripts/checkApprovals.ts create mode 100644 scripts/deployRewardsWithRoles.ts create mode 100644 scripts/depositExtraUSDC.ts create mode 100644 scripts/setupWalletForRewards.ts create mode 100644 scripts/verifyRewards.ts diff --git a/scripts/checkApprovals.ts b/scripts/checkApprovals.ts new file mode 100644 index 0000000..66273bb --- /dev/null +++ b/scripts/checkApprovals.ts @@ -0,0 +1,40 @@ +import { ethers } from 'hardhat'; + +/** + * Check approval status for a wallet on all badge contracts + */ + +const TARGET_WALLET = '0x3E35E6713e1a03fd40a06BC406495822845d499F'; +const REWARDS_ADDRESS = '0x4163079Aa7d3ed57755c7278BA4156a826E25Ad4'; + +const BADGE_CONTRACTS = [ + { name: 'KPOP Badges', address: '0x049d3CC16a5521E1dE1922059d09FCDd719DC81c' }, + { name: 'F1 Badges', address: '0x1a7a1879bE0C3fD48e033B2eEF40063bFE551731' }, + { name: 'NewJeans Badges', address: '0x4afF7E3F1191b4dEE2a0358417a750C1c6fF9b62' }, + { name: 'Quince Badges', address: '0x40813d715Ed741C0bA6848763c93aaF75fEA7F55' }, +]; + +async function main() { + console.log('Checking approvals for wallet:', TARGET_WALLET); + console.log('Rewards contract:', REWARDS_ADDRESS); + console.log(''); + + for (const badgeInfo of BADGE_CONTRACTS) { + const badge = await ethers.getContractAt('ERC1155Soulbound', badgeInfo.address); + + const isApproved = await badge.isApprovedForAll(TARGET_WALLET, REWARDS_ADDRESS); + const balance = await badge.balanceOf(TARGET_WALLET, 1); + + console.log(`${badgeInfo.name} (${badgeInfo.address}):`); + console.log(` Approved: ${isApproved ? '✅ YES' : '❌ NO'}`); + console.log(` Balance (tokenId=1): ${balance}`); + console.log(''); + } +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployAllBadges.ts b/scripts/deployAllBadges.ts index 671249f..8fd2533 100644 --- a/scripts/deployAllBadges.ts +++ b/scripts/deployAllBadges.ts @@ -17,22 +17,25 @@ async function main() { console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); // Get Rewards contract address from env or use the deployed one - const REWARDS_CONTRACT = process.env.REWARDS_CONTRACT || '0x2E028B97F8E72b8FD934953Ee676feBdfb420C4f'; + const REWARDS_CONTRACT = process.env.REWARDS_CONTRACT || '0x5d62C8cfDe4a1B0be2Cc102023F2563bc29221Cc'; // Badge configurations + // NOTE: KPOP and F1 already deployed, only deploying remaining badges const badges = [ - { - name: 'KPOP Badges', - symbol: 'KPOP', - baseURI: 'https://summon.xyz/kpop/badges/', - contractURI: 'https://summon.xyz/kpop/contract/', - }, - { - name: 'F1 Grand Prix VIP Ticket', - symbol: 'F1VIP', - baseURI: 'https://summon.xyz/rewards/badges/', - contractURI: 'https://summon.xyz/rewards/contract/', - }, + // Already deployed: 0x3D62Dbe1806437bCA71e52Ee9581dee37a608cd8 + // { + // name: 'KPOP Badges', + // symbol: 'KPOP', + // baseURI: 'https://summon.xyz/kpop/badges/', + // contractURI: 'https://summon.xyz/kpop/contract/', + // }, + // Already deployed: 0xd7B81ABA27cDB68D79aCB1B6E6b198fF4D8EeAd7 + // { + // name: 'F1 Grand Prix VIP Ticket', + // symbol: 'F1VIP', + // baseURI: 'https://summon.xyz/rewards/badges/', + // contractURI: 'https://summon.xyz/rewards/contract/', + // }, { name: 'New Jeans New Album', symbol: 'NJALBUM', @@ -124,10 +127,12 @@ async function main() { console.log('\n========================================'); console.log('Environment Variables for Setup Script'); console.log('========================================'); - console.log(`KPOP_BADGES_ADDRESS=${deployedBadges[0].address}`); - console.log(`F1_BADGES_ADDRESS=${deployedBadges[1].address}`); - console.log(`NEWJEANS_BADGES_ADDRESS=${deployedBadges[2].address}`); - console.log(`QUINCE_BADGES_ADDRESS=${deployedBadges[3].address}`); + // Already deployed badges + console.log(`KPOP_BADGES_ADDRESS=0x3D62Dbe1806437bCA71e52Ee9581dee37a608cd8`); + console.log(`F1_BADGES_ADDRESS=0xd7B81ABA27cDB68D79aCB1B6E6b198fF4D8EeAd7`); + // Newly deployed badges + console.log(`NEWJEANS_BADGES_ADDRESS=${deployedBadges[0]?.address || 'NOT_DEPLOYED'}`); + console.log(`QUINCE_BADGES_ADDRESS=${deployedBadges[1]?.address || 'NOT_DEPLOYED'}`); console.log(`MOCK_USDC_ADDRESS=${usdcAddress}`); console.log(`REWARDS_ADDRESS=${REWARDS_CONTRACT}`); diff --git a/scripts/deployRewardsWithRoles.ts b/scripts/deployRewardsWithRoles.ts new file mode 100644 index 0000000..dd0d754 --- /dev/null +++ b/scripts/deployRewardsWithRoles.ts @@ -0,0 +1,143 @@ +import { ethers } from 'hardhat'; + +/** + * Script to deploy AccessToken and Rewards contracts + * and grant all roles to a target address without renouncing any roles. + */ + +const TARGET_ADDRESS = '0x3E35E6713e1a03fd40a06BC406495822845d499F'; + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Deploying contracts with account:', deployer.address); + console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); + console.log('Target address for roles:', TARGET_ADDRESS); + + // Configuration - using deployer as all roles initially + const devWallet = deployer.address; + const managerWallet = deployer.address; + const minterWallet = deployer.address; + + // 1. Deploy AccessToken + console.log('\n1. Deploying AccessToken...'); + const AccessToken = await ethers.getContractFactory('AccessToken'); + const accessToken = await AccessToken.deploy(devWallet); + await accessToken.waitForDeployment(); + const accessTokenAddress = await accessToken.getAddress(); + console.log('AccessToken deployed to:', accessTokenAddress); + + // 2. Initialize AccessToken + console.log('\n2. Initializing AccessToken...'); + const initAccessTokenTx = await accessToken.initialize( + 'Rewards Access Token', // name + 'RAT', // symbol + 'https://summon.xyz/metadata/', // defaultTokenURI + 'https://summon.xyz/contract/', // contractURI + devWallet, + devWallet // minterContract - will be updated to Rewards contract + ); + await initAccessTokenTx.wait(); + console.log('AccessToken initialized'); + + // 3. Deploy Rewards + console.log('\n3. Deploying Rewards...'); + const Rewards = await ethers.getContractFactory('Rewards'); + const rewards = await Rewards.deploy(devWallet); + await rewards.waitForDeployment(); + const rewardsAddress = await rewards.getAddress(); + console.log('Rewards deployed to:', rewardsAddress); + + // 4. Initialize Rewards + console.log('\n4. Initializing Rewards...'); + const initRewardsTx = await rewards.initialize( + devWallet, + managerWallet, + minterWallet, + accessTokenAddress + ); + await initRewardsTx.wait(); + console.log('Rewards initialized'); + + // 5. Grant MINTER_ROLE to Rewards contract on AccessToken + console.log('\n5. Granting MINTER_ROLE to Rewards on AccessToken...'); + const MINTER_ROLE = ethers.keccak256(ethers.toUtf8Bytes('MINTER_ROLE')); + const grantRoleTx = await accessToken.grantRole(MINTER_ROLE, rewardsAddress); + await grantRoleTx.wait(); + console.log('MINTER_ROLE granted to Rewards'); + + // 6. Add Rewards to whitelist on AccessToken (for whitelistBurn) + console.log('\n6. Adding Rewards to whitelist on AccessToken...'); + try { + const addWhitelistTx = await accessToken.addToWhitelist(rewardsAddress); + await addWhitelistTx.wait(); + console.log('Rewards added to whitelist'); + } catch (error) { + console.log('Note: addToWhitelist may need to be called separately or function name differs'); + } + + // 7. Grant all roles to TARGET_ADDRESS on Rewards contract + console.log('\n7. Granting all roles to target address on Rewards...'); + const DEFAULT_ADMIN_ROLE = await rewards.DEFAULT_ADMIN_ROLE(); + const DEV_CONFIG_ROLE = await rewards.DEV_CONFIG_ROLE(); + const MANAGER_ROLE = await rewards.MANAGER_ROLE(); + const REWARDS_MINTER_ROLE = await rewards.MINTER_ROLE(); + + console.log(' Granting DEFAULT_ADMIN_ROLE...'); + await (await rewards.grantRole(DEFAULT_ADMIN_ROLE, TARGET_ADDRESS)).wait(); + console.log(' Granting DEV_CONFIG_ROLE...'); + await (await rewards.grantRole(DEV_CONFIG_ROLE, TARGET_ADDRESS)).wait(); + console.log(' Granting MANAGER_ROLE...'); + await (await rewards.grantRole(MANAGER_ROLE, TARGET_ADDRESS)).wait(); + console.log(' Granting MINTER_ROLE...'); + await (await rewards.grantRole(REWARDS_MINTER_ROLE, TARGET_ADDRESS)).wait(); + console.log('All roles granted to target on Rewards'); + + // 8. Grant all roles to TARGET_ADDRESS on AccessToken contract + console.log('\n8. Granting all roles to target address on AccessToken...'); + const AT_DEFAULT_ADMIN_ROLE = await accessToken.DEFAULT_ADMIN_ROLE(); + const AT_DEV_CONFIG_ROLE = await accessToken.DEV_CONFIG_ROLE(); + const AT_MINTER_ROLE = await accessToken.MINTER_ROLE(); + + console.log(' Granting DEFAULT_ADMIN_ROLE...'); + await (await accessToken.grantRole(AT_DEFAULT_ADMIN_ROLE, TARGET_ADDRESS)).wait(); + console.log(' Granting DEV_CONFIG_ROLE...'); + await (await accessToken.grantRole(AT_DEV_CONFIG_ROLE, TARGET_ADDRESS)).wait(); + console.log(' Granting MINTER_ROLE...'); + await (await accessToken.grantRole(AT_MINTER_ROLE, TARGET_ADDRESS)).wait(); + console.log('All roles granted to target on AccessToken'); + + console.log('\n========================================'); + console.log('Deployment Summary:'); + console.log('========================================'); + console.log('AccessToken:', accessTokenAddress); + console.log('Rewards:', rewardsAddress); + console.log('Dev Wallet:', devWallet); + console.log('Manager Wallet:', managerWallet); + console.log('Minter Wallet:', minterWallet); + console.log('Target Address (all roles):', TARGET_ADDRESS); + console.log('========================================'); + + console.log('\nRoles granted to', TARGET_ADDRESS, ':'); + console.log(' Rewards: DEFAULT_ADMIN_ROLE, DEV_CONFIG_ROLE, MANAGER_ROLE, MINTER_ROLE'); + console.log(' AccessToken: DEFAULT_ADMIN_ROLE, DEV_CONFIG_ROLE, MINTER_ROLE'); + + // Verify instructions + console.log('\nTo verify contracts on Etherscan:'); + console.log(`npx hardhat verify --network sepolia ${accessTokenAddress} ${devWallet}`); + console.log(`npx hardhat verify --network sepolia ${rewardsAddress} ${devWallet}`); + + console.log('\n========================================'); + console.log('NEXT STEP:'); + console.log('========================================'); + console.log('Update REWARDS_ADDRESS in scripts/setupAllRewards.ts to:'); + console.log(rewardsAddress); + console.log('\nThen run:'); + console.log('pnpm hardhat run scripts/setupAllRewards.ts --network sepolia'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/depositExtraUSDC.ts b/scripts/depositExtraUSDC.ts new file mode 100644 index 0000000..76a03df --- /dev/null +++ b/scripts/depositExtraUSDC.ts @@ -0,0 +1,84 @@ +import { ethers } from 'hardhat'; + +/** + * Script to deposit extra USDC to treasury so there's available balance + */ + +const REWARDS_ADDRESS = '0x4163079Aa7d3ed57755c7278BA4156a826E25Ad4'; +const MOCK_USDC_ADDRESS = '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; + +// Amount to deposit (extra, not reserved for rewards) +const EXTRA_DEPOSIT = ethers.parseUnits('1000000', 6); // 1,000,000 USDC extra + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Depositing extra USDC with account:', deployer.address); + + const rewards = await ethers.getContractAt('Rewards', REWARDS_ADDRESS); + const usdc = await ethers.getContractAt('MockUSDC', MOCK_USDC_ADDRESS); + + // Check current balances + console.log('\n========================================'); + console.log('Before Deposit:'); + console.log('========================================'); + + const [addresses, totalBalances, reservedBalances, availableBalances] = + await rewards.getAllTreasuryBalances(); + + const usdcIndex = addresses.findIndex( + (addr: string) => addr.toLowerCase() === MOCK_USDC_ADDRESS.toLowerCase() + ); + + if (usdcIndex >= 0) { + console.log('USDC Total Balance:', ethers.formatUnits(totalBalances[usdcIndex], 6)); + console.log('USDC Reserved Balance:', ethers.formatUnits(reservedBalances[usdcIndex], 6)); + console.log('USDC Available Balance:', ethers.formatUnits(availableBalances[usdcIndex], 6)); + } + + // Check deployer USDC balance + const deployerBalance = await usdc.balanceOf(deployer.address); + console.log('\nDeployer USDC Balance:', ethers.formatUnits(deployerBalance, 6)); + + if (deployerBalance < EXTRA_DEPOSIT) { + console.log('\nMinting more USDC to deployer...'); + const mintTx = await usdc.mint(deployer.address, EXTRA_DEPOSIT); + await mintTx.wait(); + console.log('Minted', ethers.formatUnits(EXTRA_DEPOSIT, 6), 'USDC'); + } + + // Approve and deposit + console.log('\nApproving USDC...'); + const approveTx = await usdc.approve(REWARDS_ADDRESS, EXTRA_DEPOSIT); + await approveTx.wait(); + + console.log('Depositing', ethers.formatUnits(EXTRA_DEPOSIT, 6), 'USDC to treasury...'); + const depositTx = await rewards.depositToTreasury(MOCK_USDC_ADDRESS, EXTRA_DEPOSIT); + await depositTx.wait(); + + // Check new balances + console.log('\n========================================'); + console.log('After Deposit:'); + console.log('========================================'); + + const [addresses2, totalBalances2, reservedBalances2, availableBalances2] = + await rewards.getAllTreasuryBalances(); + + const usdcIndex2 = addresses2.findIndex( + (addr: string) => addr.toLowerCase() === MOCK_USDC_ADDRESS.toLowerCase() + ); + + if (usdcIndex2 >= 0) { + console.log('USDC Total Balance:', ethers.formatUnits(totalBalances2[usdcIndex2], 6)); + console.log('USDC Reserved Balance:', ethers.formatUnits(reservedBalances2[usdcIndex2], 6)); + console.log('USDC Available Balance:', ethers.formatUnits(availableBalances2[usdcIndex2], 6)); + } + + console.log('\nDone! Extra USDC deposited to treasury.'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/setupAllRewards.ts b/scripts/setupAllRewards.ts index 0061738..ce1aa71 100644 --- a/scripts/setupAllRewards.ts +++ b/scripts/setupAllRewards.ts @@ -22,7 +22,7 @@ const F1_BADGES_ADDRESS = process.env.F1_BADGES_ADDRESS || '0x1a7a1879bE0C3fD48e const NEWJEANS_BADGES_ADDRESS = process.env.NEWJEANS_BADGES_ADDRESS || '0x4afF7E3F1191b4dEE2a0358417a750C1c6fF9b62'; const QUINCE_BADGES_ADDRESS = process.env.QUINCE_BADGES_ADDRESS || '0x40813d715Ed741C0bA6848763c93aaF75fEA7F55'; const MOCK_USDC_ADDRESS = process.env.MOCK_USDC_ADDRESS || '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; -const REWARDS_ADDRESS = process.env.REWARDS_ADDRESS || '0x2E028B97F8E72b8FD934953Ee676feBdfb420C4f'; +const REWARDS_ADDRESS = process.env.REWARDS_ADDRESS || '0x4163079Aa7d3ed57755c7278BA4156a826E25Ad4'; // Configuration const BADGES_TO_MINT = 100000; // How many badges to mint per contract @@ -63,34 +63,24 @@ async function setupBadgeReward( const badge = await ethers.getContractAt('ERC1155Soulbound', badgeConfig.address); - // Step 1: Whitelist Rewards contract - console.log('Step 1: Whitelisting Rewards contract...'); + // Step 1: Whitelist Rewards contract on badge + console.log('Step 1: Whitelisting Rewards contract on badge...'); try { - const isWhitelisted = await badge.whitelistAddresses(REWARDS_ADDRESS); - if (isWhitelisted) { - console.log(' Already whitelisted'); - } else { - const tx = await badge.updateWhitelistAddress(REWARDS_ADDRESS, true); - await tx.wait(); - console.log(' Rewards contract whitelisted'); - } + const tx = await badge.updateWhitelistAddress(REWARDS_ADDRESS, true); + await tx.wait(); + console.log(' Rewards contract whitelisted'); } catch (error: any) { - console.log(' Error:', error.message); + console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); } - // Step 2: Whitelist Manager wallet - console.log('\nStep 2: Whitelisting Manager wallet...'); + // Step 2: Whitelist Manager wallet on badge + console.log('\nStep 2: Whitelisting Manager wallet on badge...'); try { - const isWhitelisted = await badge.whitelistAddresses(deployer.address); - if (isWhitelisted) { - console.log(' Already whitelisted'); - } else { - const tx = await badge.updateWhitelistAddress(deployer.address, true); - await tx.wait(); - console.log(' Manager wallet whitelisted'); - } + const tx = await badge.updateWhitelistAddress(deployer.address, true); + await tx.wait(); + console.log(' Manager wallet whitelisted'); } catch (error: any) { - console.log(' Error:', error.message); + console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); } // Step 3: Mint badges to Manager @@ -193,15 +183,22 @@ async function setupUSDCReward(rewards: any, deployer: any) { console.log(' Whitelist not required or function not available'); } - // Step 2: Approve Rewards to spend USDC - console.log('\nStep 2: Approving Rewards to spend USDC...'); + // Step 2: Approve and deposit USDC to treasury + console.log('\nStep 2: Approving and depositing USDC to treasury...'); try { const totalNeeded = USDC_REWARD_AMOUNT * BigInt(REWARD_MAX_SUPPLY); - const tx = await usdc.approve(REWARDS_ADDRESS, totalNeeded); - await tx.wait(); + + // Approve + const approveTx = await usdc.approve(REWARDS_ADDRESS, totalNeeded); + await approveTx.wait(); console.log(` Approved ${ethers.formatUnits(totalNeeded, 6)} USDC`); + + // Deposit to treasury + const depositTx = await rewards.depositToTreasury(MOCK_USDC_ADDRESS, totalNeeded); + await depositTx.wait(); + console.log(` Deposited ${ethers.formatUnits(totalNeeded, 6)} USDC to treasury`); } catch (error: any) { - console.log(' Error:', error.message); + console.log(' Error:', error.message?.slice(0, 80)); } // Step 3: Create USDC reward token diff --git a/scripts/setupWalletForRewards.ts b/scripts/setupWalletForRewards.ts new file mode 100644 index 0000000..a10ff38 --- /dev/null +++ b/scripts/setupWalletForRewards.ts @@ -0,0 +1,115 @@ +import { ethers } from 'hardhat'; + +/** + * Script to setup a specific wallet to be able to create rewards + * This includes: + * 1. Whitelisting the wallet on each badge contract + * 2. Minting badges to the wallet + * 3. Approving Rewards contract to transfer badges + */ + +const TARGET_WALLET = '0x3E35E6713e1a03fd40a06BC406495822845d499F'; +const REWARDS_ADDRESS = '0x4163079Aa7d3ed57755c7278BA4156a826E25Ad4'; + +// Badge contract addresses +const BADGE_CONTRACTS = [ + { name: 'KPOP Badges', address: '0x049d3CC16a5521E1dE1922059d09FCDd719DC81c', tokenId: 1 }, + { name: 'F1 Badges', address: '0x1a7a1879bE0C3fD48e033B2eEF40063bFE551731', tokenId: 1 }, + { name: 'NewJeans Badges', address: '0x4afF7E3F1191b4dEE2a0358417a750C1c6fF9b62', tokenId: 1 }, + { name: 'Quince Badges', address: '0x40813d715Ed741C0bA6848763c93aaF75fEA7F55', tokenId: 1 }, +]; + +const BADGES_TO_MINT = 100000; + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Setting up wallet for rewards with deployer:', deployer.address); + console.log('Target wallet:', TARGET_WALLET); + console.log('Rewards contract:', REWARDS_ADDRESS); + + for (const badgeInfo of BADGE_CONTRACTS) { + console.log(`\n========================================`); + console.log(`Setting up ${badgeInfo.name}`); + console.log(`========================================`); + + const badge = await ethers.getContractAt('ERC1155Soulbound', badgeInfo.address); + + // Step 1: Whitelist target wallet on badge contract + console.log('Step 1: Whitelisting target wallet on badge...'); + try { + const tx = await badge.updateWhitelistAddress(TARGET_WALLET, true); + await tx.wait(); + console.log(' Target wallet whitelisted'); + } catch (error: any) { + console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); + } + + // Step 2: Mint badges to target wallet + console.log(`\nStep 2: Minting ${BADGES_TO_MINT} badges to target wallet...`); + try { + const balanceBefore = await badge.balanceOf(TARGET_WALLET, badgeInfo.tokenId); + console.log(` Current balance: ${balanceBefore}`); + + if (balanceBefore >= BigInt(BADGES_TO_MINT)) { + console.log(' Already have enough badges'); + } else { + const tx = await badge.adminMintId(TARGET_WALLET, badgeInfo.tokenId, BADGES_TO_MINT, true); + await tx.wait(); + const balanceAfter = await badge.balanceOf(TARGET_WALLET, badgeInfo.tokenId); + console.log(` Minted! New balance: ${balanceAfter}`); + } + } catch (error: any) { + console.log(' Error:', error.message?.slice(0, 80)); + } + + // Step 3: Check if Rewards contract is whitelisted + console.log('\nStep 3: Ensuring Rewards contract is whitelisted...'); + try { + const tx = await badge.updateWhitelistAddress(REWARDS_ADDRESS, true); + await tx.wait(); + console.log(' Rewards contract whitelisted'); + } catch (error: any) { + console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); + } + } + + // Setup USDC + console.log(`\n========================================`); + console.log(`Setting up MockUSDC for target wallet`); + console.log(`========================================`); + + const MOCK_USDC_ADDRESS = '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; + const usdc = await ethers.getContractAt('MockUSDC', MOCK_USDC_ADDRESS); + + // Mint USDC to target wallet + console.log('Minting 1,000,000 USDC to target wallet...'); + try { + const amount = ethers.parseUnits('1000000', 6); + const tx = await usdc.mint(TARGET_WALLET, amount); + await tx.wait(); + const balance = await usdc.balanceOf(TARGET_WALLET); + console.log(` Target wallet USDC balance: ${ethers.formatUnits(balance, 6)}`); + } catch (error: any) { + console.log(' Error:', error.message?.slice(0, 80)); + } + + console.log('\n========================================'); + console.log('Setup Complete!'); + console.log('========================================'); + console.log('\nThe target wallet now needs to call setApprovalForAll'); + console.log('on each badge contract to approve the Rewards contract.'); + console.log('\nFrom the target wallet, call:'); + for (const badgeInfo of BADGE_CONTRACTS) { + console.log(`\n${badgeInfo.name} (${badgeInfo.address}):`); + console.log(` await badge.setApprovalForAll("${REWARDS_ADDRESS}", true)`); + } + console.log('\nFor USDC, call:'); + console.log(` await usdc.approve("${REWARDS_ADDRESS}", amount)`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/verifyRewards.ts b/scripts/verifyRewards.ts new file mode 100644 index 0000000..6dcb7aa --- /dev/null +++ b/scripts/verifyRewards.ts @@ -0,0 +1,94 @@ +import { ethers } from 'hardhat'; + +/** + * Script to verify Rewards contract deployment + * Tests the new getAllTreasuryBalances function and other key functions + */ + +const REWARDS_ADDRESS = '0x80C95B9EE08BA220DE5D26D19Ff23a96D52adDD6'; + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Verifying Rewards contract with account:', deployer.address); + + const rewards = await ethers.getContractAt('Rewards', REWARDS_ADDRESS); + + console.log('\n========================================'); + console.log('1. Testing getAllItemIds()'); + console.log('========================================'); + const itemIds = await rewards.getAllItemIds(); + console.log(' Item IDs:', itemIds.map((id: bigint) => id.toString())); + + console.log('\n========================================'); + console.log('2. Testing getTokenDetails() for each token'); + console.log('========================================'); + for (const tokenId of itemIds) { + const details = await rewards.getTokenDetails(tokenId); + console.log(`\n Token ID ${tokenId}:`); + console.log(' URI:', details.tokenUri); + console.log(' Max Supply:', details.maxSupply.toString()); + console.log(' Reward Types:', details.rewardTypes.map((t: bigint) => t.toString())); + console.log(' Reward Amounts:', details.rewardAmounts.map((a: bigint) => a.toString())); + } + + console.log('\n========================================'); + console.log('3. Testing getRemainingSupply()'); + console.log('========================================'); + for (const tokenId of itemIds) { + const remaining = await rewards.getRemainingSupply(tokenId); + console.log(` Token ID ${tokenId}: ${remaining.toString()} remaining`); + } + + console.log('\n========================================'); + console.log('4. Testing getWhitelistedTokens()'); + console.log('========================================'); + const whitelistedTokens = await rewards.getWhitelistedTokens(); + console.log(' Whitelisted tokens:', whitelistedTokens.length > 0 ? whitelistedTokens : 'None'); + + console.log('\n========================================'); + console.log('5. Testing getAllTreasuryBalances()'); + console.log('========================================'); + try { + const treasuryBalances = await rewards.getAllTreasuryBalances(); + console.log(' Addresses:', treasuryBalances.addresses); + console.log(' Total Balances:', treasuryBalances.totalBalances.map((b: bigint) => b.toString())); + console.log(' Reserved Balances:', treasuryBalances.reservedBalances.map((b: bigint) => b.toString())); + console.log(' Available Balances:', treasuryBalances.availableBalances.map((b: bigint) => b.toString())); + console.log(' Symbols:', treasuryBalances.symbols); + console.log(' Names:', treasuryBalances.names); + console.log(' Types:', treasuryBalances.types); // NEW: "fa" or "nft" + } catch (error: any) { + console.log(' Error:', error.message); + } + + console.log('\n========================================'); + console.log('6. Testing getRewardTokenContract()'); + console.log('========================================'); + const accessTokenAddress = await rewards.getRewardTokenContract(); + console.log(' AccessToken Address:', accessTokenAddress); + + console.log('\n========================================'); + console.log('7. Testing getChainID()'); + console.log('========================================'); + const chainId = await rewards.getChainID(); + console.log(' Chain ID:', chainId.toString()); + + console.log('\n========================================'); + console.log('8. Testing getWhitelistSigners()'); + console.log('========================================'); + const signers = await rewards.getWhitelistSigners(); + console.log(' Whitelist Signers:', signers.length > 0 ? signers : 'None configured'); + + console.log('\n========================================'); + console.log('Verification Complete!'); + console.log('========================================'); + console.log('\nNew Contract Address: ', REWARDS_ADDRESS); + console.log('AccessToken Address:', accessTokenAddress); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/test/rewardsSoulbound.test.ts b/test/rewardsSoulbound.test.ts index bb58737..c027d92 100644 --- a/test/rewardsSoulbound.test.ts +++ b/test/rewardsSoulbound.test.ts @@ -355,4 +355,349 @@ describe('Rewards with Soulbound Tokens', function () { expect(await soulboundBadge.balanceOf(user2.address, badgeTokenId)).to.equal(1); }); }); + + describe('getAllTreasuryBalances', function () { + it('Should return empty arrays when no tokens are deposited', async function () { + const [devWallet, managerWallet, minterWallet] = await ethers.getSigners(); + + // Deploy fresh contracts without any deposits + const AccessToken = await ethers.getContractFactory('AccessToken'); + const accessToken = await AccessToken.deploy(devWallet.address); + await accessToken.waitForDeployment(); + + const Rewards = await ethers.getContractFactory('Rewards'); + const rewards = await Rewards.deploy(devWallet.address); + await rewards.waitForDeployment(); + + await accessToken.initialize( + 'G7Reward', 'G7R', 'https://example.com/token/', 'https://example.com/contract/', + devWallet.address, rewards.target + ); + await rewards.initialize(devWallet.address, managerWallet.address, minterWallet.address, accessToken.target); + + const result = await rewards.getAllTreasuryBalances(); + + expect(result.addresses.length).to.equal(0); + expect(result.totalBalances.length).to.equal(0); + expect(result.reservedBalances.length).to.equal(0); + expect(result.availableBalances.length).to.equal(0); + expect(result.symbols.length).to.equal(0); + expect(result.names.length).to.equal(0); + expect(result.types.length).to.equal(0); + }); + + it('Should return ERC20 token with type "fa" after deposit to treasury', async function () { + const { rewards, mockERC20, managerWallet } = await loadFixture(deployRewardsWithSoulboundFixture); + + // Fixture already deposits 1000 MTK to treasury + const result = await rewards.getAllTreasuryBalances(); + + expect(result.addresses.length).to.equal(1); + expect(result.addresses[0]).to.equal(mockERC20.target); + expect(result.totalBalances[0]).to.equal(ethers.parseEther('1000')); + expect(result.reservedBalances[0]).to.equal(0n); // No rewards created yet + expect(result.availableBalances[0]).to.equal(ethers.parseEther('1000')); + expect(result.symbols[0]).to.equal('MTK'); + expect(result.names[0]).to.equal('Mock Token'); + expect(result.types[0]).to.equal('fa'); + }); + + it('Should return ERC1155 badge with type "nft" after creating reward', async function () { + const { rewards, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = + await loadFixture(deployRewardsWithSoulboundFixture); + + // Whitelist Rewards and manager on badge contract + await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + + // Mint badges to manager + await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); + + // Manager approves and creates reward + await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + + const rewardTokenId = 1001; + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: rewardTokenId, + maxSupply: 10, + tokenUri: 'https://example.com/reward/1001', + rewards: [{ + rewardType: 3, // ERC1155 + rewardAmount: 1, + rewardTokenAddress: soulboundBadge.target, + rewardTokenIds: [], + rewardTokenId: badgeTokenId, + }], + }); + + const result = await rewards.getAllTreasuryBalances(); + + // Should have ERC20 (from fixture) + ERC1155 badge + expect(result.addresses.length).to.equal(2); + + // First is ERC20 (fa) + expect(result.addresses[0]).to.equal(mockERC20.target); + expect(result.types[0]).to.equal('fa'); + + // Second is ERC1155 (nft) + expect(result.addresses[1]).to.equal(soulboundBadge.target); + expect(result.totalBalances[1]).to.equal(10n); // 10 badges deposited + expect(result.reservedBalances[1]).to.equal(10n); // All reserved for rewards + expect(result.availableBalances[1]).to.equal(0n); + expect(result.types[1]).to.equal('nft'); + }); + + it('Should return both ERC20 and ERC1155 with correct types in mixed reward', async function () { + const { rewards, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = + await loadFixture(deployRewardsWithSoulboundFixture); + + // Whitelist Rewards and manager on badge contract + await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + + // Mint badges to manager + await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); + + // Manager approves badge + await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + + // Create reward with BOTH ERC20 and ERC1155 + const rewardTokenId = 2001; + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: rewardTokenId, + maxSupply: 5, + tokenUri: 'https://example.com/reward/2001', + rewards: [ + { + rewardType: 1, // ERC20 + rewardAmount: ethers.parseEther('10'), + rewardTokenAddress: mockERC20.target, + rewardTokenIds: [], + rewardTokenId: 0, + }, + { + rewardType: 3, // ERC1155 + rewardAmount: 2, // 2 badges per claim + rewardTokenAddress: soulboundBadge.target, + rewardTokenIds: [], + rewardTokenId: badgeTokenId, + }, + ], + }); + + const result = await rewards.getAllTreasuryBalances(); + + // Should have 2 entries: ERC20 + ERC1155 + expect(result.addresses.length).to.equal(2); + + // ERC20 (fa) + const erc20Index = result.addresses.findIndex((addr: string) => addr === mockERC20.target); + expect(result.types[erc20Index]).to.equal('fa'); + expect(result.symbols[erc20Index]).to.equal('MTK'); + expect(result.reservedBalances[erc20Index]).to.equal(ethers.parseEther('50')); // 5 * 10 ETH + + // ERC1155 (nft) + const nftIndex = result.addresses.findIndex((addr: string) => addr === soulboundBadge.target); + expect(result.types[nftIndex]).to.equal('nft'); + expect(result.totalBalances[nftIndex]).to.equal(10n); // 5 * 2 badges deposited + expect(result.reservedBalances[nftIndex]).to.equal(10n); // All reserved + }); + + it('Should update balances after user claims reward', async function () { + const { rewards, accessToken, soulboundBadge, mockERC20, devWallet, managerWallet, minterWallet, user1, badgeTokenId } = + await loadFixture(deployRewardsWithSoulboundFixture); + + // Setup: Whitelist and mint badges + await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); + await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + + // Create reward with ERC20 and ERC1155 + const rewardTokenId = 3001; + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: rewardTokenId, + maxSupply: 10, + tokenUri: 'https://example.com/reward/3001', + rewards: [ + { + rewardType: 1, // ERC20 + rewardAmount: ethers.parseEther('5'), + rewardTokenAddress: mockERC20.target, + rewardTokenIds: [], + rewardTokenId: 0, + }, + { + rewardType: 3, // ERC1155 + rewardAmount: 1, + rewardTokenAddress: soulboundBadge.target, + rewardTokenIds: [], + rewardTokenId: badgeTokenId, + }, + ], + }); + + // Check initial treasury state + let result = await rewards.getAllTreasuryBalances(); + const erc20Index = result.addresses.findIndex((addr: string) => addr === mockERC20.target); + const nftIndex = result.addresses.findIndex((addr: string) => addr === soulboundBadge.target); + + expect(result.totalBalances[erc20Index]).to.equal(ethers.parseEther('1000')); + expect(result.reservedBalances[erc20Index]).to.equal(ethers.parseEther('50')); // 10 * 5 + expect(result.totalBalances[nftIndex]).to.equal(10n); + expect(result.reservedBalances[nftIndex]).to.equal(10n); + + // Mint reward token to user and claim + await rewards.connect(minterWallet).adminMintById(user1.address, rewardTokenId, 1, true); + await rewards.connect(user1).claimReward(rewardTokenId); + + // Check treasury state after claim + result = await rewards.getAllTreasuryBalances(); + + // ERC20: balance decreased by 5 ETH, reserved decreased by 5 ETH + expect(result.totalBalances[erc20Index]).to.equal(ethers.parseEther('995')); + expect(result.reservedBalances[erc20Index]).to.equal(ethers.parseEther('45')); // 9 remaining * 5 + + // ERC1155: balance decreased by 1, reserved decreased by 1 + expect(result.totalBalances[nftIndex]).to.equal(9n); + expect(result.reservedBalances[nftIndex]).to.equal(9n); + }); + + it('Should handle multiple ERC1155 badge contracts', async function () { + const { rewards, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = + await loadFixture(deployRewardsWithSoulboundFixture); + + // Deploy a second ERC1155Soulbound badge contract + const ERC1155Soulbound = await ethers.getContractFactory('ERC1155Soulbound'); + const secondBadge = await ERC1155Soulbound.deploy( + 'F1 Badges', 'F1', 'https://f1.xyz/badges/', 'https://f1.xyz/contract/', + 100, false, devWallet.address + ); + await secondBadge.waitForDeployment(); + + // Add token to second badge + const secondBadgeTokenId = 2; + await secondBadge.connect(devWallet).addNewToken({ + tokenId: secondBadgeTokenId, + tokenUri: 'https://f1.xyz/badges/2', + receiver: ethers.ZeroAddress, + feeBasisPoints: 0, + }); + + // Whitelist Rewards on both badge contracts + await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + await secondBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await secondBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + + // Mint badges to manager + await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 50, true); + await secondBadge.connect(devWallet).adminMintId(managerWallet.address, secondBadgeTokenId, 50, true); + + // Approve both + await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + await secondBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + + // Create rewards with both badges + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: 4001, + maxSupply: 5, + tokenUri: 'https://example.com/reward/4001', + rewards: [{ + rewardType: 3, + rewardAmount: 1, + rewardTokenAddress: soulboundBadge.target, + rewardTokenIds: [], + rewardTokenId: badgeTokenId, + }], + }); + + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: 4002, + maxSupply: 5, + tokenUri: 'https://example.com/reward/4002', + rewards: [{ + rewardType: 3, + rewardAmount: 2, + rewardTokenAddress: secondBadge.target, + rewardTokenIds: [], + rewardTokenId: secondBadgeTokenId, + }], + }); + + const result = await rewards.getAllTreasuryBalances(); + + // Should have 3 entries: 1 ERC20 + 2 ERC1155 + expect(result.addresses.length).to.equal(3); + + // Count types + const faCount = result.types.filter((t: string) => t === 'fa').length; + const nftCount = result.types.filter((t: string) => t === 'nft').length; + + expect(faCount).to.equal(1); + expect(nftCount).to.equal(2); + + // Verify both NFT addresses are present + expect(result.addresses).to.include(soulboundBadge.target); + expect(result.addresses).to.include(secondBadge.target); + }); + + it('Should not duplicate NFT addresses when same badge used in multiple rewards', async function () { + const { rewards, soulboundBadge, mockERC20, devWallet, managerWallet, badgeTokenId } = + await loadFixture(deployRewardsWithSoulboundFixture); + + // Whitelist + await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); + await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + + // Add another token ID to the same badge contract + const secondTokenId = 2; + await soulboundBadge.connect(devWallet).addNewToken({ + tokenId: secondTokenId, + tokenUri: 'https://kpop.xyz/badges/2', + receiver: ethers.ZeroAddress, + feeBasisPoints: 0, + }); + + // Mint badges + await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 50, true); + await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, secondTokenId, 50, true); + await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + + // Create two rewards using SAME badge contract but different token IDs + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: 5001, + maxSupply: 5, + tokenUri: 'https://example.com/reward/5001', + rewards: [{ + rewardType: 3, + rewardAmount: 1, + rewardTokenAddress: soulboundBadge.target, + rewardTokenIds: [], + rewardTokenId: badgeTokenId, + }], + }); + + await rewards.connect(managerWallet).createTokenAndDepositRewards({ + tokenId: 5002, + maxSupply: 5, + tokenUri: 'https://example.com/reward/5002', + rewards: [{ + rewardType: 3, + rewardAmount: 1, + rewardTokenAddress: soulboundBadge.target, + rewardTokenIds: [], + rewardTokenId: secondTokenId, + }], + }); + + const result = await rewards.getAllTreasuryBalances(); + + // Should have 2 entries: 1 ERC20 + 1 ERC1155 (not duplicated) + expect(result.addresses.length).to.equal(2); + + const nftCount = result.types.filter((t: string) => t === 'nft').length; + expect(nftCount).to.equal(1); // Only one NFT entry despite two rewards using same contract + }); + }); }); From 8b4c7d00df0fd24d1e9aec92c157092dcadce7d6 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Thu, 26 Jun 2025 12:44:03 +0300 Subject: [PATCH 03/12] Feat: New params --- constants/constructor-args.ts | 15 +++++++-- .../deployments/deployments-arbitrum-one.ts | 31 ++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/constants/constructor-args.ts b/constants/constructor-args.ts index 3642d6a..4cc5ec2 100644 --- a/constants/constructor-args.ts +++ b/constants/constructor-args.ts @@ -11,7 +11,10 @@ const MOCK_USDC_ARB_SEPOLIA = '0x39B29A0Da967CDd29B45e4f942086839795c32B0'; const USDC_ARBITRUM_ONE = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; const USDT_ARBITRUM_ONE = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'; const USDC_ARBITRUM_SEPOLIA = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'; -const GUNITS_ARBITRUM_SEPOLIA = '0x0000000000000000000000000000000000000000'; +const GUNITS_ARBITRUM_SEPOLIA = '0x3E95CB707F2274e91Cd643f445128818304eD4B5'; +const GUNITS_ARBITRUM_ONE = '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; +const USDXM_ARBITRUM_SEPOLIA = '0x14196F08a4Fa0B66B7331bC40dd6bCd8A1dEeA9F' + export interface ConstructorArgs { name: string; symbol: string; @@ -382,12 +385,12 @@ export const GUnitsArgs = { _devWallet: TESTNET_DEV_WALLET, }, ARBITRUM_SEPOLIA: { - _token: MOCK_USDC_ARB_SEPOLIA, + _token: USDXM_ARBITRUM_SEPOLIA, _isPaused: false, _devWallet: TESTNET_DEV_WALLET, }, ARBITRUM_ONE: { - _token: USDT_ARBITRUM_ONE, + _token: USDC_ARBITRUM_ONE, _isPaused: false, _devWallet: 'DEPLOYER_WALLET', }, @@ -400,6 +403,12 @@ export const GReceiptsArgs = { _isPaused: false, _devWallet: TESTNET_DEV_WALLET, }, + ARBITRUM_ONE: { + _gUnits: GUNITS_ARBITRUM_ONE, + _paymentToken: USDC_ARBITRUM_ONE, + _isPaused: false, + _devWallet: 'DEPLOYER_WALLET', + }, }; export const AccessTokenG7Args = { diff --git a/constants/deployments/deployments-arbitrum-one.ts b/constants/deployments/deployments-arbitrum-one.ts index 8c23fa7..32bc6d5 100644 --- a/constants/deployments/deployments-arbitrum-one.ts +++ b/constants/deployments/deployments-arbitrum-one.ts @@ -2,7 +2,7 @@ import { CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_UPGRADABLE_FILE_NAME } from '@co import { DeploymentContract } from '../../types/deployment-type'; import { NETWORK_TYPE, NetworkName } from '../network'; import { TENANT } from '@constants/tenant'; -import { GUnitsArgs } from '@constants/constructor-args'; +import { GReceiptsArgs, GUnitsArgs } from '@constants/constructor-args'; const chain = NetworkName.ArbitrumOne; const networkType = NETWORK_TYPE.MAINNET; @@ -35,4 +35,33 @@ export const ARBITRUM_ONE_CONTRACTS: DeploymentContract[] = [ GUnitsArgs.ARBITRUM_ONE._devWallet ], }, + { + contractFileName: CONTRACT_UPGRADABLE_FILE_NAME.GReceipts, + type: CONTRACT_TYPE.GReceipts, + name: CONTRACT_NAME.GReceipts, + chain, + networkType, + tenants: [TENANT.Game7], + verify: true, + upgradable: true, + dependencies: [], + functionCalls: [ + { + contractName: CONTRACT_NAME.GUnits, + functionName: 'initialize', + args: [ + GReceiptsArgs.ARBITRUM_SEPOLIA._gUnits, + GReceiptsArgs.ARBITRUM_SEPOLIA._paymentToken, + GReceiptsArgs.ARBITRUM_SEPOLIA._isPaused, + GReceiptsArgs.ARBITRUM_SEPOLIA._devWallet, + ], + }, + ], + args: [ + GReceiptsArgs.ARBITRUM_SEPOLIA._gUnits, + GReceiptsArgs.ARBITRUM_SEPOLIA._paymentToken, + GReceiptsArgs.ARBITRUM_SEPOLIA._isPaused, + GReceiptsArgs.ARBITRUM_SEPOLIA._devWallet, + ], + } ]; From 8f3cbf4f9e50f1b0f466faea7ded748e620da5c0 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Fri, 27 Jun 2025 10:19:09 +0300 Subject: [PATCH 04/12] Chore: Update deployment args --- arbitrum.config.ts | 6 ++++-- constants/constructor-args.ts | 2 +- constants/upgrades/index.ts | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/arbitrum.config.ts b/arbitrum.config.ts index 9150c62..e323f88 100644 --- a/arbitrum.config.ts +++ b/arbitrum.config.ts @@ -25,13 +25,15 @@ defaultConfig.networks = { }; defaultConfig.etherscan = { - apiKey: ARBISCAN_API_KEY, + apiKey: { + [NetworkName.ArbitrumOne]: ARBISCAN_API_KEY!, + }, customChains: [ { network: NetworkName.ArbitrumOne, chainId: ChainId.ArbitrumOne, urls: { - apiURL: 'https://arbitrum.blockscout.com/api', + apiURL: 'https://api.arbiscan.io/api', browserURL: NetworkExplorer.ArbitrumOne, }, }, diff --git a/constants/constructor-args.ts b/constants/constructor-args.ts index 4cc5ec2..911263f 100644 --- a/constants/constructor-args.ts +++ b/constants/constructor-args.ts @@ -11,7 +11,7 @@ const MOCK_USDC_ARB_SEPOLIA = '0x39B29A0Da967CDd29B45e4f942086839795c32B0'; const USDC_ARBITRUM_ONE = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; const USDT_ARBITRUM_ONE = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'; const USDC_ARBITRUM_SEPOLIA = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d'; -const GUNITS_ARBITRUM_SEPOLIA = '0x3E95CB707F2274e91Cd643f445128818304eD4B5'; +const GUNITS_ARBITRUM_SEPOLIA = '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; const GUNITS_ARBITRUM_ONE = '0x3E3a445731d7881a3729A3898D532D5290733Eb5'; const USDXM_ARBITRUM_SEPOLIA = '0x14196F08a4Fa0B66B7331bC40dd6bCd8A1dEeA9F' diff --git a/constants/upgrades/index.ts b/constants/upgrades/index.ts index 55003af..fdd2888 100644 --- a/constants/upgrades/index.ts +++ b/constants/upgrades/index.ts @@ -46,6 +46,21 @@ export const CONTRACTS: DeploymentContract[] = [ dependencies: [], functionCalls: [], }, + { + name: 'GReceipts', // Logical name used in the --name parameter of the upgrade task + contractFileName: 'GReceipts', // The .sol file name of the NEW GUnits implementation (e.g., GUnitsV2 if different) + type: CONTRACT_TYPE.GReceipts, // Replace with your actual CONTRACT_TYPE if available and applicable + chain: NetworkName.ArbitrumOne, // << UPDATE THIS: e.g., 'arbitrumSepolia', 'mainnet', 'sepolia' + networkType: NETWORK_TYPE.MAINNET, // << UPDATE THIS: Your NETWORK_TYPE if applicable + version: 1, // << UPDATE THIS: The NEW version number this configuration represents + tenants: [TENANT.Game7], // << UPDATE THIS: Array of applicable TENANTs. Cast as any if TENANT is a complex type not imported. + upgradable: true, + proxyAddress: '0x6Bdecc2f78A911D5b166ce111cCd8b0e2703cE57', // << From your example + verify: true, + args: {}, + dependencies: [], + functionCalls: [], + }, // Add configurations for other contracts or other versions you might want to upgrade ]; From ac84b03f9f2381c9a56237913589a77c9c47579a Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Fri, 27 Jun 2025 10:21:47 +0300 Subject: [PATCH 05/12] Chore: Arb config --- arbitrum.config.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/arbitrum.config.ts b/arbitrum.config.ts index e323f88..9150c62 100644 --- a/arbitrum.config.ts +++ b/arbitrum.config.ts @@ -25,15 +25,13 @@ defaultConfig.networks = { }; defaultConfig.etherscan = { - apiKey: { - [NetworkName.ArbitrumOne]: ARBISCAN_API_KEY!, - }, + apiKey: ARBISCAN_API_KEY, customChains: [ { network: NetworkName.ArbitrumOne, chainId: ChainId.ArbitrumOne, urls: { - apiURL: 'https://api.arbiscan.io/api', + apiURL: 'https://arbitrum.blockscout.com/api', browserURL: NetworkExplorer.ArbitrumOne, }, }, From 8b60854dbc8762915e33bc1b34c24230a68ce050 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Thu, 29 Jan 2026 18:05:01 -0300 Subject: [PATCH 06/12] Feat: Add nfts treasury --- contracts/soulbounds/Rewards.sol | 228 ++++++++--- hardhat.config.ts | 3 + test/rewardsNftTreasury.test.ts | 644 +++++++++++++++++++++++++++++++ test/rewardsSoulbound.test.ts | 47 ++- 4 files changed, 858 insertions(+), 64 deletions(-) create mode 100644 test/rewardsNftTreasury.test.ts diff --git a/contracts/soulbounds/Rewards.sol b/contracts/soulbounds/Rewards.sol index 0991b66..aa9676b 100644 --- a/contracts/soulbounds/Rewards.sol +++ b/contracts/soulbounds/Rewards.sol @@ -108,6 +108,15 @@ contract Rewards is // Per-user nonce tracking mapping(address => mapping(uint256 => bool)) public userNonces; // user => nonce => used + // ERC721 Reservation Tracking + mapping(address => mapping(uint256 => bool)) public isErc721Reserved; // token address => tokenId => isReserved + mapping(address => uint256) public erc721TotalReserved; // token address => reserved amounts + + // ERC1155 Reservation Tracking + mapping(address => mapping(uint256 => uint256)) public erc1155ReservedAmounts; // token address => tokenId => reserved + mapping(address => uint256) public erc1155TotalReserved; // token address => total reserved (all IDs) + mapping(address => LibItems.RewardType) public tokenTypes; // token address => type + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -302,6 +311,42 @@ contract Rewards is } // Reserve the amount reservedAmounts[reward.rewardTokenAddress] += totalAmount; + } else if (reward.rewardType == LibItems.RewardType.ERC721) { + // Validate token is whitelisted + if (!whitelistedTokens[reward.rewardTokenAddress]) { + revert TokenNotWhitelisted(); + } + IERC721 nftContract = IERC721(reward.rewardTokenAddress); + // Verify all tokenIds are owned by this contract and unreserved + for (uint256 j = 0; j < reward.rewardTokenIds.length; j++) { + uint256 tokenId = reward.rewardTokenIds[j]; + // Check contract owns this NFT and it is not already reserved + if (nftContract.ownerOf(tokenId) != address(this) || isErc721Reserved[reward.rewardTokenAddress][tokenId]) { + revert InsufficientTreasuryBalance(); + } + } + // Reserve all tokenIds + for (uint256 j = 0; j < reward.rewardTokenIds.length; j++) { + uint256 tokenId = reward.rewardTokenIds[j]; + isErc721Reserved[reward.rewardTokenAddress][tokenId] = true; + erc721TotalReserved[reward.rewardTokenAddress]++; + } + } else if (reward.rewardType == LibItems.RewardType.ERC1155) { + // Validate token is whitelisted + if (!whitelistedTokens[reward.rewardTokenAddress]) { + revert TokenNotWhitelisted(); + } + uint256 totalAmount = reward.rewardAmount * _token.maxSupply; + uint256 balance = IERC1155(reward.rewardTokenAddress).balanceOf(address(this), reward.rewardTokenId); + uint256 reserved = erc1155ReservedAmounts[reward.rewardTokenAddress][reward.rewardTokenId]; + if (balance < reserved + totalAmount) { + revert InsufficientTreasuryBalance(); + } + // Reserve the amount + erc1155ReservedAmounts[reward.rewardTokenAddress][ + reward.rewardTokenId + ] += totalAmount; + erc1155TotalReserved[reward.rewardTokenAddress] += totalAmount; } } @@ -310,36 +355,6 @@ contract Rewards is itemIds.push(_token.tokenId); rewardTokenContract.addNewToken(_token.tokenId); - // Transfer rewards (for non-ERC20 types that are not from treasury) - address _from = _msgSender(); - address _to = address(this); - - for (uint256 i = 0; i < _token.rewards.length; i++) { - LibItems.Reward memory reward = _token.rewards[i]; - // ERC20 rewards now come from treasury (already deposited) - // So we skip transferFrom for ERC20 - if (reward.rewardType == LibItems.RewardType.ERC721) { - IERC721 token = IERC721(reward.rewardTokenAddress); - for (uint256 j = 0; j < reward.rewardTokenIds.length; j++) { - _transferERC721( - token, - _from, - _to, - reward.rewardTokenIds[j] - ); - } - } else if (reward.rewardType == LibItems.RewardType.ERC1155) { - IERC1155 token = IERC1155(reward.rewardTokenAddress); - _transferERC1155( - token, - _from, - _to, - reward.rewardTokenId, - reward.rewardAmount * _token.maxSupply - ); - } - } - emit TokenAdded(_token.tokenId); } @@ -398,9 +413,13 @@ contract Rewards is /** * @dev Whitelist a token for use in the treasury system. - * @param _token The address of the ERC20 token to whitelist. + * @param _token The address of the token to whitelist. + * @param _type The type of the token (ERC20, ERC721, ERC1155). */ - function whitelistToken(address _token) external onlyRole(MANAGER_ROLE) { + function whitelistToken( + address _token, + LibItems.RewardType _type + ) external onlyRole(MANAGER_ROLE) { if (_token == address(0)) { revert AddressIsZero(); } @@ -408,14 +427,19 @@ contract Rewards is revert TokenAlreadyWhitelisted(); } whitelistedTokens[_token] = true; + tokenTypes[_token] = _type; whitelistedTokenList.push(_token); - reservedAmounts[_token] = 0; + + if (_type == LibItems.RewardType.ERC20) { + reservedAmounts[_token] = 0; + } + emit TokenWhitelisted(_token); } /** * @dev Remove a token from the whitelist. - * @param _token The address of the ERC20 token to remove. + * @param _token The address of the token to remove. */ function removeTokenFromWhitelist( address _token @@ -423,14 +447,34 @@ contract Rewards is if (!whitelistedTokens[_token]) { revert TokenNotWhitelisted(); } - // Ensure no reserved amounts before removing - if (reservedAmounts[_token] > 0) { - revert TokenHasReserves(); - } - // Ensure contract has no balance for this token - uint256 balance = IERC20(_token).balanceOf(address(this)); - if (balance > 0) { - revert TokenHasReserves(); + + LibItems.RewardType _type = tokenTypes[_token]; + + if (_type == LibItems.RewardType.ERC20) { + // Ensure no reserved amounts before removing + if (reservedAmounts[_token] > 0) { + revert TokenHasReserves(); + } + // Ensure contract has no balance for this token + uint256 balance = IERC20(_token).balanceOf(address(this)); + if (balance > 0) { + revert TokenHasReserves(); + } + } else if (_type == LibItems.RewardType.ERC721) { + // Ensure no reserved amounts before removing + if (erc721TotalReserved[_token] > 0) { + revert TokenHasReserves(); + } + // Ensure contract has no balance for this token + uint256 balance = IERC721(_token).balanceOf(address(this)); + if (balance > 0) { + revert TokenHasReserves(); + } + } else if (_type == LibItems.RewardType.ERC1155) { + // Ensure no reserved amounts before removing + if (erc1155TotalReserved[_token] > 0) { + revert TokenHasReserves(); + } } whitelistedTokens[_token] = false; @@ -496,6 +540,72 @@ contract Rewards is SafeERC20.safeTransfer(IERC20(_token), _to, withdrawable); } + /** + * @dev Withdraw unreserved ERC721 tokens from the treasury. + * @param _token The address of the ERC721 token to withdraw. + * @param _to The address to send the tokens to. + * @param _tokenId The token ID to withdraw. + */ + function withdrawERC721UnreservedTreasury( + address _token, + address _to, + uint256 _tokenId + ) external onlyRole(MANAGER_ROLE) { + if (_to == address(0)) { + revert AddressIsZero(); + } + if (!whitelistedTokens[_token]) { + revert TokenNotWhitelisted(); + } + + if (isErc721Reserved[_token][_tokenId]) { + revert InsufficientTreasuryBalance(); + } + + // This will revert if we don't own it + if (IERC721(_token).ownerOf(_tokenId) != address(this)) { + revert InsufficientBalance(); + } + + IERC721(_token).safeTransferFrom(address(this), _to, _tokenId); + } + + /** + * @dev Withdraw unreserved ERC1155 tokens from the treasury. + * @param _token The address of the ERC1155 token to withdraw. + * @param _to The address to send the tokens to. + * @param _tokenId The token ID to withdraw. + * @param _amount The amount to withdraw. + */ + function withdrawERC1155UnreservedTreasury( + address _token, + address _to, + uint256 _tokenId, + uint256 _amount + ) external onlyRole(MANAGER_ROLE) { + if (_to == address(0)) { + revert AddressIsZero(); + } + if (!whitelistedTokens[_token]) { + revert TokenNotWhitelisted(); + } + + uint256 balance = IERC1155(_token).balanceOf(address(this), _tokenId); + uint256 reserved = erc1155ReservedAmounts[_token][_tokenId]; + + if (balance <= reserved) { + revert InsufficientBalance(); + } + + uint256 withdrawable = balance - reserved; + + if (_amount > withdrawable) { + revert InsufficientBalance(); + } + + IERC1155(_token).safeTransferFrom(address(this), _to, _tokenId, _amount, ""); + } + /** * @dev Get the treasury balance for a token. * @param _token The address of the ERC20 token. @@ -693,6 +803,10 @@ contract Rewards is } else if (_rewardType == LibItems.RewardType.ERC721) { IERC721 token = IERC721(_tokenAddress); for (uint256 i = 0; i < _tokenIds.length; i++) { + // Check if NFT is reserved + if (isErc721Reserved[_tokenAddress][_tokenIds[i]]) { + revert InsufficientTreasuryBalance(); + } _transferERC721(token, _from, _to, _tokenIds[i]); } } else if (_rewardType == LibItems.RewardType.ERC1155) { @@ -700,6 +814,13 @@ contract Rewards is revert InvalidLength(); } for (uint256 i = 0; i < _tokenIds.length; i++) { + // Check if amount exceeds unreserved balance + uint256 balance = IERC1155(_tokenAddress).balanceOf(address(this), _tokenIds[i]); + uint256 reserved = erc1155ReservedAmounts[_tokenAddress][_tokenIds[i]]; + uint256 available = balance > reserved ? balance - reserved : 0; + if (_amounts[i] > available) { + revert InsufficientTreasuryBalance(); + } _transferERC1155( IERC1155(_tokenAddress), _from, @@ -826,20 +947,35 @@ contract Rewards is ]; uint256[] memory tokenIds = reward.rewardTokenIds; for (uint256 j = 0; j < reward.rewardAmount; j++) { - if (currentIndex >= tokenIds.length) { + if (currentIndex + j > tokenIds.length) { revert InsufficientBalance(); } + uint256 tokenId = tokenIds[currentIndex + j]; + + // Release reservation + isErc721Reserved[reward.rewardTokenAddress][tokenId] = false; + erc721TotalReserved[reward.rewardTokenAddress]--; + _transferERC721( IERC721(reward.rewardTokenAddress), _from, _to, - tokenIds[currentIndex + j] + tokenId ); } - erc721RewardCurrentIndex[_rewardTokenId][i] += reward - .rewardAmount; + erc721RewardCurrentIndex[_rewardTokenId][i] += reward.rewardAmount; } else if (reward.rewardType == LibItems.RewardType.ERC1155) { + // Release reservation + if (erc1155ReservedAmounts[reward.rewardTokenAddress][reward.rewardTokenId] >= reward.rewardAmount) { + erc1155ReservedAmounts[reward.rewardTokenAddress][ + reward.rewardTokenId + ] -= reward.rewardAmount; + if (erc1155TotalReserved[reward.rewardTokenAddress] >= reward.rewardAmount) { + erc1155TotalReserved[reward.rewardTokenAddress] -= reward.rewardAmount; + } + } + _transferERC1155( IERC1155(reward.rewardTokenAddress), _from, diff --git a/hardhat.config.ts b/hardhat.config.ts index f7e814f..579f25c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -39,6 +39,9 @@ const config: HardhatUserConfig = { }, defaultNetwork: 'hardhat', networks: { + hardhat: { + allowUnlimitedContractSize: true, + }, localhost: { accounts: [DEPLOYER_PRIVATE_KEY || PRIVATE_KEY], url: 'http://127.0.0.1:7545/', diff --git a/test/rewardsNftTreasury.test.ts b/test/rewardsNftTreasury.test.ts new file mode 100644 index 0000000..9c6e35f --- /dev/null +++ b/test/rewardsNftTreasury.test.ts @@ -0,0 +1,644 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; + +/** + * Test Suite: NFT Treasury for Rewards Contract + * + * ERC721/ERC1155 treasury flow: + * - NFTs are transferred directly to the contract by admin (manager sends to Rewards) + * - Contract uses whitelistToken for ERC20, ERC721, and ERC1155 + * - createTokenAndDepositRewards reserves from contract balance (isErc721Reserved, erc1155ReservedAmounts) + * - Withdraw via withdrawAssets; reserved assets cannot be withdrawn (InsufficientTreasuryBalance) + */ +describe('Rewards NFT Treasury', function () { + async function deployRewardsNftTreasuryFixture() { + const [devWallet, adminWallet, managerWallet, minterWallet, user1, user2] = await ethers.getSigners(); + + // Deploy mock ERC20 for mixed rewards tests + const MockERC20 = await ethers.getContractFactory('MockERC20'); + const mockERC20 = await MockERC20.deploy('Mock Token', 'MTK'); + await mockERC20.waitForDeployment(); + + // Deploy mock ERC721 + const MockERC721 = await ethers.getContractFactory('MockERC721'); + const mockERC721 = await MockERC721.deploy(); + await mockERC721.waitForDeployment(); + + // Deploy mock ERC1155 + const MockERC1155 = await ethers.getContractFactory('MockERC1155'); + const mockERC1155 = await MockERC1155.deploy(); + await mockERC1155.waitForDeployment(); + + // Deploy AccessToken (soulbound ERC1155 for reward tokens) + const AccessToken = await ethers.getContractFactory('AccessToken'); + const accessToken = await AccessToken.deploy(devWallet.address); + await accessToken.waitForDeployment(); + + // Deploy Rewards contract + const Rewards = await ethers.getContractFactory('Rewards'); + const rewards = await Rewards.deploy(devWallet.address); + await rewards.waitForDeployment(); + + // Initialize AccessToken with Rewards as minter + await accessToken.initialize( + 'G7Reward', + 'G7R', + 'https://example.com/token/', + 'https://example.com/contract/', + devWallet.address, + rewards.target + ); + + // Initialize Rewards contract + await rewards.initialize(devWallet.address, managerWallet.address, minterWallet.address, accessToken.target); + + // Whitelist tokens (unified whitelist for all token types) + await rewards.connect(managerWallet).whitelistToken(mockERC20.target, 1); // ERC20 + await rewards.connect(managerWallet).whitelistToken(mockERC721.target, 2); // ERC721 + await rewards.connect(managerWallet).whitelistToken(mockERC1155.target, 3); // ERC1155 + + // Deposit ERC20 to treasury + await mockERC20.mint(managerWallet.address, ethers.parseEther('10000')); + await mockERC20.connect(managerWallet).approve(rewards.target, ethers.parseEther('10000')); + await rewards.connect(managerWallet).depositToTreasury(mockERC20.target, ethers.parseEther('10000')); + + return { + rewards, + accessToken, + mockERC20, + mockERC721, + mockERC1155, + devWallet, + adminWallet, + managerWallet, + minterWallet, + user1, + user2, + }; + } + + describe('ERC721 Treasury Management', function () { + describe('Create Reward with ERC721', function () { + it('Should create reward using ERC721 transferred directly to contract', async function () { + const { rewards, mockERC721, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Admin transfers NFTs directly to contract + for (let i = 0; i < 10; i++) { + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, i); + } + + // Create reward with ERC721 + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 2, // ERC721 + rewardAmount: 2, // 2 NFTs per claim + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + rewardTokenId: 0, + }, + ], + maxSupply: 5, // 5 claims * 2 NFTs = 10 NFTs + }; + + await expect(rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken)) + .to.emit(rewards, 'TokenAdded') + .withArgs(1); + + // Verify NFTs are reserved + for (let i = 0; i < 10; i++) { + expect(await rewards.isErc721Reserved(mockERC721.target, i)).to.be.true; + } + expect(await rewards.erc721TotalReserved(mockERC721.target)).to.equal(10); + }); + + it('Should revert if ERC721 not owned by contract', async function () { + const { rewards, mockERC721, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Mint NFTs but don't transfer to contract + await mockERC721.mint(managerWallet.address); + await mockERC721.mint(managerWallet.address); + + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 2, // ERC721 + rewardAmount: 1, + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0, 1], + rewardTokenId: 0, + }, + ], + maxSupply: 2, + }; + + await expect( + rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + + it('Should revert if ERC721 not whitelisted', async function () { + const { rewards, managerWallet, devWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Deploy a new ERC721 that's not whitelisted + const MockERC721 = await ethers.getContractFactory('MockERC721'); + const notWhitelisted = await MockERC721.deploy(); + await notWhitelisted.waitForDeployment(); + + // Mint and transfer to contract + await notWhitelisted.mint(managerWallet.address); + await notWhitelisted.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 2, + rewardAmount: 1, + rewardTokenAddress: notWhitelisted.target, + rewardTokenIds: [0], + rewardTokenId: 0, + }, + ], + maxSupply: 1, + }; + + await expect(rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken)) + .to.be.revertedWithCustomError(rewards, 'TokenNotWhitelisted'); + }); + + it('Should revert if ERC721 already reserved', async function () { + const { rewards, mockERC721, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer 4 NFTs to contract + for (let i = 0; i < 4; i++) { + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, i); + } + + // Create first reward using tokenIds 0 and 1 + const rewardToken1 = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 2, + rewardAmount: 1, + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0, 1], + rewardTokenId: 0, + }, + ], + maxSupply: 2, + }; + await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken1); + + // Try to create second reward using tokenId 0 (already reserved) + const rewardToken2 = { + tokenId: 2, + tokenUri: 'https://example.com/reward/2', + rewards: [ + { + rewardType: 2, + rewardAmount: 1, + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0, 2], // tokenId 0 is already reserved + rewardTokenId: 0, + }, + ], + maxSupply: 2, + }; + + await expect( + rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken2) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + }); + + describe('Withdraw ERC721', function () { + it('Should withdraw unreserved ERC721 via withdrawAssets', async function () { + const { rewards, mockERC721, managerWallet, user1 } = await loadFixture( + deployRewardsNftTreasuryFixture + ); + + // Transfer NFT to contract (not part of any reward) + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + + await expect( + rewards.connect(managerWallet).withdrawAssets( + 2, // LibItems.RewardType.ERC721 + user1.address, + mockERC721.target, + [0], + [] + ) + ) + .to.emit(rewards, 'AssetsWithdrawn') + .withArgs(2, user1.address, 0); + + expect(await mockERC721.ownerOf(0)).to.equal(user1.address); + }); + + it('Should revert withdraw for reserved ERC721', async function () { + const { rewards, mockERC721, managerWallet, user1 } = await loadFixture( + deployRewardsNftTreasuryFixture + ); + + // Transfer NFT to contract + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + + // Create reward to reserve the NFT + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 2, + rewardAmount: 1, + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0], + rewardTokenId: 0, + }, + ], + maxSupply: 1, + }; + await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); + + // Try to withdraw reserved NFT via withdrawAssets + await expect( + rewards.connect(managerWallet).withdrawAssets( + 2, // ERC721 + user1.address, + mockERC721.target, + [0], + [] + ) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + }); + }); + + describe('ERC1155 Treasury Management', function () { + describe('Create Reward with ERC1155', function () { + it('Should create reward using ERC1155 transferred directly to contract', async function () { + const { rewards, mockERC1155, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Admin transfers ERC1155 directly to contract + await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + + // Create reward with ERC1155 + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 3, // ERC1155 + rewardAmount: 10, // 10 tokens per claim + rewardTokenAddress: mockERC1155.target, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + maxSupply: 10, // 10 claims * 10 tokens = 100 total + }; + + await expect(rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken)) + .to.emit(rewards, 'TokenAdded') + .withArgs(1); + + // Verify amount is reserved + expect(await rewards.erc1155ReservedAmounts(mockERC1155.target, 1)).to.equal(100); + }); + + it('Should revert if insufficient ERC1155 balance', async function () { + const { rewards, mockERC1155, managerWallet } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer only 50 tokens to contract + await mockERC1155.mint(managerWallet.address, 1, 50, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 50, '0x'); + + // Try to create reward requiring 100 tokens + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 3, // ERC1155 + rewardAmount: 10, + rewardTokenAddress: mockERC1155.target, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + maxSupply: 10, // 10 * 10 = 100 required, only 50 available + }; + + await expect( + rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + }); + + describe('Withdraw ERC1155', function () { + it('Should withdraw unreserved ERC1155 via withdrawAssets', async function () { + const { rewards, mockERC1155, managerWallet, user1 } = await loadFixture( + deployRewardsNftTreasuryFixture + ); + + // Transfer tokens to contract (not part of any reward) + await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); + await mockERC1155 + .connect(managerWallet) + .safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + + await expect( + rewards.connect(managerWallet).withdrawAssets( + 3, // LibItems.RewardType.ERC1155 + user1.address, + mockERC1155.target, + [1], + [50] + ) + ) + .to.emit(rewards, 'AssetsWithdrawn') + .withArgs(3, user1.address, 50); + + expect(await mockERC1155.balanceOf(user1.address, 1)).to.equal(50); + expect(await mockERC1155.balanceOf(rewards.target, 1)).to.equal(50); + }); + + it('Should revert if withdraw amount exceeds unreserved', async function () { + const { rewards, mockERC1155, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer tokens to contract + await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + + // Create reward reserving 80 tokens + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 3, + rewardAmount: 8, + rewardTokenAddress: mockERC1155.target, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + maxSupply: 10, + }; + await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); + + // Try to withdraw 30 (only 20 unreserved) + await expect( + rewards.connect(managerWallet).withdrawAssets( + 3, // ERC1155 + user1.address, + mockERC1155.target, + [1], + [30] + ) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + }); + }); + + describe('Claim Rewards', function () { + it('Should distribute ERC721 on claim and release reservation', async function () { + const { rewards, accessToken, mockERC721, managerWallet, minterWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer NFTs to contract + for (let i = 0; i < 2; i++) { + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, i); + } + + // Create reward + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 2, // ERC721 + rewardAmount: 1, + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0, 1], + rewardTokenId: 0, + }, + ], + maxSupply: 2, + }; + await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); + + // Mint reward token to user + await rewards.connect(minterWallet).adminMintById(user1.address, 1, 1, true); + + // Verify user has reward token + expect(await accessToken.balanceOf(user1.address, 1)).to.equal(1); + + // Claim reward + await expect(rewards.connect(user1).claimReward(1)) + .to.emit(rewards, 'Claimed') + .withArgs(user1.address, 1, 1); + + // Verify user received NFT + expect(await mockERC721.ownerOf(0)).to.equal(user1.address); + + // Verify reservation released + expect(await rewards.isErc721Reserved(mockERC721.target, 0)).to.be.false; + + // TokenId 1 should still be reserved + expect(await rewards.isErc721Reserved(mockERC721.target, 1)).to.be.true; + expect(await rewards.erc721TotalReserved(mockERC721.target)).to.equal(1); + }); + + it('Should distribute ERC1155 on claim', async function () { + const { rewards, accessToken, mockERC1155, managerWallet, minterWallet, user1 } = await loadFixture( + deployRewardsNftTreasuryFixture + ); + + // Transfer tokens to contract + await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); + await mockERC1155 + .connect(managerWallet) + .safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + + // Create reward + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 3, // ERC1155 + rewardAmount: 10, + rewardTokenAddress: mockERC1155.target, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + maxSupply: 10, + }; + await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); + + // Mint reward token to user + await rewards.connect(minterWallet).adminMintById(user1.address, 1, 1, true); + + // Claim reward + await expect(rewards.connect(user1).claimReward(1)) + .to.emit(rewards, 'Claimed') + .withArgs(user1.address, 1, 1); + + // Verify user received tokens + expect(await mockERC1155.balanceOf(user1.address, 1)).to.equal(10); + + // Verify reserved amount decreased after claim + expect(await rewards.erc1155ReservedAmounts(mockERC1155.target, 1)).to.equal(90); + }); + }); + + describe('Mixed Rewards (ERC20 + ERC721 + ERC1155)', function () { + it('Should create and claim reward with mixed asset types', async function () { + const { rewards, accessToken, mockERC20, mockERC721, mockERC1155, managerWallet, minterWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer ERC721 to contract + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + + // Transfer ERC1155 to contract + await mockERC1155.mint(managerWallet.address, 1, 10, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 10, '0x'); + + // Create mixed reward + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 1, // ERC20 + rewardAmount: ethers.parseEther('100'), + rewardTokenAddress: mockERC20.target, + rewardTokenIds: [], + rewardTokenId: 0, + }, + { + rewardType: 2, // ERC721 + rewardAmount: 1, + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0], + rewardTokenId: 0, + }, + { + rewardType: 3, // ERC1155 + rewardAmount: 10, + rewardTokenAddress: mockERC1155.target, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + maxSupply: 1, + }; + await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); + + // Mint reward token to user + await rewards.connect(minterWallet).adminMintById(user1.address, 1, 1, true); + + // Record initial balances + const initialERC20Balance = await mockERC20.balanceOf(user1.address); + + // Claim reward + await rewards.connect(user1).claimReward(1); + + // Verify user received all rewards + expect(await mockERC20.balanceOf(user1.address)).to.equal(initialERC20Balance + ethers.parseEther('100')); + expect(await mockERC721.ownerOf(0)).to.equal(user1.address); + expect(await mockERC1155.balanceOf(user1.address, 1)).to.equal(10); + }); + }); + + + describe('withdrawAssets Protection', function () { + it('Should protect reserved ERC721 via withdrawAssets', async function () { + const { rewards, mockERC721, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer NFT to contract + await mockERC721.mint(managerWallet.address); + await mockERC721.connect(managerWallet).transferFrom(managerWallet.address, rewards.target, 0); + + // Create reward reserving the NFT + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 2, + rewardAmount: 1, + rewardTokenAddress: mockERC721.target, + rewardTokenIds: [0], + rewardTokenId: 0, + }, + ], + maxSupply: 1, + }; + await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); + + // Try to withdraw reserved NFT via withdrawAssets + await expect( + rewards.connect(managerWallet).withdrawAssets( + 2, // ERC721 + user1.address, + mockERC721.target, + [0], + [] + ) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + + it('Should protect reserved ERC1155 via withdrawAssets', async function () { + const { rewards, mockERC1155, managerWallet, user1 } = await loadFixture(deployRewardsNftTreasuryFixture); + + // Transfer tokens to contract + await mockERC1155.mint(managerWallet.address, 1, 100, '0x'); + await mockERC1155.connect(managerWallet).safeTransferFrom(managerWallet.address, rewards.target, 1, 100, '0x'); + + // Create reward reserving 80 tokens + const rewardToken = { + tokenId: 1, + tokenUri: 'https://example.com/reward/1', + rewards: [ + { + rewardType: 3, + rewardAmount: 8, + rewardTokenAddress: mockERC1155.target, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + maxSupply: 10, + }; + await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); + + // Try to withdraw 30 via withdrawAssets (only 20 unreserved) + await expect( + rewards.connect(managerWallet).withdrawAssets( + 3, // ERC1155 + user1.address, + mockERC1155.target, + [1], + [30] + ) + ).to.be.revertedWithCustomError(rewards, 'InsufficientTreasuryBalance'); + }); + }); +}); diff --git a/test/rewardsSoulbound.test.ts b/test/rewardsSoulbound.test.ts index c027d92..da61567 100644 --- a/test/rewardsSoulbound.test.ts +++ b/test/rewardsSoulbound.test.ts @@ -55,12 +55,7 @@ describe('Rewards with Soulbound Tokens', function () { ); // Initialize Rewards contract - await rewards.initialize( - devWallet.address, - managerWallet.address, - minterWallet.address, - accessToken.target - ); + await rewards.initialize(devWallet.address, managerWallet.address, minterWallet.address, accessToken.target); // Setup: Add a token to the soulbound badge contract const badgeTokenId = 1; @@ -72,7 +67,7 @@ describe('Rewards with Soulbound Tokens', function () { }); // Whitelist ERC20 for treasury - await rewards.connect(managerWallet).whitelistToken(mockERC20.target); + await rewards.connect(managerWallet).whitelistToken(mockERC20.target, 1); // ERC20 // Mint ERC20 to manager and deposit to treasury await mockERC20.mint(managerWallet.address, ethers.parseEther('1000')); @@ -179,8 +174,17 @@ describe('Rewards with Soulbound Tokens', function () { }); it('Complete flow: Create reward with ERC1155 soulbound badge as prize', async function () { - const { rewards, accessToken, soulboundBadge, mockERC20, devWallet, managerWallet, minterWallet, user1, badgeTokenId } = - await loadFixture(deployRewardsWithSoulboundFixture); + const { + rewards, + accessToken, + soulboundBadge, + mockERC20, + devWallet, + managerWallet, + minterWallet, + user1, + badgeTokenId, + } = await loadFixture(deployRewardsWithSoulboundFixture); // Step 1: Whitelist Rewards contract AND managerWallet on soulbound badge contract // - Rewards contract needs whitelist to RECEIVE and DISTRIBUTE soulbound badges @@ -188,16 +192,22 @@ describe('Rewards with Soulbound Tokens', function () { await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); - // Step 2: Mint soulbound badges to managerWallet (who will deposit them) - // NOTE: If we mint directly to Rewards without whitelist, we'd need a different approach - await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); + // Step 2: Whitelist the ERC1155 (soulbound badge) in the Rewards treasury (unified whitelistToken) + await rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // ERC1155 - // Step 3: Manager approves Rewards contract to transfer badges + // Step 3: Mint soulbound badges to managerWallet, then transfer to Rewards contract + await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + await soulboundBadge + .connect(managerWallet) + .safeTransferFrom(managerWallet.address, rewards.target, badgeTokenId, 10, '0x'); + + // Verify Rewards contract now has the badges + expect(await soulboundBadge.balanceOf(rewards.target, badgeTokenId)).to.equal(10); // Step 4: Create a reward token that gives: // - 10 ERC20 tokens (from treasury) - // - 1 soulbound badge (transferred from manager during create) + // - 1 soulbound badge (from contract balance; createTokenAndDepositRewards reserves them) const rewardTokenId = 1001; const rewardToken = { tokenId: rewardTokenId, @@ -221,11 +231,11 @@ describe('Rewards with Soulbound Tokens', function () { ], }; - // This transfers 10 soulbound badges (1 per maxSupply) from manager to Rewards + // This reserves 10 soulbound badges (1 per maxSupply) await rewards.connect(managerWallet).createTokenAndDepositRewards(rewardToken); - // Verify Rewards contract now has the badges - expect(await soulboundBadge.balanceOf(rewards.target, badgeTokenId)).to.equal(10); + // Verify reserved amounts (ERC1155 treasury tracking) + expect(await rewards.erc1155ReservedAmounts(soulboundBadge.target, badgeTokenId)).to.equal(10); // Step 5: Mint reward token to user (tokenId, amount, isSoulbound) await rewards.connect(minterWallet).adminMintById(user1.address, rewardTokenId, 1, true); @@ -251,8 +261,9 @@ describe('Rewards with Soulbound Tokens', function () { soulboundBadge.connect(user1).safeTransferFrom(user1.address, devWallet.address, badgeTokenId, 1, '0x') ).to.be.revertedWithCustomError(soulboundBadge, 'SoulboundAmountError'); - // Step 8: Verify Rewards contract still has remaining badges + // Step 8: Verify Rewards contract still has remaining badges and reserved amount decreased expect(await soulboundBadge.balanceOf(rewards.target, badgeTokenId)).to.equal(9); + expect(await rewards.erc1155ReservedAmounts(soulboundBadge.target, badgeTokenId)).to.equal(9); }); }); From bc773358efcbf459fdd1a7a6534f578f2adb2f48 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Thu, 29 Jan 2026 19:36:22 -0300 Subject: [PATCH 07/12] Feat: Deploy scripts rewards upgradeable --- contracts/soulbounds/Rewards.sol | 65 ++++++---- scripts/createInitialRewards.ts | 204 +++++++++++++++++++++++++++++++ scripts/deployRewardsUUPS.ts | 100 +++++++++++++++ test/rewardsNftTreasury.test.ts | 13 +- test/rewardsSoulbound.test.ts | 13 +- 5 files changed, 358 insertions(+), 37 deletions(-) create mode 100644 scripts/createInitialRewards.ts create mode 100644 scripts/deployRewardsUUPS.ts diff --git a/contracts/soulbounds/Rewards.sol b/contracts/soulbounds/Rewards.sol index aa9676b..c5ae42f 100644 --- a/contracts/soulbounds/Rewards.sol +++ b/contracts/soulbounds/Rewards.sol @@ -27,34 +27,39 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { - ERC1155Holder -} from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; + AccessControlUpgradeable +} from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; import { - ERC721Holder -} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; + ERC1155HolderUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; import { - AccessControl -} from "@openzeppelin/contracts/access/AccessControl.sol"; -import { Pausable } from "@openzeppelin/contracts/utils/Pausable.sol"; + ERC721HolderUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; import { - ReentrancyGuard -} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + ContextUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { + ReentrancyGuardUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { Initializable -} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; +} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import { AccessToken } from "../soulbounds/AccessToken.sol"; import { ERCWhitelistSignature } from "../ercs/ERCWhitelistSignature.sol"; import { LibItems } from "../libraries/LibItems.sol"; contract Rewards is - ERCWhitelistSignature, - AccessControl, - Pausable, - ReentrancyGuard, Initializable, - ERC1155Holder, - ERC721Holder + ERCWhitelistSignature, + AccessControlUpgradeable, + PausableUpgradeable, + ReentrancyGuardUpgradeable, + UUPSUpgradeable, + ERC1155HolderUpgradeable, + ERC721HolderUpgradeable { /*////////////////////////////////////////////////////////////// ERRORS @@ -85,6 +90,7 @@ contract Rewards is bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); bytes32 public constant MANAGER_ROLE = keccak256("MANAGER_ROLE"); bytes32 public constant DEV_CONFIG_ROLE = keccak256("DEV_CONFIG_ROLE"); + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); /*////////////////////////////////////////////////////////////// STATE-VARS @@ -145,19 +151,21 @@ contract Rewards is ); event TokenURIChanged(uint256 indexed tokenId, string newUri); - constructor(address devWallet) { - if (devWallet == address(0)) { - revert AddressIsZero(); - } - _grantRole(DEFAULT_ADMIN_ROLE, devWallet); - } - function initialize( address _devWallet, address _managerWallet, address _minterWallet, address _rewardTokenAddress - ) external initializer onlyRole(DEFAULT_ADMIN_ROLE) { + ) external initializer { + if (_devWallet == address(0)) { + revert AddressIsZero(); + } + + __AccessControl_init(); + __Pausable_init(); + __ReentrancyGuard_init(); + __ERC1155Holder_init(); + __ERC721Holder_init(); if ( _devWallet == address(0) || _managerWallet == address(0) || @@ -172,9 +180,16 @@ contract Rewards is _grantRole(DEV_CONFIG_ROLE, _devWallet); _grantRole(MANAGER_ROLE, _managerWallet); _grantRole(MINTER_ROLE, _minterWallet); + _grantRole(UPGRADER_ROLE, _devWallet); _addWhitelistSigner(_devWallet); } + function _authorizeUpgrade( + address newImplementation + ) internal override { + _checkRole(UPGRADER_ROLE); + } + function updateRewardTokenContract( address _rewardTokenAddress ) external onlyRole(DEV_CONFIG_ROLE) { @@ -1291,7 +1306,7 @@ contract Rewards is function supportsInterface( bytes4 interfaceId - ) public view override(AccessControl, ERC1155Holder) returns (bool) { + ) public view override(AccessControlUpgradeable, ERC1155HolderUpgradeable) returns (bool) { return super.supportsInterface(interfaceId); } diff --git a/scripts/createInitialRewards.ts b/scripts/createInitialRewards.ts new file mode 100644 index 0000000..999ea85 --- /dev/null +++ b/scripts/createInitialRewards.ts @@ -0,0 +1,204 @@ +import { ethers } from 'hardhat'; + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Creating rewards with account:', deployer.address); + + // Configuration + const REWARDS_ADDRESS = process.env.REWARDS_ADDRESS || '0x53b27dE8fb05A051d5CA601Eb71505C508789102'; + + if (!REWARDS_ADDRESS) { + console.error('Please set REWARDS_ADDRESS environment variable'); + process.exit(1); + } + + const rewards = await ethers.getContractAt('Rewards', REWARDS_ADDRESS); + + // Addresses on Sepolia + const addresses = { + F1: ethers.getAddress('0x1a7a1879bE0C3fD48e033B2eEF40063bFE551731'), + NewJeans: ethers.getAddress('0x4afF7E3F1191b4dEE2a0358417a750C1c6fF9b62'), + Quince: ethers.getAddress('0x40813d715Ed741C0bA6848763c93aaF75fEA7F55'), + KPOP: ethers.getAddress('0x049d3CC16a5521E1dE1922059d09FCDd719DC81c'), + USDC: ethers.getAddress('0x3E3a445731d7881a3729A3898D532D5290733Eb5'), + }; + + // Reward Configuration + const rewardsToCreate = [ + { + name: 'F1 Reward', + tokenId: 1, + tokenUri: 'https://summon.xyz/rewards/1', + maxSupply: 100, + rewards: [ + { + rewardType: 3, // ERC1155 + rewardAmount: 1, + rewardTokenAddress: addresses.F1, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + }, + { + name: 'New Jeans Reward', + tokenId: 2, + tokenUri: 'https://summon.xyz/rewards/2', + maxSupply: 100, + rewards: [ + { + rewardType: 3, // ERC1155 + rewardAmount: 1, + rewardTokenAddress: addresses.NewJeans, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + }, + { + name: 'Quince Reward', + tokenId: 3, + tokenUri: 'https://summon.xyz/rewards/3', + maxSupply: 100, + rewards: [ + { + rewardType: 3, // ERC1155 + rewardAmount: 1, + rewardTokenAddress: addresses.Quince, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + }, + { + name: 'KPOP Reward', + tokenId: 4, + tokenUri: 'https://summon.xyz/rewards/4', + maxSupply: 100, + rewards: [ + { + rewardType: 3, // ERC1155 + rewardAmount: 1, + rewardTokenAddress: addresses.KPOP, + rewardTokenIds: [], + rewardTokenId: 1, + }, + ], + }, + { + name: 'USDC Reward', + tokenId: 5, + tokenUri: 'https://summon.xyz/rewards/5', + maxSupply: 100, + rewards: [ + { + rewardType: 1, // ERC20 + rewardAmount: ethers.parseUnits('1', 6), // 1 USDC + rewardTokenAddress: addresses.USDC, + rewardTokenIds: [], + rewardTokenId: 0, + }, + ], + }, + ]; + + console.log(`\nCreating ${rewardsToCreate.length} reward bundles...`); + + for (const rewardConfig of rewardsToCreate) { + console.log(`\nProcessing Reward Token ID: ${rewardConfig.tokenId} (${rewardConfig.name})`); + + // 1. Calculate required amounts for deposit + let usdcRequired = 0n; + let erc1155Required = 0n; + let erc1155Address = ''; + let erc1155Id = 0; + + for (const r of rewardConfig.rewards) { + if (r.rewardType === 1) { + // ERC20 + usdcRequired += BigInt(r.rewardAmount) * BigInt(rewardConfig.maxSupply); + } else if (r.rewardType === 3) { + // ERC1155 + erc1155Required += BigInt(r.rewardAmount) * BigInt(rewardConfig.maxSupply); + erc1155Address = r.rewardTokenAddress; + erc1155Id = r.rewardTokenId; + } + } + + // 2. Approve and Deposit USDC + if (usdcRequired > 0n) { + console.log(`Required USDC: ${ethers.formatUnits(usdcRequired, 6)}`); + const MockERC20 = await ethers.getContractAt( + '@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20', + addresses.USDC + ); + + // Check allowance + const allowance = await MockERC20.allowance(deployer.address, REWARDS_ADDRESS); + if (allowance < usdcRequired) { + console.log('Approving USDC...'); + await(await MockERC20.approve(REWARDS_ADDRESS, ethers.MaxUint256)).wait(); + } + + console.log('Depositing USDC to Treasury...'); + const txDeposit = await rewards.depositToTreasury(addresses.USDC, usdcRequired); + await txDeposit.wait(); + console.log('USDC Deposited'); + } + + // 3. Transfer ERC1155 directly to Rewards contract + if (erc1155Required > 0n && erc1155Address) { + console.log(`Required ERC1155 (${erc1155Address}, ID ${erc1155Id}): ${erc1155Required}`); + const MockERC1155 = await ethers.getContractAt('IERC1155', erc1155Address); + + // Check balance + const balance = await MockERC1155.balanceOf(deployer.address, erc1155Id); + if (balance < erc1155Required) { + console.warn( + `WARNING: Insufficient balance for ${rewardConfig.name}. Have ${balance}, need ${erc1155Required}. Transaction might fail.` + ); + } + + console.log('Transferring ERC1155 to Treasury...'); + const txTransfer = await MockERC1155.safeTransferFrom( + deployer.address, + REWARDS_ADDRESS, + erc1155Id, + erc1155Required, + '0x' + ); + await txTransfer.wait(); + console.log('ERC1155 Transferred'); + + // Debug: Check balance of Rewards contract + const rewardsBalance = await MockERC1155.balanceOf(REWARDS_ADDRESS, erc1155Id); + console.log(`Rewards Contract Balance for ${erc1155Address} ID ${erc1155Id}: ${rewardsBalance}`); + } + + /* // 4. Create Reward Token + console.log('Creating Reward Token...'); + try { + // Check if token already exists to avoid revert + const exists = await rewards.isTokenExist(rewardConfig.tokenId); + if (exists) { + console.log(`Reward Token ${rewardConfig.tokenId} already exists. Skipping...`); + continue; + } + + const tx = await rewards.createTokenAndDepositRewards(rewardConfig); + await tx.wait(); + console.log(`Reward Token ${rewardConfig.tokenId} created successfully!`); + } catch (e: any) { + console.error(`Failed to create reward ${rewardConfig.tokenId}:`, e.message); + } */ + } + + console.log('\nAll rewards processed!'); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/scripts/deployRewardsUUPS.ts b/scripts/deployRewardsUUPS.ts new file mode 100644 index 0000000..d80c3ba --- /dev/null +++ b/scripts/deployRewardsUUPS.ts @@ -0,0 +1,100 @@ +import { ethers, upgrades } from 'hardhat'; + +async function main() { + const [deployer] = await ethers.getSigners(); + console.log('Deploying contracts with account:', deployer.address); + console.log('Account balance:', (await ethers.provider.getBalance(deployer.address)).toString()); + + // Configuration + const devWallet = deployer.address; + const managerWallet = deployer.address; + const minterWallet = deployer.address; + + // 1. Deploy AccessToken (Standard Deployment) + console.log('\n1. Deploying AccessToken...'); + const AccessToken = await ethers.getContractFactory('AccessToken'); + const accessToken = await AccessToken.deploy(devWallet); + await accessToken.waitForDeployment(); + const accessTokenAddress = await accessToken.getAddress(); + console.log('AccessToken deployed to:', accessTokenAddress); + + // 2. Deploy Rewards (UUPS Proxy) + console.log('\n2. Deploying Rewards (UUPS Proxy)...'); + const Rewards = await ethers.getContractFactory('Rewards'); + + // Deploy proxy + // Pass constructor arguments in `constructorArgs` and initializer arguments in second array + const rewards = await upgrades.deployProxy( + Rewards, + [devWallet, managerWallet, minterWallet, accessTokenAddress], + { + kind: 'uups', + initializer: 'initialize', + } + ); + await rewards.waitForDeployment(); + const rewardsAddress = await rewards.getAddress(); + console.log('Rewards Proxy deployed to:', rewardsAddress); + + // 3. Initialize AccessToken + console.log('\n3. Initializing AccessToken...'); + const initAccessTokenTx = await accessToken.initialize( + 'Rewards Access Token', // name + 'RAT', // symbol + 'https://summon.xyz/metadata/', // defaultTokenURI + 'https://summon.xyz/contract/', // contractURI + devWallet, + rewardsAddress // minterContract is the Rewards contract + ); + await initAccessTokenTx.wait(); + console.log('AccessToken initialized with Rewards contract as minter'); + + // 4. Whitelist tokens in Rewards contract + console.log('\n4. Whitelisting existing tokens in Rewards contract...'); + + // Existing token addresses on Sepolia + const existingTokens = [ + { address: '0x1a7a1879bE0C3fD48e033B2eEF40063bFE551731', type: 3, name: 'F1 Grand Prix VIP Ticket (ERC1155)' }, + { address: '0x4afF7E3F1191b4dEE2a0358417a750C1c6fF9b62', type: 3, name: 'New Jeans New Album (ERC1155)' }, + { address: '0x40813d715Ed741C0bA6848763c93aaF75fEA7F55', type: 3, name: 'Quince Discount (ERC1155)' }, + { address: '0x049d3CC16a5521E1dE1922059d09FCDd719DC81c', type: 3, name: 'KPOP Badges (ERC1155)' }, + { address: '0x3E3a445731d7881a3729A3898D532D5290733Eb5', type: 1, name: 'USDC (ERC20)' }, + ]; + + // Helper to map type number to enum name for logging + const typeNames = { 1: 'ERC20', 2: 'ERC721', 3: 'ERC1155' }; + + for (const token of existingTokens) { + try { + console.log(`Whitelisting ${token.name} (${token.address})...`); + // whitelistToken(address _token, LibItems.RewardType _type) + const tx = await rewards.whitelistToken(token.address, token.type); + await tx.wait(); + console.log(`Confirmed: ${token.name} whitelisted`); + } catch (error: any) { + console.error(`Failed to whitelist ${token.name}:`, error.message); + } + } + + console.log('\n========================================'); + console.log('Deployment Summary:'); + console.log('========================================'); + console.log('AccessToken:', accessTokenAddress); + console.log('Rewards (Proxy):', rewardsAddress); + console.log('Dev Wallet:', devWallet); + console.log('Manager Wallet:', managerWallet); + console.log('Minter Wallet:', minterWallet); + console.log('========================================'); + + // Verify instructions + console.log('\nTo verify contracts on Etherscan:'); + console.log(`npx hardhat verify --network sepolia ${accessTokenAddress} ${devWallet}`); + console.log(`npx hardhat verify --network sepolia ${rewardsAddress}`); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); diff --git a/test/rewardsNftTreasury.test.ts b/test/rewardsNftTreasury.test.ts index 9c6e35f..ad9bf2f 100644 --- a/test/rewardsNftTreasury.test.ts +++ b/test/rewardsNftTreasury.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; /** @@ -35,9 +35,13 @@ describe('Rewards NFT Treasury', function () { const accessToken = await AccessToken.deploy(devWallet.address); await accessToken.waitForDeployment(); - // Deploy Rewards contract + // Deploy Rewards contract (UUPS proxy) const Rewards = await ethers.getContractFactory('Rewards'); - const rewards = await Rewards.deploy(devWallet.address); + const rewards = await upgrades.deployProxy( + Rewards, + [devWallet.address, managerWallet.address, minterWallet.address, accessToken.target], + { kind: 'uups', initializer: 'initialize' } + ); await rewards.waitForDeployment(); // Initialize AccessToken with Rewards as minter @@ -50,9 +54,6 @@ describe('Rewards NFT Treasury', function () { rewards.target ); - // Initialize Rewards contract - await rewards.initialize(devWallet.address, managerWallet.address, minterWallet.address, accessToken.target); - // Whitelist tokens (unified whitelist for all token types) await rewards.connect(managerWallet).whitelistToken(mockERC20.target, 1); // ERC20 await rewards.connect(managerWallet).whitelistToken(mockERC721.target, 2); // ERC721 diff --git a/test/rewardsSoulbound.test.ts b/test/rewardsSoulbound.test.ts index da61567..74ff1ee 100644 --- a/test/rewardsSoulbound.test.ts +++ b/test/rewardsSoulbound.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { ethers } from 'hardhat'; +import { ethers, upgrades } from 'hardhat'; import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; /** @@ -39,9 +39,13 @@ describe('Rewards with Soulbound Tokens', function () { ); await soulboundBadge.waitForDeployment(); - // Deploy Rewards contract + // Deploy Rewards contract (UUPS proxy) const Rewards = await ethers.getContractFactory('Rewards'); - const rewards = await Rewards.deploy(devWallet.address); + const rewards = await upgrades.deployProxy( + Rewards, + [devWallet.address, managerWallet.address, minterWallet.address, accessToken.target], + { kind: 'uups', initializer: 'initialize' } + ); await rewards.waitForDeployment(); // Initialize AccessToken with Rewards as minter @@ -54,9 +58,6 @@ describe('Rewards with Soulbound Tokens', function () { rewards.target ); - // Initialize Rewards contract - await rewards.initialize(devWallet.address, managerWallet.address, minterWallet.address, accessToken.target); - // Setup: Add a token to the soulbound badge contract const badgeTokenId = 1; await soulboundBadge.connect(devWallet).addNewToken({ From 06760ad575d2d7e490f85e4d5dfaf6894d45510b Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Thu, 29 Jan 2026 20:03:04 -0300 Subject: [PATCH 08/12] Fix: Array check --- contracts/soulbounds/Rewards.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/soulbounds/Rewards.sol b/contracts/soulbounds/Rewards.sol index c5ae42f..3421495 100644 --- a/contracts/soulbounds/Rewards.sol +++ b/contracts/soulbounds/Rewards.sol @@ -962,7 +962,7 @@ contract Rewards is ]; uint256[] memory tokenIds = reward.rewardTokenIds; for (uint256 j = 0; j < reward.rewardAmount; j++) { - if (currentIndex + j > tokenIds.length) { + if (currentIndex + j >= tokenIds.length) { revert InsufficientBalance(); } uint256 tokenId = tokenIds[currentIndex + j]; From c0db2e3a5aa2f55adbef0085496d5116e5cec012 Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Fri, 30 Jan 2026 10:20:17 -0300 Subject: [PATCH 09/12] Chore: Move rewards contract --- contracts/{ => upgradeables}/soulbounds/Rewards.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename contracts/{ => upgradeables}/soulbounds/Rewards.sol (99%) diff --git a/contracts/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol similarity index 99% rename from contracts/soulbounds/Rewards.sol rename to contracts/upgradeables/soulbounds/Rewards.sol index 3421495..6185de8 100644 --- a/contracts/soulbounds/Rewards.sol +++ b/contracts/upgradeables/soulbounds/Rewards.sol @@ -47,9 +47,9 @@ import { } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import { AccessToken } from "../soulbounds/AccessToken.sol"; -import { ERCWhitelistSignature } from "../ercs/ERCWhitelistSignature.sol"; -import { LibItems } from "../libraries/LibItems.sol"; +import { AccessToken } from "../../soulbounds/AccessToken.sol"; +import { ERCWhitelistSignature } from "../../ercs/ERCWhitelistSignature.sol"; +import { LibItems } from "../../libraries/LibItems.sol"; contract Rewards is Initializable, From 620ba3afcb004582ecbce8f023f85ada30ab849e Mon Sep 17 00:00:00 2001 From: Daniel Lima Date: Mon, 2 Feb 2026 16:38:31 -0300 Subject: [PATCH 10/12] Fix: PR fixes --- .../ercs/ERCWhitelistSignatureUpgradeable.sol | 22 ++++++++++++++++++- contracts/upgradeables/soulbounds/Rewards.sol | 8 +++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/contracts/upgradeables/ercs/ERCWhitelistSignatureUpgradeable.sol b/contracts/upgradeables/ercs/ERCWhitelistSignatureUpgradeable.sol index 1afaa8c..967d065 100644 --- a/contracts/upgradeables/ercs/ERCWhitelistSignatureUpgradeable.sol +++ b/contracts/upgradeables/ercs/ERCWhitelistSignatureUpgradeable.sol @@ -29,6 +29,7 @@ import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract ERCWhitelistSignatureUpgradeable is Initializable { mapping(address => bool) public whitelistSigners; mapping(bytes => bool) private usedSignatures; + address[] private whitelistSignerList; event WhitelistSignerAdded(address indexed signer); event WhitelistSignerRemoved(address indexed signer); @@ -51,6 +52,7 @@ contract ERCWhitelistSignatureUpgradeable is Initializable { require(_signer != address(0), "ERCWhitelistSignature: signer is the zero address"); require(!whitelistSigners[_signer], "ERCWhitelistSignature: signer is already in the whitelist"); whitelistSigners[_signer] = true; + whitelistSignerList.push(_signer); emit WhitelistSignerAdded(_signer); } @@ -58,6 +60,16 @@ contract ERCWhitelistSignatureUpgradeable is Initializable { require(_signer != address(0), "ERCWhitelistSignature: signer is the zero address"); require(whitelistSigners[_signer], "ERCWhitelistSignature: signer is not in the whitelist"); whitelistSigners[_signer] = false; + // Remove from list + for (uint256 i = 0; i < whitelistSignerList.length; i++) { + if (whitelistSignerList[i] == _signer) { + whitelistSignerList[i] = whitelistSignerList[ + whitelistSignerList.length - 1 + ]; + whitelistSignerList.pop(); + break; + } + } emit WhitelistSignerRemoved(_signer); } @@ -100,6 +112,14 @@ contract ERCWhitelistSignatureUpgradeable is Initializable { return values; } + function _getWhitelistSigners() + internal + view + returns (address[] memory) + { + return whitelistSignerList; + } + // Reserved storage space to allow for layout changes in the future. - uint256[48] private __gap; + uint256[47] private __gap; } diff --git a/contracts/upgradeables/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol index 6185de8..7bef235 100644 --- a/contracts/upgradeables/soulbounds/Rewards.sol +++ b/contracts/upgradeables/soulbounds/Rewards.sol @@ -48,12 +48,12 @@ import { import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; import { AccessToken } from "../../soulbounds/AccessToken.sol"; -import { ERCWhitelistSignature } from "../../ercs/ERCWhitelistSignature.sol"; +import { ERCWhitelistSignatureUpgradeable } from "../ercs/ERCWhitelistSignatureUpgradeable.sol"; import { LibItems } from "../../libraries/LibItems.sol"; contract Rewards is Initializable, - ERCWhitelistSignature, + ERCWhitelistSignatureUpgradeable, AccessControlUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, @@ -123,6 +123,8 @@ contract Rewards is mapping(address => uint256) public erc1155TotalReserved; // token address => total reserved (all IDs) mapping(address => LibItems.RewardType) public tokenTypes; // token address => type + uint256[33] private __gap; + /*////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ @@ -166,6 +168,8 @@ contract Rewards is __ReentrancyGuard_init(); __ERC1155Holder_init(); __ERC721Holder_init(); + __ERCWhitelistSignatureUpgradeable_init(); + if ( _devWallet == address(0) || _managerWallet == address(0) || From 49308b3c612ddf3235c8adb541e8e4067d6817d3 Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Wed, 4 Feb 2026 15:54:41 +0100 Subject: [PATCH 11/12] Feat: Include getAllTreasuryBalance with new treasury for nfts --- contracts/upgradeables/soulbounds/Rewards.sol | 223 ++++++++++++++++++ test/rewardsSoulbound.test.ts | 99 ++++++-- 2 files changed, 308 insertions(+), 14 deletions(-) diff --git a/contracts/upgradeables/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol index 7bef235..2a1f61a 100644 --- a/contracts/upgradeables/soulbounds/Rewards.sol +++ b/contracts/upgradeables/soulbounds/Rewards.sol @@ -21,8 +21,11 @@ pragma solidity ^0.8.28; //.................................................................................................................................................... import { IERC1155 } from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import { IERC1155MetadataURI } from "@openzeppelin/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol"; import { IERC721 } from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -212,6 +215,226 @@ contract Rewards is return address(rewardTokenContract); } + /** + * @dev Get treasury balances for all whitelisted tokens with full balance breakdown. + * Includes ERC20 tokens (fa), ERC721 tokens (nft), and ERC1155 tokens (nft) from the treasury. + * @return addresses Array of token addresses. + * @return totalBalances Array of total balances in the contract. + * @return reservedBalances Array of reserved amounts for rewards. + * @return availableBalances Array of available (unreserved) balances. + * @return symbols Array of token symbols. + * @return names Array of token names. + * @return types Array of token types ("fa" for fungible assets, "nft" for NFTs). + */ + function getAllTreasuryBalances() + external + view + returns ( + address[] memory addresses, + uint256[] memory totalBalances, + uint256[] memory reservedBalances, + uint256[] memory availableBalances, + string[] memory symbols, + string[] memory names, + string[] memory types + ) + { + // Count ERC20 and ERC721 tokens from whitelistedTokenList (excluding ERC1155) + uint256 erc20AndErc721Count = 0; + for (uint256 i = 0; i < whitelistedTokenList.length; i++) { + LibItems.RewardType tokenType = tokenTypes[whitelistedTokenList[i]]; + if (tokenType == LibItems.RewardType.ERC20 || tokenType == LibItems.RewardType.ERC721) { + erc20AndErc721Count++; + } + } + + // Count unique ERC1155 token IDs (since one ERC1155 contract can have multiple token IDs) + uint256 erc1155Count = _countUniqueErc1155TokenIds(); + uint256 totalCount = erc20AndErc721Count + erc1155Count; + + addresses = new address[](totalCount); + totalBalances = new uint256[](totalCount); + reservedBalances = new uint256[](totalCount); + availableBalances = new uint256[](totalCount); + symbols = new string[](totalCount); + names = new string[](totalCount); + types = new string[](totalCount); + + uint256 currentIndex = 0; + + // Process all whitelisted tokens + for (uint256 i = 0; i < whitelistedTokenList.length; i++) { + address tokenAddress = whitelistedTokenList[i]; + LibItems.RewardType tokenType = tokenTypes[tokenAddress]; + + addresses[currentIndex] = tokenAddress; + + if (tokenType == LibItems.RewardType.ERC20) { + // ERC20 token + uint256 totalBalance = IERC20(tokenAddress).balanceOf(address(this)); + uint256 reserved = reservedAmounts[tokenAddress]; + + totalBalances[currentIndex] = totalBalance; + reservedBalances[currentIndex] = reserved; + availableBalances[currentIndex] = totalBalance > reserved ? totalBalance - reserved : 0; + + try IERC20Metadata(tokenAddress).symbol() returns (string memory symbol) { + symbols[currentIndex] = symbol; + } catch { + symbols[currentIndex] = "UNKNOWN"; + } + + try IERC20Metadata(tokenAddress).name() returns (string memory name) { + names[currentIndex] = name; + } catch { + names[currentIndex] = "Unknown Token"; + } + + types[currentIndex] = "fa"; + currentIndex++; + + } else if (tokenType == LibItems.RewardType.ERC721) { + // ERC721 token + uint256 totalBalance = IERC721(tokenAddress).balanceOf(address(this)); + uint256 reserved = erc721TotalReserved[tokenAddress]; + + totalBalances[currentIndex] = totalBalance; + reservedBalances[currentIndex] = reserved; + availableBalances[currentIndex] = totalBalance > reserved ? totalBalance - reserved : 0; + + // Try to get ERC721 metadata + try IERC721Metadata(tokenAddress).symbol() returns (string memory symbol) { + symbols[currentIndex] = symbol; + } catch { + symbols[currentIndex] = "ERC721"; + } + + try IERC721Metadata(tokenAddress).name() returns (string memory name) { + names[currentIndex] = name; + } catch { + names[currentIndex] = "NFT Collection"; + } + + types[currentIndex] = "nft"; + currentIndex++; + + } else if (tokenType == LibItems.RewardType.ERC1155) { + // ERC1155 tokens - need to iterate through rewards to get token IDs + // We'll handle these separately below + continue; + } + } + + // Process ERC1155 tokens separately (since they have multiple token IDs per contract) + // Track processed ERC1155 combinations to avoid duplicates + address[] memory processedErc1155Addresses = new address[](erc1155Count); + uint256[] memory processedErc1155TokenIds = new uint256[](erc1155Count); + uint256 processedCount = 0; + + for (uint256 i = 0; i < itemIds.length; i++) { + uint256 tokenId = itemIds[i]; + LibItems.RewardToken storage rewardToken = tokenRewards[tokenId]; + + for (uint256 j = 0; j < rewardToken.rewards.length; j++) { + LibItems.Reward storage reward = rewardToken.rewards[j]; + + if (reward.rewardType != LibItems.RewardType.ERC1155) { + continue; + } + + address erc1155Address = reward.rewardTokenAddress; + uint256 erc1155TokenId = reward.rewardTokenId; + + // Check if this exact address+tokenID combination was already added + bool alreadyAdded = false; + for (uint256 k = 0; k < processedCount; k++) { + if (processedErc1155Addresses[k] == erc1155Address && + processedErc1155TokenIds[k] == erc1155TokenId) { + alreadyAdded = true; + break; + } + } + + if (!alreadyAdded && currentIndex < totalCount) { + // Track this combination + processedErc1155Addresses[processedCount] = erc1155Address; + processedErc1155TokenIds[processedCount] = erc1155TokenId; + processedCount++; + + addresses[currentIndex] = erc1155Address; + + uint256 balance = IERC1155(erc1155Address).balanceOf(address(this), erc1155TokenId); + uint256 reserved = erc1155ReservedAmounts[erc1155Address][erc1155TokenId]; + + totalBalances[currentIndex] = balance; + reservedBalances[currentIndex] = reserved; + availableBalances[currentIndex] = balance > reserved ? balance - reserved : 0; + + // Try to get ERC1155 metadata (note: name() and symbol() are not part of ERC1155 standard, + // but many implementations include them). Using IERC20Metadata for the interface since it + // has the same name()/symbol() signatures. + try IERC20Metadata(erc1155Address).name() returns (string memory contractName) { + names[currentIndex] = contractName; + } catch { + names[currentIndex] = "ERC1155 Collection"; + } + + try IERC20Metadata(erc1155Address).symbol() returns (string memory contractSymbol) { + symbols[currentIndex] = contractSymbol; + } catch { + symbols[currentIndex] = "ERC1155"; + } + + types[currentIndex] = "nft"; + currentIndex++; + } + } + } + } + + /** + * @dev Count unique ERC1155 token IDs used in rewards. + * ERC1155 contracts can have multiple token IDs, so we need to count them separately. + */ + function _countUniqueErc1155TokenIds() private view returns (uint256) { + // Use a large enough array to track unique combinations + address[] memory uniqueAddresses = new address[](itemIds.length * 10); + uint256[] memory uniqueTokenIds = new uint256[](itemIds.length * 10); + uint256 count = 0; + + for (uint256 i = 0; i < itemIds.length; i++) { + uint256 tokenId = itemIds[i]; + LibItems.RewardToken storage rewardToken = tokenRewards[tokenId]; + + for (uint256 j = 0; j < rewardToken.rewards.length; j++) { + LibItems.Reward storage reward = rewardToken.rewards[j]; + + if (reward.rewardType == LibItems.RewardType.ERC1155) { + address erc1155Address = reward.rewardTokenAddress; + uint256 erc1155TokenId = reward.rewardTokenId; + + // Check if this combination already exists + bool found = false; + for (uint256 k = 0; k < count; k++) { + if (uniqueAddresses[k] == erc1155Address && uniqueTokenIds[k] == erc1155TokenId) { + found = true; + break; + } + } + + if (!found) { + uniqueAddresses[count] = erc1155Address; + uniqueTokenIds[count] = erc1155TokenId; + count++; + } + } + } + } + + return count; + } + + function getAllItemIds() external view returns (uint256[] memory) { return itemIds; } diff --git a/test/rewardsSoulbound.test.ts b/test/rewardsSoulbound.test.ts index 74ff1ee..9675a13 100644 --- a/test/rewardsSoulbound.test.ts +++ b/test/rewardsSoulbound.test.ts @@ -377,15 +377,19 @@ describe('Rewards with Soulbound Tokens', function () { const accessToken = await AccessToken.deploy(devWallet.address); await accessToken.waitForDeployment(); + // Deploy Rewards using UUPS proxy const Rewards = await ethers.getContractFactory('Rewards'); - const rewards = await Rewards.deploy(devWallet.address); + const rewards = await upgrades.deployProxy( + Rewards, + [devWallet.address, managerWallet.address, minterWallet.address, accessToken.target], + { kind: 'uups', initializer: 'initialize' } + ); await rewards.waitForDeployment(); await accessToken.initialize( 'G7Reward', 'G7R', 'https://example.com/token/', 'https://example.com/contract/', devWallet.address, rewards.target ); - await rewards.initialize(devWallet.address, managerWallet.address, minterWallet.address, accessToken.target); const result = await rewards.getAllTreasuryBalances(); @@ -422,11 +426,20 @@ describe('Rewards with Soulbound Tokens', function () { await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + // Whitelist soulboundBadge in Rewards treasury + await rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // 3 = ERC1155 + // Mint badges to manager await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); - // Manager approves and creates reward - await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + // Transfer badges to Rewards contract + await soulboundBadge.connect(managerWallet).safeTransferFrom( + managerWallet.address, + rewards.target, + badgeTokenId, + 10, // Transfer 10 badges (same as maxSupply * rewardAmount) + '0x' + ); const rewardTokenId = 1001; await rewards.connect(managerWallet).createTokenAndDepositRewards({ @@ -467,11 +480,20 @@ describe('Rewards with Soulbound Tokens', function () { await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + // Whitelist soulboundBadge in Rewards treasury + await rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // 3 = ERC1155 + // Mint badges to manager await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); - // Manager approves badge - await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + // Transfer badges to Rewards contract (5 maxSupply * 2 rewardAmount = 10 badges) + await soulboundBadge.connect(managerWallet).safeTransferFrom( + managerWallet.address, + rewards.target, + badgeTokenId, + 10, + '0x' + ); // Create reward with BOTH ERC20 and ERC1155 const rewardTokenId = 2001; @@ -522,8 +544,17 @@ describe('Rewards with Soulbound Tokens', function () { // Setup: Whitelist and mint badges await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + await rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // 3 = ERC1155 await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, true); - await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + + // Transfer badges to Rewards contract (10 maxSupply * 1 rewardAmount = 10 badges) + await soulboundBadge.connect(managerWallet).safeTransferFrom( + managerWallet.address, + rewards.target, + badgeTokenId, + 10, + '0x' + ); // Create reward with ERC20 and ERC1155 const rewardTokenId = 3001; @@ -602,13 +633,29 @@ describe('Rewards with Soulbound Tokens', function () { await secondBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); await secondBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + // Whitelist both badges in Rewards treasury + await rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // 3 = ERC1155 + await rewards.connect(managerWallet).whitelistToken(secondBadge.target, 3); // 3 = ERC1155 + // Mint badges to manager await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 50, true); await secondBadge.connect(devWallet).adminMintId(managerWallet.address, secondBadgeTokenId, 50, true); - // Approve both - await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); - await secondBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + // Transfer badges to Rewards contract + await soulboundBadge.connect(managerWallet).safeTransferFrom( + managerWallet.address, + rewards.target, + badgeTokenId, + 5, // First reward: 5 maxSupply * 1 rewardAmount = 5 badges + '0x' + ); + await secondBadge.connect(managerWallet).safeTransferFrom( + managerWallet.address, + rewards.target, + secondBadgeTokenId, + 10, // Second reward: 5 maxSupply * 2 rewardAmount = 10 badges + '0x' + ); // Create rewards with both badges await rewards.connect(managerWallet).createTokenAndDepositRewards({ @@ -662,6 +709,9 @@ describe('Rewards with Soulbound Tokens', function () { await soulboundBadge.connect(devWallet).updateWhitelistAddress(rewards.target, true); await soulboundBadge.connect(devWallet).updateWhitelistAddress(managerWallet.address, true); + // Whitelist soulboundBadge in Rewards treasury + await rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // 3 = ERC1155 + // Add another token ID to the same badge contract const secondTokenId = 2; await soulboundBadge.connect(devWallet).addNewToken({ @@ -674,7 +724,22 @@ describe('Rewards with Soulbound Tokens', function () { // Mint badges await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 50, true); await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, secondTokenId, 50, true); - await soulboundBadge.connect(managerWallet).setApprovalForAll(rewards.target, true); + + // Transfer both token IDs to Rewards contract + await soulboundBadge.connect(managerWallet).safeTransferFrom( + managerWallet.address, + rewards.target, + badgeTokenId, + 5, // First reward: 5 maxSupply * 1 rewardAmount = 5 badges + '0x' + ); + await soulboundBadge.connect(managerWallet).safeTransferFrom( + managerWallet.address, + rewards.target, + secondTokenId, + 5, // Second reward: 5 maxSupply * 1 rewardAmount = 5 badges + '0x' + ); // Create two rewards using SAME badge contract but different token IDs await rewards.connect(managerWallet).createTokenAndDepositRewards({ @@ -705,11 +770,17 @@ describe('Rewards with Soulbound Tokens', function () { const result = await rewards.getAllTreasuryBalances(); - // Should have 2 entries: 1 ERC20 + 1 ERC1155 (not duplicated) - expect(result.addresses.length).to.equal(2); + // Should have 3 entries: 1 ERC20 + 2 ERC1155 token IDs (tracked separately) + // Each unique ERC1155 token ID is tracked separately since they have independent balances + expect(result.addresses.length).to.equal(3); const nftCount = result.types.filter((t: string) => t === 'nft').length; - expect(nftCount).to.equal(1); // Only one NFT entry despite two rewards using same contract + expect(nftCount).to.equal(2); // Two NFT entries (same contract, different token IDs) + + // Verify both use the same contract address + const nftAddresses = result.addresses.filter((_: string, idx: number) => result.types[idx] === 'nft'); + expect(nftAddresses[0]).to.equal(soulboundBadge.target); + expect(nftAddresses[1]).to.equal(soulboundBadge.target); }); }); }); From 616a449a8025d44581ace8231279fe7c2b4a6600 Mon Sep 17 00:00:00 2001 From: ogarciarevett Date: Wed, 4 Feb 2026 17:06:34 +0100 Subject: [PATCH 12/12] Chore: Remove test code for name and symbol on erc1155 --- contracts/upgradeables/soulbounds/Rewards.sol | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/contracts/upgradeables/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol index 2a1f61a..c620eb7 100644 --- a/contracts/upgradeables/soulbounds/Rewards.sol +++ b/contracts/upgradeables/soulbounds/Rewards.sol @@ -370,21 +370,10 @@ contract Rewards is reservedBalances[currentIndex] = reserved; availableBalances[currentIndex] = balance > reserved ? balance - reserved : 0; - // Try to get ERC1155 metadata (note: name() and symbol() are not part of ERC1155 standard, - // but many implementations include them). Using IERC20Metadata for the interface since it - // has the same name()/symbol() signatures. - try IERC20Metadata(erc1155Address).name() returns (string memory contractName) { - names[currentIndex] = contractName; - } catch { - names[currentIndex] = "ERC1155 Collection"; - } - - try IERC20Metadata(erc1155Address).symbol() returns (string memory contractSymbol) { - symbols[currentIndex] = contractSymbol; - } catch { - symbols[currentIndex] = "ERC1155"; - } - + // ERC1155 standard does not include name() or symbol() functions + // Use generic names for ERC1155 tokens + names[currentIndex] = "ERC1155 Collection"; + symbols[currentIndex] = "ERC1155"; types[currentIndex] = "nft"; currentIndex++; }