How to build a Referral Coupon Smart Contract on Morph L2 Consumer Chain

How to build a Referral Coupon Smart Contract on Morph L2 Consumer Chain

Introduction

Blockchain technology faces challenges in consumer adoption. This article explores implementing Web2's successful referral and coupon systems in Web3, focusing on Morph L2's consumer-centric approach.

Try out

https://referral-coupon-tracker.vercel.app/

Referrals and Coupons in Web3

Referral and coupon systems are powerful Web2 marketing tools for user acquisition and retention. They offer:

  • Cost-effective growth

  • Higher quality leads

  • Increased user engagement

  • Enhanced brand credibility

  • Measurable ROI

Adapting these strategies to Web3 could accelerate adoption and create engaging blockchain experiences. On-chain implementation enables transparent, automated, and trustless reward distribution.

Morph L2's Consumer Chain Mission

Morph L2, an Ethereum Layer 2 solution, aims to make blockchain technology accessible for everyday use. Its mission includes:

  1. Empowering practical blockchain applications

  2. Enhancing user experience for non-crypto natives

  3. Fostering innovation in consumer-focused dApps

  4. Driving mainstream adoption

By combining Morph L2's infrastructure with referral and coupon systems, we can create synergies between proven marketing strategies and blockchain technology, potentially accelerating user adoption and engagement in the Web3 ecosystem.

Smart Contract Overview

The ReferralCouponTracking smart contract is a Solidity-based solution for managing product referrals and coupon-based discounts on the Morph L2 blockchain. It automates and decentralizes the entire process of product sales, referrals, and reward distribution.

Purpose and Functionalities

This contract enables:

  1. Blockchain-based product management

  2. Unique coupon code generation for referrers

  3. Automated purchase processing with discounts

  4. Transparent referral and coupon usage tracking

  5. Instant reward distribution to referrers

By leveraging blockchain technology, it eliminates intermediaries, reduces fraud, and ensures transparent operations.

This smart contract transforms traditional referral and coupon programs into a trustless, efficient, and engaging system. It encourages customer acquisition and provides valuable marketing insights, all secured by blockchain technology.

Contract Structure

For this project, we are going to use Foundry. To set it up, I recommend following the official installation and first steps guide, which is very well written.

https://book.getfoundry.sh/

Inside the src folder, delete all files and create a new file named ReferralCouponTracking.sol with the following content:

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

contract ReferralCouponTracking {
}

State Variables

The ReferralCouponTracking contract defines several state variables that are crucial for its functionality:

  • owner: A payable address that represents the contract owner.

  • referralSharePercentage: A uint256 value that determines the percentage of the sale price that goes to the referrer.

  • refereeDiscountPercentage: A uint256 value that determines the discount percentage for the referee (buyer).

  • nextProductId: A private uint256 variable used to generate unique product IDs.

  • simpleNFTContract: An instance of the ISimpleNFT interface, used for minting NFTs after successful purchases.

address payable public owner;
uint256 public referralSharePercentage;
uint256 public refereeDiscountPercentage;
uint256 private nextProductId = 1;

ISimpleNFT public simpleNFTContract;

where the ISimpleNFT interface is defined at the beginning, outside of the ReferralCouponTracking contract.

interface ISimpleNFT {
    function mintNFT(address recipient, string memory tokenURI) external returns (uint256);
}

Structs and Mappings

The contract uses several structs and mappings to organize and store data efficiently:Structs:

  • Product: Represents a product with fields for id, price, title, and existence.

  • Coupon: Represents a coupon with fields for referrer address, product ID, and usage count.

Mappings:

  • productImageURIs: Maps product IDs to their image URIs.

  • products: Maps product IDs to Product structs.

  • coupons: Maps coupon codes (strings) to Coupon structs.

  • referrerCoupons: Maps referrer addresses to arrays of their generated coupon codes.

These structs and mappings allow for efficient storage and retrieval of product, coupon, and referral information.

struct Product {
    uint256 id;
    uint256 price;
    string title;
    bool exists;
}

struct Coupon {
    address referrer;
    uint256 usageCount;
}

mapping(uint256 => string) public productImageURIs;
mapping(uint256 => Product) public products;
mapping(string => Coupon) public coupons;
mapping(address => string[]) public referrerCoupons;

Events

The contract emits several events to log important actions:

  • CouponGenerated: Triggered when a new coupon is generated, including the referrer's address, coupon code, and product ID.

  • ProductPurchased: Emitted when a product is successfully purchased, logging the referee's address, referrer's address, purchase amount, and timestamp.

  • ProductAdded: Fired when a new product is added to the contract, including the product ID, price, and title.

These events provide a way to track and monitor the contract's activities, which is useful for both off-chain applications and blockchain explorers.

event CouponGenerated(address indexed referrer, string couponCode, uint256 productId);
event ProductPurchased(address indexed referee, address indexed referrer, uint256 amount, uint256 timestamp);
event ProductAdded(uint256 indexed productId, uint256 price, string title);

Core Functions

The ReferralCouponTracking contract implements several core functions that enable its referral and coupon system. Let's break down these key functionalities:

Creating Referral Codes and Generating Coupons

The generateCoupon function serves both purposes:

function generateCoupon(string memory _couponCode) external {
    require(coupons[_couponCode].referrer == address(0), "Coupon code already exists");

    coupons[_couponCode] = Coupon(msg.sender, 0);
    referrerCoupons[msg.sender].push(_couponCode);

    emit CouponGenerated(msg.sender, _couponCode);
}

This function lets users create a unique coupon code for a specific product. It checks if the product exists and if the coupon code is unique. The coupon is then stored in the coupons mapping and added to the referrer's list of coupons.

Tracking Referrals

Referral tracking is primarily done through thecouponsmapping and theCouponstruct:

mapping(string => Coupon) public coupons;
struct Coupon {
    address referrer;
    uint256 usageCount;
}

Each coupon is associated with a referrer and a specific product. TheusageCountfield keeps track of how many times the coupon has been used.

Redeeming Coupons

Coupon redemption occurs in the purchase function:

function purchase(string memory _couponCode, uint256 _productId) public payable {
    require(products[_productId].exists, "Product does not exist");

    uint256 price = products[_productId].price;
    address referrer = address(0);

    if (keccak256(abi.encodePacked(_couponCode)) != keccak256(abi.encodePacked("DEMO"))) {
        require(isCouponValid(_couponCode), "Invalid coupon code");
        Coupon storage coupon = coupons[_couponCode];
        price = getDiscountedPrice(_couponCode, _productId);
        referrer = coupon.referrer;
        coupon.usageCount++;
    }

    require(msg.value >= price, "Insufficient payment");

    if (referrer != address(0)) {
        uint256 referralShare = (price * referralSharePercentage) / 100;
        uint256 ownerShare = price - referralShare;

        (bool success,) = referrer.call{value: referralShare}("");
        require(success, "Transfer failed");
        (bool success2,) = owner.call{value: ownerShare}("");
        require(success2, "Transfer2 failed");
    } else {
        (bool success,) = owner.call{value: price}("");
        require(success, "Transfer failed");
    }

    // Mint NFT after successful purchase
    require(address(simpleNFTContract) != address(0), "SimpleNFT contract not set");
    string memory imageURI = productImageURIs[_productId];
    require(bytes(imageURI).length > 0, "Image URI not set for this product");
    simpleNFTContract.mintNFT(msg.sender, imageURI);

    if (msg.value > price) {
        payable(msg.sender).transfer(msg.value - price);
    }

    emit ProductPurchased(msg.sender, referrer, price, block.timestamp);
}

This function validates the coupon, applies the discount, and processes the purchase. It also increments the coupon's usage count.

Calculating Rewards

Reward calculation is handled within thepurchasefunction:

if (referrer != address(0)) {
    uint256 referralShare = (price * referralSharePercentage) / 100;
    uint256 ownerShare = price - referralShare;

    (bool success,) = referrer.call{value: referralShare}("");
    require(success, "Transfer failed");
    (bool success2,) = owner.call{value: ownerShare}("");
    require(success2, "Transfer2 failed");
} else {
    (bool success,) = owner.call{value: price}("");
    require(success, "Transfer failed");
}

This code calculates the referral share based on the referralSharePercentage and transfers the appropriate amounts to the referrer and the contract owner. The discount for the buyer is calculated in the getDiscountedPrice function:

function getDiscountedPrice(string memory _couponCode, uint256 _productId) public view returns (uint256) {
    require(isCouponValid(_couponCode), "Invalid coupon code");
    require(products[_productId].exists, "Product does not exist");

    uint256 productPrice = products[_productId].price;
    uint256 discountAmount = (productPrice * refereeDiscountPercentage) / 100;
    return productPrice - discountAmount;
}

This function applies the refereeDiscountPercentage to the product price to determine the final discounted price for the buyer. These core functions work together to create a complete referral and coupon system. They enable the creation, tracking, and redemption of coupons, as well as the calculation and distribution of rewards to both referrers and buyers.

Complete Code

So, we covered the most important contract structure and functions. Now, let's see what the entire code looks like.

In src/ReferralCouponTracking.sol, we have:

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

interface ISimpleNFT {
    function mintNFT(address recipient, string memory tokenURI) external returns (uint256);
}

contract ReferralCouponTracking {
    ISimpleNFT public simpleNFTContract;

    address payable public owner;
    uint256 public referralSharePercentage;
    uint256 public refereeDiscountPercentage;
    uint256 private nextProductId = 1;

    struct Product {
        uint256 id;
        uint256 price;
        string title;
        bool exists;
    }

    struct Coupon {
        address referrer;
        uint256 usageCount;
    }

    mapping(uint256 => string) public productImageURIs;
    mapping(uint256 => Product) public products;
    mapping(string => Coupon) public coupons;
    mapping(address => string[]) public referrerCoupons;

    event CouponGenerated(address indexed referrer, string couponCode);
    event ProductPurchased(address indexed referee, address indexed referrer, uint256 amount, uint256 timestamp);
    event ProductAdded(uint256 indexed productId, uint256 price, string title);

    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this function");
        _;
    }

    constructor(uint256 _referralSharePercentage, uint256 _refereeDiscountPercentage) {
        owner = payable(msg.sender);
        referralSharePercentage = _referralSharePercentage;
        refereeDiscountPercentage = _refereeDiscountPercentage;
    }

    function setSimpleNFTContract(address _simpleNFTAddress) external onlyOwner {
        simpleNFTContract = ISimpleNFT(_simpleNFTAddress);
    }

    function setProductImageURI(uint256 _productId, string memory _imageURI) external onlyOwner {
        require(products[_productId].exists, "Product does not exist");
        productImageURIs[_productId] = _imageURI;
    }

    function changePercentages(uint256 _referralSharePercentage, uint256 _refereeDiscountPercentage)
        external
        onlyOwner
    {
        referralSharePercentage = _referralSharePercentage;
        refereeDiscountPercentage = _refereeDiscountPercentage;
    }

    function addProduct(string memory _title, uint256 _price) external onlyOwner {
        require(_price > 0, "Price cannot be 0");
        uint256 productId = nextProductId;
        products[productId] = Product(productId, _price, _title, true);
        nextProductId++;
        emit ProductAdded(productId, _price, _title);
    }

    function generateCoupon(string memory _couponCode) external {
        require(coupons[_couponCode].referrer == address(0), "Coupon code already exists");

        coupons[_couponCode] = Coupon(msg.sender, 0);
        referrerCoupons[msg.sender].push(_couponCode);

        emit CouponGenerated(msg.sender, _couponCode);
    }

    function purchase(string memory _couponCode, uint256 _productId) public payable {
        require(products[_productId].exists, "Product does not exist");

        uint256 price = products[_productId].price;
        address referrer = address(0);

        if (keccak256(abi.encodePacked(_couponCode)) != keccak256(abi.encodePacked("DEMO"))) {
            require(isCouponValid(_couponCode), "Invalid coupon code");
            Coupon storage coupon = coupons[_couponCode];
            price = getDiscountedPrice(_couponCode, _productId);
            referrer = coupon.referrer;
            coupon.usageCount++;
        }

        require(msg.value >= price, "Insufficient payment");

        if (referrer != address(0)) {
            uint256 referralShare = (price * referralSharePercentage) / 100;
            uint256 ownerShare = price - referralShare;

            (bool success,) = referrer.call{value: referralShare}("");
            require(success, "Transfer failed");
            (bool success2,) = owner.call{value: ownerShare}("");
            require(success2, "Transfer2 failed");
        } else {
            (bool success,) = owner.call{value: price}("");
            require(success, "Transfer failed");
        }

        // Mint NFT after successful purchase
        require(address(simpleNFTContract) != address(0), "SimpleNFT contract not set");
        string memory imageURI = productImageURIs[_productId];
        require(bytes(imageURI).length > 0, "Image URI not set for this product");
        simpleNFTContract.mintNFT(msg.sender, imageURI);

        if (msg.value > price) {
            payable(msg.sender).transfer(msg.value - price);
        }

        emit ProductPurchased(msg.sender, referrer, price, block.timestamp);
    }

    function getAllProducts() external view returns (Product[] memory) {
        uint256 productCount = nextProductId - 1;
        Product[] memory allProducts = new Product[](productCount);

        for (uint256 i = 1; i <= productCount; i++) {
            allProducts[i - 1] = products[i];
        }

        return allProducts;
    }

    function isCouponValid(string memory _couponCode) public view returns (bool) {
        Coupon storage coupon = coupons[_couponCode];
        return coupon.referrer != address(0);
    }

    function getProductInfo(uint256 _productId) external view returns (uint256, uint256, string memory) {
        require(products[_productId].exists, "Product does not exist");
        return (products[_productId].id, products[_productId].price, products[_productId].title);
    }

    function getDiscountedPrice(string memory _couponCode, uint256 _productId) public view returns (uint256) {
        require(isCouponValid(_couponCode), "Invalid coupon code");
        require(products[_productId].exists, "Product does not exist");

        uint256 productPrice = products[_productId].price;
        uint256 discountAmount = (productPrice * refereeDiscountPercentage) / 100;
        return productPrice - discountAmount;
    }

    function getReferrerCoupons(address _referrer) external view returns (string[] memory) {
        return referrerCoupons[_referrer];
    }

    function getCouponUsageCount(string memory _couponCode) external view returns (uint256) {
        return coupons[_couponCode].usageCount;
    }
}

and in src/SimpleNFT.sol, we have:

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

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract SimpleNFT is ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;
    address public referralCouponTrackingAddress;

    constructor(string memory name, string memory symbol, address _referralCouponTrackingAddress)
        ERC721(name, symbol)
        Ownable(msg.sender)
    {
        referralCouponTrackingAddress = _referralCouponTrackingAddress;
    }

    function mintNFT(address recipient, string memory tokenURI) public returns (uint256) {
        require(msg.sender == owner() || msg.sender == referralCouponTrackingAddress, "Not authorized to mint NFT");
        uint256 tokenId = _nextTokenId++;
        _safeMint(recipient, tokenId);
        _setTokenURI(tokenId, tokenURI);

        return tokenId;
    }

    function setReferralCouponTrackingAddress(address _newAddress) public onlyOwner {
        referralCouponTrackingAddress = _newAddress;
    }
}

Writing Unit Tests

The given test file ReferralCouponTrackingTest.sol uses Foundry's testing framework to create comprehensive unit tests for the ReferralCouponTracking contract.

lets create a new file in the test folder test/ReferralCouponTracking.t.sol with:

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

import "forge-std/Test.sol";
import "../src/ReferralCouponTracking.sol";
import "../src/SimpleNFT.sol";

contract ReferralCouponTrackingTest is Test {
    ReferralCouponTracking public tracking;
    SimpleNFT public simpleNFT;
    address public owner;
    address public user1;
    address public user2;

    // we are going to functions here

    receive() external payable {}
}

Here are the key aspects of the testing suite:

Setup: The setUp function initializes the contract and necessary variables for testing.

function setUp() public {
    owner = address(this);
    user1 = address(0x1);
    user2 = address(0x2);
    tracking = new ReferralCouponTracking(10, 5); // 10% referral share, 5% referee discount
    simpleNFT = new SimpleNFT("TestNFT", "TNFT", address(tracking));
    tracking.setSimpleNFTContract(address(simpleNFT));
}

Functionality Tests

testChangePercentages: Verifies the ability to change referral and discount percentages.

function testChangePercentages() public {
    tracking.changePercentages(15, 7);
    assertEq(tracking.referralSharePercentage(), 15);
    assertEq(tracking.refereeDiscountPercentage(), 7);
}

testAddProduct: Checks product addition functionality.

function testAddProduct() public {
    tracking.addProduct("Product 1", 0.05 ether);
    (uint256 id, uint256 price, string memory title) = tracking.getProductInfo(1);
    assertEq(id, 1);
    assertEq(price, 0.05 ether);
    assertEq(title, "Product 1");
}

testGenerateCoupon: Tests coupon generation.

testGenerateCoupon: Tests coupon generation.

function testGenerateCoupon() public {
    vm.prank(user1);
    tracking.generateCoupon("COUPON1");
    assertTrue(tracking.isCouponValid("COUPON1"));
}

testPurchaseWithCoupon: Simulates a purchase using a coupon and verifies balance changes.

function testPurchaseWithCoupon() public {
    tracking.addProduct("Product 1", 0.05 ether);
    tracking.setProductImageURI(1, "ipfs://QmTest");
    vm.prank(user1);
    tracking.generateCoupon("COUPON1");

    uint256 initialOwnerBalance = address(owner).balance;
    uint256 initialUser1Balance = user1.balance;
    uint256 discountedPrice = tracking.getDiscountedPrice("COUPON1", 1);
    uint256 referralShare = (discountedPrice * tracking.referralSharePercentage()) / 100;
    uint256 ownerShare = discountedPrice - referralShare;

    vm.prank(user2);
    vm.deal(user2, 0.1 ether);
    tracking.purchase{value: discountedPrice}("COUPON1", 1);

    assertEq(address(owner).balance, initialOwnerBalance + ownerShare);
    assertEq(user1.balance, initialUser1Balance + referralShare);
    assertEq(user2.balance, 0.1 ether - discountedPrice);
    assertEq(simpleNFT.balanceOf(user2), 1);
}

testGetAllProducts: Ensures correct retrieval of all added products.

function testGetAllProducts() public {
    tracking.addProduct("Product 1", 0.05 ether);
    tracking.addProduct("Product 2", 0.1 ether);
    ReferralCouponTracking.Product[] memory products = tracking.getAllProducts();
    assertEq(products.length, 2);
    assertEq(products[0].id, 1);
    assertEq(products[0].price, 0.05 ether);
    assertEq(products[0].title, "Product 1");
    assertEq(products[1].id, 2);
    assertEq(products[1].price, 0.1 ether);
    assertEq(products[1].title, "Product 2");
}

testGetReferrerCoupons: Checks the retrieval of coupons for a specific referrer.

function testGetReferrerCoupons() public {
    tracking.addProduct("Product 1", 0.05 ether);
    vm.prank(user1);
    tracking.generateCoupon("COUPON1", 1);
    vm.prank(user1);
    tracking.generateCoupon("COUPON2", 1);

    string[] memory coupons = tracking.getReferrerCoupons(user1);
    assertEq(coupons.length, 2);
    assertEq(coupons[0], "COUPON1");
    assertEq(coupons[1], "COUPON2");
}

testGetCouponUsageCount: Verifies the tracking of coupon usage.

function testGetCouponUsageCount() public {
    tracking.addProduct("Product 1", 0.05 ether);
    tracking.setProductImageURI(1, "ipfs://QmTest");
    vm.prank(user1);
    tracking.generateCoupon("COUPON1");

    vm.prank(user2);
    vm.deal(user2, 0.1 ether);
    tracking.purchase{value: 0.05 ether}("COUPON1", 1);

    assertEq(tracking.getCouponUsageCount("COUPON1"), 1);
}

testSetProductImageURI: Tests setting and retrieving product image URIs.

function testSetProductImageURI() public {
    tracking.addProduct("Product 1", 0.05 ether);
    tracking.setProductImageURI(1, "ipfs://QmTest");
    assertEq(tracking.productImageURIs(1), "ipfs://QmTest");
}

Failure Tests:

  • testFailAddExistingProduct: Ensures products with duplicate names can't be added.

  • testFailGenerateDuplicateCoupon: Verifies that duplicate coupon codes are not allowed.

  • testFailPurchaseNonExistentProduct: Checks that purchasing non-existent products fails.

  • testFailPurchaseWithInvalidCoupon: Ensures purchases with invalid coupons are rejected.

  • testFailPurchaseWithInsufficientPayment: Verifies that underpayment for products is not allowed.

  • testFailPurchaseWithoutImageURI: Checks that purchases fail when product image URI is not set.

function testFailAddExistingProduct() public {
    tracking.addProduct("Product 1", 0.05 ether);
    tracking.addProduct("Product 1", 0 ether); // Should fail
}

function testFailGenerateDuplicateCoupon() public {
    vm.prank(user1);
    tracking.generateCoupon("COUPON1");
    vm.prank(user2);
    tracking.generateCoupon("COUPON1"); // Should fail
}

function testFailPurchaseNonExistentProduct() public {
    vm.prank(user2);
    vm.deal(user2, 0.1 ether);
    tracking.purchase{value: 0.05 ether}("", 1); // Should fail
}

function testFailPurchaseWithInvalidCoupon() public {
    tracking.addProduct("Product 1", 0.05 ether);
    vm.prank(user2);
    vm.deal(user2, 0.1 ether);
    tracking.purchase{value: 0.05 ether}("INVALID", 1); // Should fail
}

function testFailPurchaseWithInsufficientPayment() public {
    tracking.addProduct("Product 1", 0.05 ether);
    vm.prank(user2);
    vm.deal(user2, 0.01 ether);
    tracking.purchase{value: 0.01 ether}("", 1); // Should fail
}

function testFailPurchaseWithoutImageURI() public {
    tracking.addProduct("Product 1", 0.05 ether);
    vm.prank(user2);
    vm.deal(user2, 0.05 ether);
    tracking.purchase{value: 0.05 ether}("", 1); // Should fail because no image URI is set
}

To run these tests using Foundry, use the following command:

forge test

All the tests should pass successfully and look like this in the terminal:

Deploying to Morph L2 Testnet

To deploy the ReferralCouponTracking contract to the Morph L2 testnet using Foundry:

Create a deployment script in the script directory, e.g., DeployReferralCouponTracking.s.sol:

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

import "forge-std/Script.sol";
import "../src/ReferralCouponTracking.sol";
import "../src/SimpleNFT.sol";

contract DeployReferralCouponTracking is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        // Deploy the ReferralCouponTracking contract
        uint256 initialReferralSharePercentage = 30; // 30%
        uint256 initialRefereeDiscountPercentage = 10; // 10%
        ReferralCouponTracking referralCouponTracking =
            new ReferralCouponTracking(initialReferralSharePercentage, initialRefereeDiscountPercentage);

        // Deploy the SimpleNFT contract
        SimpleNFT simpleNFT = new SimpleNFT("Moprhy", "M", address(referralCouponTracking));

        // Set the SimpleNFT contract address in ReferralCouponTracking
        referralCouponTracking.setSimpleNFTContract(address(simpleNFT));

        // Add products
        referralCouponTracking.addProduct("Hi", 0.01 ether);
        referralCouponTracking.addProduct("Question", 0.02 ether);
        referralCouponTracking.addProduct("Working", 0.03 ether);
        referralCouponTracking.addProduct("Dance", 0.04 ether);

        // Set product image URIs (example URIs, replace with actual ones)
        referralCouponTracking.setProductImageURI(
            1, "https://gateway.irys.xyz/E87y2Ci8axHbsrEXPVj5RCbmLEsjVAsYCtunmSWbxF2a"
        );
        referralCouponTracking.setProductImageURI(
            2, "https://gateway.irys.xyz/3BdaShEdpTgjxEdMcwx5VJGvkVRKu8xsuuL6GSxnCCLy"
        );
        referralCouponTracking.setProductImageURI(
            3, "https://gateway.irys.xyz/96CoHFiCZFeWZLNDqFuCD8shDWKMRKXH5qcnk9J9cgs6"
        );
        referralCouponTracking.setProductImageURI(
            4, "https://gateway.irys.xyz/CxEkMLqWE8qa8RHWRqMLC3DhmPEQxuADLFDqQFX7KNMf"
        );

        // Generate a test coupon
        referralCouponTracking.generateCoupon("DEMO");

        vm.stopBroadcast();

        // Log the deployed contract addresses
        console.log("ReferralCouponTracking deployed at:", address(referralCouponTracking));
        console.log("SimpleNFT deployed at:", address(simpleNFT));
    }
}

Set up your .env file with your private key:

PRIVATE_KEY=your_private_key_here

Get some testnet ETH for the deployment from MorphFaucet.

Configure Foundry for Morph L2 testnet by adding the network to your foundry.toml:

[rpc_endpoints]
morph_testnet = "https://testnet-rpc.morphl2.io"
  1. Deploy to Morph L2 testnet:
forge script script/DeployReferralCouponTracking.s.sol:DeployReferralCouponTracking --rpc-url morph_testnet --broadcast

Once deployed, you can visit the explorer at https://explorer-holesky.morphl2.io/ to check the contract.

Security Considerations

This ReferralCouponTracking contract is not audited and is created solely for educational purposes. It should not be deployed on a live network without thorough professional review and testing.

Future Improvements

From here, you can continue your learning journey by adding more features. Here are three potential upgrades and additional feature ideas to enhance the ReferralCouponTracking contract:

  1. Dynamic Reward Structure

    • Implement tiered rewards based on referral volume

    • Offer time-based incentives for early adopters

    • Allow product-specific referral rates and discounts

  2. Enhanced NFT Integration

    • Enable customizable NFT metadata

    • Implement NFT-based rewards or exclusive access

    • Introduce NFT staking for additional benefits

  3. Community-driven Governance

    • Create a proposal system for parameter changes

    • Implement a community treasury

    • Develop a reputation system for referrers

Conclusion

This article has explored the implementation of a Web3 referral and coupon system using smart contracts on the Morph L2 Consumer Chain. Let's recap the key points:

  1. We introduced the concept of referral and coupon systems in Web3.

  2. Walked through the contract structure, examining state variables, structs, mappings, and events that form the backbone of the system.

  3. Core functionalities were explained, including referral code creation, coupon generation, referral tracking, coupon redemption, and reward calculation.

  4. Covered testing strategies using Foundry and provided guidance on deploying the contract to the Morph L2 testnet.

This implementation demonstrates how traditional referral and coupon systems can be enhanced with blockchain technology, offering transparency, automation, and unique digital asset integration. To stay updated on Morph L2 developments and connect with the community:

By engaging with these platforms, you'll gain access to the latest updates, developer resources, and opportunities to collaborate within the Morph ecosystem. Whether you're looking to build, learn, or contribute, the Morph community welcomes your participation in shaping the future of L2 solutions.