Using Smart Contracts to Charge Monthly Subscriptions| Expanding the Utility of Web3

TechJD
5 min readAug 29, 2023

If you’re looking to create a smart contract that enables subscription-based access to your service or product while also rewarding subscribers and referrers, you’ve come to the right place! In this guide, I’m going to walk you through my idea on how to build a subscription smart contract using Solidity. This contract will not automatically charge subscribers’ wallets every month, but it will encourage consistent payments by providing rewards points. It will also restrict access if a certain period of time has elapsed since the last payment.

Prerequisites

Before we get started, make sure you have the following:

  • Basic understanding of Ethereum and smart contracts.
  • An Ethereum development environment set up, such as Remix or Truffle.
  • An ERC-20 token contract for handling payments (any testnet works)

Step 1: Contract Initialization

Let’s start by creating the smart contract and setting up the basic structure. In this example, the contract extends the Ownable contract, which provides ownership management.

pragma solidity ^0.8.0;

// Import necessary libraries and interfaces
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract CadetSub is Ownable {
using SafeMath for uint256;

IERC20 public token;

// Other contract variables and mappings...

// Constructor to set the token address
constructor(address _token) {
token = IERC20(_token);
}

// Other contract functions...
}

In this code, we’re importing the required libraries and interfaces from the OpenZeppelin library. Make sure to replace the import paths with the correct paths based on your project setup.

Step 2: Subscription Logic

Now let’s implement the subscription logic. This contract supports three subscription types: monthly, yearly, and lifetime. When a user subscribes, they can use a referral code to earn rewards points, and the referrer gets a commission.

// Inside the CadetSub contract

contract CadetSub is Ownable {
// ...

struct User {
address user;
uint8 subType;
uint256 termStart;
uint256 nextPayment;
address referrer;
}

mapping(address => User) public users;

uint256 public MONTH_PRICE = 15 * 10**6;
uint256 public YEAR_PRICE = 150 * 10**6;
uint256 public LIFETIME_PRICE = 200 * 10**6;

// Events...

// Constructor...

function subscribe(uint8 tokenId, string memory _refCode) external {
uint256 amountPaid;
uint256 nextPayment;
address referrer;

bytes32 refCode = bytes32(abi.encodePacked(_refCode));

require(addrByRef[refCode] != msg.sender, "Can't refer yourself");
require(
token.balanceOf(msg.sender) >= amountPaid,
"Insufficient funds"
);

if (tokenId == 1) {
amountPaid = MONTH_PRICE;
nextPayment = block.timestamp + (86400 * 30);
} else if (tokenId == 2) {
amountPaid = YEAR_PRICE;
nextPayment = block.timestamp + (86400 * 365);
} else if (tokenId == 3) {
amountPaid = LIFETIME_PRICE;
} else {
revert("invalid tokenId input");
}

subscriptions++;

if (addrByRef[refCode] != address(0)) {
//Add 10% referral logic to change the amount Paid
referrer = addrByRef[refCode];
amountPaid = amountPaid - ((amountPaid * discount) / 100);
uint256 commission = (amountPaid * bonus) / 100;
amountPaid = amountPaid - commission;
token.transferFrom(msg.sender, addrByRef[refCode], commission);

emit ReferralEarned(referrer, block.timestamp);
}

users[msg.sender] = User(
msg.sender,
tokenId,
block.timestamp,
nextPayment,
referrer
);

token.transferFrom(msg.sender, owner(), amountPaid);
}

function checkStatus(address _addr) external view returns (bool) {
// Status checking logic...
}

function payBill(address _addr) external {
// Bill payment logic...
}

// Other functions...
}

In the subscribe function, users can choose a subscription type (1 for monthly, 2 for yearly, 3 for lifetime) and provide a referral code. The contract calculates the payment amount, updates the user's subscription information, transfers tokens, and emits appropriate events.

 function checkStatus(address _addr) external view returns (bool) {
if (users[_addr].subType == 3) {
return true;
} else if (block.timestamp < users[_addr].nextPayment) {
return true;
} else {
return false;
}
}

The checkStatus function lets users check whether they have access based on their subscription type and payment status.

function payBill(address _addr) external {
require(users[_addr].user != address(0), "User not found!");
require(
users[_addr].subType != 3,
"Lifetime membership does not have to pay bill!"
);
uint256 amount;
uint256 nextPayment;

if (users[_addr].subType == 2) {
amount = YEAR_PRICE;
nextPayment = block.timestamp + (86400 * 365);
} else {
amount = MONTH_PRICE;
nextPayment = block.timestamp + (86400 * 30);
}

users[_addr].nextPayment = nextPayment;

if (users[_addr].referrer != address(0)) {
uint256 commission = (amount * bonus) / 100;
amount = amount - commission;
token.transferFrom(msg.sender, users[_addr].referrer, commission);

emit ReferralEarned(users[_addr].referrer, block.timestamp);
}

token.transferFrom(msg.sender, owner(), amount);

emit BillPaid(msg.sender, amount);
}

The payBill function allows users to pay their subscription bill, updating their payment timestamp and transferring tokens accordingly.

Step 3: Referral Program

Let’s add the functionality for the referral program. Subscribers can refer others using a referral code, and referrers earn rewards points and commissions.

// Inside the CadetSub contract

contract CadetSub is Ownable {
// ...

mapping(bytes32 => address) public addrByRef;
mapping(address => bytes32) public refByAddress;

uint8 public discount = 10;
uint8 public bonus = 20;

// Constructor...

// Subscription and status functions...

function subscribe(uint8 tokenId, string memory _refCode) external {
// Subscription logic with referral rewards...
}

function payBill(address _addr) external {
// Bill payment logic with referral commissions...
}

function addReferralAddress(string memory userName, address _addr) public {
bytes32 _referralCode = bytes32(abi.encodePacked(userName));
refByAddress[_addr] = _referralCode;
addrByRef[_referralCode] = _addr;
}

// Other functions...
}

The addReferralAddress function lets users register their referral addresses, and the contract calculates rewards and commissions accordingly when users subscribe or pay their bills.

Step 4: Additional Features

You can further enhance your subscription contract by adding features like updating subscription plans, canceling subscriptions, changing prices, and adjusting referral bonuses.

// Inside the CadetSub contract

contract CadetSub is Ownable {
// ...

// Constructor...

// Subscription and status functions...

function updateSubscription(address _addr) external {
require(users[_addr].user != address(0), "User not found!");
require(
users[_addr].subType != 3,
"User already has lifetime membership"
);
uint256 nextPayment;

if (users[_addr].subType == 2) {
nextPayment = block.timestamp + (86400 * 30);
users[_addr].subType = 1;
} else {
uint256 amount = YEAR_PRICE;
nextPayment = block.timestamp + (86400 * 365);
users[_addr].subType = 2;

if (users[_addr].referrer != address(0)) {
uint256 commission = (amount * bonus) / 100;
amount = amount - commission;
token.transferFrom(
msg.sender,
users[_addr].referrer,
commission
);

emit ReferralEarned(users[_addr].referrer, block.timestamp);
}

token.transferFrom(msg.sender, owner(), amount);
}

users[_addr].nextPayment = nextPayment;
}

function cancelSubscription(address _addr) external {
delete users[_addr];

subscriptions--;

emit CancelSubscription(_addr, block.timestamp);
}

function setPrices(
uint256 _newMonthPrice,
uint256 _newYearPrice,
uint256 _newLifetimePrice
) external onlyOwner {
// Price update logic...
}

function setBonus(uint8 _bonus) external onlyOwner{
// Bonus update logic...
}

function setDiscount(uint8 _disc) external onlyOwner{
// Discount update logic...
}

// Other functions...
}

Step 5: Incentives vs. Penalties

A model like this requires a bit of creativity in terms of incentivizing your audience to continue coming back, rather the traditional payment model of storing credit card information and charging them every month. This can be accomplished in a number of ways that you can try: reward points that accumulate faster depending on the length of the payment streak, special access or increased functionality/hidden features, etc. At the end of the day, the service has to be something that people will continuously want to use, so that should be at top of mind during your development process.

TechJD teaches developers how to transition to or start a career in web3. He discusses topics that go beyond just the code, including the business and legal aspects of running a successful web3 startup or being an impactful member of a team. You can learn more about the CryptoCadet Academy here or join the Discord.

twitter: https://twitter.com/Cryptocadetapp

--

--

TechJD

Law, programming, and everything in-between! Coming up with fun coding projects with real-world application.