Merchandising Revenue Contract

What it does:
Automatically collects revenue from merchandise sales and distributes earnings to creators, brands, designers, manufacturers, and partners based on predefined on-chain splits.

Why it matters:
Eliminates manual accounting, prevents disputes over revenue shares, ensures instant and fair payouts, and enables trustless collaboration across global teams.

How it works:

  • Merch collaborators register payout addresses and revenue percentages

  • Each merch item or campaign is linked to a revenue split configuration

  • Sales payments (ETH or tokens) are sent directly to the smart contract

  • Smart contract calculates and allocates earnings to each stakeholder

  • Funds accumulate in on-chain balances per collaborator

  • Collaborators withdraw earnings independently at any time

  • All sales, splits, and payouts are transparent and auditable

      // SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title MerchandisingRevenue
 * @author Nam
 * @notice Collects merch sales revenue and splits it automatically on-chain
 */
contract MerchandisingRevenue is Ownable {

    struct Collaborator {
        address payable wallet;
        uint256 share; // percentage (out of 10000 = 100%)
    }

    struct MerchCampaign {
        bool active;
        Collaborator[] collaborators;
        mapping(address => uint256) pendingETH;
        mapping(address => mapping(address => uint256)) pendingToken; 
        // token => collaborator => amount
    }

    uint256 public campaignCount;
    mapping(uint256 => MerchCampaign) private campaigns;

    // -------------------- EVENTS --------------------

    event CampaignCreated(uint256 indexed campaignId);
    event SaleRecorded(uint256 indexed campaignId, address indexed buyer, uint256 amount);
    event Withdrawal(address indexed collaborator, address token, uint256 amount);

    // -------------------- CAMPAIGN SETUP --------------------

    function createCampaign(
        address[] calldata _wallets,
        uint256[] calldata _shares
    ) external onlyOwner {
        require(_wallets.length == _shares.length, "Length mismatch");
        require(_wallets.length > 0, "No collaborators");

        uint256 totalShare;
        campaignCount++;
        MerchCampaign storage c = campaigns[campaignCount];
        c.active = true;

        for (uint256 i = 0; i  0, "No payment");

        for (uint256 i = 0; i  0, "Invalid amount");

        IERC20(_token).transferFrom(msg.sender, address(this), _amount);

        for (uint256 i = 0; i  0, "Nothing to withdraw");

        c.pendingETH[msg.sender] = 0;
        payable(msg.sender).transfer(amount);

        emit Withdrawal(msg.sender, address(0), amount);
    }

    function withdrawToken(uint256 _campaignId, address _token) external {
        MerchCampaign storage c = campaigns[_campaignId];
        uint256 amount = c.pendingToken[_token][msg.sender];
        require(amount > 0, "Nothing to withdraw");

        c.pendingToken[_token][msg.sender] = 0;
        IERC20(_token).transfer(msg.sender, amount);

        emit Withdrawal(msg.sender, _token, amount);
    }

    // -------------------- ADMIN --------------------

    function setCampaignActive(uint256 _campaignId, bool _active) external onlyOwner {
        campaigns[_campaignId].active = _active;
    }
}