Skip to content

Commit

Permalink
feat: abs-compatible subscription payment system
Browse files Browse the repository at this point in the history
Description
-----------
- SubscriptionRegistry: Manages plans and subscriptions
- SubscriptionAccount: Smart wallet for automated payments
- Full test coverage.

Includes ECDSA validation, reentrancy protection, and
OpenZeppelin security features.

Usage
-----
Please check `README.md`.
  • Loading branch information
0xObsidian committed Dec 19, 2024
1 parent cf1c587 commit 0009628
Show file tree
Hide file tree
Showing 18 changed files with 1,085 additions and 0 deletions.
43 changes: 43 additions & 0 deletions subscription-payments/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
node_modules
.env
.env.local

# Hardhat files
cache
cache-zk
artifacts
artifacts-zk
artifacts-abstract
deployments
deployments-zk
deployments-abstract
anvil-zksync.log
anvil-abstract.log

# TypeChain files
typechain
typechain-types

# solidity-coverage files
coverage
coverage.json

# Hardhat Ignition deployment files
ignition/deployments
ignition/deployments/chain-31337

# IDE files
.vscode
.idea
*.swp
*.swo

# OS files
.DS_Store
Thumbs.db

# Debug logs
debug.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
4 changes: 4 additions & 0 deletions subscription-payments/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
loglevel=error
audit=false
fund=false
update-notifier=false
25 changes: 25 additions & 0 deletions subscription-payments/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Dependencies
node_modules

# Build artifacts
artifacts
artifacts-zk
artifacts-abstract
cache
cache-zk
coverage
typechain-types
dist
deployments-zk
deployments-abstract

# Logs and environment
.env
*.log
anvil-zksync.log
anvil-abstract.log

# IDE and system files
.DS_Store
.idea
.vscode
22 changes: 22 additions & 0 deletions subscription-payments/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"semi": true,
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"useTabs": false,
"arrowParens": "avoid",
"overrides": [
{
"files": "*.sol",
"options": {
"printWidth": 100,
"tabWidth": 4,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false
}
}
]
}
11 changes: 11 additions & 0 deletions subscription-payments/.solhint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error", "^0.8.24"],
"func-visibility": ["error", { "ignoreConstructors": true }],
"not-rely-on-time": "off",
"no-empty-blocks": "off",
"no-inline-assembly": "off",
"avoid-low-level-calls": "off"
}
}
80 changes: 80 additions & 0 deletions subscription-payments/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Abstract Subscription Payments example

This project demonstrates a subscription payment system built on Abstract. It includes smart contracts for managing subscriptions and payments.

## Flow:

1. Registry owner creates subscription plans
2. Users deploy SubscriptionAccount
3. Account owner subscribes to plans
4. External services/automation can trigger subscription payments when due
5. Payments are processed through the account and sent to the registry

## Architecture

### SubscriptionRegistry

- Central contract managing all subscription plans
- Handles plan creation and management
- Tracks all active subscriptions
- Processes payments and withdrawals
- Owned by a central administrator

### SubscriptionAccount

- Smart contract wallet for subscription management
- zk compatible account implementation
- Secure transaction validation using `ECDSA`
- Only owner can modify subscription status

## Security Considerations

1. All contracts use OpenZeppelin's security primitives
2. Reentrancy protection on critical functions
3. Proper access control with ownership
4. Signature validation for all transactions
5. Balance checks before payments


## Setup

1. Install dependencies:

```bash
npm install
```

2. Set your deployment private key:
Make to go to faucet and get testnet funds

```bash
npx hardhat vars set DEPLOYER_PRIVATE_KEY <your_private_key_here>
```

## Available Commands

```bash
# Clean the project
npm run clean

# Compile contracts for Abstract
npm run compile

# Run tests (uses local Hardhat network)
npm test

# Deploy contracts to Abstract testnet
npm run deploy

# Verify contract on Abstract Explorer (replace CONTRACT_ADDRESS with your deployed contract address)
npm run verify CONTRACT_ADDRESS

# Check deployer balance
npm run check-balance
```

## Important Notes

1. Make sure to get testnet funds from the Abstract faucet before deployment
2. Use a new wallet for testing - don't use your real private key associated with real real trading wallet.
3. Tests run on local network, and deployment goes to Abstract testnet
186 changes: 186 additions & 0 deletions subscription-payments/contracts/SubscriptionAccount.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {SubscriptionRegistry} from "./SubscriptionRegistry.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {IAccount} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IAccount.sol";
import {Transaction, TransactionHelper} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";
import {SystemContractsCaller} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/SystemContractsCaller.sol";
import {BOOTLOADER_FORMAL_ADDRESS, NONCE_HOLDER_SYSTEM_CONTRACT} from "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";
import {INonceHolder} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/INonceHolder.sol";

/**
* @title SubscriptionAccount
* @notice Smart contract account that handles automated subscription payments
*/
contract SubscriptionAccount is IAccount {
using TransactionHelper for Transaction;
using ECDSA for bytes32;

bytes4 constant VALIDATION_SUCCESS_MAGIC = 0x1626ba7e;

error OnlyOwner();
error InvalidSignature();
error TransactionFailed();
error SubscriptionNotActive();
error PlanNotActive();
error PaymentProcessingFailed();
error PaymentTransferFailed();
error OnlyBootloader();

SubscriptionRegistry public immutable REGISTRY;
address public immutable OWNER;

event PaymentFailed(uint256 indexed planId, string reason);

constructor(address payable _registry, address _owner) {
REGISTRY = SubscriptionRegistry(_registry);
OWNER = _owner;
}

modifier onlyBootloader() {
if (msg.sender != BOOTLOADER_FORMAL_ADDRESS) revert OnlyBootloader();
_;
}

modifier onlyOwner() {
if (msg.sender != OWNER) revert OnlyOwner();
_;
}

function validateTransaction(
bytes32,
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) external payable override onlyBootloader returns (bytes4 magic) {
return _validateTransaction(_suggestedSignedHash, _transaction);
}

function executeTransactionFromOutside(
Transaction calldata _transaction
) external payable override onlyBootloader {
_executeTransaction(_transaction);
}

function _validateTransaction(
bytes32 _suggestedSignedHash,
Transaction calldata _transaction
) internal returns (bytes4 magic) {
// Increments nonce
SystemContractsCaller.systemCallWithPropagatedRevert(
uint32(gasleft()),
address(NONCE_HOLDER_SYSTEM_CONTRACT),
0,
abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce))
);

// Validates signature
bytes32 txHash = _suggestedSignedHash == bytes32(0)
? _transaction.encodeHash()
: _suggestedSignedHash;
if (!_validateSignature(txHash, _transaction.signature)) revert InvalidSignature();

// Checks balance
uint256 totalRequiredBalance = _transaction.totalRequiredBalance();
require(
totalRequiredBalance <= address(this).balance,
"Not enough balance for fee + value"
);

magic = VALIDATION_SUCCESS_MAGIC;
}

function executeTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable override onlyBootloader {
_executeTransaction(_transaction);
}

function _executeTransaction(Transaction calldata _transaction) internal {
address to = address(uint160(_transaction.to));
uint256 value = _transaction.value;
bytes memory data = _transaction.data;

bool success;
assembly {
success := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0)
}
if (!success) revert TransactionFailed();
}

function subscribe(uint256 planId) external onlyOwner {
REGISTRY.subscribe(planId);
}

function cancelSubscription(uint256 planId) external onlyOwner {
REGISTRY.cancelSubscription(planId);
}

function processSubscriptionPayment(uint256 planId) external {
// Gets subscription details
SubscriptionRegistry.Subscription memory sub = REGISTRY.getSubscription(
address(this),
planId
);
if (!sub.active) revert SubscriptionNotActive();

// Gets plan details
SubscriptionRegistry.SubscriptionPlan memory plan = REGISTRY.getPlan(planId);
if (!plan.active) revert PlanNotActive();

// Processes the payment
bool success = REGISTRY.processPayment(address(this), planId);
if (!success) revert PaymentProcessingFailed();

// Transfers the subscription amount
(bool transferred, ) = payable(address(REGISTRY)).call{value: plan.amount}("");
if (!transferred) revert PaymentTransferFailed();
}

function _validateSignature(bytes32 hash, bytes memory signature) internal view returns (bool) {
if (signature.length != 65) {
return false;
}

uint8 v;
bytes32 r;
bytes32 s;
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := and(mload(add(signature, 0x41)), 0xff)
}

if (v != 27 && v != 28) {
return false;
}

if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return false;
}

address recoveredAddress = ecrecover(hash, v, r, s);
return recoveredAddress == OWNER && recoveredAddress != address(0);
}

function payForTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable override onlyBootloader {
bool success = _transaction.payToTheBootloader();
require(success, "Failed to pay bootloader operator fee");
}

function prepareForPaymaster(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable override onlyBootloader {
_transaction.processPaymasterInput();
}

receive() external payable {}
}
Loading

0 comments on commit 0009628

Please sign in to comment.