F-2024-0001·access-control

Logic flaw in the subscription's handling functions execution

Fixedaccount-abstractionerc-4337subscriptiongithub.com/bastion-wallet
TL;DR

Subscription functions in Initiator can be called by both EOAs and SCAs, breaking the SubExecutor mapping invariant and preventing modification, removal, and payment initiation for subscriptions registered directly from the Initiator.

Severity
HIGH
Impact
HIGH
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
LOW
Exploitability
MEDIUM
02Section · Description

Description

Important state-changing functions in the Initiator contract can currently be called by both EOAs and SCAs. If the executor is an EOA without going through Bastion SDK, then the subscriber will be the address of the EOA account and therefore the following attack vectors are present:

  • Subscriptions registered from the Initiator contract can not be modified in any way compared to those added from the SubExecutor contract.

    Example: Let's say that afterward, the user wants to modify his subscription through the SubExecutor's logic by calling modifySubscription(). What would happen then is that because in this same function, the subscriber is the msg.sender, the subscription previously created through my EOA account will not be able to be modified, because the struct that will be returned will be the one to which the address of the SCA's SubExecutor is mapped and not the one mapped to users' EOA address.

  • The initiatePayment() function will revert every time since in the SubExecutor.processPayment() there is a check that ensures the msg.sender is the creator of the subscription which in this case will be the address of the Initiator contract.

solidity
require(msg.sender == sub.initiator, "Only the initiator can initiate payments");
  • The removeSubscription() function executed from an EOA can not delete a subscription created from the SubExecutor contract.

All of these problems arise from the scenario where the user is able to call the Initiator.registerSubscription() function without first calling SubExecutor.createSubscription().

Expected functions execution flow should be:

  1. SubExecutor.createSubscription()
  2. Initiator.registerSubscription()
  3. Initiator.initiatePayment()
  4. SubExecutor.processPayment()
solidity
function registerSubscription(address _subscriber, uint256 _amount,
uint256 _validUntil, uint256 _paymentInterval, address _erc20Token)
public {
require(msg.sender == _subscriber, "Only the subscriber can register a subscription");
ISubExecutor.SubStorage memory sub = ISubExecutor.SubStorage({
amount: _amount,
validUntil: _validUntil,
validAfter: block.timestamp,
paymentInterval: _paymentInterval,
subscriber: _subscriber,
initiator: address(this),
erc20TokensValid: _erc20Token == address(0) ? false : true,
erc20Token: _erc20Token
});
}
03Section · Impact

Impact

The subscription model is broken end to end for any user who interacts with Initiator directly: registered subscriptions cannot be modified, payments cannot be initiated, and removals leave stale state. Any EOA that bypasses the SDK ends up with an unusable subscription and may have funds tied up.

04Section · Recommendation

Recommendation

To address this vulnerability, it is crucial to have the core logic and validation in the Initiator.sol contract and restrict only the SubExecutor.sol contract to call registerSubscription(), removeSubscription() and initiatePayment() functions.

05Section · Resolution

Resolution

Team Response: Acknowledged and fixed as suggested.

Status
Fixed
Fix commit
79cddfeb6070
F-2024-0001

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx