You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The TokenAccountUpdateIterator iterates over an AccountUpdate forest using a pre-order depth-first traversal. It does this by maintaining a stack of Merkle lists, each representing the siblings of the current node’s ancestors which have yet to be processed. Since ZK Circuits are limited in their size at compile time, callers can only support a fixed number of iterations.
To help mitigate this problem, the TokenAccountUpdateIterator attempts to skip sub-trees which may not update the derived token id of the token contract. To do so, the iterator skips the children of the current accountUpdate if the accountUpdate is either
An AccountUpdate for the token contract itself. This tree will be verified independently during transaction validation.
An AccountUpdate whose tokenId cannot be the token’s derived token-id, nor can any of its children’s.
Snippet from next() which skips the child subtree if either case (1.) (isSelf) or case (2.) (canAccessThisToken.not()) is true.
// if we don't have to check the children, ignore the forest by jumping to its endletskipSubtree=canAccessThisToken.not().or(isSelf);childForest.jumpToStartIf(skipSubtree);
Identifying case (1.) is easy. Identifying case (2.) has two sub-cases:
Top-level: if accountUpdate.mayUseToken.parentsOwnToken is true, the account update’s token id may be set to the token contract’s derived token ID
Not top-level: Since (by (1.)) all AccountUpdates which could derive the token contract’s derived token ID are skipped, we know that the derived ID of the parent of this inner node is not equal to the token contract’s derived token ID. Therefore, the only way we can use the token contract’s derived token ID is if we inherit it from our parent. In particular, accountUpdate.mayUseToken.parentsOwnToken must be true.
To implement the check for case (2.), an invariant is maintained on the iteration “layers” stored in the stack. layer.mayUseToken is MayUseToken.ParentsOwnToken iff its layer is the top-level. Otherwise, layer.mayUseToken is MayUseToken.InheritFromParent. So, the developers can determine whether this account update’s children can access this token by comparing accountUpdate.mayUseToken.parentsOwnToken to the current layer’s mayUseToken field.
Computation of canAccessThisToken.
// check if this account update / it's children can use the tokenletcanAccessThisToken=Provable.equal(MayUseToken.type,update.body.mayUseToken,this.currentLayer.mayUseToken);
Unfortunately, as described in the o1js audit report, the mayUseToken has four representations:
This fourth case is accepted by the VM, and treated as equivalent to case 2.
In particular, this causes the iteration to skip any account update with both flags set to true. This can be seen in the below code snippet, in which iteration skips the entire bottom level of a tree of account updates.
Malicious AccountUpdate tree construction. This PoC can be used in the setup provided by
et updates: AccountUpdate[]=[]constlength=7;for(leti=0;i<length;++i){letupdate=AccountUpdate.defaultAccountUpdate(alexa.publicKey,token.deriveTokenId());update.label=`Update ${i}`;updates.push(update);}// forest[0]// Build a complete, 3-level tree// t0// t1 t2// t3 t4 t5 t6updates[3]!.body.callDepth=2;updates[4]!.body.callDepth=2;updates[5]!.body.callDepth=2;updates[6]!.body.callDepth=2;updates[1]!.body.callDepth=1;updates[2]!.body.callDepth=1;updates[0]!.body.callDepth=0;lettrees: AccountUpdateTree[]=[]for(leti=0;i<length;++i){letupdate=updates[i]!;update.body.mayUseToken=update.body.callDepth===0
? {parentsOwnToken: newBool(true),inheritFromParent: newBool(false)}
: {parentsOwnToken: newBool(true),inheritFromParent: newBool(true)};update.body.balanceChange=update.body.callDepth===2 ? newInt64(newUInt64(100n)) : Int64.zero;trees.push(AccountUpdateTree.from(update))}trees[1]!.children.push(trees[3]!)trees[1]!.children.push(trees[4]!)trees[2]!.children.push(trees[5]!)trees[2]!.children.push(trees[6]!)trees[0]!.children.push(trees[1]!)trees[0]!.children.push(trees[2]!)// top-level tree to forestletforest=AccountUpdateForest.empty();forest.push(trees[0]!);// Iterate over the forest so we can see what will be accessed!letiterator=TokenAccountUpdateIterator.create(forest,token.deriveTokenId());for(leti=0;i<7;++i){let{accountUpdate, usesThisToken}=iterator.next();Provable.log(i,usesThisToken,accountUpdate.toPretty())}console.log("Attack transaction.")constattackTx=awaitMina.transaction({sender: feepayer.publicKey,
fee,},async()=>{AccountUpdate.fundNewAccount(feepayer.publicKey,1);awaittoken.approveBase(forest);})console.log(attackTx.toPretty());awaitattackTx.prove()attackTx.sign([feepayer.privateKey,alexa.privateKey])constattackTxResult=awaitattackTx.send()console.log("Attack tx:",attackTxResult.hash)awaitattackTxResult.wait()awaitprintTxn(attackTx,"Attack tx",legend)awaitshowTxn(attackTx,"Attack tx",legend)
As a consequence, AccountUpdates which may not provable ignore token balances will be ignored by the iterator.
However, when a transaction is hashed by the network, it serialized and then de-serialized. During serialization, the mayUseToken field is validated to have at most one true flag, preventing this attack from successfully executing on the network.
Impact
Currently, it seems this can attack cannot be executed due to the serialization and de-serialization routines. However, changes to the codebase could enable this attack in the future if it is not carefully documented for developers.
Recommendation
Since the checks preventing this attack are far from the in-scope code and not familiar to the auditors, we recommend the Mina team take extra care to validate that there is no path by which a transaction can be submitted without being de-serialized.
At the very least, add documentation to the AccountUpdate struct and the relevant portion of the mina repository indicating the importance of checking that mayUseToken has at most one true boolean flag.
A more robust fix would ensure this situation cannot occur within any valid proof. To do this, consider updating the check() function of AccountUpdate to rule out the {true, true} case.
Developer Response
We rate this as more severe than a warning. We want correct code execution to be proven and be universally verifiable, and not rely on each node operator to check wellformedness on deserialization
The text was updated successfully, but these errors were encountered:
kantp
added
audit
Addresses findings from the audit
low
o1js
Issues that require changes in o1js itself, rather than in this repository
labels
Jul 2, 2024
The
TokenAccountUpdateIterator
iterates over anAccountUpdate
forest using a pre-order depth-first traversal. It does this by maintaining a stack of Merkle lists, each representing the siblings of the current node’s ancestors which have yet to be processed. Since ZK Circuits are limited in their size at compile time, callers can only support a fixed number of iterations.To help mitigate this problem, the
TokenAccountUpdateIterator
attempts to skip sub-trees which may not update the derived token id of the token contract. To do so, the iterator skips the children of the currentaccountUpdate
if theaccountUpdate
is eitherAccountUpdate
for the token contract itself. This tree will be verified independently during transaction validation.AccountUpdate
whosetokenId
cannot be the token’s derived token-id, nor can any of its children’s.Identifying case (1.) is easy. Identifying case (2.) has two sub-cases:
accountUpdate.mayUseToken.parentsOwnToken
is true, the account update’s token id may be set to the token contract’s derived token IDAccountUpdate
s which could derive the token contract’s derived token ID are skipped, we know that the derived ID of the parent of this inner node is not equal to the token contract’s derived token ID. Therefore, the only way we can use the token contract’s derived token ID is if we inherit it from our parent. In particular,accountUpdate.mayUseToken.parentsOwnToken
must be true.To implement the check for case (2.), an invariant is maintained on the iteration “
layer
s” stored in the stack.layer.mayUseToken
isMayUseToken.ParentsOwnToken
iff itslayer
is the top-level. Otherwise,layer.mayUseToken
isMayUseToken.InheritFromParent
. So, the developers can determine whether this account update’s children can access this token by comparingaccountUpdate.mayUseToken.parentsOwnToken
to the current layer’smayUseToken
field.Unfortunately, as described in the o1js audit report, the
mayUseToken
has four representations:{parentsOwnToken: false, inheritFromParent: false}
MayUseToken.InheritFromParent := {parentsOwnToken: false, inheritFromParent: true}
MayUseToken.ParentsOwnToken := {parentsOwnToken: true, inheritFromParent: false}
{parentsOwnToken: true, inheritFromParent: true}
This fourth case is accepted by the VM, and treated as equivalent to case 2.
In particular, this causes the iteration to skip any account update with both flags set to
true
. This can be seen in the below code snippet, in which iteration skips the entire bottom level of a tree of account updates.This outputs
As a consequence,
AccountUpdate
s which may not provable ignore token balances will be ignored by the iterator.However, when a transaction is hashed by the network, it serialized and then de-serialized. During serialization, the
mayUseToken
field is validated to have at most onetrue
flag, preventing this attack from successfully executing on the network.Impact
Currently, it seems this can attack cannot be executed due to the serialization and de-serialization routines. However, changes to the codebase could enable this attack in the future if it is not carefully documented for developers.
Recommendation
Since the checks preventing this attack are far from the in-scope code and not familiar to the auditors, we recommend the Mina team take extra care to validate that there is no path by which a transaction can be submitted without being de-serialized.
At the very least, add documentation to the
AccountUpdate
struct and the relevant portion of themina
repository indicating the importance of checking thatmayUseToken
has at most onetrue
boolean flag.A more robust fix would ensure this situation cannot occur within any valid proof. To do this, consider updating the
check()
function ofAccountUpdate
to rule out the{true, true}
case.Developer Response
We rate this as more severe than a warning. We want correct code execution to be proven and be universally verifiable, and not rely on each node operator to check wellformedness on deserialization
The text was updated successfully, but these errors were encountered: