Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions Scarb.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,129 @@ version = 1
name = "chain_lib"
version = "0.1.0"
dependencies = [
"openzeppelin",
"snforge_std",
]

[[package]]
name = "openzeppelin"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:320185f3e17cf9fafda88b1ce490f5eaed0bfcc273036b56cd22ce4fb8de628f"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_finance",
"openzeppelin_governance",
"openzeppelin_introspection",
"openzeppelin_merkle_tree",
"openzeppelin_presets",
"openzeppelin_security",
"openzeppelin_token",
"openzeppelin_upgrades",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_access"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:a39a4ea1582916c637bf7e3aee0832c3fe1ea3a3e39191955e8dc39d08327f9b"
dependencies = [
"openzeppelin_introspection",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_account"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:7e943a2de32ddca4d48e467e52790e380ab1f49c4daddbbbc4634dd930d0243f"
dependencies = [
"openzeppelin_introspection",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_finance"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:9fa9e91d39b6ccdfa31eef32fdc087cd06c0269cc9c6b86e32d57f5a6997d98b"
dependencies = [
"openzeppelin_access",
"openzeppelin_token",
]

[[package]]
name = "openzeppelin_governance"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:c05add2974b3193c3a5c022b9586a84cf98c5970cdb884dcf201c77dbe359f55"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_introspection",
"openzeppelin_token",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_introspection"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:34e088ecf19e0b3012481a29f1fbb20e600540cb9a5db1c3002a97ebb7f5a32a"

[[package]]
name = "openzeppelin_merkle_tree"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:a5341705514a3d9beeeb39cf11464111f7355be621639740d2c5006786aa63dc"

[[package]]
name = "openzeppelin_presets"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:4eb098e2ee3ac0e67b6828115a7de62f781418beab767d4e80b54e176808369d"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_finance",
"openzeppelin_introspection",
"openzeppelin_token",
"openzeppelin_upgrades",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_security"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:1deb811a239c4f9cc28fc302039e2ffcb19911698a8c612487207448d70d2e6e"

[[package]]
name = "openzeppelin_token"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:33fcb84a1a76d2d3fff9302094ff564f78d45b743548fd7568c130b272473f66"
dependencies = [
"openzeppelin_access",
"openzeppelin_account",
"openzeppelin_introspection",
"openzeppelin_utils",
]

[[package]]
name = "openzeppelin_upgrades"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:36f7a03e7e7111577916aacf31f88ad0053de20f33ee10b0ab3804849c3aa373"

[[package]]
name = "openzeppelin_utils"
version = "1.0.0"
source = "registry+https://scarbs.xyz/"
checksum = "sha256:fd348b31c4a4407add33adc3c2b8f26dca71dbd7431faaf726168f37a91db0c1"

[[package]]
name = "snforge_scarb_plugin"
version = "0.40.0"
Expand Down
1 change: 1 addition & 0 deletions Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2023_11"

[dependencies]
starknet = "2.11.2"
openzeppelin = "1.0.0"

[dev-dependencies]
snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.40.0" }
Expand Down
5 changes: 5 additions & 0 deletions src/base/errors.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ pub mod permission_errors {
pub const INVALID_PERMISSION: felt252 = 'Invalid permission value';
pub const ZERO_ADDRESS: felt252 = 'Zero address';
}

pub mod payment_errors {
pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Insufficient token allowance';
pub const INSUFFICIENT_BALANCE: felt252 = 'Insufficient token balance';
}
70 changes: 68 additions & 2 deletions src/chainlib/ChainLib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ pub mod ChainLib {
use core::array::{Array, ArrayTrait};
use core::option::OptionTrait;
use core::traits::Into;
use openzeppelin::token::erc20::interface::{IERC20Dispatcher, IERC20DispatcherTrait};
use starknet::storage::{
Map, MutableVecTrait, StorageMapReadAccess, StorageMapWriteAccess, StoragePathEntry,
StoragePointerReadAccess, StoragePointerWriteAccess, Vec, VecTrait,
};
use starknet::{
ContractAddress, contract_address_const, get_block_timestamp, get_caller_address,
get_contract_address,
};
use crate::base::errors::payment_errors;
use crate::base::types::{
AccessRule, AccessType, Permissions, Purchase, PurchaseStatus, Rank, Role, Status,
TokenBoundAccount, User, VerificationRequirement, VerificationType, permission_flags,
Expand Down Expand Up @@ -211,14 +214,18 @@ pub mod ChainLib {
subscription_record: Map<u256, Vec<Subscription>>, // subcription id to subscription record
subscription_count: Map<
u256, u256,
> // subscriber count to number of times the subscription record has been updated
>, // subscriber count to number of times the subscription record has been updated
token_address: ContractAddress // Address of the token contract used for payments
}


#[constructor]
fn constructor(ref self: ContractState, admin: ContractAddress) {
fn constructor(
ref self: ContractState, admin: ContractAddress, token_address: ContractAddress,
) {
// Store the values in contract state
self.admin.write(admin);
self.token_address.write(token_address);
// Initialize purchase ID counter
self.next_purchase_id.write(1_u256);
self.purchase_timeout_duration.write(3600);
Expand Down Expand Up @@ -760,6 +767,8 @@ pub mod ChainLib {
// Only allow the subscriber themselves to create a subscription
assert(caller == subscriber, 'Only subscriber can call');

self._process_payment(amount);

// Create a new subscription
let subscription_id = self.subscription_id.read();
let subscription_plan: Subscription = self.subscriptions.read(subscription_id);
Expand Down Expand Up @@ -862,6 +871,9 @@ pub mod ChainLib {
// For simplicity, we'll allow any recurring payment after the initial payment
assert(current_time > subscription.last_payment_date, 'Payment not due yet');

// Process the payment
self._process_payment(subscription.amount);

// Default subscription period is 30 days (in seconds)
let subscription_period: u64 = 30 * 24 * 60 * 60;

Expand Down Expand Up @@ -979,6 +991,9 @@ pub mod ChainLib {
// Verify payment exists and is not already refunded
assert(!payment.is_refunded, 'Payment already refunded');

// process the refund
self._process_refund(payment.amount, get_caller_address());

// Mark payment as refunded
payment.is_refunded = true;
self.payments.write(payment_id, payment);
Expand Down Expand Up @@ -1739,6 +1754,9 @@ pub mod ChainLib {
let price = self.content_prices.read(content_id);
assert!(price > 0, "Content either doesn't exist or has no price");

// process the purchase
self._process_payment(price);

let buyer = get_caller_address();
let current_time = get_block_timestamp();

Expand Down Expand Up @@ -1967,4 +1985,52 @@ pub mod ChainLib {
true
}
}

#[generate_trait]
impl internal of InternalTraits {
/// @notice Processes a payment for a subscription or content purchase.
/// @dev Checks the token allowance and balance before transferring tokens.
/// @param self The contract state reference.
/// @param amount The amount of tokens to transfer.
/// @require The caller must have sufficient token allowance and balance.
fn _process_payment(ref self: ContractState, amount: u256) {
let strk_token = IERC20Dispatcher { contract_address: self.token_address.read() };
let caller = get_caller_address();
let contract_address = get_contract_address();
self._check_token_allowance(caller, amount);
self._check_token_balance(caller, amount);
strk_token.transfer_from(caller, contract_address, amount);
}

/// @notice Checks if the caller has sufficient token allowance.
/// @dev Asserts that the caller has enough allowance to transfer the specified amount.
/// @param self The contract state reference.
/// @param spender The address of the spender (usually the contract itself).
/// @param amount The amount of tokens to check allowance for.
/// @require The caller must have sufficient token allowance.
fn _check_token_allowance(ref self: ContractState, spender: ContractAddress, amount: u256) {
let token = IERC20Dispatcher { contract_address: self.token_address.read() };
let allowance = token.allowance(spender, starknet::get_contract_address());
assert(allowance >= amount, payment_errors::INSUFFICIENT_ALLOWANCE);
}

/// @notice Checks if the caller has sufficient token balance.
/// @dev Asserts that the caller has enough balance to transfer the specified amount.
/// @param self The contract state reference.
/// @param caller The address of the caller (usually the user).
/// @param amount The amount of tokens to check balance for.
/// @require The caller must have sufficient token balance.
fn _check_token_balance(ref self: ContractState, caller: ContractAddress, amount: u256) {
let token = IERC20Dispatcher { contract_address: self.token_address.read() };
let balance = token.balance_of(caller);
assert(balance >= amount, payment_errors::INSUFFICIENT_BALANCE);
}

fn _process_refund(ref self: ContractState, amount: u256, refund_address: ContractAddress) {
let token = IERC20Dispatcher { contract_address: self.token_address.read() };
let contract_address = get_contract_address();
self._check_token_balance(contract_address, amount);
token.transfer(refund_address, amount);
}
}
}
4 changes: 4 additions & 0 deletions src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ pub mod interfaces {
pub mod IChainLib;
}

pub mod presets {
pub mod mock_erc20;
}

76 changes: 76 additions & 0 deletions src/presets/mock_erc20.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
use starknet::ContractAddress;

#[starknet::interface]
pub trait IExternal<ContractState> {
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256);
}
#[starknet::contract]
pub mod mock_erc20 {
use openzeppelin::access::ownable::OwnableComponent;
use openzeppelin::token::erc20::interface::IERC20Metadata;
use openzeppelin::token::erc20::{ERC20Component, ERC20HooksEmptyImpl};
use starknet::ContractAddress;

component!(path: ERC20Component, storage: erc20, event: ERC20Event);
component!(path: OwnableComponent, storage: ownable, event: OwnableEvent);

#[storage]
pub struct Storage {
#[substorage(v0)]
pub erc20: ERC20Component::Storage,
#[substorage(v0)]
pub ownable: OwnableComponent::Storage,
custom_decimals: u8 // Add custom decimals storage
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
#[flat]
ERC20Event: ERC20Component::Event,
#[flat]
OwnableEvent: OwnableComponent::Event,
}

#[constructor]
fn constructor(
ref self: ContractState, recipient: ContractAddress, owner: ContractAddress, decimals: u8,
) {
self.erc20.initializer(format!("USDC"), format!("USDC"));
self.ownable.initializer(owner);
self.custom_decimals.write(decimals);

self.erc20.mint(recipient, core::num::traits::Bounded::<u256>::MAX);
}

#[abi(embed_v0)]
impl CustomERC20MetadataImpl of IERC20Metadata<ContractState> {
fn name(self: @ContractState) -> ByteArray {
self.erc20.name()
}

fn symbol(self: @ContractState) -> ByteArray {
self.erc20.symbol()
}

fn decimals(self: @ContractState) -> u8 {
self.custom_decimals.read() // Return custom value
}
}

// Keep existing implementations
#[abi(embed_v0)]
impl ERC20Impl = ERC20Component::ERC20Impl<ContractState>;
#[abi(embed_v0)]
impl OwnableImpl = OwnableComponent::OwnableImpl<ContractState>;
impl InternalImpl = ERC20Component::InternalImpl<ContractState>;
impl OwnableInternalImpl = OwnableComponent::InternalImpl<ContractState>;

#[abi(embed_v0)]
impl ExternalImpl of super::IExternal<ContractState> {
fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) {
self.erc20.mint(recipient, amount);
}
}
}
1 change: 1 addition & 0 deletions tests/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub mod test_contentpost;
pub mod test_permissions;
pub mod test_subscription;

pub mod test_utils;
Loading
Loading