A valid recurring ERC-20 payment must move tokens from the contract that holds them to the subscription initiator without requiring that contract to approve itself.
The answer in 30 seconds
Bastion Wallet's reviewed subscription contracts were designed to let an account-abstraction wallet execute recurring native-token or ERC-20 payments. Shieldify Security led the six-day review and owned the client relationship; Zealynx Security contributed as co-auditor. This was partner-delivered work, not a direct Zealynx engagement.[1][3]
The pivotal issue was an authorization mismatch in SubExecutor. The contract held the subscriber's ERC-20 tokens, but _processERC20Payment() called transferFrom(address(this), initiator, amount). That call asked the token contract to spend SubExecutor's balance through an allowance, even though SubExecutor was already the token owner. The report's fuzz test reverted with ERC20: insufficient allowance; its recommended change used transfer() and the team response records the finding as fixed.[1]
This is a technical engagement narrative, not a customer transformation story. Public evidence supports the reviewed mechanism and recorded remediation, but not deployment, usage, monetary exposure, customer sentiment, or Bastion's approval of this derivative narrative.
The buyer decision: can the full recurring-payment path execute?
Bastion publicly describes itself as an open-source modular wallet SDK and smart-contract system for multi-chain account abstraction. Its public organization identifies a TypeScript SDK and Solidity smart-wallet contracts among its components.[2] The reviewed scope was narrower: four subscription contracts and interfaces totaling 253 nSLOC at commit 75b2053670153bc5bc003d1e43ce0e54445a542a, with fixes reviewed at 79cddfeb6070140a24a2cb5029faa6c01088ffba.[1]
For a wallet or subscription team, the buyer question is: before enabling automated ERC-20 charges, how do you prove that custody, authorization, and transfer semantics agree from registration through settlement?
The system invariant was:
When
SubExecutorholds enough of the configured ERC-20, an otherwise valid payment must transfer the stated amount to the initiator without depending on an irrelevant self-allowance.
The dangerous assumption: every token pull uses transferFrom
transferFrom is appropriate when a spender moves tokens owned by another address under an allowance. That was not the reviewed state. The report says SubExecutor held the funds and needed to send them directly to the subscription initiator.[1]
The code first checked token.balanceOf(address(this)), then called:
token.transferFrom(address(this), sub.initiator, sub.amount);
Those two lines described different authorization models. The balance check treated SubExecutor as owner; the transfer treated the same contract as if it needed delegated spending authority over its own balance. Having enough tokens was therefore not sufficient for payment execution.
How the connected failure path worked
The report documents a complete payment sequence and a fuzz test for the affected path:[1]
- A subscription identifies an ERC-20, payment amount, initiator, and validity window.
- ERC-20 tokens are held by
SubExecutor. - The initiator calls
processPayment()after the payment interval. SubExecutorverifies that its token balance covers the amount._processERC20Payment()callstransferFrom()withSubExecutoras the source.- The token enforces an allowance that the direct-owner transfer should not require.
- The report's test reverts with
ERC20: insufficient allowance, so the valid subscription payment does not settle.
The bug was not a missing balance check. It was disagreement between asset custody and the transfer primitive selected after that check.
How the review tested the invariant
1. Identify the asset owner at execution time
The review followed the tokens rather than inferring custody from function names. The report's proof of concept minted tokens to SubExecutor and recorded its balance before payment.[1]
2. Identify the actor authorized to trigger payment
processPayment() restricted execution to the subscription initiator. That caller authorization was separate from token ownership: the initiator could trigger settlement without owning the tokens held by SubExecutor.[1]
3. Exercise the real ERC-20 authorization rule
The fuzz test varied token balances and payment amounts, advanced time beyond the interval, called payment as the initiator, and asserted the expected balance changes. The test instead hit the allowance revert, exposing that adequate balance did not imply an executable transfer.[1]
4. Correct the primitive and record the response
The report recommended replacing transferFrom(address(this), sub.initiator, sub.amount) with transfer(sub.initiator, sub.amount). The team response states: "Acknowledged and fixed as suggested."[1]
Before, intervention, and recorded after-state
| Before the review | Review intervention | Recorded after-state |
|---|---|---|
SubExecutor held the ERC-20 balance but attempted to spend its own tokens through transferFrom | Traced custody and caller authority through the full payment path | Report recommends a direct transfer from the holding contract |
| The balance guard could pass while the transfer reverted for insufficient allowance | Built a fuzz test across valid balances and payment amounts | Team response records H-02 as acknowledged and fixed |
| A locally plausible transfer call made an otherwise valid subscription unpayable | Reframed correctness as custody-authority-execution alignment | Fix-review commit is identified in the report |
"Fixed" here means the report records the suggested correction and identifies a fix-review commit. The public Bastion repositories currently visible from its GitHub organization do not expose the historical subscription repository or either reviewed commit, so this narrative cannot independently inspect the code change or establish production deployment.[1][2]
Why ordinary happy-path tests can miss it
A mock token or test harness can accidentally hide authorization mistakes if it skips allowance enforcement, preconfigures approvals, or transfers funds manually. A useful recurring-payment test must preserve all four roles:
| Role | Question to assert |
|---|---|
| token owner | Which address holds the balance immediately before settlement? |
| payment trigger | Which address may call processPayment()? |
| token spender | Does the selected ERC-20 function require an allowance, and from whom? |
| recipient | Does the intended initiator receive exactly the subscription amount? |
The transferable lesson is broader than replacing one function name: choose the transfer primitive from actual custody and authority at execution time, then test it against a standards-compliant token.
A custody-to-call decision rule
Use the narrowest transfer model that matches the state at execution time:
| Asset location and authority | Expected operation | Required proof |
|---|---|---|
| Payment contract owns the tokens | direct send from the contract | recipient balance rises and contract balance falls by the same amount |
| User owns tokens; payment contract is approved spender | delegated pull under allowance | allowance owner, spender, amount, expiry/revocation, and replay behavior are explicit |
| Smart account owns tokens and executes a batch | wallet-authorized call from the account | account authorization and the complete batched state transition succeed together |
| Third-party vault or paymaster owns tokens | protocol-specific withdrawal or settlement | external authorization, callback, and final recipient are tested end to end |
Do not select the operation from the label “subscription payment.” Select it from the actual owner-spender-recipient tuple at the moment the token call executes.
A checklist for automated ERC-20 payment systems
Before enabling recurring token payments, verify:
- Custody: identify the token owner at every payment state, including prefunding, execution, cancellation, and refund.
- Trigger authority: separate permission to initiate payment from authority to spend the held asset.
- Transfer semantics: use
transferfor an owner's direct send andtransferFromonly for a documented allowance relationship. - Allowance direction: when delegated spending is intended, assert the exact owner, spender, amount, and approval lifecycle.
- Token behavior: handle false-returning and non-standard ERC-20s deliberately rather than assuming every call reverts on failure.
- Temporal boundaries: test first payment, same-batch registration and payment, interval boundaries, expiry, and cancellation.
- Balance deltas: assert sender and recipient changes instead of treating a non-reverting call as proof of settlement.
- Fix verification: retain the test that reproduced the failure and run it against the exact remediation commit.
Impact and evidence limits
The strongest supportable result is finding-specific: the report demonstrates that the reviewed ERC-20 recurring-payment path could revert despite sufficient contract balance, recommends the custody-consistent transfer primitive, and records the issue as fixed.[1]
The report also records two High, three Medium, and five Low findings, all with team responses saying they were fixed. This narrative does not turn that inventory into a claim that the entire wallet was secure.[1]
No public source establishes that the affected contracts were deployed, that users encountered the failure, that tokens became permanently inaccessible, or that any monetary amount was exposed or preserved. The public evidence also does not identify which co-auditor authored H-02. Those claims are deliberately excluded.
Evidence and attribution
- Partner-authored canonical record: the March 1, 2024 report names Shieldify Security, defines the scope and commits, documents H-02, includes its proof of concept and recommendation, and records the team response.[1]
- Client-controlled public context: Bastion's GitHub organization describes the open-source wallet SDK and smart-contract system, but does not expose the historical reviewed subscription code in its currently listed repositories.[2]
- Zealynx-authored engagement record: the public audit record identifies Shieldify Security as lead auditor and Zealynx Security as co-auditor.[3]
- Attribution class: partner-delivered. Shieldify Security led the engagement and owned the client relationship; Zealynx contributed as co-auditor.
- Evidence cutoff: March 1, 2024 report and the public repositories inspected for this narrative.
- Publication blocker: no public artifact establishes approval from an authorized Shieldify Security or Bastion Wallet contact for Zealynx to publish this derivative case study. It therefore remains an unpublished preview pending written approval.
Reviewing an automated payment or account-abstraction flow?
Map custody, caller authority, allowance direction, and final balance deltas across one complete payment lifecycle. Explore the smart contract audit service or request a payment-flow review.
Frequently asked questions
Why did the ERC-20 subscription payment revert despite enough balance?
The holding contract used transferFrom to move its own tokens. The report's test reached ERC20: insufficient allowance because the chosen primitive introduced an allowance requirement that did not match the custody model.[1]
What was the recorded fix?
The report recommended replacing the self-sourced transferFrom call with a direct transfer to the initiator. The team response records the High-severity finding as acknowledged and fixed.[1]
Was this a direct Zealynx engagement?
No. Shieldify Security led the review and owned the client relationship. Zealynx Security contributed as co-auditor.[3]
Does the report prove the fix was deployed?
No. It identifies a fix-review commit and records the issue as fixed, but the currently public Bastion repositories do not expose that historical code or prove a production deployment.[1][2]
How much value did the issue affect?
The public evidence provides no dated, scoped asset value for the reviewed payment path. This narrative makes no TVL, value-protected, money-saved, or loss-avoided claim.
Sources
[1] https://raw.githubusercontent.com/ZealynxSecurity/audits/main/web3/2024-03-zealynx-shieldify-bastion-wallet.pdf — Bastion Wallet Security Review [2] https://github.com/bastion-wallet — Bastion Wallet GitHub organization [3] https://zealynx.io/audits/bastion-wallet/account-abstraction-q1-2024 — Zealynx Bastion Wallet engagement record