In October of this year, we revealed an incident of a Vault owner cheating and illegally acquiring rewards meant for Delegators, and the details have been disclosed here.
To avoid innocent losses in the community, we will provide full subsidy to all victims through the mining reward addresses. The Delegators who incurred losses in the event can claim subsidy on this page now.
Please convey this message to each other in the community, and visit this page to claim your own subsidy.
For Multisig addresses owners, please check the your subsidy number in the list first, and execute the claim transaction via Polkadot.js. Here is the tutorial that how to claim it via Polkadot.js.
And for all the cheating Vault owners, please return the funds to this address â436H4jat7TobTbNYLdSJ3cmNy9K4frmE4Yuc4R2nNnaf56DLâ, or return to your delegators directly.
We, our team and all the delegators who incurred losses, reserve the right to legally address this incident and may take action at any time.
How many $PHAs will be paid
This depends on the impact you suffered in this issue.
You can find the subsidy list here.
The Sheet âCompensation listâ includes the Compensation list and the list of cheating vault owners. For delegators, you can check how much funds can be paid back (from each pools and a total number). And for Vault owners, you will know how much funds you should return.
How is it calculated?
As mentioned in the incident report, the Vault owner acquired delegatorsâ staking rewards by maliciously adjusting commissions. Therefore, we traced back all on-chain records of the Vault ownerâs reward claiming (specific code can be found in Note-1), manually filtered out all malicious events, and calculated the losses of delegators in all malicious events (calculation code can be found in Note-2).
This resulted in the final compensation list above.
Three kinds of addresses will not be compensated
We donât mind compensating the delegators at the same time as the vault owner return the funds to delegators, althrough this would result in double compensation. Just hope that the delegators wonât suffer any loss.
However, we donât want cheaters to reap more benefits, such as staking to their own pool with other accounts, then collecting part of the rewards as a pool owner, and receiving a sum of money from our compensation.
So, this three kinds of addresses will not be compensated:
- Vault owner itself
- The addresses which received owner rewards from the cheating Vault
- Addresses which has close financial turnover connections with above 2 kinds of addresses
Address list of above 3 kinds, are listed in the sheet âCheating address/Elimination listâ. You can check here to track it.
Timeline review of the issue
- 6th Oct to 7th Oct, the issue occurred, cheating vault owners claimed the rewards.
- 7th Oct to 8th Oct, the community started sharing feedback with the team.
- Night of 8th Oct, the influence was confirmed, and work started on a solution to prevent the risk.
- 9th Oct, the motion for an on-chain upgrade was proposed.
- 10th Oct, the on-chain upgrade was completed, the issue was mitigated.
- 11th Oct, the situation was announced to the community.
- 16th Oct, a list of vault ownersâ claims was prepared.
- 23th Oct, all malicious events were manually filtered out.
- 24th Oct, a list of losses for each address was created.
- 7th Nov, the âElimination listâ was tracked and filtered.
- 8th Nov to 22th Nov, the compensation list was internally double-checked with some community members.
- 23th Nov, the compensation list was published.
- 27th Nov, the claims processing function was launched.
- 7th Dec, the claims processing page was launched.
Thank you again for the help from the community
As I wrote following the post in the forum:
A belated thank you to the Tucuman team who raised doubts about the Vault reward logic in February. Even though we decided to stick to the original logic of the Vault after multiple discussions at that time, itâs clear that our decision was incorrect.
To rectify this mistake, we categorize this incident as a âHigh-Levelâ issue internally, and I will apply for a belated Bug Bounty for the Tucuman team, valued at 4500 USD.
At the same time, we also need to thank community member âHigh/Stakeâ for sending the vulnerability report on October 1st, and community member âSugeâ for continuous assistance and research after the incident. These efforts have been instrumental in our understanding of the incident. I will submit the proposal to distribute additional bounties to both, each valued at 1800 USD.
The healthy development of the community is inseparable from the hard work of developers and the collaboration of the community members. We appreciate those who pointed out our shortcomings and offered help, and look forward to more community contributors stepping forward to contribute to the growth of the Phala Network.
Note-1 SQL for tracing back all on-chain records of the Vault ownerâs reward claiming.
select
event.id,
event.name,
block.height as block,
event.index_in_block,
block.timestamp,
block.hash,
(event.args ->> 'pid')::integer as pid,
event.args ->> 'shares' as shares,
event.args ->> 'checkoutPrice' as checkout_price
from
public.event
join public.block on event.block_id = public.block.id
where
event.name = 'PhalaVault.OwnerSharesGained'
and (event.args ->> 'pid')::integer = any ($1)
order by
block.height asc;
Note-2 Code for calculating the losses of delegators in all malicious events
import {Parser} from '@json2csv/plainjs'
import csv from 'csvtojson'
import Decimal from 'decimal.js'
import _ from 'lodash'
import {createApi, stakeInfoToShares} from '../phala/lib'
import {log} from '../utils'
Decimal.set({toExpPos: 99, toExpNeg: -99})
const api = await createApi('khala')
const BASE_POOL = '42qnPyfw3sbWMGGtTPPc2YFNZRKPGXswRszyQQjGs2FDxdim'
const json = await csv().fromFile(
'./src/commissionCheat/out/commission_events_mark.csv',
)
const cheatPools = [
3473, 3481, 3601, 3612, 3617, 3618, 3714, 3730, 3749, 3752, 3796, 3924, 4034,
4343, 4686, 4720, 4850, 4852, 4880, 4927, 4951,
]
const vaultGainAddress = new Map([
[3473, '45BsgW5wSLE38P2AeogXRp67wge9pvP1sdm2ZgSWkLEPTBgX'],
[3481, '45t484rzjQzsDTc6RhbrpkZb3SFvfPZicicjDBH2u4TEcmBs'],
[3601, '46HgLRQTJ6oGDfiF213BpqRGCaW22zQ564fjDLmcnw8j3Pba'],
[3612, '41ffwYTKytjq6CvuGZH6956owzm4hQcmi9hx7hoeAAmWFdV4'],
[3617, '43Wu9TQZ3Dh3WUjtu2uMRtRDMv4ueJzY9p1uu2goL2scUd4h'],
[3618, '43Wu9TQZ3Dh3WUjtu2uMRtRDMv4ueJzY9p1uu2goL2scUd4h'],
[3714, '41dFyFd21YnzkjFAWJoMrnShDjfWr46fMxAG9ms8UDtM6JCs'],
[3730, '41KL4ipqhHovt2U7cXtXQYU2iRnVmuQrotAn2tWzTbYtoxp9'],
[3749, '42CcPMQbyUsRShqDcEF1ioC8itJQjRcdMZ4QaK9eZgDsuNS2'],
[3752, '41AVnddVZ9zrmvL5XGyAQjoXA1B4RjwQPNMrwgoMH8cHM9ff'],
[3796, '42qr283P7XXnNrYCHxZtaVvjx25dfbeyEstwuzFMaQiZ5N8i'],
[3924, '41KL4ipqhHovt2U7cXtXQYU2iRnVmuQrotAn2tWzTbYtoxp9'],
[4034, '45etpeyNYjT6W9B4i15kUzPteUvVLgFRU5zgbGJNgKmtYsnk'],
[4343, '41KL4ipqhHovt2U7cXtXQYU2iRnVmuQrotAn2tWzTbYtoxp9'],
[4686, '42htLs2GGqUavbp4swXUVPnEZvHSRZJVzDvmCRSdFj7Q7tpa'],
[4720, '3zxRSK5DquqD1f53e8CXTjqMb9wVBJxPDqb534w77147b5Cz'],
[4850, '3zz9XwgFmBUh86tEBdWXgoiPmFARSvoDjSgCPkL9r36xAbQ7'],
[4852, '44qTASZSLp9TxEMazC9GpizhQXvWq17WjMhWfxYyFFkG42jV'],
[4880, '3zz9XwgFmBUh86tEBdWXgoiPmFARSvoDjSgCPkL9r36xAbQ7'],
[4927, '45bFuYvSPzT9KBWmkrfkELLH8RYsk5aoPWVpUCjHgU8MrUQh'],
[4951, '43Wu9TQZ3Dh3WUjtu2uMRtRDMv4ueJzY9p1uu2goL2scUd4h'],
])
const parser = new Parser()
const out = await Promise.all(
cheatPools.map(async (pid) => {
const gainAddress = vaultGainAddress.get(pid)
const events = json.filter(
(x) =>
x.pid === pid.toString() && (x.is_cheat === '1'),
)
const cid = await api.query.phalaBasePool
.pools(pid)
.then((x) => x.unwrap().asVault.basepool.cid.toNumber())
log(pid, cid)
const res = []
for (const event of events) {
if (event.name !== 'PhalaVault.OwnerSharesGained') continue
const cheatShares = event.shares
const {
block: {
header: {parentHash},
},
} = await api.rpc.chain.getBlock(event.hash)
const apiAt = await api.at(parentHash)
const totalShares = await apiAt.query.phalaBasePool
.pools(pid)
.then(
(r) => new Decimal(r.unwrap().asVault.basepool.totalShares.toHex()),
)
const withdrawQueue = await apiAt.query.phalaBasePool
.pools(pid)
.then((r) => r.unwrap().asVault.basepool.withdrawQueue)
const nfts = await apiAt.query.rmrkCore.nfts.entries(cid).then((r) =>
r.map((x) => {
const owner = x[1].unwrap().owner.asAccountId.toString()
const nftId = x[0].args[1].toNumber()
let user: string | undefined = owner
if (owner === BASE_POOL) {
const withdrawal = withdrawQueue.find(
(x) => x.nftId.toNumber() === nftId,
)
if (withdrawal != null) {
user = withdrawal.user.toString()
}
}
return {
...event,
totalShares: totalShares.div(1e12).toString(),
cid,
nftId,
owner,
user,
isVaultOwner: user === gainAddress,
}
}),
)
const nftsInfo = await apiAt.query.rmrkCore.properties
.multi(nfts.map((x) => [x.cid, x.nftId, 'stake-info']))
.then((r) =>
r.map((x) => {
let shares = new Decimal(0)
let error = false
try {
shares = new Decimal(stakeInfoToShares(x.unwrap()).toString())
} catch (err) {
error = true
}
const nftShares = shares.div(1e12).toString()
const nftCheatShares = shares.div(totalShares).times(cheatShares)
return {
nftShares,
nftCheatShares: nftCheatShares.toString(),
nftCheatValue: nftCheatShares
.times(event.checkout_price)
.toDP(12, 0)
.toString(),
error,
}
}),
)
_.merge(nfts, nftsInfo)
res.push(...nfts.filter((x) => x.nftShares !== '0'))
}
return res
}),
)
await Bun.write(`./src/commissionCheat/out/nfts.csv`, parser.parse(out.flat()))