From 75570d60721ec80b03073e8d20c450d7d6e06f52 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:35:25 -0400 Subject: [PATCH 01/15] fix(circuits): zero the correct number of Poseidon padding slots PoseidonDecrypt(l) pads the message up to a multiple of three, so the number of padding slots is decryptedLength - l: two when l % 3 == 1, one when l % 3 == 2. The two branch bodies were the other way round, which the comment directly above them already describes correctly as "(3 - (l mod 3))". Every PCT in the system is PoseidonDecrypt(1), so the live effect was the l % 3 == 1 branch checking one slot instead of two, leaving decrypted[1] unconstrained. The auth tag does not cover that gap: a prover chooses ciphertext[0..2] freely and then computes the tag over them, so an arbitrary field element could be smuggled through every transfer, mint, burn and withdraw PCT. Verified with a forged-but-valid-tag ciphertext, which the old code accepts and the new code rejects. The l % 3 == 2 branch was over-constrained for the same reason and would have rejected valid two-element ciphertexts. Latent, since nothing instantiates l=2. --- circom/components.circom | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circom/components.circom b/circom/components.circom index 216b6d3..55e286c 100644 --- a/circom/components.circom +++ b/circom/components.circom @@ -59,9 +59,9 @@ template PoseidonDecrypt(l) { if (l % 3 > 0) { if (l % 3 == 1) { decrypted[decryptedLength - 1] === 0; + decrypted[decryptedLength - 2] === 0; } else if (l % 3 == 2) { decrypted[decryptedLength - 1] === 0; - decrypted[decryptedLength - 2] === 0; } } } From 53add20cff8a9ff632e020bdcbe7c33ff0da3b62 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:35:37 -0400 Subject: [PATCH 02/15] fix(circuits): reject zero encryption randomness Both randomness inputs carried only a range check, and zero passes it. CheckPublicKey already asserts privKey != 0; these bring the randomness up to the same standard. In ElGamalEncrypt, random = 0 makes c1 the identity and leaves the message unblinded, so the ciphertext is (identity, value*G) and anyone can recover the amount by a short discrete-log search. In CheckPCT, random = 0 makes both the auth key and the Poseidon encryption key the identity point, which is public, so anyone can decrypt the PCT. The sender is the only party who picks these values, so this is disclosure of their own transfer rather than theft - but it is silent, undetectable by the counterparty or the auditor, and a clean covert channel. There is no legitimate reason for either value to be zero. --- circom/components.circom | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/circom/components.circom b/circom/components.circom index 55e286c..d7f1a30 100644 --- a/circom/components.circom +++ b/circom/components.circom @@ -113,6 +113,13 @@ template ElGamalEncrypt() { checkPoint2.x <== msg[0]; checkPoint2.y <== msg[1]; + // Verify the randomness is not zero + // With random = 0 the ciphertext degenerates to (identity, msg), which any observer can + // read, so the encrypted value would no longer be private. + component checkRandomIsZero = IsZero(); + checkRandomIsZero.in <== random; + checkRandomIsZero.out === 0; + component randomBits = Num2Bits(253); randomBits.in <== random; @@ -335,6 +342,13 @@ template CheckPCT() { lt.in[1] <== baseOrder; lt.out === 1; + // Verify the randomness is not zero + // With random = 0 both the auth key and the encryption key become the identity point, + // which is public knowledge, so anyone could decrypt the PCT. + component checkRandomIsZero = IsZero(); + checkRandomIsZero.in <== random; + checkRandomIsZero.out === 0; + component checkAuthKey = BabyPbk(); checkAuthKey.in <== random; From 38e694ac5695f11f0de03a6320cbd0f3b3a0ae08 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:36:32 -0400 Subject: [PATCH 03/15] fix(registrar): prevent a registered account from overwriting its public key register() combined its two guards with &&, so it reverted only when the registration hash was already used AND the account already held a key. A user re-registering with a different private key produces a different registration hash, so the first clause was false and the guard let the overwrite through. Nothing migrates the balance. The stored ciphertext stays encrypted under the old key while _validatePublicKey compares proofs against the new one, so every spend proof the account can build is rejected and the funds are unreachable, with no warning and no recovery path. The same overwrite silently staleness the eERC's auditorPublicKey if the auditor re-registers, leaving PCTs encrypted to a key the auditor may no longer hold. The circuit side matters too: register() reads the account as address(uint160(input[2])) while CheckRegistrationHash hashed the full field element, so senderAddress + k * 2^160 minted fresh hashes for the same account. Without the 160-bit bound the guard stays bypassable however the clauses combine. The existing "already registered user can not register again" test replays the same proof, for which both clauses are true either way, so it could not distinguish a correct guard from a broken one. The added test registers a second time with a freshly generated key, which is the case that actually fails against the old condition. --- circom/components.circom | 7 +++++++ contracts/Registrar.sol | 2 +- test/EncryptedERC-Standalone.ts | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/circom/components.circom b/circom/components.circom index d7f1a30..f84124c 100644 --- a/circom/components.circom +++ b/circom/components.circom @@ -391,6 +391,13 @@ template CheckRegistrationHash() { signal input senderPrivateKey; signal input senderAddress; + // Verify the sender address fits in 160 bits + // Registrar.register() reads the account as address(uint160(input[2])). Without this + // bound, senderAddress + k * 2^160 hashes differently while resolving to the same + // account, which defeats the duplicate-registration guard. + component addressBits = Num2Bits(160); + addressBits.in <== senderAddress; + component hash = Poseidon(3); hash.inputs[0] <== chainID; hash.inputs[1] <== senderPrivateKey; diff --git a/contracts/Registrar.sol b/contracts/Registrar.sol index c4b1cb1..c37f5c5 100644 --- a/contracts/Registrar.sol +++ b/contracts/Registrar.sol @@ -102,7 +102,7 @@ contract Registrar { } // check if the user is already registered - if (isRegistered[registrationHash] && isUserRegistered(account)) { + if (isRegistered[registrationHash] || isUserRegistered(account)) { revert UserAlreadyRegistered(); } diff --git a/test/EncryptedERC-Standalone.ts b/test/EncryptedERC-Standalone.ts index b586798..07efd2d 100644 --- a/test/EncryptedERC-Standalone.ts +++ b/test/EncryptedERC-Standalone.ts @@ -209,6 +209,30 @@ describe("EncryptedERC - Standalone", () => { ).to.be.revertedWithCustomError(registrar, "UserAlreadyRegistered"); }); + it("already registered user can not register again with a new key", async () => { + const alreadyRegisteredSigner = users[4].signer; + const rotated = new User(alreadyRegisteredSigner); + const chainId = await ethers.provider + .getNetwork() + .then((network) => network.chainId); + + const zkProof = await registrationCircuit.generateProof({ + SenderPrivateKey: rotated.formattedPrivateKey, + SenderPublicKey: rotated.publicKey, + SenderAddress: BigInt(alreadyRegisteredSigner.address), + ChainID: chainId, + RegistrationHash: rotated.genRegistrationHash(chainId), + }); + const calldata = await registrationCircuit.generateCalldata(zkProof); + + await expect( + registrar.connect(alreadyRegisteredSigner).register({ + proofPoints: calldata.proofPoints, + publicSignals: calldata.publicSignals, + } as RegisterProofStruct), + ).to.be.revertedWithCustomError(registrar, "UserAlreadyRegistered"); + }); + it("should revert if sender is not matching", async () => { // valid proof is for user[4] but we are using user[0] const sender = users[0]; From 737fd076dfd6520f41660d17e0525dd837b985aa Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:37:25 -0400 Subject: [PATCH 04/15] docs(converter): record that deposit encryption uses a fixed randomness BabyJubJub.encrypt hardcodes random = 1, which is a reasonable choice given a contract has no source of randomness, but the consequences were undocumented. Deposit ciphertexts are deterministic: equal amounts to the same key produce identical ciphertexts, and the accumulated randomness of a deposit-only account is just its publicly observable operation count, so anyone can compute b*G = c2 - n*pk and confirm the balance. That leaks nothing beyond the deposit and withdrawal amounts, which are already public in the ERC20 transfers - but it does mean a converter balance carries no confidentiality at all until the account receives a private transfer with real randomness, and that is worth stating where someone reading the deposit path will see it. --- contracts/EncryptedERC.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/EncryptedERC.sol b/contracts/EncryptedERC.sol index e953807..61370cc 100644 --- a/contracts/EncryptedERC.sol +++ b/contracts/EncryptedERC.sol @@ -657,6 +657,15 @@ contract EncryptedERC is uint256[2] memory publicKey = registrar.getUserPublicKey(to); // Encrypt the value with the receiver's public key + // + // NOTE: BabyJubJub.encrypt uses a fixed randomness of 1, because a contract has no + // source of randomness. Deposit ciphertexts are therefore deterministic: equal + // amounts to the same key produce identical ciphertexts, and the accumulated + // randomness of a deposit-only account is just its publicly observable operation + // count. Such a balance is derivable by anyone from the deposit and withdrawal + // amounts, which are already public in the ERC20 transfers -- but it does mean a + // converter balance carries no confidentiality until the account receives a + // private transfer with real randomness. EGCT memory eGCT = BabyJubJub.encrypt( Point({x: publicKey[0], y: publicKey[1]}), value From ed992b794a58a93930b8ecb64ad182392a5524fa Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:37:32 -0400 Subject: [PATCH 05/15] fix(converter): capture token decimals once, at registration decimals() was read live from the token on both legs of every conversion, and nothing in ERC20 requires it to stay constant. A token free to change what it reports could be scaled one way going in and another coming out: deposit while it reports 10 decimals and no scaling applies, flip it to 18, then withdraw the same encrypted units and _convertTo multiplies the payout by 10^8. The difference is paid out of the contract's holdings of that token, which is every other depositor of it. It does not even need malice - a legitimately upgradeable token that changes its reported decimals corrupts accounting for every holder. Reading it once in _addToken and storing it per token id makes the in-scaling and out-scaling agree by construction, whatever the token does afterwards. Two smaller things fall out: the two call sites no longer disagree on type (uint8 on deposit, uint256 on withdrawal), and a token with no decimals() now reverts when it is first registered rather than on every deposit attempt. _convertFrom had to be reordered to register before reading, since registration is what populates the cache. Note for anyone applying this to a live instance: tokens registered before this change would read 0 from the new mapping and need a backfill. --- contracts/EncryptedERC.sol | 38 +++++++++++++++---------------- contracts/tokens/TokenTracker.sol | 5 ++++ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/contracts/EncryptedERC.sol b/contracts/EncryptedERC.sol index 61370cc..6faaa3b 100644 --- a/contracts/EncryptedERC.sol +++ b/contracts/EncryptedERC.sol @@ -27,7 +27,6 @@ import {IWithdrawVerifier} from "./interfaces/verifiers/IWithdrawVerifier.sol"; import {ITransferVerifier} from "./interfaces/verifiers/ITransferVerifier.sol"; import {IBurnVerifier} from "./interfaces/verifiers/IBurnVerifier.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; // /$$$$$$$$ /$$$$$$$ /$$$$$$ // | $$_____/| $$__ $$ /$$__ $$ @@ -621,31 +620,32 @@ contract EncryptedERC is address tokenAddress, uint256[7] memory amountPCT ) internal returns (uint256 dust, uint256 tokenId) { - // Get token decimals and handle scaling - uint8 tokenDecimals = IERC20Metadata(tokenAddress).decimals(); + // Register the token if it's new. This captures decimals() once, at registration, so a + // token cannot report one value on deposit and another on withdrawal. + if (tokenIds[tokenAddress] == 0) { + _addToken(tokenAddress); + } + tokenId = tokenIds[tokenAddress]; + + // Get the registered token decimals and handle scaling + uint8 registeredDecimals = tokenDecimals[tokenId]; uint256 value = amount; dust = 0; // Scale down if token has more decimals - if (tokenDecimals > decimals) { - uint256 scalingFactor = 10 ** (tokenDecimals - decimals); + if (registeredDecimals > decimals) { + uint256 scalingFactor = 10 ** (registeredDecimals - decimals); value = amount / scalingFactor; dust = amount % scalingFactor; } // Scale up if token has fewer decimals - else if (tokenDecimals < decimals) { - uint256 scalingFactor = 10 ** (decimals - tokenDecimals); + else if (registeredDecimals < decimals) { + uint256 scalingFactor = 10 ** (decimals - registeredDecimals); value = amount * scalingFactor; dust = 0; } - // Register the token if it's new - if (tokenIds[tokenAddress] == 0) { - _addToken(tokenAddress); - } - tokenId = tokenIds[tokenAddress]; - // Return early if the scaled value is zero if (value == 0) { return (dust, tokenId); @@ -708,20 +708,20 @@ contract EncryptedERC is uint256 amount, address tokenAddress ) internal { - // Get token decimals and handle scaling - uint256 tokenDecimals = IERC20Metadata(tokenAddress).decimals(); + // Get the registered token decimals and handle scaling + uint256 registeredDecimals = tokenDecimals[tokenIds[tokenAddress]]; uint256 value = amount; uint256 scalingFactor = 0; // Scale up if token has more decimals - if (tokenDecimals > decimals) { - scalingFactor = 10 ** (tokenDecimals - decimals); + if (registeredDecimals > decimals) { + scalingFactor = 10 ** (registeredDecimals - decimals); value = amount * scalingFactor; } // Scale down if token has fewer decimals - else if (tokenDecimals < decimals) { - scalingFactor = 10 ** (decimals - tokenDecimals); + else if (registeredDecimals < decimals) { + scalingFactor = 10 ** (decimals - registeredDecimals); value = amount / scalingFactor; } diff --git a/contracts/tokens/TokenTracker.sol b/contracts/tokens/TokenTracker.sol index 3691750..7bb6745 100644 --- a/contracts/tokens/TokenTracker.sol +++ b/contracts/tokens/TokenTracker.sol @@ -7,6 +7,7 @@ pragma solidity 0.8.27; import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol"; import {TokenBlacklisted, InvalidOperation} from "../errors/Errors.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; /** * @title TokenTracker @@ -45,6 +46,9 @@ contract TokenTracker is Ownable2Step { mapping(address tokenAddress => bool isBlacklisted) public blacklistedTokens; + /// @notice Decimals reported by each registered token, captured at registration + mapping(uint256 tokenId => uint8 decimals) public tokenDecimals; + /////////////////////////////////////////////////// /// Modifiers /// /////////////////////////////////////////////////// @@ -139,6 +143,7 @@ contract TokenTracker is Ownable2Step { uint256 newTokenId = nextTokenId; tokenIds[tokenAddress] = newTokenId; tokenAddresses[newTokenId] = tokenAddress; + tokenDecimals[newTokenId] = IERC20Metadata(tokenAddress).decimals(); tokens.push(tokenAddress); nextTokenId++; } From 6d79445310fb0f071aa756f0581197ab24d5e065 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:40:42 -0400 Subject: [PATCH 06/15] fix(converter): reject withdrawals that scale down to zero tokens When the eERC has more decimals than the token, _convertTo divides, and an amount below the scaling factor floors to zero. _privateBurn has already debited the caller's encrypted balance in full by that point, so the call succeeded, emitted a Withdraw event for the full amount, and moved no tokens. The deposit leg handles the mirror case properly by returning the remainder as dust; this leg dropped it. The loss per call is bounded by scalingFactor - 1 encrypted units, always less than one indivisible unit of the underlying token, and it is never directable at anyone else - so the magnitude is small. What earns the fix is that the call reports success: a 10-decimal eERC over a 0-decimal token gives a 10^10 threshold, under which every withdrawal silently pays nothing. The test suite encoded the broken behaviour as correct. The 6-decimal block withdrew 1000 units through a 10^4 scaling factor - exactly zero tokens - and passed, because every deposit test checks the real ERC20 delta while none of the three withdraw blocks referenced the token at all. They now assert the balance moved and by how much, the amount is raised above the scaling factor, and a case below it asserts the revert. The assertions fail against the pre-fix contract. --- contracts/EncryptedERC.sol | 9 +++- contracts/errors/Errors.sol | 1 + test/EncryptedERC-Converter.ts | 78 +++++++++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/contracts/EncryptedERC.sol b/contracts/EncryptedERC.sol index 6faaa3b..c00bccb 100644 --- a/contracts/EncryptedERC.sol +++ b/contracts/EncryptedERC.sol @@ -18,7 +18,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol import {CreateEncryptedERCParams, Point, EGCT, EncryptedBalance, AmountPCT, MintProof, TransferProof, WithdrawProof, BurnProof, TransferInputs} from "./types/Types.sol"; // errors -import {UserNotRegistered, InvalidProof, TransferFailed, UnknownToken, InvalidChainId, InvalidNullifier, ZeroAddress} from "./errors/Errors.sol"; +import {UserNotRegistered, InvalidProof, TransferFailed, UnknownToken, InvalidChainId, InvalidNullifier, ZeroAddress, AmountTooSmall} from "./errors/Errors.sol"; // interfaces import {IRegistrar} from "./interfaces/IRegistrar.sol"; @@ -725,6 +725,13 @@ contract EncryptedERC is value = amount / scalingFactor; } + // Reject amounts that scale down to nothing. The caller's encrypted balance has + // already been debited by the full amount at this point, so paying out zero would + // destroy the value silently and still emit a Withdraw event for it. + if (value == 0) { + revert AmountTooSmall(); + } + // Transfer the tokens to the receiver IERC20 token = IERC20(tokenAddress); SafeERC20.safeTransfer(token, to, value); diff --git a/contracts/errors/Errors.sol b/contracts/errors/Errors.sol index 02da42a..13df08b 100644 --- a/contracts/errors/Errors.sol +++ b/contracts/errors/Errors.sol @@ -19,3 +19,4 @@ error InvalidSender(); error InvalidRegistrationHash(); error ZeroAddress(); error TokenBlacklisted(address token); +error AmountTooSmall(); diff --git a/test/EncryptedERC-Converter.ts b/test/EncryptedERC-Converter.ts index bbe04b8..2212b24 100644 --- a/test/EncryptedERC-Converter.ts +++ b/test/EncryptedERC-Converter.ts @@ -790,7 +790,8 @@ describe("EncryptedERC - Converter", () => { describe("Withdrawing Tokens - Lower ERC20 Decimals (6)", () => { const tokenId = 2; - const withdrawAmount = 1000n; + // eERC has 10 decimals and this token has 6, check that the scaling factor is applied properly + const withdrawAmount = 10_000n; let userInitialBalance: bigint; let validProof: { proof: CalldataWithdrawCircuitGroth16; @@ -834,6 +835,9 @@ describe("EncryptedERC - Converter", () => { const encryptedMetadata = encryptMetadata(user.publicKey, MESSAGE); + const token = erc20s[0]; // tokenId 2 is the 6-decimal token + const erc20BalanceBefore = await token.balanceOf(user.signer.address); + const tx = await encryptedERC .connect(user.signer) [ @@ -841,6 +845,12 @@ describe("EncryptedERC - Converter", () => { ](tokenId, proof, userBalancePCT, encryptedMetadata); const receipt = await tx.wait(); + const erc20BalanceAfter = await token.balanceOf(user.signer.address); + expect(erc20BalanceAfter).to.be.greaterThan(erc20BalanceBefore); + expect(erc20BalanceAfter).to.equal( + erc20BalanceBefore + withdrawAmount / 10n ** BigInt(DECIMALS - 6), + ); + const events = await encryptedERC.queryFilter( encryptedERC.filters.PrivateMessage, receipt?.blockNumber || 0, @@ -879,6 +889,40 @@ describe("EncryptedERC - Converter", () => { expect(totalBalance).to.equal(userInitialBalance - withdrawAmount); }); + it("should revert if the amount scales down to zero tokens", async () => { + const user = users[0]; + const balance = await encryptedERC.balanceOf( + user.signer.address, + tokenId, + ); + const userEncryptedBalance = [...balance.eGCT.c1, ...balance.eGCT.c2]; + const currentBalance = await getDecryptedBalance( + user.privateKey, + balance.amountPCTs, + balance.balancePCT, + balance.eGCT, + ); + + // below the 10^4 scaling factor, so _convertTo would floor the payout to 0 + // while the encrypted balance had already been debited in full + const dustAmount = 999n; + const { proof, userBalancePCT } = await withdraw( + dustAmount, + user, + userEncryptedBalance, + currentBalance, + auditorPublicKey, + ); + + await expect( + encryptedERC + .connect(user.signer) + [ + "withdraw(uint256,((uint256[2],uint256[2][2],uint256[2]),uint256[16]),uint256[7])" + ](tokenId, proof, userBalancePCT), + ).to.be.revertedWithCustomError(encryptedERC, "AmountTooSmall"); + }); + it("should revert if public keys are not matching", async () => { const user = users[1]; @@ -956,6 +1000,12 @@ describe("EncryptedERC - Converter", () => { auditorPublicKey, ); + const tokenAddress = await encryptedERC.tokenAddresses(tokenId); + const token = erc20s.find((t) => t.target === tokenAddress); + if (!token) throw new Error("token for tokenId not found"); + const tokenDecimals = Number(await token.decimals()); + const erc20BalanceBefore = await token.balanceOf(user.signer.address); + expect( await encryptedERC .connect(user.signer) @@ -964,6 +1014,16 @@ describe("EncryptedERC - Converter", () => { ](tokenId, proof, userBalancePCT), ).to.be.not.reverted; + // the underlying token must actually move, and by the scaled amount + const expectedRaw = + tokenDecimals >= DECIMALS + ? withdrawAmount * 10n ** BigInt(tokenDecimals - DECIMALS) + : withdrawAmount / 10n ** BigInt(DECIMALS - tokenDecimals); + expect(expectedRaw).to.be.greaterThan(0n); + expect(await token.balanceOf(user.signer.address)).to.equal( + erc20BalanceBefore + expectedRaw, + ); + validProof = { proof, userBalancePCT }; }); @@ -1080,6 +1140,12 @@ describe("EncryptedERC - Converter", () => { auditorPublicKey, ); + const tokenAddress = await encryptedERC.tokenAddresses(tokenId); + const token = erc20s.find((t) => t.target === tokenAddress); + if (!token) throw new Error("token for tokenId not found"); + const tokenDecimals = Number(await token.decimals()); + const erc20BalanceBefore = await token.balanceOf(user.signer.address); + expect( await encryptedERC .connect(user.signer) @@ -1088,6 +1154,16 @@ describe("EncryptedERC - Converter", () => { ](tokenId, proof, userBalancePCT), ).to.be.not.reverted; + // the underlying token must actually move, and by the scaled amount + const expectedRaw = + tokenDecimals >= DECIMALS + ? withdrawAmount * 10n ** BigInt(tokenDecimals - DECIMALS) + : withdrawAmount / 10n ** BigInt(DECIMALS - tokenDecimals); + expect(expectedRaw).to.be.greaterThan(0n); + expect(await token.balanceOf(user.signer.address)).to.equal( + erc20BalanceBefore + expectedRaw, + ); + validProof = { proof, userBalancePCT }; }); From 85b1524ade288160214931431ebb8bd705c5ce2a Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:41:04 -0400 Subject: [PATCH 07/15] fix(balances): bound the pending amount PCT history per account and token amountPCTs is an unbounded storage array. Every credit pushes an entry and every spend walks the whole array in _deleteUserHistory, popping matches. The asymmetry is the problem: inbound transfers grow the array, and the account pays to walk it. A third party could send transfers until the victim's next spend exceeded the block gas limit, at which point that balance is unspendable permanently, with no way to prune. One transfer per entry is expensive for the attacker but bounded; the victim's loss is total and irreversible. Capping the pending entries changes the failure mode from the victim's funds being frozen to the attacker's transfer reverting, and the account clears its history with a single spend. That is still a griefing vector - an attacker can block an account from receiving once it holds 300 unspent credits - but it is recoverable rather than permanent. 300 keeps the prune loop well inside the gas limit while leaving generous headroom for ordinary use. --- contracts/EncryptedUserBalances.sol | 14 +++++++++++++- contracts/errors/Errors.sol | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/contracts/EncryptedUserBalances.sol b/contracts/EncryptedUserBalances.sol index 7f02b19..8221c06 100644 --- a/contracts/EncryptedUserBalances.sol +++ b/contracts/EncryptedUserBalances.sol @@ -6,7 +6,7 @@ pragma solidity 0.8.27; import {EncryptedBalance, EGCT, BalanceHistory, AmountPCT} from "./types/Types.sol"; -import {InvalidProof} from "./errors/Errors.sol"; +import {InvalidProof, PendingHistoryLimitReached} from "./errors/Errors.sol"; import {BabyJubJub} from "./libraries/BabyJubJub.sol"; /** @@ -21,6 +21,13 @@ import {BabyJubJub} from "./libraries/BabyJubJub.sol"; * allowing users to prove they have sufficient funds without revealing the actual amount. */ contract EncryptedUserBalances { + /////////////////////////////////////////////////// + /// Constants /// + /////////////////////////////////////////////////// + + // Add limit to pending history to keep the gas cost of pruning history affordable for each account. + uint256 public constant MAX_PENDING_AMOUNT_PCTS = 300; + /////////////////////////////////////////////////// /// State Variables /// /////////////////////////////////////////////////// @@ -183,6 +190,11 @@ contract EncryptedUserBalances { ) internal { EncryptedBalance storage balance = balances[user][tokenId]; + // bound the pending history so the prune loop stays affordable for the account + if (balance.amountPCTs.length >= MAX_PENDING_AMOUNT_PCTS) { + revert PendingHistoryLimitReached(); + } + uint256 nonce = balance.nonce; uint256 balanceHash = _hashEGCT(balance.eGCT); balanceHash = uint256(keccak256(abi.encode(balanceHash, nonce))); diff --git a/contracts/errors/Errors.sol b/contracts/errors/Errors.sol index 13df08b..565019a 100644 --- a/contracts/errors/Errors.sol +++ b/contracts/errors/Errors.sol @@ -19,4 +19,5 @@ error InvalidSender(); error InvalidRegistrationHash(); error ZeroAddress(); error TokenBlacklisted(address token); +error PendingHistoryLimitReached(); error AmountTooSmall(); From ac9b5ccd5c40c3d2c629135cdd272fbe2cf89265 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:41:11 -0400 Subject: [PATCH 08/15] fix(converter): use the shared balance helper on the deposit path _convertFrom reimplemented _addToUserBalance inline. The helper had existed for a week when the deposit path was written, and the copy then drifted from it, which is the usual fate of duplicated state-machine code. The consequence that matters is that the copy bypassed the MAX_PENDING_AMOUNT_PCTS ceiling, because that guard lives in _addToUserHistory and only the helper calls it. Nobody can weaponise that - _executeDeposit credits msg.sender only, so deposits cannot grow someone else's history - but an account could grow its own past the point where its prune loop fits in a block and then be unable to spend. Measured: with the inline copy the 301st deposit succeeds and the array reaches 301; via the helper it reverts with PendingHistoryLimitReached. The other drift goes away with it: deposits incremented transactionIndex twice, once directly and once inside _commitUserBalance, where every other credit increments it once. Deposits now advance it by one like the rest. Balance ciphertexts, decrypted balances, nonces and pruning behaviour are unchanged - verified by dumping every observable field across three deposits and a withdrawal under both versions - but the counter's absolute value is lower, so any off-chain consumer reading transactionIndex or an amount PCT index will see different numbers. --- contracts/EncryptedERC.sol | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/contracts/EncryptedERC.sol b/contracts/EncryptedERC.sol index c00bccb..6344e36 100644 --- a/contracts/EncryptedERC.sol +++ b/contracts/EncryptedERC.sol @@ -15,7 +15,7 @@ import {BabyJubJub} from "./libraries/BabyJubJub.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; // types -import {CreateEncryptedERCParams, Point, EGCT, EncryptedBalance, AmountPCT, MintProof, TransferProof, WithdrawProof, BurnProof, TransferInputs} from "./types/Types.sol"; +import {CreateEncryptedERCParams, Point, EGCT, AmountPCT, MintProof, TransferProof, WithdrawProof, BurnProof, TransferInputs} from "./types/Types.sol"; // errors import {UserNotRegistered, InvalidProof, TransferFailed, UnknownToken, InvalidChainId, InvalidNullifier, ZeroAddress, AmountTooSmall} from "./errors/Errors.sol"; @@ -671,24 +671,11 @@ contract EncryptedERC is value ); - // Add to the receiver's balance - EncryptedBalance storage balance = balances[to][tokenId]; - - if (balance.eGCT.c1.x == 0 && balance.eGCT.c1.y == 0) { - balance.eGCT = eGCT; - } else { - balance.eGCT.c1 = BabyJubJub._add(balance.eGCT.c1, eGCT.c1); - balance.eGCT.c2 = BabyJubJub._add(balance.eGCT.c2, eGCT.c2); - } - - // Update transaction history - balance.amountPCTs.push( - AmountPCT({pct: amountPCT, index: balance.transactionIndex}) - ); - balance.transactionIndex++; - - // Commit the new balance - _commitUserBalance(to, tokenId); + // Add to the receiver's balance and record the amount PCT. + // Shared with the transfer and mint paths on purpose: this used to be an inline + // copy of _addToUserBalance, which meant the MAX_PENDING_AMOUNT_PCTS ceiling in + // _addToUserHistory did not apply to deposits. + _addToUserBalance(to, tokenId, eGCT, amountPCT); } return (dust, tokenId); From f04f52a2a4872894b9b94ef19178d3cf77ac0305 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:41:43 -0400 Subject: [PATCH 09/15] fix(setup): require a phase-2 contribution, and regenerate the keys contributionSettings.contributions was 0, so hardhat-zkit emitted verification keys straight from the initial zkey, where the phase-2 trapdoor delta is still 1. All five generated verifiers had DELTA byte-identical to GAMMA, both equal to the BN254 G2 generator. Groth16 soundness rests entirely on delta being secret. With delta = 1 the verification equation collapses and a proof can be forged for any public input from public data alone: take A = alpha, B = beta + t*g2, C = t*alpha - L, and e(-A,B)*e(alpha,beta)*e(L,gamma)*e(C,delta) = 1 holds by construction. Confirmed against the committed constants for t = 1, 2 and 7, with no witness and no proving key. Against a deployment built from these scripts that means registering any key to any address, forging withdraw proofs to drain the converter's reserves, and forging transfers and mints. The keys here are regenerated with one contribution, which makes them unforgeable. It does NOT make them trustworthy: whoever ran the setup knows the toxic waste. Production keys must come from a multi-party ceremony ending in a public beacon, with the transcript published so the key can be reproduced from these circuits. The IC constants are unchanged by the contribution and reproduce exactly from the circuit source, so only DELTA moves here beyond the circuit fixes earlier in this series. --- .../verifiers/BurnCircuitGroth16Verifier.sol | 88 +++++------ .../verifiers/MintCircuitGroth16Verifier.sol | 108 +++++++------- .../RegistrationCircuitGroth16Verifier.sol | 32 ++-- .../TransferCircuitGroth16Verifier.sol | 140 +++++++++--------- .../WithdrawCircuitGroth16Verifier.sol | 76 +++++----- hardhat.config.ts | 2 +- 6 files changed, 223 insertions(+), 223 deletions(-) diff --git a/contracts/verifiers/BurnCircuitGroth16Verifier.sol b/contracts/verifiers/BurnCircuitGroth16Verifier.sol index 9729b12..379b9ab 100644 --- a/contracts/verifiers/BurnCircuitGroth16Verifier.sol +++ b/contracts/verifiers/BurnCircuitGroth16Verifier.sol @@ -34,94 +34,94 @@ contract BurnCircuitGroth16Verifier { uint256 public constant GAMMA_Y2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; uint256 public constant DELTA_X1 = - 11559732032986387107991004021392285783925812861821192530917403151452391805634; + 18068269036347605663473046598932117849250599112501411921245619099639563001463; uint256 public constant DELTA_X2 = - 10857046999023057135944570762232829481370756359578518086990519993285655852781; + 2301073142475396993773978505633718782750287996612198183593216235602225785330; uint256 public constant DELTA_Y1 = - 4082367875863433681332203403145435568316851327593401208105741076214120093531; + 3073145107905170746687841711267307719073762517109452847153814168939630075772; uint256 public constant DELTA_Y2 = - 8495653923123431417604973247489272438418190587263600148770280649306958101930; + 10961296788526809772353740184537068352606427639693845063421291358100730194549; uint256 public constant IC0_X = - 6901175356638081608311197414548846861180656487838344346821425826024891512425; + 14874534560863506187492139018634993019793584672384344989283815636520951511298; uint256 public constant IC0_Y = - 16944064565335445729234872967061190126362989003416853826016805426130180238191; + 19433767728097878428205026753935379413283455462187619513628741184855994988529; uint256 public constant IC1_X = - 17908121343774581376092646765067170799970925894057937307165335210962871507135; + 766312427469543414624781412742178740436577019446000599209144238628925292414; uint256 public constant IC1_Y = - 6739483966006459958647545663755815825611388802168101513616676265213547687212; + 11956596380120466965959213032841211036588141256244479446912141909087743946449; uint256 public constant IC2_X = - 766312427469543414624781412742178740436577019446000599209144238628925292414; + 19382367631125777024854089319372587940591402333899244040325149591553843397579; uint256 public constant IC2_Y = - 11956596380120466965959213032841211036588141256244479446912141909087743946449; + 21403189616005894915395729633979911132499330629008284763677855495728980685589; uint256 public constant IC3_X = - 9834218022277006635455703178457965188100848955744692108208207593466809819916; + 16873607939260699782152987377956714326707996147740333372471409440806666007870; uint256 public constant IC3_Y = - 5985476150835890762503639978680541204908901815001963876083809679448099772187; + 4823813718136831027557208358400077597181121551803781304866108582065943338847; uint256 public constant IC4_X = - 14827234556798753501841602996273223453620286706976793196274886644504394417263; + 1929042304597372185937147232117975513235946186018483019137219886969879342564; uint256 public constant IC4_Y = - 17185284608112950506629319460803937653685676730131697135772583956246683209368; + 16041200717617907290182387211838729202813038927716409271158182722220738184272; uint256 public constant IC5_X = - 17529671244301122497049371127369274616617935801308036584230660946886543200270; + 10100132593539651075092860762732050318282008836770498758516080145892203319933; uint256 public constant IC5_Y = - 21375965875653643781871988041044406562099360028820246922430008152355558076267; + 20955597788595471241113414219690472105385243566077669548840483820580533529000; uint256 public constant IC6_X = - 2071394549783234155236090578090590694025714713435714686283263242515370887037; + 8470926007784285402889150113766497592074358400891318532322995138506403384279; uint256 public constant IC6_Y = - 1360924170815016231172148644602657489538698591468910875031083731851187567999; + 8310950274532158280101970082879701558099714295351544024093409514778108626712; uint256 public constant IC7_X = - 10294779462906061052931321788054691400173934802237652306928745571023248394387; + 19978173907576057065444722831289895208882188362747184086352903798679295119201; uint256 public constant IC7_Y = - 6555248292739776048079409559973894167013450042783785080733702895769749566613; + 15183736862969267858591121906253501216802708436126732016994140949207117595013; uint256 public constant IC8_X = - 4433257889246655161353853222728772973385032114589816543634201923482683435584; + 4241991590840894812521558873691922742505044386758211905612996694701369418750; uint256 public constant IC8_Y = - 14737241692493285661348398748630216205024305186831037514190361703045651615269; + 4712959178917328940867212891418986989954694508210057281434702756286365863599; uint256 public constant IC9_X = - 14662929317288257322870633955431196105372083992377701985085624881517883523163; + 17384256984235466163214682780551664577091564886844441439437963151187110314064; uint256 public constant IC9_Y = - 13045157851276202363238319024416490799176208505845980771766086158756862458455; + 20296571661331082956968105383442818985984992535310861363986529155505576301282; uint256 public constant IC10_X = - 10464607531594481747431773960796216724225398526452385781876076453050117351904; + 17027431616611009304378410217174272480143349850791163137585063755548142788022; uint256 public constant IC10_Y = - 17227459943128246860971844058080372581110192073599679941420162883374908732283; + 20118286149998664902129684191444860379452289136986877466159896830141693633470; uint256 public constant IC11_X = - 745839102907798772556197076666683733791895469218705021459706695701505037999; + 20835339050759225768703712848075529739635791514709813530848117559567280414938; uint256 public constant IC11_Y = - 11282995679713265974084133361652481201591627543141168082075682177048149337453; + 7040368151836048017923729126580755497072054472683430007432499359028824547816; uint256 public constant IC12_X = - 17929575843708091862123276864146152761508698565499701379217956191629740643940; + 20062320405151278545619584038566861254083477237663892701639147906154006315477; uint256 public constant IC12_Y = - 19694868618398715637769102075783868212304717698983166562284533403937378692315; + 17523041307652932190125641730118738313390334657045299125403856004388564657068; uint256 public constant IC13_X = - 19788140654052540486857453303544057569825570368720987020309299320004547960581; + 13052049171820219952433931907681657393694797308477474074698115842501295147916; uint256 public constant IC13_Y = - 6883822212531810402176678585679740471039138816982867861815020183660906571642; + 1425640142139256702451940144848264084151874944916879752002334659577479016496; uint256 public constant IC14_X = - 6016243318855719046735289125777883097005663608233773332990082345142498280371; + 4181557413177047880795852900430710069406648434565017206282272466766706932824; uint256 public constant IC14_Y = - 21204516959587943017810864594613242220785397660490599724380523851223540954454; + 1076490384996164187408939956231269371661746698521764954497727070931306852324; uint256 public constant IC15_X = - 9891573095652303648634434897797773521123041746620991186215470158961116717665; + 10477370403873963103009023823231137877228587266934872051738072656898318034377; uint256 public constant IC15_Y = - 15129361566109667756079905439551006265474721391302786501924664404711249139905; + 11502980528392834515632224102695746324276378296202650174921624745967235773112; uint256 public constant IC16_X = - 9439270817025434198507441583978754692817321436471324362734438362897111301924; + 7349403818132382069242774903158300506281160147593600216352343709478834507839; uint256 public constant IC16_Y = - 17736726489009951896104159456871058989709210357063650885766330312145063724146; + 5374170652494686658605512370545122605691962258852673576709426080364171645245; uint256 public constant IC17_X = - 7287813417026604917333767312326818451716773551965506932628922979658721400484; + 8971493727614399525916816483074861570535678898719425110239304688703813492918; uint256 public constant IC17_Y = - 21190069513632277449640126663995547292551888059992765226941465993312580710136; + 11607506224512849900066565004118629841042445789946723208255902316075236531264; uint256 public constant IC18_X = - 18870427155174730509179857527710685647524233977259143240992234215761896727263; + 11633076084066714087007356724903241887267836906856444905783519811005119761103; uint256 public constant IC18_Y = - 21595208618611453816106702920660402890633778123650117008434425697322727495324; + 15434440522489358811942244383448136662781089528873872838765763684395176114351; uint256 public constant IC19_X = - 2474736367332117814843921180480737874685068379574560055026846626150944220277; + 14649394179930981032894187227537055207828915729716249122436764256795900134741; uint256 public constant IC19_Y = - 11798959751686284671429858411798094190882362783079508053655067482307321072055; + 14359000287667049950671539992097470155808991877715693905424132958170028923673; /// @dev memory pointer sizes uint16 public constant P_PUBLIC_SIGNALS_ACCUMULATOR_SIZE = 128; diff --git a/contracts/verifiers/MintCircuitGroth16Verifier.sol b/contracts/verifiers/MintCircuitGroth16Verifier.sol index 2d82ad6..1185292 100644 --- a/contracts/verifiers/MintCircuitGroth16Verifier.sol +++ b/contracts/verifiers/MintCircuitGroth16Verifier.sol @@ -34,114 +34,114 @@ contract MintCircuitGroth16Verifier { uint256 public constant GAMMA_Y2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; uint256 public constant DELTA_X1 = - 11559732032986387107991004021392285783925812861821192530917403151452391805634; + 18310150044426911246155186910490046153313456452399821848345822479171865644945; uint256 public constant DELTA_X2 = - 10857046999023057135944570762232829481370756359578518086990519993285655852781; + 5264749735931748850388186461242850611426001497774606367467905923816395425584; uint256 public constant DELTA_Y1 = - 4082367875863433681332203403145435568316851327593401208105741076214120093531; + 14605690245348196568105292901835213150229505840089104796535493429006577126729; uint256 public constant DELTA_Y2 = - 8495653923123431417604973247489272438418190587263600148770280649306958101930; + 6630427478337662007171878244275165608118871321429624618994700701405970952688; uint256 public constant IC0_X = - 10121904041541460154551260390411876526139574666949788432677764574044027742051; + 20861492296691856165309547776629146952527944203201524873006782894760706976421; uint256 public constant IC0_Y = - 6395114105467352463516423639828272877440036954754220055848039516989817256674; + 3358824539307473736604976612722723713450165337804134402240835675528178962335; uint256 public constant IC1_X = - 5586762719201810155133862083740714325655748243483738877755810028542797644739; + 5511753296331491509569296110445894989507167702892869680373063711781065132586; uint256 public constant IC1_Y = - 9387879719287370926595730382657598610335084850078203728936667449453246554359; + 13287934466428012933987353229261913144286871473679533187235673876285899045743; uint256 public constant IC2_X = - 19420434315212518030589186167981244595406343548992012458488730749825996100239; + 19440605787728638102373686494108905895654642611628169437813543523912920918974; uint256 public constant IC2_Y = - 3482657374383353276279298693752239555763142972311841530862645391131143881873; + 14274851756231214444056769636488865604307347698663402267645432934893285974382; uint256 public constant IC3_X = - 21667521945257749121492909465236139091096077691835364325355606431375269082282; + 15096206644290302729673494593775111017994893610444238941512555843761977798983; uint256 public constant IC3_Y = - 12156175257782822331801928906256367913221851960540994586932487722549923413345; + 10592933405320090517798391416276382767273192109395080940602914940118569597175; uint256 public constant IC4_X = - 16828357126590671631798447095302651492589140460428270542352941484237941156483; + 11189740705109916645368104876865492850094752501799788866350480656624008137866; uint256 public constant IC4_Y = - 3785759973408012513975276660945174958105045591429994983968533282503371539336; + 6373283667837734104667322526110821843542106635322725504401355277194447781077; uint256 public constant IC5_X = - 21244321325522105477060254124915502034148248502931960755663601081500950076300; + 12519464150710176464255689182191223185996416021638864414936748671904429695824; uint256 public constant IC5_Y = - 14687177467139662957844534389099557537735007475047254307829149459337997284150; + 2150352325795278553853916266025032932948033124823106315746500341814425169422; uint256 public constant IC6_X = - 21404447565616225355770439075122853382066792760583316781611395109968504614761; + 15670400985668054227710197257556796204699390148155253521504180141434411169010; uint256 public constant IC6_Y = - 3777910210505077428584254669424713454396973987622630869249106067166590299357; + 8015372489282794800866289250911385611602689183902606383808208685650011045563; uint256 public constant IC7_X = - 5644690004940482546460856256670502914882531219591199090065888636589494736039; + 12391374210603786527505301556990321980257270091806041113978279818540065899945; uint256 public constant IC7_Y = - 16345970805899097192278870928687506101715169625232631539981164972434652655022; + 8515915070842771734194345922664587925823550006589425467896744043022744763705; uint256 public constant IC8_X = - 16593367704572145491497340027058421406291597779030039134573703992464863830646; + 13807538579064566476415694067988365108026713189554400912099239363422127740084; uint256 public constant IC8_Y = - 19275772679724763916884927502599327291258343548738975909520313332884772129528; + 20995630364107562144456318907019459226461425603435703531146140720469843551846; uint256 public constant IC9_X = - 18338635157990571739660769292280252719367811451620901782236478705200001456515; + 16081085598581166173056602246854317663562052634922574354324946451000607120736; uint256 public constant IC9_Y = - 12580365946981294431067603413481570211192375767222833113101936660967490853342; + 4806824913347623486886828480516106843275460973906843187481538273603254862860; uint256 public constant IC10_X = - 19880773599746588943745194809450382516728304373879942615347513263173728275196; + 11631320333320389002311111478825925862454422531020071627586226192130355210011; uint256 public constant IC10_Y = - 128548754750873577554444615682746852358621588347676854260473817368140033519; + 2122514671452018862991564551128667346496598894933582018300009947844480163204; uint256 public constant IC11_X = - 6476719107464483530164368276818653110527411272303410163053488615734788766586; + 15815064186883060030591419160901286559864343154013673260150235611788231912868; uint256 public constant IC11_Y = - 13331539055996208679678758084320772815107338352425913859433335410845899223811; + 20370993525566739028237212283048185476369537192297038935645044346593403445660; uint256 public constant IC12_X = - 17393436839515851750885277096607160266036302533767007170590322671069079326483; + 6595190593163887992768043198968800372382627330961015186275030802761168515160; uint256 public constant IC12_Y = - 17336491525577930609097646312281682071744277090982994234361498925040239739760; + 2507789053059248650011919918385719955910150531313679742117670915630070953153; uint256 public constant IC13_X = - 14059139663320156276001577403575266145309546640724899667521029234074221180403; + 4769024494559532962607971843474329930995364751968913219837397535853989902361; uint256 public constant IC13_Y = - 5963232262834884334488206091864494084893490405377454589196552297795638041488; + 19797571185823382274710714732907626050151097014530262793833343110696004901030; uint256 public constant IC14_X = - 13813470258321934033051704279698252148783024920181797002453328082368105474789; + 3741095076389852568348466376789570229681540891762671038336587701809594295528; uint256 public constant IC14_Y = - 19760333797981660449134124218356560284106192590380149679255903109561686546313; + 21880696756868695397495499774269592625633170331780293853746719644130396141624; uint256 public constant IC15_X = - 11858117840318587547791069185551586430435948995333725070770764697634898894992; + 19517234766997622544803863027312351274688065611950961197914008880953737422764; uint256 public constant IC15_Y = - 16621799951101200543458233987158204772158254202860685491749532125765298740749; + 12766977633088554535159910591318146119749041565098004885165775551772871421614; uint256 public constant IC16_X = - 13584356250500421508084566767718022822120888453124821650989966819848963029582; + 10127707076448967603220797369646569980071434903529645864304691091289486555787; uint256 public constant IC16_Y = - 21121442557480691564262522113115528175177021588076736568194595390503813944019; + 8950621622365938933033410282918462563356088154788396334376499614106955989143; uint256 public constant IC17_X = - 4326226245481542040767942040881999777137473163343274779447151141265545792486; + 320788284794753344254157752272658952160576068967386302073453451172649214954; uint256 public constant IC17_Y = - 21183580388477113613358837765893868421637670220571970210021457037640835173809; + 12072329550178607667412696356075787995621403634036884084426834721001660976904; uint256 public constant IC18_X = - 2704745980008452624320271214496436840853635777289985169457654318338905712053; + 9716709810086398193300932341527162316204048472244916702006365450976523531860; uint256 public constant IC18_Y = - 5498942242451865915343845370075402718280213375705928811712272696867696623122; + 18512681717254787569432573766259952720830104316573624498577507861680140169670; uint256 public constant IC19_X = - 18021323881247358977456866382503667963535769536490271165761105041938333860259; + 15583210451318479388973607383966221956407458266371632140871864321825927352219; uint256 public constant IC19_Y = - 15056011404667104875167366478630077579580289596849055349190254216447781136049; + 7052911310999904477056051359102541062683487166996917722730719394904318138991; uint256 public constant IC20_X = - 2428670635260390602473903592482933886252233168437493591205178349351514000281; + 14254488977079195307288668274622096883962637439954171948445632822972231171922; uint256 public constant IC20_Y = - 862350060646299115005420081148519933461348146447694025974629367115211964519; + 16635798420556842961701542474296813958571732882037007055705935524616967766953; uint256 public constant IC21_X = - 20557922324599650576215164022162024912213509843003042307546636524489843547931; + 15442808645804769798081357271547630043038154266326625746271433261881929480822; uint256 public constant IC21_Y = - 1061522392679391742130691544383963509531829000498209937975610885874122363387; + 15692133379817904664248323757151648294530592660167042783943929714054883152410; uint256 public constant IC22_X = - 9114256858842496488535186312068922854228150055952688114444862375910254881995; + 3959038117463195264225173297414802417787229629774522795646934460427059533984; uint256 public constant IC22_Y = - 20868937436899006110617091404782344741103549613810098330797141428653796030423; + 18559902898279327965625446054916413498906915191943220953721617187208287362108; uint256 public constant IC23_X = - 392332888128429361037702708407695212788446855280618226506466973682041689462; + 13117164475757255667489585615497830505771053345067993075233739336894728532922; uint256 public constant IC23_Y = - 10986782868722900357976121669302445297980271976862420955075995610762868745084; + 7549162224805556704746428222814274942147810722690549422145605522701753608061; uint256 public constant IC24_X = - 12335561474727855982123382035622477594232361573497391299243618736725476900631; + 14334222819352589667606721913122724056272442669634927211261504255948038740273; uint256 public constant IC24_Y = - 20340286770637453607265533856091115758478742882346824576410498508028320071146; + 2481751947144260802788900797067145915535069398032899055229950556775735697875; /// @dev memory pointer sizes uint16 public constant P_PUBLIC_SIGNALS_ACCUMULATOR_SIZE = 128; diff --git a/contracts/verifiers/RegistrationCircuitGroth16Verifier.sol b/contracts/verifiers/RegistrationCircuitGroth16Verifier.sol index 4c73ac2..c7dc437 100644 --- a/contracts/verifiers/RegistrationCircuitGroth16Verifier.sol +++ b/contracts/verifiers/RegistrationCircuitGroth16Verifier.sol @@ -34,38 +34,38 @@ contract RegistrationCircuitGroth16Verifier { uint256 public constant GAMMA_Y2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; uint256 public constant DELTA_X1 = - 11559732032986387107991004021392285783925812861821192530917403151452391805634; + 17876654898335325630068479520728289743106751388573052234350723906294440839692; uint256 public constant DELTA_X2 = - 10857046999023057135944570762232829481370756359578518086990519993285655852781; + 19282391219741381721894031299608776000039341958173844018547499085433811244729; uint256 public constant DELTA_Y1 = - 4082367875863433681332203403145435568316851327593401208105741076214120093531; + 15871735808708861193067982989955676797325237075069869946722392318532642264642; uint256 public constant DELTA_Y2 = - 8495653923123431417604973247489272438418190587263600148770280649306958101930; + 19708622698696259895236324656242665245096896533632764685573452118252374979496; uint256 public constant IC0_X = - 4004410872179300339480249405398939298715031489893009961199535208964457923750; + 692105244798714282968778033574767764788237535539454617180733345581560966526; uint256 public constant IC0_Y = - 11142026210898871476346451274761099606196839371161747578101583645702654533240; + 14949244044653984138443076667526001433989242452238052053640315799204091900706; uint256 public constant IC1_X = - 14970325264892984291437720194401230916657388050759523602370378142660744831477; + 21624277186876277282610441935385969538861234782040498867008018611983063222295; uint256 public constant IC1_Y = - 15860538555168123807647719982845297214031403618163443664157964964439662885432; + 9560437690960488495368890950231067296265617249061671980466162364249184005034; uint256 public constant IC2_X = - 2280562765509182195246897364500489648120102222444059313572774422753200337271; + 20975413741014623923220607600192046765007607319092408363742354325276831378512; uint256 public constant IC2_Y = - 7147694953124310609924568435428058789638619830198023240430532891482445253803; + 17098496999943410739691157771500542090730527298216613791131394980089852049506; uint256 public constant IC3_X = - 7737404298715916349870992960929602974683638711993694006376159661700137192127; + 11115533847113251325546915709756661686214330311154568812439324739880035102953; uint256 public constant IC3_Y = - 7116770325362339113448473214465082117296435085200119804036017348236401720128; + 17534035871294802715323825733736090079789520156028784272032671096408474909815; uint256 public constant IC4_X = - 3130277824222995531291107528843021785954629147236040284065307643519664903928; + 11574661945916509572718310335470045504590589360285124181916070574857759592473; uint256 public constant IC4_Y = - 11742475342174768235971584303810158858484260897318069476115756668898865449280; + 16617592405786925561930723699988576764724991912935168189284557159888787690750; uint256 public constant IC5_X = - 14791539702458079086636207858304521437578092734215012107895193807307152746110; + 18014926008436876908878223643157973700321174654128239431311107643294114881073; uint256 public constant IC5_Y = - 12489284483607948781669905789845942689563255773386215312172350852214666005897; + 15299062161003064518451841683402629795680575009368842994113276129387303699181; /// @dev memory pointer sizes uint16 public constant P_PUBLIC_SIGNALS_ACCUMULATOR_SIZE = 128; diff --git a/contracts/verifiers/TransferCircuitGroth16Verifier.sol b/contracts/verifiers/TransferCircuitGroth16Verifier.sol index 148128e..24c7ba6 100644 --- a/contracts/verifiers/TransferCircuitGroth16Verifier.sol +++ b/contracts/verifiers/TransferCircuitGroth16Verifier.sol @@ -34,146 +34,146 @@ contract TransferCircuitGroth16Verifier { uint256 public constant GAMMA_Y2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; uint256 public constant DELTA_X1 = - 11559732032986387107991004021392285783925812861821192530917403151452391805634; + 7373421924848631289729447888934244711954095305224812054165477109239634485930; uint256 public constant DELTA_X2 = - 10857046999023057135944570762232829481370756359578518086990519993285655852781; + 17573321420925543398098938841372725442795740456981756646006455900667019185665; uint256 public constant DELTA_Y1 = - 4082367875863433681332203403145435568316851327593401208105741076214120093531; + 6692063395579459319241310567160387612371059945372121369244265991917716889279; uint256 public constant DELTA_Y2 = - 8495653923123431417604973247489272438418190587263600148770280649306958101930; + 14769154824729601962487485960591065507136111829346285041829452037354398605022; uint256 public constant IC0_X = - 11078456175176508947665060839367063181390473473270508006260718733884763446509; + 12860613047485174337572800144367534566676749966427263510786233641288361323072; uint256 public constant IC0_Y = - 6041753414651001127438360352721137264917051499794520184043293475204165190685; + 3117092304906614020388401274042152195443668310800814064571900198159814372456; uint256 public constant IC1_X = - 15162904757480315297547642942646568889101029164132171480256830884259150251283; + 3987203900176313648721373326838294062064875524041579268053604363771992813663; uint256 public constant IC1_Y = - 5543236365374398176471504758808152977042933422387633848583378303299793270756; + 2540870705672409242382324915793065925212335203467790590740070818381371996118; uint256 public constant IC2_X = - 17328060769291355736429270023867089282242463190765930364066672240606322731528; + 2160351009242570345101990598387012245257165805803521679126798467310545830036; uint256 public constant IC2_Y = - 16889028711432227360380575065980535632722375252171627947743748499665179759152; + 2050456376645372578396035075358789073979003008436938146845128992698746141877; uint256 public constant IC3_X = - 8534951274187844857754798765054711469756707542878193818609654720983424682295; + 9819249055709022471783469269872195968940696597382036472257350992099603166183; uint256 public constant IC3_Y = - 8945913628923857906349475216922865350407303486167834270986003060482590360541; + 2401786491025438669071658710401341692741617050151593778325339189669301220899; uint256 public constant IC4_X = - 15451872962493168690150192361017318203301132735619727602712141758148948638807; + 7422007307044478259226064473692631752662233050292352222869411054288745570789; uint256 public constant IC4_Y = - 3257041611034212540028719703800773323519734826811773698850078555263697393038; + 10017850150189997840383565765235365224515214690165127564532641750692280241903; uint256 public constant IC5_X = - 16501527896500369578902237454836690085885521711966021988157538332449691146412; + 19221738488703246937975790853705478702845308522128613703152831351556301989763; uint256 public constant IC5_Y = - 16277901177749764148365216633606366116251292755281475194298759272707407824018; + 10183107801189396942167758711051912294619018039092495644809682417460643023810; uint256 public constant IC6_X = - 10378146563933233933703734244718759999027501367391365750021798315897563444591; + 9076649205859590603560715606056701346727113477211950055621087628388769368386; uint256 public constant IC6_Y = - 6842563751857803307953802478770242999171402515835370020586447923920873099219; + 52455929519905290524842189199006340052096446699011578823444788358568039702; uint256 public constant IC7_X = - 20352721715392263662971537971767346903027859101481416488967643016592847282380; + 18103653070682127942942702359086473888645990538117645003998683697246715437040; uint256 public constant IC7_Y = - 14260881413352838566197164407396428620727197892843554333450508049100242677628; + 20937350421085763292785746194370377209032774713987029070743280053317168289872; uint256 public constant IC8_X = - 21057739816355151565092401964767622282102551265481668904500681957228063344238; + 9758634944624942528872916404218984802068150789828935243358049923155742940304; uint256 public constant IC8_Y = - 16180780523793713645254963281457716926007937226125045923262954827309238281194; + 5615975277864739173632599296778201253863610964605697390030280177993730165032; uint256 public constant IC9_X = - 14537516127080012005197736574343683034537191970611258246890082854105324760933; + 10670001473472856457996828759289176589182316531259991403627649917035565048429; uint256 public constant IC9_Y = - 18442918359750026099515052740471376065513561505084248781730547784475549682043; + 16737644016567474899525575815101539395782539042839027261623709392334896614435; uint256 public constant IC10_X = - 9945678304537190581884908568330352663212605281307078311554845796978128487311; + 5697191124914901219408901513601442561324208164645566497511242718204397743535; uint256 public constant IC10_Y = - 9922952547073080040816726971142108048165624682232524310828932207260681346925; + 13398161619207397794324308592162009615115451562662451839052676578420491271203; uint256 public constant IC11_X = - 121018551111798889520476374750956766867835537125258291153678626712032749835; + 703123888741314718612309048386350636226232723398947337322732826265873810481; uint256 public constant IC11_Y = - 29265305386115914605361057732389333018602730942100360415826037370925178043; + 17990589954423602614263242460599822606354926012790592127594635530017447736000; uint256 public constant IC12_X = - 19393638291779014670875425462009705376277440547297152509532737472607672105229; + 20393624977578917018885892283158705958560546595002668452425361396957472131164; uint256 public constant IC12_Y = - 12637387498267415299358604851073087307458804722402979421904536853588103603115; + 19590758776110701291071416345258278715869484586955132519963026375488500561474; uint256 public constant IC13_X = - 19535226022618801566939497106610112373065946755466822996888611481583748235579; + 12436544099918355712785605826913391779448933207323030677346777208134860342988; uint256 public constant IC13_Y = - 18338463551063409694995636016403970595398232669703454914987172540276159653613; + 12934270829268999096893372612764637040591991408236553218822437244622467331607; uint256 public constant IC14_X = - 8008700345935321927788580584227425845966574602899033915258149217767990418718; + 17956572623335638994475857660706169025078837539836899885807999861812007891829; uint256 public constant IC14_Y = - 185856258559502906253092650522696470738095546583256434424552656167093996151; + 15788541231435136223906443344147580532437154635912716511551645127845433777427; uint256 public constant IC15_X = - 7820954574137895573811525844670273368496497357173205474113176992404687726697; + 17372427373514078940943510142559667491564581290960965924927622740257982261705; uint256 public constant IC15_Y = - 956512349033607095044956195378412396626953742914598187567649677456879320228; + 15857915612980925933684826813016864811106014452704060191542323376399865010576; uint256 public constant IC16_X = - 15573333703789237575530866563016649620625650785245004564133502761898657171416; + 17706658396584983918235581578937131335399954610126317122698339854402062323466; uint256 public constant IC16_Y = - 4318045992191734813427473662920467578914718989290966190779465757655054024866; + 19825721779656394025821381305312046990958274866881259324875151902399022916528; uint256 public constant IC17_X = - 14251137277887630388639941167105402370548590355743069148064181443318051385186; + 20620988361254936794552318469605522229142851776281478715836384141520080141570; uint256 public constant IC17_Y = - 3156000259325297871563018277964544792850822239901608666349236165443650945279; + 8859059503123755609281367511103088644004110641968218967910338769873511051761; uint256 public constant IC18_X = - 5872390235919681263129087861488743087046671364741206348090856282223602944462; + 12631122413809660711555817700981016176438142813482101243197149713058144542227; uint256 public constant IC18_Y = - 16668195091836658784208651432085131085595308441293778882239710806888813163568; + 2556186502760854102348974448979574846991952002272770169461095031136952271616; uint256 public constant IC19_X = - 18514753246385905128088201279079181382420042068951125186140903026107781270079; + 17458248129570466726463169081527160609041285936662432661537029195640964876334; uint256 public constant IC19_Y = - 12671409848969278422503585680858897360251449228569325218012677111477592298915; + 13311660137139351881541212173790929136853538186322689971135798269503166365287; uint256 public constant IC20_X = - 14795534685259818800016539466295714973315210330368104927561037919830323460988; + 20376216183950978302528448687935566484992782628561612737636988612841263279656; uint256 public constant IC20_Y = - 2713630188989527621944627278467445252191128824455445277832095608562212679464; + 20169118954698379344261930520187924042126994913369409717143332017835499608184; uint256 public constant IC21_X = - 14146928308724125026725614632454585439232772772913032956834281111360859175326; + 13590288787452426901921148286585278258898382446775320387044914269174787786911; uint256 public constant IC21_Y = - 11696713101615308965286605927940420687716057765241336395590899692577157174575; + 8747169299236194328036487885749156195653967424912382461452920152586239170329; uint256 public constant IC22_X = - 2235128209339941496221360806283506282754206127175685907922436886644178469613; + 7674325401976365261930217729547312366599000999965896222120534931642700524203; uint256 public constant IC22_Y = - 6082007467329063519640234159158138154643669546952627326867255812484441045661; + 19068217506565083003578053277160250956988289919655860507582338843332571021978; uint256 public constant IC23_X = - 4778277300486848528134752164009065478749360605826055092389954662792598494091; + 19352748579310488859684030546811483397732185861890924210248349868791909751480; uint256 public constant IC23_Y = - 3024652709566317167468475499107319235166534387809203994443662489737171429439; + 12154048074587184269542372566563175850267148361618941040848511983070399994545; uint256 public constant IC24_X = - 20553119678962413077240456112807143224561227202753679727141414460261204287602; + 5781302722013869065299528236353742940267161069046098906267033438704274873397; uint256 public constant IC24_Y = - 353033850139697523308575010181001615711175280648155651143918443424069251681; + 17692093638206231489380310174596348635669977742969481065378179222721950073816; uint256 public constant IC25_X = - 12320369853960213401201536713826981257726604458607453897067807791457490307319; + 1822112472095935411318382311555409139784633448197418086992049484249877317223; uint256 public constant IC25_Y = - 12895878247228443034004154694080502857256319561028939353010666652708930585511; + 21492042244465545003762241091589873862989079378893002689743486138813696694800; uint256 public constant IC26_X = - 772255132879173785462217420966153746864602649093557997703481412106975381771; + 5603351942102452705019860367516099398860849474250143800383911460118571587263; uint256 public constant IC26_Y = - 7590818764269556518156184415909156972825437157984074557355089651665498212893; + 3094600863124428771041895118538692524518496368868992103906097304480737853640; uint256 public constant IC27_X = - 15656520006351949412863400564011439085326031727827048952749172502151384928426; + 7441415126376982767803363803637519563709295595343439983482489919240779377645; uint256 public constant IC27_Y = - 9520418046246602777065803395409630022848696572538622647916460498453086071681; + 3076359757048038301182852617243931521181585988154293760812390384006896356182; uint256 public constant IC28_X = - 203389407731559972686963839373673159907560284831081500283956869457261227598; + 14900441524872817256320500300380092737890857259674155501716427566825132598416; uint256 public constant IC28_Y = - 7452820531660814855014532611843194786449585917006722468041952200572306428747; + 12494120348218091892039246129978021993414195558596456765490457556412967109951; uint256 public constant IC29_X = - 1294403555916294449795173178919244618506718557207433405402610910324041999043; + 5750365781992332384417028751570408231938568123690438954844611265235763415721; uint256 public constant IC29_Y = - 3417552333935828954715155980714171844696841479989449134034592386673267645237; + 12272625903360264588429425090935995728552800950462484265649059624128745160550; uint256 public constant IC30_X = - 4057510152773123091521740504550701903278483323514428067579004823698831606746; + 1862621976991045800447155762811695415777350522333110092968742495664769562796; uint256 public constant IC30_Y = - 255222888879429197662829341108999392987181390679120827600125684828907967217; + 18559201839005851072407866227831468715642200861728631518878477425805757269298; uint256 public constant IC31_X = - 16993165161373202096858097803614125959054307247005919250330093739286073007809; + 1385302304779399923164390639587851358941432391526277932382765163789040623703; uint256 public constant IC31_Y = - 1630518864111342581154347255970093862315458024055440432677999240937750277089; + 11968611566051487792573876012784928369981716184185714307126319967096287002644; uint256 public constant IC32_X = - 1342624472930637816128297475680967043884959574033915950794605458813343173455; + 16587830688199775995326445992945741349754658301661942765538031557545371551275; uint256 public constant IC32_Y = - 10644443477057701293111051850964191288457516065268084028669805887784546476470; + 19754146478491369725133569461490869112850762561747377519184934519660398688407; /// @dev memory pointer sizes uint16 public constant P_PUBLIC_SIGNALS_ACCUMULATOR_SIZE = 128; diff --git a/contracts/verifiers/WithdrawCircuitGroth16Verifier.sol b/contracts/verifiers/WithdrawCircuitGroth16Verifier.sol index f500169..48d31b1 100644 --- a/contracts/verifiers/WithdrawCircuitGroth16Verifier.sol +++ b/contracts/verifiers/WithdrawCircuitGroth16Verifier.sol @@ -34,82 +34,82 @@ contract WithdrawCircuitGroth16Verifier { uint256 public constant GAMMA_Y2 = 8495653923123431417604973247489272438418190587263600148770280649306958101930; uint256 public constant DELTA_X1 = - 11559732032986387107991004021392285783925812861821192530917403151452391805634; + 18413979168680880508427857573499344678105139700199040952837451066239717429821; uint256 public constant DELTA_X2 = - 10857046999023057135944570762232829481370756359578518086990519993285655852781; + 9898626626077518568379228412902424898657681523885533885565411617328460389922; uint256 public constant DELTA_Y1 = - 4082367875863433681332203403145435568316851327593401208105741076214120093531; + 6007184160786324903714517413881807875499448146223314045078954717411897818016; uint256 public constant DELTA_Y2 = - 8495653923123431417604973247489272438418190587263600148770280649306958101930; + 17841574180482311547923292068248229483419102900173905611468380600372583372822; uint256 public constant IC0_X = - 11668030768982174988315578800515098268095958336328620786726427170889418741301; + 1989347808741798604476145984547616952751352055055300243388942465316163536890; uint256 public constant IC0_Y = - 12830128751532076647154044575439533260884816271528132751378948213598797056659; + 18287141091950813472142285432947217857255288156969105476385431648941747790000; uint256 public constant IC1_X = - 122553302691155487971695272446778073436405101025840723275402329359390195354; + 21588062611189889344864608007646956350568930448852990739689758952508651394965; uint256 public constant IC1_Y = - 20406127155715319057309683179282528477509489482001054804640976841555607842541; + 98867617019240069306705043616311394330926299234980402726491820133169484093; uint256 public constant IC2_X = - 10893636774628876142899763192646905724708189468282917758613650147010478133301; + 15550575461234686720063491014368064382464483659875934656158426803714221382565; uint256 public constant IC2_Y = - 148896310776292842756025446759984436974180847194558332931999839598427070960; + 19120379509284667271969003793820599158352811626916363073794854942946766897866; uint256 public constant IC3_X = - 15550575461234686720063491014368064382464483659875934656158426803714221382565; + 20223630843990402409968710763948629834919161710796588491358359466297300700679; uint256 public constant IC3_Y = - 19120379509284667271969003793820599158352811626916363073794854942946766897866; + 2675223734590396478598414102060532012408669002722152868471093004240099725728; uint256 public constant IC4_X = - 11997276881897936472837561222757310995977345728486444480476752931527806516438; + 9784390272502725404771544918680482012603159796207186791664900226814915108846; uint256 public constant IC4_Y = - 737024421379298120335516868702549484509013191532965487105618647991650298484; + 4889052086745915475149417214030367513059993224424723225882729911060823610425; uint256 public constant IC5_X = - 19677974929815656195387967292048914984373675040193074047636585472049442720491; + 2802268021394373347198041589581099782960862283447005903992849422477600826465; uint256 public constant IC5_Y = - 10141508255865662035934756555228488100770997341006900106739896716315583398755; + 10141289372445744520279703235726021593976465063055028344566354047067642069932; uint256 public constant IC6_X = - 17729740485077231525915005240789252003871266035358118446803131706576150070834; + 17981075212502931682373066520328626140643179374293563468968969591565544442937; uint256 public constant IC6_Y = - 21463211066327337387171909090874947331633898607474204358116806224285045707724; + 12322953543578476859639640121428089812676529591711769361698560759547148735859; uint256 public constant IC7_X = - 2541364605460121444367617049274470896879571465893010763118465395946894872402; + 17529192146275002547478358208807882132911034198502579757089079197086721890541; uint256 public constant IC7_Y = - 15666472859923637808751528199087900998876037477297252503877624663975157070339; + 4388812328349628025776515576481472717076265011573169880617467587465858259016; uint256 public constant IC8_X = - 19412597310563204613184634003034326013515621831694714651927930505680641506032; + 13462596197467101747423899664117767499325135553613560455509720229714799487830; uint256 public constant IC8_Y = - 7090834739055515275081335901169439667521190970728112165903260270557944931447; + 12083203262673030953642459095709136489775976186296926881097299438023841911749; uint256 public constant IC9_X = - 687622791121058684572490068566579804627881043520672621913250421784418936468; + 17019906054902305959332406672890853967066335368735806389106047492332171614235; uint256 public constant IC9_Y = - 12438570622645808676528087047069161296724662341925122787880168407993098364378; + 11321377648794280332725602412481768552079979051137249946557057349865848834435; uint256 public constant IC10_X = - 2256336126216590424153819634776022210916631465813758319833132994414000265810; + 12004837936896928828186069165842330195823900241045766933887963792976213731479; uint256 public constant IC10_Y = - 14871133877320414522348861289280860819407005829851620950752250168049105134244; + 2795017854439755881864709191776142922107159587230115159310641655955551793825; uint256 public constant IC11_X = - 5268036746963758940162316014618837296844723451466645707274717691934126976172; + 21721751483859419544076386106520492385804231583471287102329184574269367382127; uint256 public constant IC11_Y = - 14043587525208299419769690167114748602978120733028534419518881521020011459176; + 1494799036427623303630306049192407908077332540741044828452816678230690958760; uint256 public constant IC12_X = - 2154876666208174266715465795960362078373910330071013833946880236172287268609; + 2761434005088267474318136430578008369202159868741997526572692295542766967068; uint256 public constant IC12_Y = - 21039580639663636314965780229016132769762902314737515280504608091159095741241; + 4036048972999556212132214896337493500664620928442864032564561739128205321674; uint256 public constant IC13_X = - 16336909857979898535516215802114635770223252233010396112078559223843933803808; + 3489522740644890307435542545453392570336902680579166380511301988065761857470; uint256 public constant IC13_Y = - 7004567302322641279975389394870214367266003425057422009964578684235048564110; + 20937161376515314963749249507373036130492475294409777928513175104326755904935; uint256 public constant IC14_X = - 7510101589966203229059356316761275261569766791979628349139748348788399588502; + 15246253735871791668224050351529893541576242280197703313685085786454308195198; uint256 public constant IC14_Y = - 6787100935552764653462602515138571334407597634374519037897835434137613528973; + 14542825447756802434365841254734849043796005892758280359295110591666008608130; uint256 public constant IC15_X = - 15839334625619375482935791705322004043072260453789756012451905370323543014491; + 1875928679557964182259760561824797539257356137127515597882745846130733296261; uint256 public constant IC15_Y = - 21752850639855585991626846970887014669858240281956242791436741246893327062812; + 15445101728156033566794154649854201878640042973936971994075877498715300589664; uint256 public constant IC16_X = - 8486962044828549404108781901934570149077571365702200623962117478945962708012; + 7704361725176004808064710428275595097272350957161158742117148391001036974188; uint256 public constant IC16_Y = - 5177923901509395134163622097195381342319188207383408808200785221500245384712; + 2616681396458669498399019027534121017878488418160952267486192744404696102688; /// @dev memory pointer sizes uint16 public constant P_PUBLIC_SIGNALS_ACCUMULATOR_SIZE = 128; diff --git a/hardhat.config.ts b/hardhat.config.ts index b865883..61f1892 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -54,7 +54,7 @@ const config: HardhatUserConfig = { setupSettings: { contributionSettings: { provingSystem: "groth16", - contributions: 0, + contributions: 1, }, onlyFiles: [], skipFiles: [], From 5728cacdf4df43e1f0195fd9b7cbcc466098e80b Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:42:01 -0400 Subject: [PATCH 10/15] ci: fail the build on delta = 1 or stale production verification keys Two failure modes that have both occurred in this repository, neither of which any existing check would catch. delta == gamma in a verification key means the phase-2 trapdoor is 1 and the key accepts forged proofs for any statement. That is what a setup with zero contributions produces, so this makes the unsound configuration fail the build rather than ship quietly. Drift between the production keys and the circuits is the second. With gamma fixed to the G2 generator, the IC public-input commitments depend only on the phase-1 ptau and the compiled constraint system, not on the contributions - so the IC array is a fingerprint of the circuit. Comparing the production and generated verifiers per circuit detects a key that no longer describes the source it is committed alongside. This currently fails on all five production keys, which is correct: they predate the circuit changes in this series and cannot be used until the ceremony is re-run. Written as plain JS rather than TypeScript so it needs no ts-node, which the repo does not declare. --- .github/workflows/ci.yml | 5 +++ package.json | 3 +- scripts/check-verifier-keys.js | 69 ++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 scripts/check-verifier-keys.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67210f4..8b11124 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,3 +33,8 @@ jobs: - name: "Run linter" run: "npm run lint" + + - name: "Check verification keys" + # Fails if any Groth16 key has delta == gamma (trapdoor delta = 1, forgeable for any + # statement), or if a production key's IC commitments no longer match the circuits. + run: "npm run check:verifiers" diff --git a/package.json b/package.json index f4bcde9..202f382 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "postinstall": "npx hardhat compile & npx hardhat zkit make --force && npx hardhat zkit verifiers", "lint:sol": "solhint '**/*.sol' --config ./.solhint.json --ignore-path ./.solhintignore --max-warnings 0 && npx prettier --check '**/*.sol' --config ./.prettierrc", "lint:ts": "npx biome lint .", - "lint": "npm run lint:sol && npm run lint:ts" + "lint": "npm run lint:sol && npm run lint:ts", + "check:verifiers": "node scripts/check-verifier-keys.js" }, "keywords": [], "author": "", diff --git a/scripts/check-verifier-keys.js b/scripts/check-verifier-keys.js new file mode 100644 index 0000000..362115a --- /dev/null +++ b/scripts/check-verifier-keys.js @@ -0,0 +1,69 @@ +const { readFileSync } = require("node:fs"); + +const CIRCUITS = ["Registration", "Mint", "Transfer", "Burn", "Withdraw"]; + +// generated (hardhat-zkit): `uint256 public constant DELTA_X1 = ...;` +// production (snarkjs): `uint256 constant deltax1 = ...;` +const parse = (path) => { + const src = readFileSync(path, "utf8"); + const consts = new Map(); + for (const m of src.matchAll( + /uint256\s+(?:public\s+)?constant\s+([A-Za-z0-9_]+)\s*=\s*(\d+);/g, + )) { + consts.set(m[1].toLowerCase().replace(/_/g, ""), m[2]); + } + + const at = (name) => { + const v = consts.get(name); + if (v === undefined) throw new Error(`${path}: missing constant ${name}`); + return v; + }; + + const ic = []; + for (let i = 0; consts.has(`ic${i}x`); i++) ic.push(at(`ic${i}x`), at(`ic${i}y`)); + if (ic.length === 0) throw new Error(`${path}: no IC constants found`); + + return { + gamma: ["gammax1", "gammax2", "gammay1", "gammay2"].map(at).join(","), + delta: ["deltax1", "deltax2", "deltay1", "deltay2"].map(at).join(","), + ic: ic.join(","), + }; +}; + +const failures = []; + +for (const circuit of CIRCUITS) { + const generatedPath = `contracts/verifiers/${circuit}CircuitGroth16Verifier.sol`; + const prodPath = `contracts/prod/${circuit}Verifier.sol`; + const generated = parse(generatedPath); + const prod = parse(prodPath); + + for (const [label, path, key] of [ + ["generated", generatedPath, generated], + ["production", prodPath, prod], + ]) { + // delta and gamma are independent group elements in any honest setup; equality means + // delta is the G2 generator, i.e. the trapdoor was never randomized + if (key.delta === key.gamma) { + failures.push( + `${path}: delta == gamma, so the phase-2 trapdoor delta is 1 and this ${label} verifier accepts forged proofs for any public input. Re-run the setup with contributions >= 1 (see hardhat.config.ts).`, + ); + } + } + + if (generated.ic !== prod.ic) { + failures.push( + `${prodPath}: IC commitments differ from ${generatedPath}, so the production key was generated from a different constraint system than the current circuits. Proofs built from these circuits will not verify against it, and the key cannot be reproduced from this source tree. Re-run the ceremony.`, + ); + } +} + +if (failures.length > 0) { + console.error(`\nVerification-key checks FAILED (${failures.length}):\n`); + for (const f of failures) console.error(` - ${f}\n`); + process.exit(1); +} + +console.log( + `Verification-key checks passed for ${CIRCUITS.length} circuits (delta != gamma, generated and production IC commitments agree).`, +); From 6b61cd96de054ccb6146d6ce90814d24e7dc4ccd Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:42:22 -0400 Subject: [PATCH 11/15] chore(deploy): make the verifier key set an explicit choice deployVerifiers took isProd as an optional flag defaulting to the generated verifiers, and both deploy scripts called it without passing anything. So the branch that deploys contracts/prod was unreachable and the scripts shipped the development keys - the ones with delta = 1 - purely by omission. A footgun that silent is worth removing even now that the generated keys are sound, because they are still not ceremony output. The parameter is now required, so every caller has to state which key set it means, and the deploy scripts pass true. Note that npm run check:verifiers currently reports the production keys as stale against these circuits, so the setup has to be re-run before either script is pointed at a live network. --- scripts/deploy-converter.ts | 3 +-- scripts/deploy-standalone.ts | 3 +-- test/EncryptedERC-Converter.ts | 2 +- test/helpers.ts | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/deploy-converter.ts b/scripts/deploy-converter.ts index ffc4221..267e625 100644 --- a/scripts/deploy-converter.ts +++ b/scripts/deploy-converter.ts @@ -8,14 +8,13 @@ const main = async () => { const [deployer] = await ethers.getSigners(); // deploy verifiers - // if true, deploys verifiers for prod, generated with proper trusted setup const { registrationVerifier, mintVerifier, withdrawVerifier, transferVerifier, burnVerifier, - } = await deployVerifiers(deployer); + } = await deployVerifiers(deployer, true); // deploy babyjub library const babyJubJub = await deployLibrary(deployer); diff --git a/scripts/deploy-standalone.ts b/scripts/deploy-standalone.ts index b28f6ca..d8ecd48 100644 --- a/scripts/deploy-standalone.ts +++ b/scripts/deploy-standalone.ts @@ -8,14 +8,13 @@ const main = async () => { const [deployer] = await ethers.getSigners(); // deploy verifiers - // if true, deploys verifiers for prod, generated with proper trusted setup const { registrationVerifier, mintVerifier, withdrawVerifier, transferVerifier, burnVerifier, - } = await deployVerifiers(deployer); + } = await deployVerifiers(deployer, true); // deploy babyjub library const babyJubJub = await deployLibrary(deployer); diff --git a/test/EncryptedERC-Converter.ts b/test/EncryptedERC-Converter.ts index 2212b24..b8fa91a 100644 --- a/test/EncryptedERC-Converter.ts +++ b/test/EncryptedERC-Converter.ts @@ -58,7 +58,7 @@ describe("EncryptedERC - Converter", () => { withdrawVerifier, transferVerifier, burnVerifier, - } = await deployVerifiers(owner); + } = await deployVerifiers(owner, false); const babyJubJub = await deployLibrary(owner); for (const d of [6, 18, DECIMALS]) { diff --git a/test/helpers.ts b/test/helpers.ts index d885d93..a1a69f9 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -45,7 +45,7 @@ import type { User } from "./user"; */ export const deployVerifiers = async ( signer: SignerWithAddress, - isProd?: boolean, + isProd: boolean, ) => { if (isProd) { const registrationVerifierFactory = new RegistrationVerifier__factory( From 37a2f560289a486e6936a8d7e6da9b5cc00fa298 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:43:55 -0400 Subject: [PATCH 12/15] docs(circuits): state the Poseidon padding rule in one line Belongs with the padding-count fix earlier in this series; kept separate only to avoid rewriting signed history. Squash it into that commit if you prefer. --- circom/components.circom | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/circom/components.circom b/circom/components.circom index f84124c..7f41774 100644 --- a/circom/components.circom +++ b/circom/components.circom @@ -54,8 +54,7 @@ template PoseidonDecrypt(l) { // Check the last ciphertext element ciphertext[decryptedLength] === strategies[n].out[1]; - // If length > 3, check if the last (3 - (l mod 3)) elements of the message - // are 0 + // If length not devisible by 3 then add padding if (l % 3 > 0) { if (l % 3 == 1) { decrypted[decryptedLength - 1] === 0; From 258a5c5518adafa4e611d274f48c16e2ea31bdd9 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Wed, 19 Aug 2026 11:56:48 -0400 Subject: [PATCH 13/15] chore: set min-release-age --- .npmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..7253a5c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +min-release-age=7 From 8d2bf6ba580f746f70ccdc378e31fb27fd3d35a8 Mon Sep 17 00:00:00 2001 From: Brian Ridings Date: Thu, 20 Aug 2026 12:56:31 -0400 Subject: [PATCH 14/15] Revert "ci: fail the build on delta = 1 or stale production verification keys" This reverts commit 5728cacdf4df43e1f0195fd9b7cbcc466098e80b. --- .github/workflows/ci.yml | 5 --- package.json | 3 +- scripts/check-verifier-keys.js | 69 ---------------------------------- 3 files changed, 1 insertion(+), 76 deletions(-) delete mode 100644 scripts/check-verifier-keys.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b11124..67210f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,8 +33,3 @@ jobs: - name: "Run linter" run: "npm run lint" - - - name: "Check verification keys" - # Fails if any Groth16 key has delta == gamma (trapdoor delta = 1, forgeable for any - # statement), or if a production key's IC commitments no longer match the circuits. - run: "npm run check:verifiers" diff --git a/package.json b/package.json index 202f382..f4bcde9 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,7 @@ "postinstall": "npx hardhat compile & npx hardhat zkit make --force && npx hardhat zkit verifiers", "lint:sol": "solhint '**/*.sol' --config ./.solhint.json --ignore-path ./.solhintignore --max-warnings 0 && npx prettier --check '**/*.sol' --config ./.prettierrc", "lint:ts": "npx biome lint .", - "lint": "npm run lint:sol && npm run lint:ts", - "check:verifiers": "node scripts/check-verifier-keys.js" + "lint": "npm run lint:sol && npm run lint:ts" }, "keywords": [], "author": "", diff --git a/scripts/check-verifier-keys.js b/scripts/check-verifier-keys.js deleted file mode 100644 index 362115a..0000000 --- a/scripts/check-verifier-keys.js +++ /dev/null @@ -1,69 +0,0 @@ -const { readFileSync } = require("node:fs"); - -const CIRCUITS = ["Registration", "Mint", "Transfer", "Burn", "Withdraw"]; - -// generated (hardhat-zkit): `uint256 public constant DELTA_X1 = ...;` -// production (snarkjs): `uint256 constant deltax1 = ...;` -const parse = (path) => { - const src = readFileSync(path, "utf8"); - const consts = new Map(); - for (const m of src.matchAll( - /uint256\s+(?:public\s+)?constant\s+([A-Za-z0-9_]+)\s*=\s*(\d+);/g, - )) { - consts.set(m[1].toLowerCase().replace(/_/g, ""), m[2]); - } - - const at = (name) => { - const v = consts.get(name); - if (v === undefined) throw new Error(`${path}: missing constant ${name}`); - return v; - }; - - const ic = []; - for (let i = 0; consts.has(`ic${i}x`); i++) ic.push(at(`ic${i}x`), at(`ic${i}y`)); - if (ic.length === 0) throw new Error(`${path}: no IC constants found`); - - return { - gamma: ["gammax1", "gammax2", "gammay1", "gammay2"].map(at).join(","), - delta: ["deltax1", "deltax2", "deltay1", "deltay2"].map(at).join(","), - ic: ic.join(","), - }; -}; - -const failures = []; - -for (const circuit of CIRCUITS) { - const generatedPath = `contracts/verifiers/${circuit}CircuitGroth16Verifier.sol`; - const prodPath = `contracts/prod/${circuit}Verifier.sol`; - const generated = parse(generatedPath); - const prod = parse(prodPath); - - for (const [label, path, key] of [ - ["generated", generatedPath, generated], - ["production", prodPath, prod], - ]) { - // delta and gamma are independent group elements in any honest setup; equality means - // delta is the G2 generator, i.e. the trapdoor was never randomized - if (key.delta === key.gamma) { - failures.push( - `${path}: delta == gamma, so the phase-2 trapdoor delta is 1 and this ${label} verifier accepts forged proofs for any public input. Re-run the setup with contributions >= 1 (see hardhat.config.ts).`, - ); - } - } - - if (generated.ic !== prod.ic) { - failures.push( - `${prodPath}: IC commitments differ from ${generatedPath}, so the production key was generated from a different constraint system than the current circuits. Proofs built from these circuits will not verify against it, and the key cannot be reproduced from this source tree. Re-run the ceremony.`, - ); - } -} - -if (failures.length > 0) { - console.error(`\nVerification-key checks FAILED (${failures.length}):\n`); - for (const f of failures) console.error(` - ${f}\n`); - process.exit(1); -} - -console.log( - `Verification-key checks passed for ${CIRCUITS.length} circuits (delta != gamma, generated and production IC commitments agree).`, -); From ace4e9d797ad5475b3b1373b67fd4533dfeb2c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Berat=20=C3=96zt=C3=BCrk?= Date: Mon, 24 Aug 2026 13:39:47 +0300 Subject: [PATCH 15/15] test(history): add pending PCT withdrawal gas coverage (#81) * test(history): add pending PCT withdrawal gas coverage * chore: fix lint issues --- contracts/mocks/EncryptedERCHarness.sol | 31 ++++ .../mocks/EncryptedUserBalancesHarness.sol | 57 ++++++ package-lock.json | 84 ++++----- package.json | 3 +- test/EncryptedERC-Converter.ts | 4 +- test/EncryptedERC-HistoryGas.ts | 164 ++++++++++++++++++ test/EncryptedUserBalances.ts | 56 ++++++ 7 files changed, 347 insertions(+), 52 deletions(-) create mode 100644 contracts/mocks/EncryptedERCHarness.sol create mode 100644 contracts/mocks/EncryptedUserBalancesHarness.sol create mode 100644 test/EncryptedERC-HistoryGas.ts create mode 100644 test/EncryptedUserBalances.ts diff --git a/contracts/mocks/EncryptedERCHarness.sol b/contracts/mocks/EncryptedERCHarness.sol new file mode 100644 index 0000000..2982060 --- /dev/null +++ b/contracts/mocks/EncryptedERCHarness.sol @@ -0,0 +1,31 @@ +// (c) 2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: Ecosystem + +pragma solidity 0.8.27; + +import {EncryptedERC} from "../EncryptedERC.sol"; +import {AmountPCT, CreateEncryptedERCParams, EncryptedBalance} from "../types/Types.sol"; + +/** + * @dev Test-only EncryptedERC variant that can construct a worst-case pending history. + */ +contract EncryptedERCHarness is EncryptedERC { + constructor(CreateEncryptedERCParams memory params) EncryptedERC(params) {} + + function seedPendingHistory( + address user, + uint256 tokenId, + uint256[7] calldata pct, + uint256 count, + uint256 checkpointIndex + ) external { + EncryptedBalance storage balance = balances[user][tokenId]; + for (uint256 i = 0; i < count; i++) { + balance.amountPCTs.push( + AmountPCT({pct: pct, index: checkpointIndex}) + ); + } + } +} diff --git a/contracts/mocks/EncryptedUserBalancesHarness.sol b/contracts/mocks/EncryptedUserBalancesHarness.sol new file mode 100644 index 0000000..27c5002 --- /dev/null +++ b/contracts/mocks/EncryptedUserBalancesHarness.sol @@ -0,0 +1,57 @@ +// (c) 2026, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: Ecosystem + +pragma solidity 0.8.27; + +import {EncryptedUserBalances} from "../EncryptedUserBalances.sol"; +import {AmountPCT, EncryptedBalance} from "../types/Types.sol"; + +/** + * @dev Test-only harness for measuring the cost of pruning pending amount PCT history. + * It seeds fully non-zero entries so the benchmark reflects the expensive storage-clear path. + */ +contract EncryptedUserBalancesHarness is EncryptedUserBalances { + function seedHistory( + address user, + uint256 tokenId, + uint256 count + ) external { + EncryptedBalance storage balance = balances[user][tokenId]; + uint256 startIndex = balance.amountPCTs.length; + + for (uint256 i = 0; i < count; i++) { + uint256[7] memory pct; + for (uint256 j = 0; j < pct.length; j++) { + pct[j] = (startIndex + i) * pct.length + j + 1; + } + balance.amountPCTs.push( + AmountPCT({pct: pct, index: startIndex + i}) + ); + } + + balance.transactionIndex = startIndex + count; + } + + function appendHistory(address user, uint256 tokenId) external { + uint256[7] memory pct; + pct[0] = 1; + _addToUserHistory(user, tokenId, pct); + } + + function pruneHistory( + address user, + uint256 tokenId, + uint256 transactionIndex + ) external { + _deleteUserHistory(user, tokenId, transactionIndex); + } + + function pendingHistoryLength( + address user, + uint256 tokenId + ) external view returns (uint256) { + return balances[user][tokenId].amountPCTs.length; + } +} diff --git a/package-lock.json b/package-lock.json index d6b0b8f..4517cf6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "prettier": "^3.5.3", "prettier-plugin-solidity": "^1.4.2", "solhint": "^5.0.5", - "solidity-coverage": "^0.8.14" + "solidity-coverage": "^0.8.14", + "ts-node": "^10.9.2" } }, "node_modules/@adraffy/ens-normalize": { @@ -241,8 +242,7 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -949,27 +949,24 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -1874,36 +1871,32 @@ } }, "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/@typechain/ethers-v6": { "version": "0.5.1", @@ -2233,12 +2226,11 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2247,12 +2239,11 @@ } }, "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "acorn": "^8.11.0" }, @@ -2412,8 +2403,7 @@ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", @@ -3425,8 +3415,7 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -5952,8 +5941,7 @@ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, - "optional": true, - "peer": true + "license": "ISC" }, "node_modules/markdown-table": { "version": "2.0.0", @@ -8433,8 +8421,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -8474,12 +8461,11 @@ } }, "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, - "optional": true, - "peer": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -8791,8 +8777,7 @@ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true, - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/viem": { "version": "2.29.3", @@ -9223,8 +9208,7 @@ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "engines": { "node": ">=6" } diff --git a/package.json b/package.json index f4bcde9..0b98094 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "prettier": "^3.5.3", "prettier-plugin-solidity": "^1.4.2", "solhint": "^5.0.5", - "solidity-coverage": "^0.8.14" + "solidity-coverage": "^0.8.14", + "ts-node": "^10.9.2" }, "scripts": { "test": "mocha 'src/**/*.test.js'", diff --git a/test/EncryptedERC-Converter.ts b/test/EncryptedERC-Converter.ts index b8fa91a..1ef75c4 100644 --- a/test/EncryptedERC-Converter.ts +++ b/test/EncryptedERC-Converter.ts @@ -302,7 +302,9 @@ describe("EncryptedERC - Converter", () => { expect(balance).to.equal(mintAmount); }); - it("should deposit tokens to EncryptedERC and return the dust properly and mint the proper balance", async () => { + it("should deposit tokens to EncryptedERC and return the dust properly and mint the proper balance", async function () { + this.timeout(120_000); + const ownerUser = users[0]; const erc20 = erc20s[1]; diff --git a/test/EncryptedERC-HistoryGas.ts b/test/EncryptedERC-HistoryGas.ts new file mode 100644 index 0000000..188669e --- /dev/null +++ b/test/EncryptedERC-HistoryGas.ts @@ -0,0 +1,164 @@ +import type { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers, zkit } from "hardhat"; +import type { RegistrationCircuit } from "../generated-types/zkit"; +import { processPoseidonEncryption } from "../src"; +import type { EncryptedERCHarness } from "../typechain-types/contracts/mocks/EncryptedERCHarness"; +import type { Registrar } from "../typechain-types/contracts/Registrar"; +import type { SimpleERC20 } from "../typechain-types/contracts/tokens/SimpleERC20"; +import { Registrar__factory } from "../typechain-types/factories/contracts"; +import { EncryptedERCHarness__factory } from "../typechain-types/factories/contracts/mocks/EncryptedERCHarness__factory"; +import { SimpleERC20__factory } from "../typechain-types/factories/contracts/tokens"; +import { deployLibrary, deployVerifiers, withdraw } from "./helpers"; +import { User } from "./user"; + +const EERC_DECIMALS = 10; +const MAX_PENDING_AMOUNT_PCTS = 300n; +const C_CHAIN_BLOCK_GAS_LIMIT = 15_000_000n; + +describe("EncryptedERC withdrawal at the pending-history cap", () => { + let owner: SignerWithAddress; + let victimSigner: SignerWithAddress; + let registrar: Registrar; + let encryptedERC: EncryptedERCHarness; + let token: SimpleERC20; + let victim: User; + let auditor: User; + + before(async () => { + [owner, victimSigner] = await ethers.getSigners(); + victim = new User(victimSigner); + auditor = new User(owner); + + const { + registrationVerifier, + mintVerifier, + withdrawVerifier, + transferVerifier, + burnVerifier, + } = await deployVerifiers(owner, false); + const babyJubJub = await deployLibrary(owner); + + registrar = await new Registrar__factory(owner).deploy( + registrationVerifier, + ); + await registrar.waitForDeployment(); + + encryptedERC = await new EncryptedERCHarness__factory( + { "contracts/libraries/BabyJubJub.sol:BabyJubJub": babyJubJub }, + owner, + ).deploy({ + registrar: registrar.target, + isConverter: true, + name: "", + symbol: "", + decimals: EERC_DECIMALS, + mintVerifier, + withdrawVerifier, + transferVerifier, + burnVerifier, + }); + await encryptedERC.waitForDeployment(); + + token = await new SimpleERC20__factory(owner).deploy( + "Test", + "TEST", + EERC_DECIMALS, + ); + await token.waitForDeployment(); + + const registrationCircuit = (await zkit.getCircuit( + "RegistrationCircuit", + )) as unknown as RegistrationCircuit; + const chainId = (await ethers.provider.getNetwork()).chainId; + + for (const user of [victim, auditor]) { + const proof = await registrationCircuit.generateProof({ + SenderPrivateKey: user.formattedPrivateKey, + SenderPublicKey: user.publicKey, + SenderAddress: BigInt(user.signer.address), + ChainID: chainId, + RegistrationHash: user.genRegistrationHash(chainId), + }); + const calldata = await registrationCircuit.generateCalldata(proof); + await registrar.connect(user.signer).register(calldata); + } + + await encryptedERC.connect(owner).setAuditorPublicKey(owner.address); + }); + + it("withdraws and clears 300 pending entries within the C-Chain block gas limit", async function () { + this.timeout(12000_000); + const depositAmount = 10_000n; + const withdrawalAmount = 1n; + const { ciphertext, nonce, authKey } = processPoseidonEncryption( + [depositAmount], + victim.publicKey, + ); + + await token.connect(owner).mint(victim.signer.address, depositAmount); + await token + .connect(victim.signer) + .approve(encryptedERC.target, depositAmount); + await encryptedERC + .connect(victim.signer) + [ + "deposit(uint256,address,uint256[7])" + ](depositAmount, token.target, [...ciphertext, ...authKey, nonce]); + + const tokenId = await encryptedERC.tokenIds(token.target); + const zeroValuePCT = processPoseidonEncryption([0n], victim.publicKey); + for (let seeded = 1n; seeded < MAX_PENDING_AMOUNT_PCTS; seeded += 50n) { + const count = + MAX_PENDING_AMOUNT_PCTS - seeded < 50n + ? MAX_PENDING_AMOUNT_PCTS - seeded + : 50n; + + + + const tx = await encryptedERC.seedPendingHistory( + victim.signer.address, + tokenId, + [ + ...zeroValuePCT.ciphertext, + ...zeroValuePCT.authKey, + zeroValuePCT.nonce, + ], + count, + 0, + ); + await tx.wait(); + } + + const balance = await encryptedERC.balanceOf( + victim.signer.address, + tokenId, + ); + expect(balance.amountPCTs).to.have.length(Number(MAX_PENDING_AMOUNT_PCTS)); + + const { proof, userBalancePCT } = await withdraw( + withdrawalAmount, + victim, + [...balance.eGCT.c1, ...balance.eGCT.c2], + depositAmount, + await encryptedERC.auditorPublicKey(), + ); + const tokenBalanceBefore = await token.balanceOf(victim.signer.address); + const tx = await encryptedERC + .connect(victim.signer) + [ + "withdraw(uint256,((uint256[2],uint256[2][2],uint256[2]),uint256[16]),uint256[7])" + ](tokenId, proof, userBalancePCT); + const receipt = await tx.wait(); + + expect(receipt?.gasUsed).to.be.lessThan(C_CHAIN_BLOCK_GAS_LIMIT); + expect(await token.balanceOf(victim.signer.address)).to.equal( + tokenBalanceBefore + withdrawalAmount, + ); + const balanceAfter = await encryptedERC.balanceOf( + victim.signer.address, + tokenId, + ); + expect(balanceAfter.amountPCTs).to.be.empty; + }); +}); diff --git a/test/EncryptedUserBalances.ts b/test/EncryptedUserBalances.ts new file mode 100644 index 0000000..c5e2519 --- /dev/null +++ b/test/EncryptedUserBalances.ts @@ -0,0 +1,56 @@ +import type { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import type { EncryptedUserBalancesHarness } from "../typechain-types/contracts/mocks/EncryptedUserBalancesHarness"; +import { EncryptedUserBalancesHarness__factory } from "../typechain-types/factories/contracts/mocks/EncryptedUserBalancesHarness__factory"; + +describe("EncryptedUserBalances pending history", () => { + let harness: EncryptedUserBalancesHarness; + let owner: SignerWithAddress; + let victim: SignerWithAddress; + + const seedHistory = async (tokenId: bigint, count: bigint) => { + const batchSize = 50n; + for (let seeded = 0n; seeded < count; seeded += batchSize) { + const batch = count - seeded < batchSize ? count - seeded : batchSize; + await harness.seedHistory(victim.address, tokenId, batch); + } + }; + + before(async () => { + [owner, victim] = await ethers.getSigners(); + const factory = new EncryptedUserBalancesHarness__factory(owner); + harness = await factory.deploy(); + await harness.waitForDeployment(); + }); + + it("rejects a credit once the pending-history cap is reached", async () => { + const maxPendingAmountPCTs = await harness.MAX_PENDING_AMOUNT_PCTS(); + await seedHistory(0n, maxPendingAmountPCTs - 1n); + await expect(harness.appendHistory(victim.address, 0)).to.not.be.reverted; + await expect( + harness.appendHistory(victim.address, 0), + ).to.be.revertedWithCustomError(harness, "PendingHistoryLimitReached"); + }); + + it("prunes a worst-case full history within the configured gas budget", async () => { + const tokenId = 1; + const maxPendingAmountPCTs = await harness.MAX_PENDING_AMOUNT_PCTS(); + await seedHistory(BigInt(tokenId), maxPendingAmountPCTs); + + const tx = await harness.pruneHistory( + victim.address, + tokenId, + maxPendingAmountPCTs - 1n, + ); + const receipt = await tx.wait(); + + // Avalanche C-Chain's documented block gas limit reference is 15M. This is + // intentionally only a benchmark guard: a full transfer/withdrawal must be + // measured separately because it also includes proof verification and payout work. + expect(receipt?.gasUsed).to.be.lessThan(12_000_000n); + expect( + await harness.pendingHistoryLength(victim.address, tokenId), + ).to.equal(0n); + }); +});