Background
The Larkspur Wallet SDK connects Cosmos dApps to wallets. A dApp builds a sign request with the SDK; the pairing relay forwards it to the wallet, which shows a summary and returns a signature over a SignDoc.
Cosmos SDK chains include the authz module. A MsgGrant with GenericAuthorization for /cosmos.bank.v1beta1.MsgSend lets the grantee send the granter’s tokens with MsgExec until the grant expires or is revoked — and a grant without an expiry never expires.
Architecture
The Signing SDK builds sign requests and their human-readable summaries; the Pairing relay pairs a dApp session with a wallet and forwards encrypted messages; the Chain registry supplies chain metadata and, after this incident, flagged addresses.
Figure 1Larkspur Wallet SDK: components and paths involved in this case
Elements:
- Look-alike dApp (adversary): cloned integrator metadata
- Signing SDK (protocol component, involved in this case): Builds sign requests (Amino and Direct) and verifies responses
- Pairing relay (protocol component, involved in this case): Session pairing and encrypted message forwarding
- Wallet approval screen (external party): shows SDK summary
- Chain registry (protocol component): Serves chain IDs, RPC endpoints and fee denominations
- Cosmos chain (authz) (external party): MsgGrant / MsgExec
Connections:
- Look-alike dApp to Signing SDK: sign request (attack path)
- Look-alike dApp to Pairing relay: session_propose (attack path)
- Pairing relay to Wallet approval screen: encrypted request (data or call flow)
- Signing SDK to Wallet approval screen: summary (data or call flow)
- Wallet approval screen to Cosmos chain (authz): broadcast MsgGrant (data or call flow)
- Chain registry to Wallet approval screen: flagged addresses (control path)
Components in the Larkspur Wallet SDK profile
Signing SDK(involved in this case)
Builds sign requests (Amino and Direct) and verifies responses
Pairing relay(involved in this case)
Session pairing and encrypted message forwarding
Chain registry
Serves chain IDs, RPC endpoints and fee denominations
Staging cloud
Build pipeline and relay hosting
Security challenge
On the morning of 18 May 2026 the authorisation-concentration rule fired: 37 accounts had granted unlimited MsgSend authority, with no expiry, to the same unknown grantee within an hour. Twenty minutes later the grantee began executing transfers under those grants.
Users reported that the wallet had asked them to approve a session for a dApp they recognised. The response had to stop new grants, protect accounts that had already granted, and explain how an open-ended grant could be approved under that description.
Attack surface
The investigation compared what users saw with what they signed, at each step of the signing path.
| Surface | Exposure | Control |
|---|---|---|
| Approval summary | Built from caller-supplied display hints, not from the SignDoc (LW-01) | Summary decoded from the bytes being signed |
| Pairing origin | Pairing not bound to the requesting origin; short identifiers (LW-02) | Origin-bound, higher-entropy pairing identifiers |
| Authz grants | No special handling; grants without expiry accepted | Decoded grant review; expiry required (SDK 2.4) |
| Known grantees | No way to warn about a flagged address | Registry flag with warn-and-block policy |
| Standing grants | Users unaware of grants they had made | Advisory and revocation guidance |
Technical investigation
Every signed transaction in the cluster contained a single MsgGrant with GenericAuthorization for MsgSend and no expiration. None contained a transfer. The sessions came from a look-alike domain presenting the name and icon of a legitimate integrator, which the relay could not distinguish from the real one.
The approval screen showed the message type URL, /cosmos.authz.v1beta1.MsgGrant, under a description supplied by the page: “Approve session”. Nothing on the screen said what the grant allowed or that it never expired, and to most users the type URL meant nothing.
Containment came from the registry: a flagged-address update made SDK clients block grants to the grantee two hours after the first alert, and SDK 2.4 shipped the next day with decoded grant review and a mandatory expiry.
The penetration test commissioned afterwards found the general cause behind the page-supplied description. The SDK accepted display hints alongside the transaction and, when present, used them as the summary, while the SignDoc was serialised separately; nothing compared the two. A page could show a one-unit transfer while the user signed a grant (LW-01). The relay’s missing origin binding was reported as LW-02.
Figure 2What the user saw versus what they signed
- Look-alike dApp (adversary) to Wallet (SDK ≤ 2.3): tx: MsgGrant · description: “Approve session” (attack step)
- Wallet (SDK ≤ 2.3) (on-chain) to User: type URL under “Approve session”. scope and expiry not shown (attack step)
- User (participant) to Wallet (SDK ≤ 2.3): approve (normal step)
- Wallet (SDK ≤ 2.3) (on-chain) to Chain: MsgGrant(GenericAuthorization: MsgSend, no expiry) (normal step)
- Grantee (adversary) to Chain: MsgExec[MsgSend(balance)] (attack step)
- Chain (on-chain) to Grantee: R-12 / R-18 alerts (defensive step)
Monitoring record
Open in Security Monitoring| Time (UTC) | Rule | Observation | Measured | Alert |
|---|---|---|---|---|
| 2026-05-18 08:20:14 | R-12Authorisation grant concentration | 37 send authorisations granted to one grantee in one hourHigh | 37 (threshold 25 grants to one grantee / 1 h) | Alert |
| 2026-05-18 08:41:56 | R-18Delegated execution burst | Grantee executed 22 transfers under authz in 40 minutesHigh | 22 (threshold 10 executions / 1 h) | Alert |
| 2026-05-18 09:05:30 | R-19Session origin mismatch | Pairing requests with cloned dApp metadataHigh | Condition matched | Alert |
| 2026-05-18 10:30:00 | R-09Protocol parameter change | Registry flags grantee; SDK warns on authz grantsMedium | Recorded for the timeline | Logged |
| 2026-05-19 14:00:00 | R-09Protocol parameter change | SDK 2.4 released with decoded grant reviewLow | Recorded for the timeline | Logged |
| 2026-05-20 09:00:00 | R-16Activity above baseline | Grant revocations at 2.1× the usual rateLow | 2.1 (threshold 3 × 30-day median) | Logged |
Timeline
Detection
37 grants to one grantee in an hour; R-12 alert
Exploitation
Grantee executes 22 transfers under authz
Pairing requests with cloned dApp metadata identified
Containment
Registry flags the grantee; SDK clients block grants to it
Fix
SDK 2.4 released with decoded grant review
Grant revocations at 2.1× the usual rate
Penetration test of SDK and relay begins
Verification
LW-01 and LW-02 fixes re-tested; report published
Root cause
The approval screen showed a description of the transaction supplied by the party requesting the signature, not a decoding of the transaction itself. Grants were not translated into what they allowed, and the SDK let requester-supplied hints stand in for the summary. Any signing flow that renders requester-supplied metadata instead of decoding the signed bytes can be made to display one action and sign another.
- Class
- Signature phishing
- Severity
- High
- Components involved
- Signing SDKPairing relay
export function buildSignRequest(tx: TxInput, display?: DisplayHints): SignRequest { const signDoc = serializeSignDoc(tx) const summary = display ? summaryFromHints(display) : summaryFromTx(tx) // hints win return { signDoc, summary }}Risk evaluation
In the before state the SDK’s two high-severity and two medium-severity issues are open, and Operational Security is reduced until grant review and address flagging existed.
The score moves from 73.2 (BB) to 81.4 (A), a change of +8.2 points. The largest single contribution is Code Security: restoring it from 58.0 to 80.0 alone moves the score from 73.2 to 79.8.
| Component | Weight | Range | During case | Today | Score points |
|---|---|---|---|---|---|
| Code Security | 30% | 58.0 | 80.0 | +6.6 | |
| Operational Security | 20% | 70.0 | 78.0 | +1.6 | |
| Governance Risk | 15% | 85.0 | 85.0 | 0.0 | |
| Economic Security | 15% | 88.0 | 88.0 | 0.0 | |
| Infrastructure Resilience | 10% | 74.0 | 74.0 | 0.0 | |
| Transparency | 10% | 84.0 | 84.0 | 0.0 | |
| Quantix Risk Index | 100% | 73.2 | 81.4 | +8.2 |
Scores use the six weighted components of the Quantix Risk Index. Open findings reduce Code Security by a fixed penalty per severity; the current profile is the protocol’s entry in Quantix Intelligence.
Mitigation strategy
The response ran in two stages. Within hours the registry flagged the grantee, and SDK 2.4 decoded every MsgGrant into plain language and refused grants without an expiry. After the assessment, display hints were removed from the public API entirely: the summary is always decoded from the exact SignDoc bytes, and unknown message types are shown as unrecognised with their type URL.
The relay now binds pairing to the requesting origin with higher-entropy identifiers (LW-02).
- 01Flag the grantee in the registry with a warn-and-block policy
- 02Decode authz grants and require an expiry (SDK 2.4)
- 03Publish revocation guidance for standing grants
- 04Remove display hints; decode every summary from the SignDoc
- 05Bind pairing to the requesting origin
import { MsgGrant } from 'cosmjs-types/cosmos/authz/v1beta1/tx' export function buildSignRequest(tx: TxInput): SignRequest { const signDoc = serializeSignDoc(tx) const decoded = decodeSignDoc(signDoc.bytes) // the bytes that will be signed return { signDoc, summary: summaryFromSignDoc(decoded), warnings: decoded.messages.flatMap(grantWarnings) }} function grantWarnings(msg: Any): Warning[] { if (msg.typeUrl !== '/cosmos.authz.v1beta1.MsgGrant') return [] const { grantee, grant } = MsgGrant.decode(msg.value) if (!grant?.expiration) throw new SignRequestError('Authorisation grants must have an expiry') return [{ level: 'high', text: 'Allows ' + grantee + ' to act for this account until ' + formatExpiry(grant.expiration) }]}Security outcome
New grants to the grantee were blocked from the morning of the incident, and revocations ran at twice the usual rate after the advisory. SDK 2.4 closed the grant-specific path the next day.
The penetration test found and closed the general cause: LW-01 and LW-02 were fixed and re-tested before the report was published.
- Detection to containment
- 2 h 10 min
- Detection to first fix
- 29 h 40 min
- Detection to verified fix
- 114 days
- Risk Index change
- BB → A (+8.2)
Lessons learned
- 01
Show users what they sign, decoded from the signed bytes, never a description from the requester.
- 02
Delegation primitives such as authz grants deserve their own review screen and an expiry.
- 03
Many grants to one grantee in an hour is a strong, cheap phishing signal.
- 04
An incident fix that closes one path is a reason to look for the general cause.

