Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b3079f4
Add attribute, setter and event for payment expiration time
maximopalopoli Dec 11, 2025
42a7330
Add attribute, setter and event for the amount to pay in wei
maximopalopoli Dec 11, 2025
05dd72b
Add a withdrawal method to send the funds to a recipient
maximopalopoli Dec 11, 2025
28e8610
Update the ABI used to fetch the payment events
maximopalopoli Dec 12, 2025
89e9e30
Add a TODO for extra check on withdrawal method
maximopalopoli Dec 12, 2025
f75cd1d
Update the deployed anvil state
maximopalopoli Dec 12, 2025
820ab24
Move the amount of eth to pay to the config files
maximopalopoli Dec 12, 2025
c046dac
Move the payment expiration time to the config files
maximopalopoli Dec 12, 2025
b185ee3
Remove unnecessary TODO
maximopalopoli Dec 12, 2025
ca81965
Add subscription limit to the Aggregation Mode Payment Service contract
maximopalopoli Dec 12, 2025
454f144
Add a onlyOwner method to add a list of addresses with a custom expiracy
maximopalopoli Dec 12, 2025
e600923
fix: replace expiracy mentions in addArbitraryExpirationSubscriptions…
maximopalopoli Dec 15, 2025
b6931c9
fix: increase the monthly subscriptions counter on each receive
maximopalopoli Dec 15, 2025
3a56b3e
fix: use the iterated address instead of the sender when adding arbit…
maximopalopoli Dec 15, 2025
3481449
Remove TODO after thinking on possible issue
maximopalopoli Dec 15, 2025
23a863f
Merge branch 'staging' into feataggmode/add-setters-and-subscriptions
maximopalopoli Dec 16, 2025
84d07af
Merge branch 'feataggmode/add-setters-and-subscriptions' into featagg…
maximopalopoli Dec 16, 2025
3ac2414
increase the monthly subscription counter on the addSubscriptions method
maximopalopoli Dec 16, 2025
17c66a5
Convert the resetSubscriptions method in a setter for monthlySubscrip…
maximopalopoli Dec 16, 2025
d73600f
Rework the subscription logic to avoid subscribing for more than N mo…
maximopalopoli Dec 16, 2025
21d2878
Create a separate admin role only for adding subscriptions
maximopalopoli Dec 16, 2025
f6d4155
Merge branch 'staging' into feataggmode/add-subscription-limit-and-ar…
maximopalopoli Dec 16, 2025
59f8ab2
Use upgradeable version of access control on the agg mode payment ser…
maximopalopoli Dec 17, 2025
06410ba
Update contracts/src/core/AggregationModePaymentService.sol
maximopalopoli Dec 17, 2025
ede8fa8
Rename the subscription limit var to remark the month period
maximopalopoli Dec 17, 2025
a90de4b
Add note about adding subscriptions on only admin method and surpassi…
maximopalopoli Dec 17, 2025
445bc04
Apply suggestions from code review
maximopalopoli Dec 17, 2025
c0f9aff
Update the newtork params and deployed state
maximopalopoli Dec 17, 2025
8004004
Fix the expiration date extension logic
maximopalopoli Dec 17, 2025
c18a988
Rename the monthly subscriptions limit var to remove the monthly part
maximopalopoli Dec 17, 2025
13fd25e
Rename the monthly subscriptions amount to rename the monthly part
maximopalopoli Dec 17, 2025
e5dee48
update the contracts deployed state
maximopalopoli Dec 17, 2025
9595158
Improve the subscriptions amount name to active ones
maximopalopoli Dec 17, 2025
fc620c4
Improve the if branches comments on receive method
maximopalopoli Dec 17, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,27 @@ contract AggregationModePaymentServiceDeployer is Script {
string memory configData = vm.readFile(configPath);

address owner = stdJson.readAddress(configData, ".permissions.paymentServiceOwner");
address admin = stdJson.readAddress(configData, ".permissions.paymentServiceAdmin");
address recipient = stdJson.readAddress(configData, ".permissions.recipient");
uint256 amountToPay = stdJson.readUint(configData, ".amounts.amountToPayInWei");
uint256 paymentExpirationTimeSeconds = stdJson.readUint(configData, ".amounts.paymentExpirationTimeSeconds");
uint256 subscriptionLimit = stdJson.readUint(configData, ".amounts.subscriptionLimit");
uint256 maxSubscriptionTimeAhead = stdJson.readUint(configData, ".amounts.maxSubscriptionTimeAhead");

vm.startBroadcast();

AggregationModePaymentService implementation = new AggregationModePaymentService();
ERC1967Proxy proxy = new ERC1967Proxy(
address(implementation),
abi.encodeWithSignature(
"initialize(address,address,uint256,uint256)",
"initialize(address,address,address,uint256,uint256,uint256,uint256)",
owner,
admin,
recipient,
amountToPay,
paymentExpirationTimeSeconds
paymentExpirationTimeSeconds,
subscriptionLimit,
maxSubscriptionTimeAhead
)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
},
"amounts": {
"amountToPayInWei": 1000000000000000000,
"paymentExpirationTimeSeconds": 86400
"paymentExpirationTimeSeconds": 86400,
"subscriptionLimit": 5,
"maxSubscriptionTimeAhead": 7776000
},
"permissions": {
"owner": "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
"paymentServiceOwner": "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
"paymentServiceAdmin": "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f",
"recipient": "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f"
}
}

Large diffs are not rendered by default.

139 changes: 129 additions & 10 deletions contracts/src/core/AggregationModePaymentService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@
pragma solidity ^0.8.12;

import {Initializable} from "@openzeppelin-upgrades/contracts/proxy/utils/Initializable.sol";
import {OwnableUpgradeable} from "@openzeppelin-upgrades/contracts/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin-upgrades/contracts/proxy/utils/UUPSUpgradeable.sol";
import {AccessControlUpgradeable} from "@openzeppelin-upgrades/contracts/access/AccessControlUpgradeable.sol";

/**
* @title AggregationModePaymentService
* @author Aligned Layer
* @notice Handles deposits that grant time-limited access to aggregation services.
*/
contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUPSUpgradeable {
contract AggregationModePaymentService is Initializable, UUPSUpgradeable, AccessControlUpgradeable {

bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");

/// @notice for how much time the payment is valid in seconds
uint256 public paymentExpirationTimeSeconds;

Expand All @@ -20,6 +24,20 @@ contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUP
/// @notice The address where the payment funds will be sent.
address public paymentFundsRecipient;

/// @notice The limit of subscriptions for different addresses
uint256 public subscriptionLimit;

/// @notice Number of current subscriptions
uint256 public activeSubscriptionsAmount;

/// @notice Maximum amount of time (in seconds) an address can be subscribed ahead of the current block timestamp.
/// Prevents stacking multiple short subscriptions and paying them over an extended period.
uint256 public maxSubscriptionTimeAhead;

/// @notice Number of addresses currently subscribed.
/// @dev `expirationTime` is a Unix timestamp (UTC seconds) compared against block timestamps.
mapping(address subscriber => uint256 expirationTime) public subscribedAddresses;

/**
* @notice Emitted when a user deposits funds to purchase service time.
* @param user Address that sent the payment.
Expand All @@ -37,6 +55,18 @@ contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUP
/// @param newAmountToPay the new amount to pay for a subscription in wei.
event AmountToPayUpdated(uint256 indexed newAmountToPay);

/// @notice Event emitted when the subscription limit is updated
/// @param newSubscriptionLimit the new subscription limit.
event SubscriptionLimitUpdated(uint256 indexed newSubscriptionLimit);

/// @notice Event emitted when the subscription amount is updated
/// @param newSubscriptionsAmount the new subscriptions amount.
event ActiveSubscriptionsAmountUpdated(uint256 indexed newSubscriptionsAmount);

/// @notice Event emitted when the max subscription time ahead is updated
/// @param newMaxSubscriptionTimeAhead the max time allowed to subscribe ahead the current timestamp.
event MaxSubscriptionTimeAheadUpdated(uint256 indexed newMaxSubscriptionTimeAhead);

/// @notice Event emitted when the funds recipient is updated
/// @param newFundsRecipient the new address for receiving the funds on withdrawal.
event FundsRecipientUpdated(address indexed newFundsRecipient);
Expand All @@ -48,6 +78,10 @@ contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUP

error InvalidDepositAmount(uint256 amountReceived, uint256 amountRequired);

error SubscriptionLimitReached(uint256 subscriptionLimit);

error SubscriptionTimeExceedsLimit(uint256 newSubscriptionTime, uint256 timeLimit);

/**
* @notice Disables initializers for the implementation contract.
*/
Expand All @@ -58,15 +92,31 @@ contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUP
/**
* @notice Initializes the contract and transfers ownership to the provided address.
* @param _owner Address that becomes the contract owner.
* @param _admin Address that becomes the contract admin.
* @param _paymentFundsRecipient Address that will receive the withdrawal funds.
* @param _amountToPayInWei Amount to pay in wei for the subscription.
* @param _paymentExpirationTimeSeconds The time in seconds that the subscription takes to expire.
* @param _subscriptionLimit The maximum subscribers that can be subscribed at the same time.
*
*/
function initialize(address _owner, address _paymentFundsRecipient, uint256 _amountToPayInWei, uint256 _paymentExpirationTimeSeconds) public initializer {
__Ownable_init();
function initialize(
address _owner,
address _admin,
address _paymentFundsRecipient,
uint256 _amountToPayInWei,
uint256 _paymentExpirationTimeSeconds,
uint256 _subscriptionLimit,
uint256 _maxSubscriptionTimeAhead
) public initializer {
__UUPSUpgradeable_init();
_transferOwnership(_owner);
_grantRole(OWNER_ROLE, _owner);
_grantRole(ADMIN_ROLE, _admin);

paymentExpirationTimeSeconds = _paymentExpirationTimeSeconds;
amountToPayInWei = _amountToPayInWei;
paymentFundsRecipient = _paymentFundsRecipient;
subscriptionLimit = _subscriptionLimit;
maxSubscriptionTimeAhead = _maxSubscriptionTimeAhead;
}

/**
Expand All @@ -76,14 +126,14 @@ contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUP
function _authorizeUpgrade(address newImplementation)
internal
override
onlyOwner // solhint-disable-next-line no-empty-blocks
onlyRole(OWNER_ROLE) // solhint-disable-next-line no-empty-blocks
{}

/**
* @notice Sets the new expiration time. Only callable by the owner
* @param newExpirationTimeInSeconds The new expiration time for the users payments in seconds.
*/
function setPaymentExpirationTimeSeconds(uint256 newExpirationTimeInSeconds) public onlyOwner() {
function setPaymentExpirationTimeSeconds(uint256 newExpirationTimeInSeconds) public onlyRole(OWNER_ROLE) {
paymentExpirationTimeSeconds = newExpirationTimeInSeconds;

emit PaymentExpirationTimeUpdated(newExpirationTimeInSeconds);
Expand All @@ -93,7 +143,7 @@ contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUP
* @notice Sets the new amount to pay. Only callable by the owner
* @param newRecipient The new address for receiving the funds on withdrawal.
*/
function setFundsRecipientAddress(address newRecipient) public onlyOwner() {
function setFundsRecipientAddress(address newRecipient) public onlyRole(OWNER_ROLE) {
paymentFundsRecipient = newRecipient;

emit FundsRecipientUpdated(newRecipient);
Expand All @@ -103,12 +153,61 @@ contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUP
* @notice Sets the new amount to pay. Only callable by the owner
* @param newAmountToPay The new amount to pay for subscription in wei.
*/
function setAmountToPay(uint256 newAmountToPay) public onlyOwner() {
function setAmountToPay(uint256 newAmountToPay) public onlyRole(OWNER_ROLE) {
amountToPayInWei = newAmountToPay;

emit AmountToPayUpdated(newAmountToPay);
}

/**
* @notice Sets the new subscription limit. Only callable by the owner
* @param newSubscriptionLimit The new subscription limit.
*/
function setSubscriptionLimit(uint256 newSubscriptionLimit) public onlyRole(OWNER_ROLE) {
subscriptionLimit = newSubscriptionLimit;

emit SubscriptionLimitUpdated(newSubscriptionLimit);
}

/**
* @notice Sets the subscriptions counter to the value received by parameter. Only callable by the owner
* @param newSubscriptionsAmount The new subscriptions amount.
*/
function setActiveSubscriptionsAmount(uint256 newSubscriptionsAmount) public onlyRole(ADMIN_ROLE) {
activeSubscriptionsAmount = newSubscriptionsAmount;

emit ActiveSubscriptionsAmountUpdated(newSubscriptionsAmount);
}

/**
* @notice Sets the max subscription time ahead to the value received by parameter. Only callable by the owner
* @param newMaxSubscriptionTimeAhead max time allowed to subscribe ahead the current timestamp.
*/
function setMaxSubscriptionTimeAhead(uint256 newMaxSubscriptionTimeAhead) public onlyRole(OWNER_ROLE) {
maxSubscriptionTimeAhead = newMaxSubscriptionTimeAhead;

emit MaxSubscriptionTimeAheadUpdated(newMaxSubscriptionTimeAhead);
}

/**
* @notice Adds an array of addresses to the payment map and emits the Payment event.
* @param addressesToAdd the addresses to be subscribed
* @param expirationTimestamp the expiration timestamp (UTC seconds) for that subscriptions
* Note: this method adds the subscriptions without checking if the final amount of subscriptions surpasses
* the subscriptionLimit
*/
function addSubscriptions(address[] memory addressesToAdd, uint256 expirationTimestamp) public onlyRole(ADMIN_ROLE) {
for (uint256 i=0; i < addressesToAdd.length; ++i) {
address addressToAdd = addressesToAdd[i];

subscribedAddresses[addressToAdd] = expirationTimestamp;

++activeSubscriptionsAmount;

emit UserPayment(addressToAdd, amountToPayInWei, block.timestamp, expirationTimestamp);
}
}

/**
* @notice Accepts payments and validates they meet the minimum requirement.
*/
Expand All @@ -119,13 +218,33 @@ contract AggregationModePaymentService is Initializable, OwnableUpgradeable, UUP
revert InvalidDepositAmount(amount, amountToPayInWei);
}

if (activeSubscriptionsAmount >= subscriptionLimit) {
revert SubscriptionLimitReached(subscriptionLimit);
}

if (subscribedAddresses[msg.sender] < block.timestamp) {
// Subscription is inactive/expired: start a new period from now.
subscribedAddresses[msg.sender] = block.timestamp + paymentExpirationTimeSeconds;
} else {
// Subscription is still active: extend the current expiry by one period.
subscribedAddresses[msg.sender] = subscribedAddresses[msg.sender] + paymentExpirationTimeSeconds;
}

uint256 newExpiration = subscribedAddresses[msg.sender];

if (newExpiration - block.timestamp > maxSubscriptionTimeAhead) {
revert SubscriptionTimeExceedsLimit(newExpiration, maxSubscriptionTimeAhead);
}

++activeSubscriptionsAmount;

emit UserPayment(msg.sender, amount, block.timestamp, block.timestamp + paymentExpirationTimeSeconds);
}

/**
* @notice Withdraws the contract balance to the recipient address.
*/
function withdraw() external onlyOwner {
function withdraw() external onlyRole(OWNER_ROLE) {
uint256 balance = address(this).balance;
payable(paymentFundsRecipient).transfer(balance);
emit FundsWithdrawn(paymentFundsRecipient, balance);
Expand Down
Loading
Loading