Early rate$2,400 of senior audit time for $500. Early members keep the rate as it climbs.$2,400 of senior audit time for $500See how
All case studies

Partner-delivered engagement

Bastion Wallet contracted Shieldify Security. Zealynx contributed as Zealynx Security contributed as co-auditor; Shieldify Security led the review and owned the client relationship. on that team. Bastion Wallet is not a Zealynx client.

Bastion Wallet · Account Abstraction · Mar 2024

Bastion: Testing ERC-20 Subscription Payments

A recurring-payment path held the tokens yet used transferFrom on itself. The review traced custody, authority, execution, and the recorded fix.

Invariant under test

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 SubExecutor holds 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]

  1. A subscription identifies an ERC-20, payment amount, initiator, and validity window.
  2. ERC-20 tokens are held by SubExecutor.
  3. The initiator calls processPayment() after the payment interval.
  4. SubExecutor verifies that its token balance covers the amount.
  5. _processERC20Payment() calls transferFrom() with SubExecutor as the source.
  6. The token enforces an allowance that the direct-owner transfer should not require.
  7. 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 reviewReview interventionRecorded after-state
SubExecutor held the ERC-20 balance but attempted to spend its own tokens through transferFromTraced custody and caller authority through the full payment pathReport recommends a direct transfer from the holding contract
The balance guard could pass while the transfer reverted for insufficient allowanceBuilt a fuzz test across valid balances and payment amountsTeam response records H-02 as acknowledged and fixed
A locally plausible transfer call made an otherwise valid subscription unpayableReframed correctness as custody-authority-execution alignmentFix-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:

RoleQuestion to assert
token ownerWhich address holds the balance immediately before settlement?
payment triggerWhich address may call processPayment()?
token spenderDoes the selected ERC-20 function require an allowance, and from whom?
recipientDoes 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 authorityExpected operationRequired proof
Payment contract owns the tokensdirect send from the contractrecipient balance rises and contract balance falls by the same amount
User owns tokens; payment contract is approved spenderdelegated pull under allowanceallowance owner, spender, amount, expiry/revocation, and replay behavior are explicit
Smart account owns tokens and executes a batchwallet-authorized call from the accountaccount authorization and the complete batched state transition succeed together
Third-party vault or paymaster owns tokensprotocol-specific withdrawal or settlementexternal 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:

  1. Custody: identify the token owner at every payment state, including prefunding, execution, cancellation, and refund.
  2. Trigger authority: separate permission to initiate payment from authority to spend the held asset.
  3. Transfer semantics: use transfer for an owner's direct send and transferFrom only for a documented allowance relationship.
  4. Allowance direction: when delegated spending is intended, assert the exact owner, spender, amount, and approval lifecycle.
  5. Token behavior: handle false-returning and non-standard ERC-20s deliberately rather than assuming every call reverts on failure.
  6. Temporal boundaries: test first payment, same-batch registration and payment, interval boundaries, expiry, and cancellation.
  7. Balance deltas: assert sender and recipient changes instead of treating a non-reverting call as proof of settlement.
  8. 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