Building a Subscription System on Ethereum
Tags: solidir, smartcontract, ethereum
Published: 2024-02-04 09:32:18

The digital age has seen a tremendous shift towards subscription models. From magazines to music streaming, monthly subscriptions dominate. But how does this translate to the decentralized world of blockchain? Enter Ethereum's smart contracts. In today's post, we delve deep into a subscription-based smart contract, breaking down its functionality and exploring how it paves the way for blockchain's take on recurring payments.

### Meet the Ethereum Subscription Contract

Before diving into the intricacies, let's introduce our smart contract - aptly named `Subscription`. Written in Solidity, it serves as a basic representation of how a subscription system might operate on the Ethereum blockchain.

#### The Blueprint: Structures and Mappings

At its core, the contract employs a structure called `Subscriber`. This structure captures vital information:

- `subscriptionId`: A unique identifier for each subscription.
- `subscriberAddress`: The Ethereum address of the subscriber.
- `startedAt`: Timestamp marking the subscription's start.
- `expiresAt`: Timestamp denoting when the subscription will expire.

To manage these subscribers efficiently, the contract utilizes a mapping, `_subscribers`, that links each subscriber's address to their respective `Subscriber` structure.

```
struct Subscriber {
        uint256 subscriptionId;
        address subscriberAddress;
        uint256 startedAt;
        uint256 expiresAt;
    }
```

### Kickstarting Subscriptions

When a user decides to subscribe, they call the `getSubscription` function. However, not just any user can do so willy-nilly. Two critical checks are in place:

1. The payment must exactly be `0.01 ether`.
2. The user shouldn't be an existing subscriber with an active subscription.

```
function getSubscription() external payable override {
        require(msg.value == 0.01 ether, "Subscription: InvalidAmount");
        require(_subscribers[msg.sender].subscriberAddress != msg.sender || _subscribers[msg.sender].expiresAt < block.timestamp, "Subscription: AlreadyExistsOrRenew");

        _currentSubscriptionId++; // Increment the ID for a new subscription

        _subscribers[msg.sender] = Subscriber(
            _currentSubscriptionId,
            msg.sender,
            block.timestamp,
            block.timestamp + 30 days
        );

        emit SubscriptionStarted(msg.sender, _currentSubscriptionId, block.timestamp, block.timestamp + 30 days);
    }
```
Upon meeting these conditions, their details are registered, and a unique subscription ID is assigned. Notably, the subscription lasts 30 days from the moment of activation. All these activities trigger an event, `SubscriptionStarted`, which logs essential details, a crucial feature for Dapps requiring real-time updates.

### Renewing & Cancelling

As with any good subscription system, our contract offers the flexibility of renewing or canceling one's subscription. The `renewSubscription` function facilitates the former. For a subscription to be renewed:
1. The caller must be an existing subscriber.
2. Their subscription must have expired.
3. The renewal fee, again, should be `0.01 ether`.
4. The provided subscription ID should match their current one.

```
function getSubscription() external payable override {
        require(msg.value == 0.01 ether, "Subscription: InvalidAmount");
        require(_subscribers[msg.sender].subscriberAddress != msg.sender || _subscribers[msg.sender].expiresAt < block.timestamp, "Subscription: AlreadyExistsOrRenew");

        _currentSubscriptionId++; // Increment the ID for a new subscription

        _subscribers[msg.sender] = Subscriber(
            _currentSubscriptionId,
            msg.sender,
            block.timestamp,
            block.timestamp + 30 days
        );

        emit SubscriptionStarted(msg.sender, _currentSubscriptionId, block.timestamp, block.timestamp + 30 days);
    }
```

If all boxes are checked, the subscription extends for another 30 days.
On the flip side, if a user no longer finds value, they can call `cancelSubscription`. This function clears their details from the system, effectively ending their subscription. A corresponding event, `SubscriptionCancelled`, is emitted, marking the conclusion of their journey.

```
function cancelSubscription(uint256 subscriptionId) external override {
        require(_subscribers[msg.sender].subscriberAddress == msg.sender, "Subscription: OnlySubscribers");
        require(_subscribers[msg.sender].subscriptionId == subscriptionId, "Subscription: InvalidSubscriptionId");

        delete _subscribers[msg.sender];

        emit SubscriptionCancelled(msg.sender);
    }
```

### Utility Features

Apart from the primary functions, this smart contract boasts a couple of utility features. The `subscriptionData` function allows anyone to retrieve subscription details of a specific address. Meanwhile, `isSubscribed` verifies the active subscription status of an address.

Lastly, an essential component of any financial contract is a withdrawal mechanism. The owner of the contract can call the `withdraw` function, transferring the contract's balance to their address, and ensuring the collected subscription fees are accessible.

**Here is the full smart contract code:**

```
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.13;

interface ISubscription {
    function getSubscription() external payable;
    function renewSubscription(uint256 subscriptionId) external payable;
    function calcleSubscription(uint256 subscriptionId) external;
    function isSubscribed(address subscriber) external view returns (bool);

    // Events
    event SubscriptionCreated(address indexed subscriber, uint256 indexed startedAt, uint256 indexed expiresAt);
    event SubscriptionRenewed(address indexed subscriber, uint256 indexed renewedAt, uint256 indexed expiresAt);
    event SubscriptionCancelled(address indexed subscriber, uint256 indexed cancelledAt);

}

contract Subscription is ISubscription {
    struct Subscriber {
        uint256 subscriptionId;
        address subscriberAddress;
        uint256 startedAt;
        uint256 expiresAt;
    }

    mapping(address => Subscriber) internal _subscribers;
    uint256 private _currentSubscriptionId = 0; // Counter for unique subscription IDs
    address private _owner; // Owner address for potential withdrawal function

    // Events
    event SubscriptionStarted(address indexed subscriber, uint256 subscriptionId, uint256 startDate, uint256 expiryDate);
    event SubscriptionRenewed(address indexed subscriber, uint256 expiryDate);
    event SubscriptionCancelled(address indexed subscriber);

    constructor() {
        _owner = msg.sender;
    }

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

    function getSubscription() external payable override {
        require(msg.value == 0.01 ether, "Subscription: InvalidAmount");
        require(_subscribers[msg.sender].subscriberAddress != msg.sender || _subscribers[msg.sender].expiresAt < block.timestamp, "Subscription: AlreadyExistsOrRenew");

        _currentSubscriptionId++; // Increment the ID for a new subscription

        _subscribers[msg.sender] = Subscriber(
            _currentSubscriptionId,
            msg.sender,
            block.timestamp,
            block.timestamp + 30 days
        );

        emit SubscriptionStarted(msg.sender, _currentSubscriptionId, block.timestamp, block.timestamp + 30 days);
    }

    function renewSubscription(uint256 subscriptionId) external payable override {
        require(_subscribers[msg.sender].subscriberAddress == msg.sender, "Subscription: OnlySubscribers");
        require(_subscribers[msg.sender].expiresAt < block.timestamp, "Subscription: SubscriptionNotExpired");
        require(msg.value == 0.01 ether, "Subscription: InvalidAmount");
        require(_subscribers[msg.sender].subscriptionId == subscriptionId, "Subscription: InvalidSubscriptionId");

        _subscribers[msg.sender].expiresAt = block.timestamp + 30 days;

        emit SubscriptionRenewed(msg.sender, block.timestamp + 30 days);
    }

    function cancelSubscription(uint256 subscriptionId) external override {
        require(_subscribers[msg.sender].subscriberAddress == msg.sender, "Subscription: OnlySubscribers");
        require(_subscribers[msg.sender].subscriptionId == subscriptionId, "Subscription: InvalidSubscriptionId");

        delete _subscribers[msg.sender];

        emit SubscriptionCancelled(msg.sender);
    }

    function subscriptionData(address subscriber) external view returns (Subscriber memory) {
        return _subscribers[subscriber];
    }

    function isSubscribed(address subscriber) external view override returns (bool) {
        return (_subscribers[subscriber].subscriberAddress == subscriber && _subscribers[subscriber].expiresAt > block.timestamp);
    }

    function withdraw() external onlyOwner {
        payable(_owner).transfer(address(this).balance);
    }
}
```

### The Implications

While the discussed contract is elementary, it serves as a foundation. Building on Ethereum enables global accessibility, unparalleled transparency, and robust security. As more industries gravitate towards subscription models, Ethereum and its smart contracts stand poised to redefine how we perceive and handle recurring payments in the digital realm.
In conclusion, blockchain's intersection with subscription systems is young, but brimming with potential. The `Subscription` smart contract showcases the tip of the iceberg, hinting at a future where decentralized subscriptions become the norm. So, the next time you jam to your favorite song on a streaming platform, imagine a world where that monthly payment is running on a blockchain. Exciting times lie ahead!