Skip to content
This repository was archived by the owner on Apr 3, 2025. It is now read-only.
2 changes: 1 addition & 1 deletion contracts/addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"Subscriptions": "0x482f58d3513E386036670404b35cB3F2DF67a750"
},
"421613": {
"Subscriptions": "0x29f49a438c747e7Dd1bfe7926b03783E47f9447B"
"Subscriptions": "0x1c4053A0CEBfA529134CB9ddaE3C3D0B144384aA"
}
}
117 changes: 117 additions & 0 deletions contracts/build/Subscriptions.abi
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
"internalType": "uint64",
"name": "_epochSeconds",
"type": "uint64"
},
{
"internalType": "address",
"name": "_recurringPayments",
"type": "address"
}
],
"stateMutability": "nonpayable",
Expand Down Expand Up @@ -53,6 +58,37 @@
"name": "AuthorizedSignerRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": false,
"internalType": "uint64",
"name": "oldEnd",
"type": "uint64"
},
{
"indexed": false,
"internalType": "uint64",
"name": "newEnd",
"type": "uint64"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "Extend",
"type": "event"
},
{
"anonymous": false,
"inputs": [
Expand All @@ -67,6 +103,12 @@
"internalType": "uint64",
"name": "epochSeconds",
"type": "uint64"
},
{
"indexed": false,
"internalType": "address",
"name": "recurringPayments",
"type": "address"
}
],
"name": "Init",
Expand Down Expand Up @@ -128,6 +170,19 @@
"name": "PendingSubscriptionCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "recurringPayments",
"type": "address"
}
],
"name": "RecurringPaymentsUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
Expand Down Expand Up @@ -228,6 +283,24 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "user",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "addTo",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down Expand Up @@ -309,6 +382,24 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "user",
"type": "address"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "create",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "currentEpoch",
Expand Down Expand Up @@ -467,6 +558,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "recurringPayments",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
Expand Down Expand Up @@ -510,6 +614,19 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_recurringPayments",
"type": "address"
}
],
"name": "setRecurringPayments",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
103 changes: 98 additions & 5 deletions contracts/contracts/Subscriptions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ contract Subscriptions is Ownable {
mapping(address => mapping(address => bool)) public authorizedSigners;
/// @notice Mapping of user to pending subscription.
mapping(address => Subscription) public pendingSubscriptions;
/// @notice Address of the recurring payments contract.
address public recurringPayments;

// -- Events --
event Init(address token, uint64 epochSeconds);
event Init(address token, uint64 epochSeconds, address recurringPayments);
event Subscribe(
address indexed user,
uint256 indexed epoch,
Expand All @@ -56,6 +58,12 @@ contract Subscriptions is Ownable {
uint128 rate
);
event Unsubscribe(address indexed user, uint256 indexed epoch);
event Extend(
address indexed user,
uint64 oldEnd,
uint64 newEnd,
uint256 amount
);
event PendingSubscriptionCreated(
address indexed user,
uint256 indexed epoch,
Expand All @@ -77,25 +85,43 @@ contract Subscriptions is Ownable {
uint256 indexed startEpoch,
uint256 indexed endEpoch
);
event RecurringPaymentsUpdated(address indexed recurringPayments);

modifier onlyRecurringPayments() {
require(
msg.sender == recurringPayments,
'caller is not the recurring payments contract'
);
_;
}

// -- Functions --
/// @param _token The ERC-20 token held by this contract
/// @param _epochSeconds The Duration of each epoch in seconds.
/// @dev Contract ownership must be transfered to the gateway after deployment.
constructor(address _token, uint64 _epochSeconds) {
constructor(
address _token,
uint64 _epochSeconds,
address _recurringPayments
) {
token = IERC20(_token);
epochSeconds = _epochSeconds;
uncollectedEpoch = block.timestamp / _epochSeconds;
_setRecurringPayments(_recurringPayments);

emit Init(_token, _epochSeconds);
emit Init(_token, _epochSeconds, _recurringPayments);
}

/// @notice Create a subscription for the sender.
/// Will override an active subscription if one exists.
/// @dev Setting a start time in the past will clamp it to the current block timestamp.
/// This protects users from paying for a subscription during a period of time they were
/// not able to use it.
/// @param start Start timestamp for the new subscription.
/// @param end End timestamp for the new subscription.
/// @param rate Rate for the new subscription.
function subscribe(uint64 start, uint64 end, uint128 rate) public {
start = uint64(Math.max(start, block.timestamp));
_subscribe(msg.sender, start, end, rate);
}

Expand Down Expand Up @@ -143,6 +169,9 @@ contract Subscriptions is Ownable {

/// @notice Creates a subscription template without requiring funds. Expected to be used with
/// `fulfil`.
/// @dev Setting a start time in the past will clamp it to the current block timestamp when fulfilled.
/// This protects users from paying for a subscription during a period of time they were
/// not able to use it.
/// @param start Start timestamp for the pending subscription.
/// @param end End timestamp for the pending subscription.
/// @param rate Rate for the pending subscription.
Expand Down Expand Up @@ -181,7 +210,7 @@ contract Subscriptions is Ownable {
);

// Create the subscription using the pending subscription details
_subscribe(_to, pendingSub.start, pendingSub.end, pendingSub.rate);
_subscribe(_to, subStart, pendingSub.end, pendingSub.rate);
delete pendingSubscriptions[_to];

// Send any extra tokens back to the user
Expand Down Expand Up @@ -218,6 +247,58 @@ contract Subscriptions is Ownable {
emit AuthorizedSignerRemoved(user, _signer);
}

/// @notice Create a subscription for a user.
/// Will override an active subscription if one exists.
/// @dev The function's name and signature, `create`, are used to comply with the `IPayment`
/// interface for recurring payments.
/// @dev Note that this function does not protect user against a start time in the past.
/// @param user Subscription owner.
/// @param data Encoded start, end and rate for the new subscription.
function create(
address user,
bytes calldata data
) public onlyRecurringPayments {
(uint64 start, uint64 end, uint128 rate) = abi.decode(
data,
(uint64, uint64, uint128)
);
_subscribe(user, start, end, rate);
}

/// @notice Extends a subscription's end time.
/// The time the subscription will be extended by is calculated as `amount / rate`, where
/// `rate` is the existing subscription rate and `amount` is the new amount of tokens provided.
/// If the subscription was expired the extension will start from the current block timestamp.
/// @dev The function's name, `addTo`, is used to comply with the `IPayment` interface for recurring payments.
/// @param user Subscription owner.
/// @param amount Total amount to be added to the subscription.
function addTo(address user, uint256 amount) public {
require(amount > 0, 'amount must be positive');
require(user != address(0), 'user is null');

Subscription memory sub = subscriptions[user];
require(sub.start != 0, 'no subscription found');
require(sub.rate != 0, 'cannot extend a zero rate subscription');
require(amount % sub.rate == 0, "amount not multiple of rate");

uint64 newEnd = uint64(Math.max(sub.end, block.timestamp)) +
uint64(amount / sub.rate);

_setEpochs(sub.start, sub.end, -int128(sub.rate));
_setEpochs(sub.start, newEnd, int128(sub.rate));

subscriptions[user].end = newEnd;

bool success = token.transferFrom(msg.sender, address(this), amount);
require(success, 'IERC20 token transfer failed');

emit Extend(user, sub.end, newEnd, amount);
}

function setRecurringPayments(address _recurringPayments) public onlyOwner {
_setRecurringPayments(_recurringPayments);
}

/// @param _user Subscription owner.
/// @param _signer Address authorized to sign messages on the owners behalf.
/// @return isAuthorized True if the given signer is set as an authorized signer for the given
Expand Down Expand Up @@ -304,8 +385,21 @@ contract Subscriptions is Ownable {
return unlocked(sub.start, sub.end, sub.rate);
}

/// @notice Sets the recurring payments contract address.
/// @param _recurringPayments Address of the recurring payments contract.
function _setRecurringPayments(address _recurringPayments) private {
require(
_recurringPayments != address(0),
'recurringPayments cannot be zero address'
);
recurringPayments = _recurringPayments;
emit RecurringPaymentsUpdated(_recurringPayments);
}

/// @notice Create a subscription for a user
/// Will override an active subscription if one exists.
/// @dev Note that setting a start time in the past is allowed. If this behavior is not desired,
/// the caller can clamp the start time to the current block timestamp.
/// @param user Owner for the new subscription.
/// @param start Start timestamp for the new subscription.
/// @param end End timestamp for the new subscription.
Expand All @@ -318,7 +412,6 @@ contract Subscriptions is Ownable {
) private {
require(user != address(0), 'user is null');
require(user != address(this), 'invalid user');
start = uint64(Math.max(start, block.timestamp));
require(start < end, 'start must be less than end');

// This avoids unexpected behavior from truncation, especially in `locked` and `unlocked`.
Expand Down
3 changes: 2 additions & 1 deletion contracts/tasks/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { deploySubscriptions } from '../utils/deploy'

task('deploy', 'Deploy the subscription contract (use L2 network!)')
.addParam('token', 'Address of the ERC20 token')
.addParam('recurringPayments', 'Address of the recurring payments contract')
.addOptionalParam('epochSeconds', 'Epoch length in seconds.', 3, types.int)
.setAction(async (taskArgs, hre: HardhatRuntimeEnvironment) => {
const accounts = await hre.ethers.getSigners()
Expand All @@ -16,7 +17,7 @@ task('deploy', 'Deploy the subscription contract (use L2 network!)')
console.log('Deploying subscriptions contract with the account:', accounts[0].address);

await deploySubscriptions(
[taskArgs.token, taskArgs.epochSeconds],
[taskArgs.token, taskArgs.epochSeconds, taskArgs.recurringPayments],
accounts[0] as unknown as Wallet,
)
})
Loading