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
-
-
-
-
-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