diff --git a/contracts/upgradeables/soulbounds/Rewards.sol b/contracts/upgradeables/soulbounds/Rewards.sol index 7bef235..c620eb7 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,215 @@ 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; + + // 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++; + } + } + } + } + + /** + * @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/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 new file mode 100644 index 0000000..8fd2533 --- /dev/null +++ b/scripts/deployAllBadges.ts @@ -0,0 +1,165 @@ +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 || '0x5d62C8cfDe4a1B0be2Cc102023F2563bc29221Cc'; + + // Badge configurations + // NOTE: KPOP and F1 already deployed, only deploying remaining badges + const badges = [ + // 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', + 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('========================================'); + // 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}`); + + // 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/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 new file mode 100644 index 0000000..ce1aa71 --- /dev/null +++ b/scripts/setupAllRewards.ts @@ -0,0 +1,345 @@ +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 || '0x4163079Aa7d3ed57755c7278BA4156a826E25Ad4'; + +// 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 on badge + console.log('Step 1: Whitelisting Rewards contract on badge...'); + 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)); + } + + // Step 2: Whitelist Manager wallet on badge + console.log('\nStep 2: Whitelisting Manager wallet on badge...'); + try { + const tx = await badge.updateWhitelistAddress(deployer.address, true); + await tx.wait(); + console.log(' Manager wallet whitelisted'); + } catch (error: any) { + console.log(' Error or already whitelisted:', error.message?.slice(0, 50)); + } + + // 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 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); + + // 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?.slice(0, 80)); + } + + // 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); + }); 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 9b4ad4a..9675a13 100644 --- a/test/rewardsSoulbound.test.ts +++ b/test/rewardsSoulbound.test.ts @@ -367,4 +367,420 @@ 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(); + + // Deploy Rewards using UUPS proxy + const Rewards = await ethers.getContractFactory('Rewards'); + 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 + ); + + 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); + + // 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); + + // 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({ + 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); + + // 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); + + // 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; + 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 rewards.connect(managerWallet).whitelistToken(soulboundBadge.target, 3); // 3 = ERC1155 + await soulboundBadge.connect(devWallet).adminMintId(managerWallet.address, badgeTokenId, 100, 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; + 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); + + // 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); + + // 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({ + 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); + + // 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({ + 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); + + // 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({ + 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 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(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); + }); + }); });