This topic is to discuss the proposal submitted by @utkarshdagoat . Please see below for the details of the proposal and discussion.
17th Dec ,2024
Current status: Under Consideration.
Opened for community discussion on : 17th Dec, 2024.
This topic is to discuss the proposal submitted by @utkarshdagoat . Please see below for the details of the proposal and discussion.
17th Dec ,2024
Current status: Under Consideration.
Opened for community discussion on : 17th Dec, 2024.
Proof of solvency is a tool for centralized exchanges to gain user trust and become transparent about their assets in equity and debt. It ensures that their equity >= debt at any point of time.
In design for proof of solvency we create a world state tree for the user’s account balances(debt and equity) and cex balances. The main objective is to ensure correctness and validitation for the total equity >= total debt constraint while building this world tree through zksnarks
It was developed by Privacy + Scaling Explorations you can read more about it here
The example below is for the “updated” proof of solvency protocol. This version of the protocol mitigates the “Dummy User Attack” vulnerability which was in the initial version of the protocol.
Let’s say there are 2 assets MINA,USDC in our CEX and 4 Users operate in out cex U1,U2,U3,U4
. The users interact with our CEX and do the following actions
NOTE: In the example below we will take USDC as base asset for other tokens 1 MINA = 100 USDC and 1 BTC = 10000 USDC.
U1
Deposites 100 MINA and 10,000 USDC to our CEX. then uses the 100 MINA as LOAN Collateral for 10000 USDC.U2
deposits 20 MINA.Swap 10 MINA for 1,000 USDCU3
deposits 10,000 USDC. Uses the 5,000 USDC as COLLATERAL Margin for 50 MINA.U4
deposits 200 MINA. Uses 20 MINA as Loan Collateral for 2000 USDC. Swap 1000 USDC for 10 MINA from U2
.NOTE The various types of loan collateral(LOAN, COLLATERAL MARGIN, and COLLATERAL PORTFOLIO MARGIN) are to cater to binance’s loan buisness logic. These loan collateral types may differ from exchange to exchange. I have included them in this implmentation for a fair benchmark angaist other implementaion as not taking them into consideration will lower the parameters.
The user balance sheet for each token:
MINA Balance Sheet
User | Equity | Debt | COLLATERAL LOAN | COLLATERAL MARGIN | COLLATERAL PORTFOLIO MARGIN |
---|---|---|---|---|---|
U1 | 100 | 0 | 100 | 0 | 0 |
U2 | 10 | 0 | 0 | 0 | 0 |
U3 | 50 | 50 | 0 | 0 | 0 |
U4 | 210 | 0 | 0 | 20 | 0 |
Total | 370 | 50 | 100 | 20 |
USDC Balance Sheet
User | Equity | Debt | COLLATERAL LOAN | COLLATERAL MARGIN | COLLATERAL PORTFOLIO MARGIN |
---|---|---|---|---|---|
U1 | 20,000 | 10,000 | 0 | 0 | 0 |
U2 | 1,000 | 0 | 0 | 0 | 0 |
U3 | 10,000 | 0 | 0 | 5,000 | 0 |
U4 | 1,000 | 2,000 | 0 | 0 | 0 |
Total | 32,000 | 12,000 | 0 | 5,000 | 0 |
So for CEX to be solvent it needs to hold net for each asset i.e Total Equity - Total Debt
. As it has 320 MINA and 20,000 USDC the platform is solvent for all user
The dummy user attack goes as follows:
A CEX has many assets under control including high market cap and low market cap coins. Let MINA be a high Market cap coin and XYZ be a low market cap. Consider the following user actions:
Thus the platform is insolvent as:
Tier Ratios
To mitigate the Dummy User attack we introduce tier ratio’s which define an array of a collateral haircut which is a reduction in the usable value for collateral asset. For example tier ratios for MINA might look like:
[0-1000 USDC: 100%, 1000-2000 USDC: 90%, 2000-5000 USDC: 80%, ...]
This means that:
If the USDC value for MINA to be used as collateral is within the first range (0-1000), 100% of its value can be used as collateral.
If it’s between 1000-2000 USDC, only 90% of its value can be used as collateral.
If it’s between 2000-5000 USDC, only 80% can be used.
Thus for low market cap we can adjust the tier ratio’s for those asset in such a way that we can prevent the dummy user attack.
Main Structures
A UserAction
struct is made for every interactions that user does with the exchange. These user actions alter the state of the CexAssetTree
and the AccountTree
.With the constraints in the circuit we ensure the correctness in building these Merkle Tree’s.
Below are some basic structures along with the UserAction
Struct.
class TierRatio extends Struct({
BoundryValue: UInt64,
Ratio: UInt64
})
class CexAssets extends Struct({
totalEquity: UInt64,
totalDebt: UInt64,
Baserice: UInt64,
// All loan Variables
LoanCollateral: UInt64,
...
LoanTierRations: Provable.Array(TierRatio,10) // There will always be 10 tier ratios for each percentage [100-90%,90-80%..]
})
class Asset extends Struct({
Equit: UInt64,
Debt: UInt64,
BasePrice: UInt64,
// All other loan variables
LoanCollateral: UInt64,
...
})
class UserAction extends Struct({
beforeAccountRoot: Field, // Account Tree Root before the user actions
afterAccountRoot: Field, // changed root after the user action
AccountIndex: UInt64, // the account index in db where the user is stored
AssetForUpdate: Asset // This will most probably be an array I will try with various lenghts and try to find the best optimized result
}){}
Commitment for Assets
The Asset Map is used to calculate the commitment for asset’s.
key
is the hash(assetIndex
) where assetIndex
represent the index that it is stored by in the database.value
is hash(TotalEquity,TotalDebt,BasePrice....commitment(loan_variables)
) and the commitment
is the root of the AssetMap
Commitment for User Accounts
An Indexed merkle map is used to represent the account tree. In this map
Key
- hash(accountId
) where the accountId
uniquely represents a User
Value
- hash(TotalEquity,TotalDebt,assetCommitment
) where TotalEquity
and TotalDebt
is in the base price. assetCommitment
is calculated simply in this case as it is infeasable to store an Asset Map for each user’s asset.In our circuit design the main goal is to verify the correctness in buidling of the world state tree i.e
CexAsset
Tree/MapUserAssets
Tree/MapWe will run our circuits for a batch of operations. Thus, Let there be k
batches for building the current world state. So the following circuits are for the k+1
batch.
CexAsset Circuit
Let Proof_k
be the proof of the last batch. Then the following are the inputs to the circuit
Proof_k
IndexedMerkleMap
of the CexAssets
userActions[]
array of useractions done in the batchCexAsset
Map.Proof_k
.publicOutput
= CurrentMap.root
userActions[]
update to the tree ensuring that for each token TotalEquity > TotalDebt
and the calculation is done in accourdance to the collateral haircuts.User Asset
For User Asset things get a little complicated as the size of IndexMerkleMap
for user Tree can get large while for the CexAsset a Centralized Exchange such as binance only supports 500 Assets So for this the depth of 9-10 is fine and we can take directly as input to the circuit. But for user’s let’s say binance
have 200million which a approx. of depth 28
which may increase proof generation time if we take it direct input. So the Approaches differ in the way we construct circuit inputs and do DB management while the constraint remains the same.
Approach 1: Direct Map Input
This is pretty much same as the last circuit.Let Proof_k
be the proof of the last batch. Then the following are the inputs to the circuit
Proof_k
IndexedMerkleMap
of the Users
userActions[]
array of useractions done in the batchCexAsset
Map.Proof_k
.publicOutput
= CurrentMap.root
user
in userActions[]
:
userActions[i].beforeAccountRoot = userActions[i-1].afterAccountRoot
Approach 2: Whole lot of MerkleWitness
In Approach 2 we store the whole Users
Map but to the Circuit we provide witness
to prove that a user is solvent. So for a Batch Let’s say:
userActions[]
These are userActions taken by the userMerkleMapWitness
it is the witness after theUser's
MerkleMap is updated with the userActions[]
.userAssets[]
is TotalAssets for the ith
user in the userActions[]
. The Asset
can also be a IndexedMerkleMap
but storing IndexedMerkleMap
for each user may be storage inffecient.LastProof
is the proof from the lastBatch for this userhash(userId,comm(userAssets))
and use the witness api to see if it matched userActions[BATCH_SIZE].root
.userActions[]
:
userActions[i].beforeRoot = userActions[i].root
Total Equity > Total Debt
for each Asset at every i.NOTE This circuit will be recursive as we would not store a proof for every evaluation of this circuit. It is better to have a aggregated proof for each user which verifies that each action that the user took does not create insolvency.
We can also aggregate each User’s proof and the Cex proof to create one big proof of solvency.
This method enables high parallelization at the trade off of storage and precomputation that we store and create the MekleMapWitness
for every.
Another Note. By parralellization I mean that we compute the circuit in a forked process on the cpu as typescript is not multi-threaded this can be a hack
Approach3: Sharding of the UserMap
This is basically same as Approach 1 but the way we store User’s Map is different.
We shard the MerkleMap say on userId
i.e create 2 or more different IndexedMerkleMap
this can decrease proving times as we have decrease the size of the map input.
This method will include a lot of hit and trials to see which height
vs storage
is the best output.
BatchProofs
The user proof and cex proof can be aggregated into a single proof and
The input to the circuit is the following BatchCircuitInput
:
Class BatchCircuitInput extends Struct({
beforeCexAssetRoot: Field,
batchCommitment: Field,
afterCexAssetRoot: Field,
beforeUserRoot: Field,
afterUserRoot: Field,
userProof: Proof<>,
cexProof:Proof<>
}){}
There is also a public input to the program BatchCommitment
which is defined as hash(beforeAccountRoot,AfterAccountRoot,beforeCexAssetRoot,AfterCexAssetRoot)
Following are the constraints for BatchCircuitInput:
batchCommitment
is computed correctly.userProof.publicOutput.root
= afterCexAssetRoot
cexProof.publicOutput.root
= afterUserRoot
Verification
The Users and auditors can verify the computation is done correctly when all the Maps and proofs are published. They can do the following steps:
batch_commitment_n = hash(cexAssetRoots[n-1], cexAssetRoots[n], userAccountRoots[n-1], userAccontRoots[n])
.
CexAssetRoots[-1]
and userAccounRoots[n-1]
is the empty root;verify(batch_commitment_0, proof_0, verifying key)
output;AssetTreeRoot
equals to the commitment of published AssetList
by CEXA user can do the following fetch the account info and the userProof
from the cex and do:
userProof.root = root
The implementation for the proof system can be done in two ways. First is to use a proof generating backend with storing the maps in the DBs. Another way is to store the commitments of users on the protokit runtime itself and change the circuits accordingly.
The following is the proof generation system/backend in place:
userBalanceSheet
/ the db entries and prepare inputs to the above mentioned circuit. I would like to test different low level languages(rust
,golang
,c
) for this as to which gives the fastest result write the results to files and have them read in the circuit’s process.For the protokit implementation we can store the each user’s asset commitments (which are hash(TotalEquity,TotalDebt,assetCommitment
)) and store the root commitments and everything in runtimes. So something along the lines:
NOTE We do not directly store the user assets on the runtime as they are private in a cex backend and cannot be exposed to the public due to privacy concerns.
export class UserRuntime extends RuntimeModule<UserRuntimeConfig> {
...
// here the key - hash(AccountId)
// the value is hash(TotalEquity,TotalDebt,assetCommitment)
@state public userCommitments: StateMap.from<Field,Field>(Field,Field)
// the account tree root commitment
@state public rootCommitment: State.from<Field>(Field)
...
}
The CEX Runtime can be as follows which stores the Cex Commitments. As Cex Assets have to be public to maintain transparency
export class CEXRuntime extends RuntimeModule<CEXRuntimeConfig> {
...
@state public assets: StateMap.from<AssetId,CEXAssets>(AssetId,CexAssets)
// here the key - hash(AccountId)
// the value is hash(TotalEquity,TotalDebt,..loan variables...)
@state public assetCommitment: StateMap.from<Field,Field>(Field,Field)
// the account Cex Asset tree root commitment
@state public rootCommitment: State.from<Field>(Field)
...
}
With above runtime and little tweaks to our approach we can have a full proof generation system in place with help of protokit.
NOTE There can be performance issues with this as in my knowledge we cannot really do batch updates on maps which would lead to multiple writes on the runtime as number of user to updates in a batch can be in thousands. I don’t know how efficient this is. So I will include a performace benchmark on both approaches
A UI dashboard is made for users to see the cexes and verify the accounts. On devnet a dummy CEX is created. In Which user can edit their token balances and collateral to play around with this stuff. User can login via credentials provided to them by the cex. I will create a mock set of credentials for people to tryout!. Here is the figma link and below are the images
IndexedMerkleMaps
input to the circuit.IndexedMerkleMaps
.IndexedMerkleMaps
which is an experimental feature in o1js.