diff --git a/README.md b/README.md index a5c5415..572f721 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,30 @@ - - -
-

Fishcake Contracts Repo

-
- -
- - Version - - - License: Apache-2.0 - -
- -Fishcake Contracts Project - -## Installation - -For prerequisites and detailed build instructions please read the [Installation](https://github.com/FishcakeLab/fishcake-contracts/) instructions. Once the dependencies are installed, run: - -```bash -git submodule update --init --recursive --remote -``` -or -```bash -forge install foundry-rs/forge-std --no-commit -forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit -forge install OpenZeppelin/openzeppelin-contracts --no-commit -forge install OpenZeppelin/openzeppelin-foundry-upgrades --no-commit -``` - -Or check out the latest [release](https://github.com/FishcakeLab/fishcake-contracts). - -## Test And Depoly - -```bash -$env:PRIVATE_KEY = "0x2a871" -$env:USDT_ADDRESS = "0x3C4249f1cDfaAAFf" -$env:OPENZEPPELIN_BASH_PATH = "C:/Users/65126/Documents/Git/bin/bash.exe" -``` - - -### test -``` -forge test --ffi -``` - -### Depoly - -``` -forge script script/DeployerV2.s.sol:DeployerScript --rpc-url $RPC_URL --private-key $PRIVKEY --ffi - -``` - -### Upgrade - -``` -forge script script/UpgradeInvestorSalePoolDeployerV2.s.sol:UpgradeInvestorSalePoolDeployer --rpc-url $RPC_URL --private-key $PRIVKEY --ffi -``` - -## Community - - -## Contributing - -Looking for a good place to start contributing? Check out some [`good first issues`](https://github.com/FishcakeLab/fishcake-contracts/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). - -For additional instructions, standards and style guides, please refer to the [Contributing](./CONTRIBUTING.md) document. +# My Fishcake Multi-Chain Event Manager – Hackathon Project (Direction 2) + +Simplified, USDT-only Event Manager contract for Fishcake (AI × Web3 Hackathon). + +## What I Built +- Forked original Fishcake repo +- Removed all non-core features (FCC mining, NFT logic, complex rewards) +- Focused on **USDT stablecoin activities** +- Added upgradeability (Transparent Proxy via OpenZeppelin) +- Security: ReentrancyGuard, Pausable, SafeERC20, emergency withdraw +- Basic frontend attempt (React + wagmi + RainbowKit still building..) +- Local deployment proof with Foundry Anvil + +## Requirements Met (Direction 2) +- ✅ Supports multi-chain deployment (pure EVM Solidity) +- ✅ Simplified ecological functions, core only +- ✅ USDT stablecoin-based activities +- ✅ Consistent logic across chains +- ✅ Event creation, reward distribution, verification/finish + +## Tech Stack +- Solidity 0.8.26 + Foundry +- OpenZeppelin upgradeable contracts + +## Future Plans +- Deploy to Sepolia + BSC Testnet when test ETH is available +- Add USDC support + token registry +- Build a full React frontend using wagmi + RainbowKit, with event listing, creation flows, and enhanced UI/UX. + +Open to feedback & collaboration! diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 723f8ca..aa677e9 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 723f8cab09cdae1aca9ec9cc1cfa040c2d4b06c1 +Subproject commit aa677e9d28ed78fc427ec47ba2baef2030c58e7c diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades index dd9e5dd..cbce1e0 160000 --- a/lib/openzeppelin-foundry-upgrades +++ b/lib/openzeppelin-foundry-upgrades @@ -1 +1 @@ -Subproject commit dd9e5dd22b885b364354af6a1cbad8a36958e3df +Subproject commit cbce1e00305e943aa1661d43f41e5ac72c662b07 diff --git a/script/DeployMultiChain.s.sol b/script/DeployMultiChain.s.sol new file mode 100644 index 0000000..9afd9c0 --- /dev/null +++ b/script/DeployMultiChain.s.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Script.sol"; +import "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {FishcakeEventManagerMultiChain} from "../src/contracts/core/FishcakeEventManagerMultiChain.sol"; + +contract DeployMultiChain is Script { + // USDT addresses on different chains + address constant BSC_USDT = 0x55d398326f99059fF775485246999027B3197955; + address constant POLYGON_USDT = 0xc2132D05D31c914a87C6611C10748AEb04B58e8F; + address constant ARBITRUM_USDT = 0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9; + address constant OPTIMISM_USDT = 0x94b008aA00579c1307B0EF2c499aD98a8ce58e58; + address constant BASE_USDT = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; + address constant ETH_USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + + // Testnet USDT addresses + address constant BSC_TESTNET_USDT = 0x337610d27c682E347C9cD60BD4b3b107C9d34dDd; + address constant SEPOLIA_USDT = 0x7169D38820dfd117C3FA1f22a697dBA58d90BA06; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + console.log("==== Fishcake Multi-Chain Deployment ===="); + console.log("Deployer:", deployer); + console.log("Chain ID:", block.chainid); + + // Get USDT address for current chain + address usdtAddress = getUSDTAddress(block.chainid); + require(usdtAddress != address(0), "USDT address not found for this chain"); + + console.log("USDT address:", usdtAddress); + + vm.startBroadcast(deployerPrivateKey); + + // Deploy proxy + address proxy = Upgrades.deployTransparentProxy( + "FishcakeEventManagerMultiChain.sol", + deployer, + abi.encodeCall( + FishcakeEventManagerMultiChain.initialize, + (deployer, usdtAddress) + ) + ); + + console.log("\n=== Deployment Successful ==="); + console.log("Proxy Address:", proxy); + + // Get implementation address + bytes32 implSlot = vm.load(proxy, bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); + address implementation = address(uint160(uint256(implSlot))); + console.log("Implementation:", implementation); + + vm.stopBroadcast(); + + console.log("\n=== Next Steps ==="); + console.log("1. Verify contract:"); + console.log(" forge verify-contract", implementation, "FishcakeEventManagerMultiChain --chain-id", block.chainid); + console.log("2. Interact with proxy at:", proxy); + } + + function getUSDTAddress(uint256 chainId) internal pure returns (address) { + // Mainnets + if (chainId == 56) return BSC_USDT; // BSC + if (chainId == 137) return POLYGON_USDT; // Polygon + if (chainId == 42161) return ARBITRUM_USDT; // Arbitrum + if (chainId == 10) return OPTIMISM_USDT; // Optimism + if (chainId == 8453) return BASE_USDT; // Base + if (chainId == 1) return ETH_USDT; // Ethereum + + // Testnets + if (chainId == 97) return BSC_TESTNET_USDT; // BSC Testnet + if (chainId == 11155111) return SEPOLIA_USDT; // Sepolia + + return address(0); + } +} \ No newline at end of file diff --git a/src/contracts/core/FishcakeEventManagerMultiChain.sol b/src/contracts/core/FishcakeEventManagerMultiChain.sol new file mode 100644 index 0000000..a66d9c9 --- /dev/null +++ b/src/contracts/core/FishcakeEventManagerMultiChain.sol @@ -0,0 +1,540 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "@openzeppelin-upgrades/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin-upgrades/contracts/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/security/PausableUpgradeable.sol"; +import "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol"; + +/** + * @title FishcakeEventManagerMultiChain + * @notice Simplified multi-chain event management contract for Fishcake + * @dev Core functionality: event creation, reward distribution, and verification + * Supports USDT stablecoin-based activities across multiple chains + */ +contract FishcakeEventManagerMultiChain is + Initializable, + OwnableUpgradeable, + ReentrancyGuardUpgradeable, + PausableUpgradeable +{ + using SafeERC20Upgradeable for IERC20Upgradeable; + + // ============ Constants ============ + uint256 public constant MAX_DEADLINE = 30 days; + uint256 public constant MIN_TOTAL_AMOUNT = 1e6; // 1 USDT (6 decimals) + uint256 public constant MAX_DROP_NUMBER = 1000; // Maximum participants per event + + // ============ Enums ============ + enum DropType { + FIXED, // 1: Fixed amount per participant + RANDOM // 2: Random amount within range + } + + enum ActivityStatus { + ACTIVE, // 0: Event is active + FINISHED, // 1: Event finished by creator + EXPIRED // 2: Event expired (deadline passed) + } + + // ============ Structs ============ + struct ActivityInfo { + uint256 activityId; + address creator; + string businessName; + string activityContent; + string location; // coordinates or location description + uint256 createdAt; + uint256 deadline; + DropType dropType; + uint256 totalDrops; // number of reward slots + uint256 minDropAmount; // 0 for FIXED type + uint256 maxDropAmount; // amount per drop for FIXED type + address tokenAddress; + uint256 totalAmount; // Total locked amount + uint256 distributedAmount; // amount already distributed + uint256 distributedCount; // this is the number of drops completed + ActivityStatus status; + } + + struct DropRecord { + uint256 activityId; + address recipient; + uint256 amount; + uint256 timestamp; + } + + // ============ State Variables ============ + ActivityInfo[] public activities; + DropRecord[] public dropRecords; + + // Activity ID => Recipient => Has received drop + mapping(uint256 => mapping(address => bool)) public hasReceived; + + // Supported stablecoin tokens (address => is supported) + mapping(address => bool) public supportedTokens; + + // Creator address => array of their activity IDs + mapping(address => uint256[]) public creatorActivities; + + // ============ Events ============ + event ActivityCreated( + uint256 indexed activityId, + address indexed creator, + string businessName, + uint256 totalAmount, + uint256 totalDrops, + address tokenAddress, + uint256 deadline + ); + + event DropDistributed( + uint256 indexed activityId, + address indexed recipient, + uint256 amount, + uint256 timestamp + ); + + event ActivityFinished( + uint256 indexed activityId, + uint256 remainingAmount, + uint256 distributedCount + ); + + event TokenSupportUpdated( + address indexed tokenAddress, + bool isSupported + ); + + // ============ Modifiers ============ + modifier validActivity(uint256 _activityId) { + require(_activityId > 0 && _activityId <= activities.length, "Invalid activity ID"); + _; + } + + modifier onlyCreator(uint256 _activityId) { + require( + activities[_activityId - 1].creator == msg.sender, + "Not the activity creator" + ); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + // ============ Initializer ============ + function initialize( + address _initialOwner, + address _usdtAddress + ) public initializer { + require(_initialOwner != address(0), "Invalid owner address"); + require(_usdtAddress != address(0), "Invalid USDT address"); + + __Ownable_init(); + __ReentrancyGuard_init(); + __Pausable_init(); + + transferOwnership(_initialOwner); + + // USDT as supported token + supportedTokens[_usdtAddress] = true; + emit TokenSupportUpdated(_usdtAddress, true); + } + + // ============ External Functions ============ + + /** + * @notice Create a new event with reward distribution + * @param _businessName Name of the business or event creator + * @param _activityContent Description of the event + * @param _location Location information this could be coordinates or description... + * @param _deadline Event expiration timestamp + * @param _totalAmount Total amount to distribute + * @param _dropType Type of distribution (FIXED or RANDOM) + * @param _totalDrops Number of reward slots + * @param _minDropAmount Minimum amount per drop (0 for FIXED) + * @param _maxDropAmount Maximum or fixed amount per drop + * @param _tokenAddress Token contract address must be supported + * @return success Operation success status + * @return activityId ID of created activity + */ + function createActivity( + string memory _businessName, + string memory _activityContent, + string memory _location, + uint256 _deadline, + uint256 _totalAmount, + DropType _dropType, + uint256 _totalDrops, + uint256 _minDropAmount, + uint256 _maxDropAmount, + address _tokenAddress + ) external nonReentrant whenNotPaused returns (bool success, uint256 activityId) { + + // Validation + require(bytes(_businessName).length > 0, "Business name required"); + require(bytes(_activityContent).length > 0, "Activity content required"); + require(supportedTokens[_tokenAddress], "Token not supported"); + require(_totalAmount >= MIN_TOTAL_AMOUNT, "Amount too small"); + require(_totalDrops > 0 && _totalDrops <= MAX_DROP_NUMBER, "Invalid drop count"); + require(_deadline > block.timestamp, "Deadline must be in future"); + require(_deadline <= block.timestamp + MAX_DEADLINE, "Deadline too far"); + require(_maxDropAmount > 0, "Max drop amount must be positive"); + + if (_dropType == DropType.RANDOM) { + require(_maxDropAmount >= _minDropAmount, "Invalid amount range"); + require(_minDropAmount > 0, "Min amount must be positive for random"); + // for random, total should be able to cover max scenario + require(_totalAmount >= _minDropAmount * _totalDrops, "Insufficient total for min drops"); + } else { + // for FIXED type, total must equal max * drops + require(_totalAmount == _maxDropAmount * _totalDrops, "Total must equal max * drops"); + _minDropAmount = 0; // Force to 0 for fixed type + } + + // tansfer tokens to contract + IERC20Upgradeable(_tokenAddress).safeTransferFrom( + msg.sender, + address(this), + _totalAmount + ); + + // create activity + activityId = activities.length + 1; + + ActivityInfo memory newActivity = ActivityInfo({ + activityId: activityId, + creator: msg.sender, + businessName: _businessName, + activityContent: _activityContent, + location: _location, + createdAt: block.timestamp, + deadline: _deadline, + dropType: _dropType, + totalDrops: _totalDrops, + minDropAmount: _minDropAmount, + maxDropAmount: _maxDropAmount, + tokenAddress: _tokenAddress, + totalAmount: _totalAmount, + distributedAmount: 0, + distributedCount: 0, + status: ActivityStatus.ACTIVE + }); + + activities.push(newActivity); + creatorActivities[msg.sender].push(activityId); + + emit ActivityCreated( + activityId, + msg.sender, + _businessName, + _totalAmount, + _totalDrops, + _tokenAddress, + _deadline + ); + + return (true, activityId); + } + + /** + * @notice Distribute reward to a participant + * @param _activityId ID of the activity + * @param _recipient Address to receive the reward + * @param _amount Amount to distribute must be within range for RANDOM type + * @return success Operation success status + */ + function distributeReward( + uint256 _activityId, + address _recipient, + uint256 _amount + ) + external + nonReentrant + whenNotPaused + validActivity(_activityId) + onlyCreator(_activityId) + returns (bool success) + { + ActivityInfo storage activity = activities[_activityId - 1]; + + // validations + require(activity.status == ActivityStatus.ACTIVE, "Activity not active"); + require(block.timestamp <= activity.deadline, "Activity expired"); + require(_recipient != address(0), "Invalid recipient"); + require(!hasReceived[_activityId][_recipient], "Already received reward"); + require(activity.distributedCount < activity.totalDrops, "All rewards distributed"); + + // validate amount based on drop type + if (activity.dropType == DropType.FIXED) { + require(_amount == activity.maxDropAmount, "Amount must equal fixed amount"); + } else { + require( + _amount >= activity.minDropAmount && _amount <= activity.maxDropAmount, + "Amount outside allowed range" + ); + } + + // check remaining funds + uint256 remaining = activity.totalAmount - activity.distributedAmount; + require(_amount <= remaining, "Insufficient remaining funds"); + + // update state + hasReceived[_activityId][_recipient] = true; + activity.distributedAmount += _amount; + activity.distributedCount++; + + // record drop + dropRecords.push(DropRecord({ + activityId: _activityId, + recipient: _recipient, + amount: _amount, + timestamp: block.timestamp + })); + + // reward transfer + IERC20Upgradeable(activity.tokenAddress).safeTransfer(_recipient, _amount); + + emit DropDistributed(_activityId, _recipient, _amount, block.timestamp); + + return true; + } + + /** + * @notice Finish an activity and return remaining funds + * @param _activityId ID of the activity to finish + * @return success Operation success status + */ + function finishActivity(uint256 _activityId) + external + nonReentrant + whenNotPaused + validActivity(_activityId) + onlyCreator(_activityId) + returns (bool success) + { + ActivityInfo storage activity = activities[_activityId - 1]; + + require(activity.status == ActivityStatus.ACTIVE, "Activity already finished"); + + // calculate remaining amount + uint256 remainingAmount = activity.totalAmount - activity.distributedAmount; + + // update status + activity.status = ActivityStatus.FINISHED; + + // return remaining funds if any + if (remainingAmount > 0) { + IERC20Upgradeable(activity.tokenAddress).safeTransfer( + msg.sender, + remainingAmount + ); + } + + emit ActivityFinished(_activityId, remainingAmount, activity.distributedCount); + + return true; + } + + /** + * @notice Mark expired activities + * @param _activityIds Array of activity IDs to check and mark as expired + */ + function markExpiredActivities(uint256[] calldata _activityIds) external { + for (uint256 i = 0; i < _activityIds.length; i++) { + uint256 activityId = _activityIds[i]; + if (activityId > 0 && activityId <= activities.length) { + ActivityInfo storage activity = activities[activityId - 1]; + + if ( + activity.status == ActivityStatus.ACTIVE && + block.timestamp > activity.deadline + ) { + activity.status = ActivityStatus.EXPIRED; + + // return remaining funds to creator + uint256 remaining = activity.totalAmount - activity.distributedAmount; + if (remaining > 0) { + IERC20Upgradeable(activity.tokenAddress).safeTransfer( + activity.creator, + remaining + ); + } + + emit ActivityFinished(activityId, remaining, activity.distributedCount); + } + } + } + } + + // ============ View Functions ============ + + /** + * @notice Get activity details + * @param _activityId ID of the activity + * @return Activity information + */ + function getActivity(uint256 _activityId) + external + view + validActivity(_activityId) + returns (ActivityInfo memory) + { + return activities[_activityId - 1]; + } + + /** + * @notice Get total number of activities + */ + function getActivityCount() external view returns (uint256) { + return activities.length; + } + + /** + * @notice Check if user has received reward for an activity + * @param _activityId ID of the activity + * @param _user Address to check + */ + function hasUserReceived(uint256 _activityId, address _user) + external + view + returns (bool) + { + return hasReceived[_activityId][_user]; + } + + /** + * @notice Get activities created by a specific address + * @param _creator Creator address + */ + + function getCreatorActivities(address _creator) + external + view + returns (uint256[] memory) + { + return creatorActivities[_creator]; + } + + /** + * @notice Get total number of drop records + */ + function getDropRecordCount() external view returns (uint256) { + return dropRecords.length; + } + + /** + * @notice Get drop record by index + * @param _index Index in dropRecords array + */ + function getDropRecord(uint256 _index) + external + view + returns (DropRecord memory) + { + require(_index < dropRecords.length, "Index out of bounds"); + return dropRecords[_index]; + } + + /** + * @notice Verify an activity and participant eligibility + * @param _activityId ID of the activity + * @param _participant Address to verify + * @return isValid True if activity is valid and participant is eligible + * @return reason Reason if not valid + */ + function verifyParticipant(uint256 _activityId, address _participant) + external + view + validActivity(_activityId) + returns (bool isValid, string memory reason) + { + ActivityInfo memory activity = activities[_activityId - 1]; + + if (activity.status != ActivityStatus.ACTIVE) { + return (false, "Activity not active"); + } + + if (block.timestamp > activity.deadline) { + return (false, "Activity expired"); + } + + if (hasReceived[_activityId][_participant]) { + return (false, "Already received reward"); + } + + if (activity.distributedCount >= activity.totalDrops) { + return (false, "All rewards distributed"); + } + + return (true, "Eligible"); + } + + // ============ Admin Functions ============ + + /** + * @notice Add or remove supported token + * @param _tokenAddress Token contract address + * @param _isSupported True to support, false to remove support + */ + function setSupportedToken(address _tokenAddress, bool _isSupported) + external + onlyOwner + { + require(_tokenAddress != address(0), "Invalid token address"); + supportedTokens[_tokenAddress] = _isSupported; + emit TokenSupportUpdated(_tokenAddress, _isSupported); + } + + /** + * @notice Pause contract + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Unpause contract + */ + function unpause() external onlyOwner { + _unpause(); + } + + /** + * @notice Emergency withdrawal (justfor stuck funds, not from active activities) + * @param _token Token address + * @param _amount Amount to withdraw + */ + function emergencyWithdraw(address _token, uint256 _amount) + external + onlyOwner + { + require(_token != address(0), "Invalid token"); + + // clculate total locked in active activities + uint256 totalLocked = 0; + for (uint256 i = 0; i < activities.length; i++) { + if ( + activities[i].status == ActivityStatus.ACTIVE && + activities[i].tokenAddress == _token + ) { + totalLocked += (activities[i].totalAmount - activities[i].distributedAmount); + } + } + + uint256 balance = IERC20Upgradeable(_token).balanceOf(address(this)); + uint256 available = balance - totalLocked; + + require(_amount <= available, "Cannot withdraw from active activities"); + + IERC20Upgradeable(_token).safeTransfer(owner(), _amount); + } + + // ============ Gap for Upgrade Safety ============ + uint256[50] private __gap; +} \ No newline at end of file diff --git a/test/contracts/core/FishcakeEventManagerChainTest.t.sol b/test/contracts/core/FishcakeEventManagerChainTest.t.sol new file mode 100644 index 0000000..c15baa9 --- /dev/null +++ b/test/contracts/core/FishcakeEventManagerChainTest.t.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import "forge-std/Test.sol"; +import "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {FishcakeEventManagerMultiChain} from "../../../src/contracts/core/FishcakeEventManagerMultiChain.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +// Simple mock token for testing +contract MockUSDT is ERC20 { + constructor() ERC20("Mock USDT", "USDT") {} + + function decimals() public pure override returns (uint8) { + return 6; + } + + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +/** + * @title FishcakeEventManagerChainTest + * @notice Basic tests for multichain event manager + */ +contract FishcakeEventManagerChainTest is Test { + FishcakeEventManagerMultiChain public manager; + MockUSDT public usdt; + + address owner = address(1); + address creator = address(2); + address user1 = address(3); + + function setUp() public { + usdt = new MockUSDT(); + + address proxy = Upgrades.deployTransparentProxy( + "FishcakeEventManagerMultiChain.sol", + owner, + abi.encodeCall( + FishcakeEventManagerMultiChain.initialize, + (owner, address(usdt)) + ) + ); + + manager = FishcakeEventManagerMultiChain(proxy); + + // Give creator some tokens + usdt.mint(creator, 1000000e6); + vm.prank(creator); + usdt.approve(address(manager), type(uint256).max); + } + + // Test basic initialization + function testInit() public { + assertEq(manager.owner(), owner); + assertTrue(manager.supportedTokens(address(usdt))); + } + + // Test creating a fixed reward activity + function testCreateFixed() public { + vm.prank(creator); + (bool ok, uint256 id) = manager.createActivity( + "Coffee Shop", + "Free coffee", + "NYC", + block.timestamp + 7 days, + 100e6, + FishcakeEventManagerMultiChain.DropType.FIXED, + 100, + 0, + 1e6, + address(usdt) + ); + + assertTrue(ok); + assertEq(id, 1); + } + + // Test distributing rewards + function testDistribute() public { + // create activity + vm.prank(creator); + (, uint256 id) = manager.createActivity( + "Test", + "Desc", + "Loc", + block.timestamp + 7 days, + 100e6, + FishcakeEventManagerMultiChain.DropType.FIXED, + 100, + 0, + 1e6, + address(usdt) + ); + + // distribute to user + vm.prank(creator); + manager.distributeReward(id, user1, 1e6); + + assertEq(usdt.balanceOf(user1), 1e6); + assertTrue(manager.hasUserReceived(id, user1)); + } + + // Test can't distribute twice + function testCantDistributeTwice() public { + vm.prank(creator); + (, uint256 id) = manager.createActivity( + "Test", + "Desc", + "Loc", + block.timestamp + 7 days, + 100e6, + FishcakeEventManagerMultiChain.DropType.FIXED, + 100, + 0, + 1e6, + address(usdt) + ); + + vm.startPrank(creator); + manager.distributeReward(id, user1, 1e6); + + vm.expectRevert("Already received reward"); + manager.distributeReward(id, user1, 1e6); + vm.stopPrank(); + } + + // Test finishing activity returns remaining funds + function testFinish() public { + vm.prank(creator); + (, uint256 id) = manager.createActivity( + "Test", + "Desc", + "Loc", + block.timestamp + 7 days, + 100e6, + FishcakeEventManagerMultiChain.DropType.FIXED, + 100, + 0, + 1e6, + address(usdt) + ); + + uint256 before = usdt.balanceOf(creator); + + vm.prank(creator); + manager.finishActivity(id); + + // should get all funds back since we didn't distribute any + assertEq(usdt.balanceOf(creator) - before, 100e6); + } + + // Test random type activity + function testRandom() public { + vm.prank(creator); + (bool ok, uint256 id) = manager.createActivity( + "Lucky Draw", + "Random", + "NYC", + block.timestamp + 7 days, + 500e6, + FishcakeEventManagerMultiChain.DropType.RANDOM, + 50, + 5e6, + 20e6, + address(usdt) + ); + + assertTrue(ok); + + // distribute random amount within range + vm.prank(creator); + manager.distributeReward(id, user1, 10e6); + + assertEq(usdt.balanceOf(user1), 10e6); + } + + // Test verification + function testVerify() public { + vm.prank(creator); + (, uint256 id) = manager.createActivity( + "Test", + "Desc", + "Loc", + block.timestamp + 7 days, + 100e6, + FishcakeEventManagerMultiChain.DropType.FIXED, + 100, + 0, + 1e6, + address(usdt) + ); + + (bool valid, string memory reason) = manager.verifyParticipant(id, user1); + assertTrue(valid); + assertEq(reason, "Eligible"); + } + + // Test pause functionality + function testPause() public { + vm.prank(owner); + manager.pause(); + + vm.prank(creator); + vm.expectRevert(); + manager.createActivity( + "Test", + "Desc", + "Loc", + block.timestamp + 7 days, + 100e6, + FishcakeEventManagerMultiChain.DropType.FIXED, + 100, + 0, + 1e6, + address(usdt) + ); + } + + // Test only creator can distribute + function testOnlyCreator() public { + vm.prank(creator); + (, uint256 id) = manager.createActivity( + "Test", + "Desc", + "Loc", + block.timestamp + 7 days, + 100e6, + FishcakeEventManagerMultiChain.DropType.FIXED, + 100, + 0, + 1e6, + address(usdt) + ); + + vm.prank(user1); + vm.expectRevert("Not the activity creator"); + manager.distributeReward(id, user1, 1e6); + } +} \ No newline at end of file