From 33a4a93e7bc9a6c26571ca9ae4dabced50d86dab Mon Sep 17 00:00:00 2001 From: Maksym Zub Date: Wed, 22 Jan 2025 10:09:31 +0100 Subject: [PATCH 1/7] ZK-382: Implement deposit for shortlist of tokens --- .gitignore | 2 +- Cargo.lock | 63 ++- Cargo.toml | 2 + Makefile | 22 + contracts/ShielderV0_1_0.sol | 397 ++++++++++++++++++ contracts/TokenList.sol | 68 +++ crates/halo2-verifier/Cargo.toml | 5 + crates/halo2-verifier/src/generator-v0_1_0.rs | 152 +++++++ scripts/ShielderV0_1_0.s.sol | 62 +++ ...pgrade.t.sol => ShielderMockUpgrade.t.sol} | 2 +- test/ShielderV0_1_0Upgrade.t.sol | 82 ++++ 11 files changed, 844 insertions(+), 13 deletions(-) create mode 100644 contracts/ShielderV0_1_0.sol create mode 100644 contracts/TokenList.sol create mode 100644 crates/halo2-verifier/src/generator-v0_1_0.rs create mode 100644 scripts/ShielderV0_1_0.s.sol rename test/{ShielderUpgrade.t.sol => ShielderMockUpgrade.t.sol} (98%) create mode 100644 test/ShielderV0_1_0Upgrade.t.sol diff --git a/.gitignore b/.gitignore index d2831e15..5e21699a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ target/ .DS_Store .vscode/ contracts/*Key.sol -contracts/*Verifier.sol +contracts/*Verifier*.sol contracts/Poseidon2T8Assembly.sol **/*output.log *.txt diff --git a/Cargo.lock b/Cargo.lock index bc5bd1a1..882ed2af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2167,7 +2167,8 @@ dependencies = [ "itertools 0.13.0", "powers-of-tau", "ruint", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=4f413a8)", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", "shielder-setup", "type-conversions", ] @@ -2761,7 +2762,7 @@ dependencies = [ "rand", "rstest", "shielder-account", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", "shielder-contract", "shielder-setup", "type-conversions", @@ -2954,6 +2955,17 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "macros" +version = "0.1.0" +source = "git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=4f413a8#4f413a87dac5ceb7cf7581691e1b799c2914cdcc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.89", +] + [[package]] name = "macros" version = "0.1.0" @@ -3562,7 +3574,7 @@ dependencies = [ "halo2_proofs", "halo2curves", "num-bigint", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", ] [[package]] @@ -4577,12 +4589,30 @@ dependencies = [ "rand", "serde", "sha3 0.10.8", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", "shielder-contract", "shielder-setup", "type-conversions", ] +[[package]] +name = "shielder-circuits" +version = "0.1.0" +source = "git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=4f413a8#4f413a87dac5ceb7cf7581691e1b799c2914cdcc" +dependencies = [ + "halo2_poseidon", + "halo2_proofs", + "human_bytes", + "macros 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=4f413a8)", + "once_cell", + "rand", + "rand_core", + "static_assertions", + "strum", + "strum_macros", + "transcript 0.2.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=4f413a8)", +] + [[package]] name = "shielder-circuits" version = "0.1.0" @@ -4592,7 +4622,7 @@ dependencies = [ "halo2_poseidon", "halo2_proofs", "human_bytes", - "macros", + "macros 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", "once_cell", "rand", "rand_chacha", @@ -4603,7 +4633,7 @@ dependencies = [ "strum", "strum_macros", "thiserror 1.0.69", - "transcript", + "transcript 0.2.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", ] [[package]] @@ -4625,7 +4655,7 @@ dependencies = [ "serde_json", "shellexpand", "shielder-account", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", "shielder-contract", "shielder-relayer", "shielder-setup", @@ -4689,7 +4719,7 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "rand", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", ] [[package]] @@ -4703,7 +4733,7 @@ dependencies = [ "rand", "rayon", "shielder-account", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", "thiserror 2.0.11", "type-conversions", "uniffi", @@ -4855,7 +4885,7 @@ dependencies = [ "rand", "reqwest", "shielder-account", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", "shielder-contract", "shielder-relayer", "shielder-setup", @@ -5039,7 +5069,7 @@ dependencies = [ "clap", "halo2_proofs", "shielder-account", - "shielder-circuits", + "shielder-circuits 0.1.0 (git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=7743b2f)", "type-conversions", ] @@ -5447,6 +5477,17 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "transcript" +version = "0.2.0" +source = "git+ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits?rev=4f413a8#4f413a87dac5ceb7cf7581691e1b799c2914cdcc" +dependencies = [ + "halo2_proofs", + "itertools 0.13.0", + "ruint", + "sha3 0.10.8", +] + [[package]] name = "transcript" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 1bdd34b6..2b6979b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,8 @@ sha3 = { version = "0.10" } shellexpand = { version = "3.1.0" } # https://github.com/Cardinal-Cryptography/zkOS-circuits/tree/7743b2f084d80685041c1b16718e6388226e9631 shielder-circuits = { git = "ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits", rev = "7743b2f" } +# https://github.com/Cardinal-Cryptography/zkOS-circuits/tree/4f413a87dac5ceb7cf7581691e1b799c2914cdcc +shielder-circuits-v0_1_0 = { package = "shielder-circuits", git = "ssh://git@github.com/Cardinal-Cryptography/zkOS-circuits", rev = "4f413a8" } testcontainers = { version = "0.19.0" } thiserror = { version = "2.0.9" } tokio = { version = "1.38.0" } diff --git a/Makefile b/Makefile index e96eef88..a76aa1ad 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,16 @@ else PRIVATE_KEY=$(PRIVATE_KEY) OWNER_ADDRESS=$(OWNER_ADDRESS) forge script DeployShielderScript --broadcast --rpc-url $(NETWORK) --sender $(shell cast wallet address $(PRIVATE_KEY)) endif +.PHONY: deploy-contracts-v0_1_0 +deploy-contracts-v0_1_0: # Deploy solidity contracts +deploy-contracts-v0_1_0: +ifeq ($(NETWORK),anvil) + $(eval PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80) \ + PRIVATE_KEY=$(PRIVATE_KEY) OWNER_ADDRESS=$(OWNER_ADDRESS) forge script DeployShielderV0_1_0Script --broadcast --rpc-url anvil --sender $(shell cast wallet address $(PRIVATE_KEY)) +else + PRIVATE_KEY=$(PRIVATE_KEY) OWNER_ADDRESS=$(OWNER_ADDRESS) forge script DeployShielderV0_1_0Script --broadcast --rpc-url $(NETWORK) --sender $(shell cast wallet address $(PRIVATE_KEY)) +endif + .PHONY: generate-poseidon-contracts generate-poseidon-contracts: # Generate Poseidon contract generate-poseidon-contracts: @@ -69,10 +79,22 @@ generate-verifier-contracts: cargo run --release --bin halo2_solidity_verifier_generator $(MAKE) format-contracts + +.PHONY: generate-verifier-contracts-v0_1_0 +generate-verifier-contracts-v0_1_0: # Generate relation verifier contracts for v0_1_0 contract +generate-verifier-contracts-v0_1_0: + cd crates/halo2-verifier + cargo run --release --bin halo2_solidity_verifier_generator_v0_1_0 + $(MAKE) format-contracts + .PHONY: generate-contracts generate-contracts: # Generate poseidon & relation verifier contracts generate-contracts: generate-poseidon-contracts generate-verifier-contracts +.PHONY: generate-contracts-v0_1_0 +generate-contracts-v0_1_0: # Generate poseidon & relation verifier contracts +generate-contracts-v0_1_0: generate-poseidon-contracts generate-verifier-contracts-v0_1_0 + .PHONY: measure-gas measure-gas: # measure shielder gas usage measure-gas: compile-contracts diff --git a/contracts/ShielderV0_1_0.sol b/contracts/ShielderV0_1_0.sol new file mode 100644 index 00000000..dfef8c78 --- /dev/null +++ b/contracts/ShielderV0_1_0.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.26; + +import { DepositLimit } from "./DepositLimit.sol"; +import { Halo2Verifier as DepositVerifier } from "./DepositVerifierV0_1_0.sol"; +import { Halo2Verifier as NewAccountVerifier } from "./NewAccountVerifierV0_1_0.sol"; +import { Halo2Verifier as WithdrawVerifier } from "./WithdrawVerifierV0_1_0.sol"; +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { MerkleTree } from "./MerkleTree.sol"; +import { Nullifiers } from "./Nullifiers.sol"; +import { TokenList } from "./TokenList.sol"; +import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title ShielderV0_1_0 + * @author CardinalCryptography + * @custom:oz-upgrades-from Shielder + * @custom:oz-upgrades-unsafe-allow external-library-linking + */ +contract ShielderV0_1_0 is + Initializable, + UUPSUpgradeable, + Ownable2StepUpgradeable, + PausableUpgradeable, + MerkleTree, + Nullifiers, + DepositLimit, + TokenList +{ + // -- Constants -- + + /// The contract version, in the form `0x{v1}{v2}{v3}`, where: + /// - `v1` is the version of the note schema, + /// - `v1.v2` is the version of the circuits used, + /// - `v1.v2.v3` is the version of the contract itself. + bytes3 public constant CONTRACT_VERSION = 0x000100; + + /// This amount of gas should be sufficient for ether transfers + /// and simple fallback function execution, yet still protecting against reentrancy attack. + uint256 public constant GAS_LIMIT = 3500; + + /// The range check in circuits will work only if we ensure bounded transaction values + /// on the contract side. + uint256 public constant MAX_TRANSACTION_AMOUNT = 2 ** 112 - 1; + + /// Safeguards against a scenario when multiple deposits create a shielded account balance + /// that fails the new account balance range check in the withdrawal circuit, thus + /// making withdrawals impossible. In the contract we can't control a single shielded balance, + /// so we control the sum of balances instead. + uint256 public constant MAX_CONTRACT_BALANCE = MAX_TRANSACTION_AMOUNT; + + /// The modulus of the field used in the circuits. + uint256 private constant FIELD_MODULUS = + 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + // -- Events -- + event NewAccountNative( + bytes3 contractVersion, + uint256 idHash, + uint256 amount, + uint256 newNote, + uint256 newNoteIndex + ); + event DepositNative( + bytes3 contractVersion, + uint256 idHiding, + uint256 amount, + uint256 newNote, + uint256 newNoteIndex, + uint256 tokenIndex + ); + event WithdrawNative( + bytes3 contractVersion, + uint256 idHiding, + uint256 amount, + address to, + uint256 newNote, + uint256 newNoteIndex, + address relayerAddress, + uint256 fee + ); + + // -- Errors -- + + error DepositVerificationFailed(); + error DuplicatedNullifier(); + error FeeHigherThanAmount(); + error MerkleRootDoesNotExist(); + error NativeTransferFailed(); + error WithdrawVerificationFailed(); + error NewAccountVerificationFailed(); + error ZeroAmount(); + error AmountTooHigh(); + error ContractBalanceLimitReached(); + error WrongContractVersion(bytes3 actual, bytes3 expectedByCaller); + error NotAFieldElement(); + error TokenDoesNotExist(); + + modifier restrictContractVersion(bytes3 expectedByCaller) { + if (expectedByCaller != CONTRACT_VERSION) { + revert WrongContractVersion(CONTRACT_VERSION, expectedByCaller); + } + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize() public reinitializer(2) { + __TokenList_init(); + } + + /// @dev required by the OZ UUPS module + function _authorizeUpgrade(address) internal override onlyOwner {} + + /// @dev disable possibility to renounce ownership + function renounceOwnership() public virtual override onlyOwner {} + + function pause() external onlyOwner { + _pause(); + } + + function unpause() external onlyOwner { + _unpause(); + } + + /* + * Creates a fresh note, with an optional native token deposit. + * + * This transaction serves as the entrypoint to the Shielder. + */ + function newAccountNative( + bytes3 expectedContractVersion, + uint256 newNote, + uint256 idHash, + bytes calldata proof + ) + external + payable + whenNotPaused + withinDepositLimit + restrictContractVersion(expectedContractVersion) + fieldElement(newNote) + fieldElement(idHash) + { + uint256 amount = msg.value; + if (nullifiers(idHash) != 0) revert DuplicatedNullifier(); + // `address(this).balance` already includes `msg.value`. + if (address(this).balance > MAX_CONTRACT_BALANCE) { + revert ContractBalanceLimitReached(); + } + + // @dev must follow the same order as in the circuit + uint256[] memory publicInputs = new uint256[](3); + publicInputs[0] = newNote; + publicInputs[1] = idHash; + publicInputs[2] = amount; + + bool success = NewAccountVerifier.verifyProof(proof, publicInputs); + + if (!success) revert NewAccountVerificationFailed(); + + uint256 index = _addNote(newNote); + _registerNullifier(idHash); + + emit NewAccountNative(CONTRACT_VERSION, idHash, amount, newNote, index); + } + + /* + * Make a native token deposit into the Shielder + */ + function depositNative( + bytes3 expectedContractVersion, + uint256 idHiding, + uint256 oldNullifierHash, + uint256 newNote, + uint256 merkleRoot, + bytes calldata proof + ) + external + payable + whenNotPaused + withinDepositLimit + restrictContractVersion(expectedContractVersion) + fieldElement(idHiding) + fieldElement(oldNullifierHash) + fieldElement(newNote) + { + uint256 amount = msg.value; + if (amount == 0) revert ZeroAmount(); + if (nullifiers(oldNullifierHash) != 0) revert DuplicatedNullifier(); + if (!_merkleRootExists(merkleRoot)) revert MerkleRootDoesNotExist(); + // `address(this).balance` already includes `msg.value`. + if (address(this).balance > MAX_CONTRACT_BALANCE) { + revert ContractBalanceLimitReached(); + } + + // @dev needs to match the order in the circuit + uint256[] memory publicInputs = new uint256[](5); + publicInputs[0] = idHiding; + publicInputs[1] = merkleRoot; + publicInputs[2] = oldNullifierHash; + publicInputs[3] = newNote; + publicInputs[4] = amount; + publicInputs[5] = 0; // token index + + bool success = DepositVerifier.verifyProof(proof, publicInputs); + + if (!success) revert DepositVerificationFailed(); + + uint256 newNoteIndex = _addNote(newNote); + _registerNullifier(oldNullifierHash); + + emit DepositNative( + CONTRACT_VERSION, + idHiding, + amount, + newNote, + newNoteIndex, + 0 + ); + } + + /* + * Make an ERC-20 token deposit into the Shielder + */ + function depositERC20( + bytes3 expectedContractVersion, + uint256 idHiding, + uint256 oldNullifierHash, + uint256 newNote, + uint256 merkleRoot, + uint256 amount, + uint256 tokenIndex, + bytes calldata proof + ) + external + restrictContractVersion(expectedContractVersion) + fieldElement(idHiding) + fieldElement(oldNullifierHash) + fieldElement(newNote) + { + if (amount == 0) revert ZeroAmount(); + if (amount > depositLimit()) revert AmountOverDepositLimit(); + if (nullifiers(oldNullifierHash) != 0) revert DuplicatedNullifier(); + if (!_merkleRootExists(merkleRoot)) revert MerkleRootDoesNotExist(); + address tokenAddress = getTokenAddress(tokenIndex); + if (tokenAddress == address(0)) revert TokenDoesNotExist(); + // transfer the tokens to the contract + IERC20(tokenAddress).transferFrom(msg.sender, address(this), amount); + if ( + IERC20(tokenAddress).balanceOf(address(this)) > MAX_CONTRACT_BALANCE + ) { + revert ContractBalanceLimitReached(); + } + // @dev needs to match the order in the circuit + uint256[] memory publicInputs = new uint256[](5); + publicInputs[0] = idHiding; + publicInputs[1] = merkleRoot; + publicInputs[2] = oldNullifierHash; + publicInputs[3] = newNote; + publicInputs[4] = amount; + publicInputs[5] = tokenIndex; + + bool success = DepositVerifier.verifyProof(proof, publicInputs); + + if (!success) revert DepositVerificationFailed(); + + uint256 newNoteIndex = _addNote(newNote); + _registerNullifier(oldNullifierHash); + + emit DepositNative( + CONTRACT_VERSION, + idHiding, + amount, + newNote, + newNoteIndex, + tokenIndex + ); + } + + /* + * Withdraw shielded native funds + */ + function withdrawNative( + bytes3 expectedContractVersion, + uint256 idHiding, + uint256 amount, + address withdrawAddress, + uint256 merkleRoot, + uint256 oldNullifierHash, + uint256 newNote, + bytes calldata proof, + address relayerAddress, + uint256 relayerFee + ) + external + whenNotPaused + restrictContractVersion(expectedContractVersion) + fieldElement(idHiding) + fieldElement(oldNullifierHash) + fieldElement(newNote) + { + if (amount == 0) revert ZeroAmount(); + if (amount <= relayerFee) revert FeeHigherThanAmount(); + if (amount > MAX_TRANSACTION_AMOUNT) revert AmountTooHigh(); + + if (!_merkleRootExists(merkleRoot)) revert MerkleRootDoesNotExist(); + if (nullifiers(oldNullifierHash) != 0) revert DuplicatedNullifier(); + + // @dev needs to match the order in the circuit + uint256[] memory publicInputs = new uint256[](6); + publicInputs[0] = idHiding; + publicInputs[1] = merkleRoot; + publicInputs[2] = oldNullifierHash; + publicInputs[3] = newNote; + publicInputs[4] = amount; + + bytes memory commitment = abi.encodePacked( + CONTRACT_VERSION, + addressToUInt256(withdrawAddress), + addressToUInt256(relayerAddress), + relayerFee + ); + // @dev shifting right by 4 bits so the commitment is smaller from r + publicInputs[5] = uint256(keccak256(commitment)) >> 4; + + bool success = WithdrawVerifier.verifyProof(proof, publicInputs); + + if (!success) revert WithdrawVerificationFailed(); + + uint256 newNoteIndex = _addNote(newNote); + _registerNullifier(oldNullifierHash); + + // return the tokens + (bool nativeTransferSuccess, ) = withdrawAddress.call{ + value: amount - relayerFee, + gas: GAS_LIMIT + }(""); + if (!nativeTransferSuccess) revert NativeTransferFailed(); + + // pay out the fee + (nativeTransferSuccess, ) = relayerAddress.call{ + value: relayerFee, + gas: GAS_LIMIT + }(""); + if (!nativeTransferSuccess) revert NativeTransferFailed(); + + emit WithdrawNative( + CONTRACT_VERSION, + idHiding, + amount, + withdrawAddress, + newNote, + newNoteIndex, + relayerAddress, + relayerFee + ); + } + + function addressToUInt256(address addr) public pure returns (uint256) { + return uint256(uint160(addr)); + } + + modifier fieldElement(uint256 x) { + require(x < FIELD_MODULUS, NotAFieldElement()); + _; + } + + // -- Setters --- + + /* + * Set the deposit limit for the maximal amount + */ + function setDepositLimit(uint256 _depositLimit) external onlyOwner { + _setDepositLimit(_depositLimit); + } + + /* + * Add a token to the list + */ + function addTokenToList(address _token) external onlyOwner { + _addTokenToList(_token); + } + + /* + * Remove the last token from the list + */ + function removeLastToken() external onlyOwner { + _removeLastToken(); + } +} diff --git a/contracts/TokenList.sol b/contracts/TokenList.sol new file mode 100644 index 00000000..f6b082ce --- /dev/null +++ b/contracts/TokenList.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.26; + +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +abstract contract TokenList is Initializable { + // keccak256(abi.encode(uint256(keccak256("zkos.storage.TokenList")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant TOKEN_LIST_LOCATION = + 0xc61234ddf70726f3926fa5321981a41638b45dd9806cf5e5b3ad94aec3c7af00; + + /// @custom:storage-location erc7201:zkos.storage.TokenList + struct TokenListStorage { + uint256 tokensNumber; + mapping(uint256 => address) tokenAddressByIndex; + } + + function _getTokenListStorage() + private + pure + returns (TokenListStorage storage $) + { + assembly { + $.slot := TOKEN_LIST_LOCATION + } + } + + // solhint-disable-next-line func-name-mixedcase + function __TokenList_init() internal onlyInitializing { + TokenListStorage storage $ = _getTokenListStorage(); + // reserve the first index for the native token + $.tokensNumber = 1; + } + + function getTokenAddress( + uint256 _tokenIndex + ) public view returns (address) { + TokenListStorage storage $ = _getTokenListStorage(); + return $.tokenAddressByIndex[_tokenIndex]; + } + + function getTokens() public view returns (address[] memory) { + TokenListStorage storage $ = _getTokenListStorage(); + address[] memory tokens = new address[]($.tokensNumber); + for (uint256 i = 0; i < $.tokensNumber; i++) { + tokens[i] = $.tokenAddressByIndex[i]; + } + return tokens; + } + + /* + * Add a token to the list + */ + function _addTokenToList(address _token) internal { + TokenListStorage storage $ = _getTokenListStorage(); + $.tokenAddressByIndex[$.tokensNumber] = _token; + $.tokensNumber++; + } + + function _removeLastToken() internal { + TokenListStorage storage $ = _getTokenListStorage(); + if ($.tokensNumber == 1) { + // cannot remove the native token + return; + } + delete $.tokenAddressByIndex[$.tokensNumber - 1]; + $.tokensNumber--; + } +} diff --git a/crates/halo2-verifier/Cargo.toml b/crates/halo2-verifier/Cargo.toml index cb926b01..807eced8 100644 --- a/crates/halo2-verifier/Cargo.toml +++ b/crates/halo2-verifier/Cargo.toml @@ -11,6 +11,10 @@ path = "src/lib/lib.rs" name = "halo2_solidity_verifier_generator" path = "src/generator.rs" +[[bin]] +name = "halo2_solidity_verifier_generator_v0_1_0" +path = "src/generator-v0_1_0.rs" + [dependencies] alloy-primitives = { workspace = true } alloy-sol-types = { workspace = true } @@ -21,5 +25,6 @@ itertools = { workspace = true } powers-of-tau = { workspace = true } ruint = { workspace = true } shielder-circuits = { workspace = true } +shielder-circuits-v0_1_0 = { workspace = true } shielder-setup = { workspace = true } type-conversions = { workspace = true } diff --git a/crates/halo2-verifier/src/generator-v0_1_0.rs b/crates/halo2-verifier/src/generator-v0_1_0.rs new file mode 100644 index 00000000..6d35f04f --- /dev/null +++ b/crates/halo2-verifier/src/generator-v0_1_0.rs @@ -0,0 +1,152 @@ +extern crate shielder_circuits_v0_1_0 as shielder_circuits; + +use std::{fs::File, io::Write, path::PathBuf, str}; + +use halo2_proofs::{ + halo2curves::bn256::{Bn256, Fr}, + poly::kzg::commitment::ParamsKZG, +}; +use halo2_solidity_verifier::{BatchOpenScheme::Bdfg21, SolidityGenerator}; +use powers_of_tau::{get_ptau_file_path, read as read_setup_parameters, Format}; +use shielder_circuits::{ + circuits::{generate_keys_with_min_k, Params}, + deposit::DepositProverKnowledge, + new_account::NewAccountProverKnowledge, + withdraw::WithdrawProverKnowledge, + EnumCount, ProverKnowledge, MAX_K, +}; + +const CONTRACTS_DIR: &str = "./contracts"; + +pub fn main() { + let full_parameters = read_setup_parameters( + get_ptau_file_path(MAX_K, Format::PerpetualPowersOfTau), + Format::PerpetualPowersOfTau, + ) + .expect("failed to read parameters from the ptau file"); + + handle_relation::>(full_parameters.clone(), "NewAccount"); + handle_relation::>(full_parameters.clone(), "Deposit"); + handle_relation::>(full_parameters, "Withdraw"); +} + +/// Generate verifier contract for the given circuit type. +fn handle_relation(full_params: Params, relation: &str) { + println!("Generating {relation} relation contracts..."); + let verifier_solidity = generate_solidity_verification_bundle::(full_params); + save_contract_source(&format!("{relation}VerifierV0_1_0.sol"), &verifier_solidity); +} + +/// Given trusted setup, generate Solidity code for the verifier with embedded verification key. +fn generate_solidity_verification_bundle( + full_parameters: ParamsKZG, +) -> String { + let (parameters, _, _, vk) = + generate_keys_with_min_k::(full_parameters).expect("Failed to generate keys"); + SolidityGenerator::new(¶meters, &vk, Bdfg21, PK::PublicInput::COUNT) + .render() + .expect("Failed to generate separate contracts") +} + +/// Writes solidity source code to the file under `CONTRACTS_DIR` directory. +fn save_contract_source(filename: &str, solidity: &str) { + let path = PathBuf::from(format!("{CONTRACTS_DIR}/{filename}")); + File::create(path) + .unwrap() + .write_all(solidity.as_bytes()) + .expect("Can write to file"); +} + +#[cfg(test)] +mod test { + use alloy_primitives::Address; + use alloy_sol_types::SolValue; + use evm_utils::{compilation::source_to_bytecode, EvmRunner, EvmRunnerError, SuccessResult}; + use halo2_proofs::halo2curves::bn256::Fr; + use halo2_solidity_verifier::verifier_contract; + use shielder_circuits::{ + circuits::{generate_proof, generate_setup_params}, + consts::MAX_K, + deposit::DepositProverKnowledge, + generate_keys_with_min_k, + new_account::NewAccountProverKnowledge, + withdraw::WithdrawProverKnowledge, + ProverKnowledge, + }; + use shielder_setup::parameter_generation::rng; + + use crate::generate_solidity_verification_bundle; + + // constants to safeguard against regressions, 110% of MEASURED_GAS + pub const NEW_ACCOUNT_VERIFICATION_GAS_COST: u64 = 706212; //1.1 * 642011; + pub const DEPOSIT_VERIFICATION_GAS_COST: u64 = 914940; //1.1 * 831764; + pub const WITHDRAW_VERIFICATION_GAS_COST: u64 = 1017855; //1.1 * 925323; + + fn deploy_source_code(source: &str, contract_name: &str, evm: &mut EvmRunner) -> Address { + let bytecode = source_to_bytecode(source, contract_name, true); + evm.create(bytecode, None) + .expect("Contract can be deployed") + } + + /// Verify proof and return the gas used + /// + /// Return an error if verifier fails on-chain. + fn verify_with_contract( + verifier_solidity: &str, + proof: &[u8], + public_input: &[Fr], + ) -> Result { + let mut evm = EvmRunner::aleph_evm(); + + // Deploy verifier and vk contracts + let verifier_address = deploy_source_code(verifier_solidity, "Halo2Verifier", &mut evm); + + // Call verifier contract + let calldata = verifier_contract::encode_calldata(proof, public_input); + match evm.call(verifier_address, calldata, None, None) { + Ok(SuccessResult { + gas_used, output, .. + }) => { + println!("Gas cost of verifying: {gas_used}"); + assert!(::abi_decode(&output, true).unwrap()); + Ok(gas_used) + } + Err(why) => Err(why), + } + } + + // Generate proof for an example relation instance and verify it with the Solidity contract. + fn prove_and_verify(cost_upper_bound: u64) { + let mut rng = rng(); + let full_parameters = generate_setup_params(MAX_K, &mut rng); + let prover_knowledge = PK::random_correct_example(&mut rng); + let public_input = prover_knowledge.serialize_public_input(); + + let verifier_solidity = + generate_solidity_verification_bundle::(full_parameters.clone()); + + let (parameters, _, pk, _) = + generate_keys_with_min_k::(full_parameters).unwrap(); + let circuit = prover_knowledge.create_circuit(); + let proof = generate_proof(¶meters, &pk, circuit, &public_input, &mut rng); + + let result = verify_with_contract(&verifier_solidity, &proof, &public_input); + assert!(result.is_ok()); + assert!(result.unwrap() <= cost_upper_bound); + } + + #[test] + fn prove_and_verify_new_account() { + prove_and_verify::>(NEW_ACCOUNT_VERIFICATION_GAS_COST); + } + + #[test] + fn prove_and_verify_deposit() { + prove_and_verify::>(DEPOSIT_VERIFICATION_GAS_COST); + } + + #[test] + fn prove_and_verify_withdraw() { + prove_and_verify::>(WITHDRAW_VERIFICATION_GAS_COST); + } +} diff --git a/scripts/ShielderV0_1_0.s.sol b/scripts/ShielderV0_1_0.s.sol new file mode 100644 index 00000000..0b95d955 --- /dev/null +++ b/scripts/ShielderV0_1_0.s.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.14; + +import { Script, console2 } from "forge-std/Script.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Shielder } from "../contracts/Shielder.sol"; +import { ShielderV0_1_0 } from "../contracts/ShielderV0_1_0.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/* solhint-disable no-console */ +contract DeployShielderV0_1_0Script is Script { + function run() external { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + + address owner = vm.envAddress("OWNER_ADDRESS"); + address broadcaster = vm.addr(privateKey); + console2.log("Using", broadcaster, "as broadcaster"); + + vm.startBroadcast(privateKey); + + address shielderImplementation = address(new Shielder()); + + console2.log( + "Shielder Implementation deployed at:", + address(shielderImplementation) + ); + + bytes memory data = abi.encodeCall( + Shielder.initialize, + (owner, type(uint256).max) + ); + + address proxy = address(new ERC1967Proxy(shielderImplementation, data)); + + address shielderV0_1_0Implementation = address(new ShielderV0_1_0()); + + console2.log( + "ShielderV0_1_0 Implementation deployed at:", + address(shielderV0_1_0Implementation) + ); + + bytes memory dataV0_1_0 = abi.encodeWithSignature("initialize()"); + + UUPSUpgradeable(proxy).upgradeToAndCall( + shielderV0_1_0Implementation, + dataV0_1_0 + ); + + Shielder shielder = Shielder(proxy); + + console2.log("Shielder deployed at:", address(shielder)); + if (owner == broadcaster) { + shielder.unpause(); + } + console2.logBytes3(shielder.CONTRACT_VERSION()); + console2.log("Owner:", shielder.owner()); + + vm.stopBroadcast(); + } +} diff --git a/test/ShielderUpgrade.t.sol b/test/ShielderMockUpgrade.t.sol similarity index 98% rename from test/ShielderUpgrade.t.sol rename to test/ShielderMockUpgrade.t.sol index 598712b8..99d0f6c8 100644 --- a/test/ShielderUpgrade.t.sol +++ b/test/ShielderMockUpgrade.t.sol @@ -8,7 +8,7 @@ import { Upgrades, Options } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { Shielder } from "../contracts/Shielder.sol"; import { ShielderV2Mock } from "../contracts/ShielderV2Mock.sol"; -contract ShielderUpgrade is Test { +contract ShielderMockUpgrade is Test { address public owner; uint256 public depositLimit = 100e18; diff --git a/test/ShielderV0_1_0Upgrade.t.sol b/test/ShielderV0_1_0Upgrade.t.sol new file mode 100644 index 00000000..a408e088 --- /dev/null +++ b/test/ShielderV0_1_0Upgrade.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.26; + +import { Test } from "forge-std/Test.sol"; +import { CustomUpgrades } from "./Utils.sol"; +import { Upgrades, Options } from "openzeppelin-foundry-upgrades/Upgrades.sol"; + +import { Shielder } from "../contracts/Shielder.sol"; +import { ShielderV0_1_0 } from "../contracts/ShielderV0_1_0.sol"; + +contract ShielderV0_1_0Upgrade is Test { + address public owner; + uint256 public depositLimit = 100e18; + string[] public allowedErrors; + + function setUp() public { + owner = msg.sender; + vm.startPrank(owner); + } + + function testUpgrade() public { + // Deploy upgradeable Shielder + address shielderProxy = Upgrades.deployUUPSProxy( + "Shielder.sol:Shielder", + abi.encodeCall(Shielder.initialize, (owner, depositLimit)) + ); + Shielder shielder = Shielder(shielderProxy); + + bytes3 version = shielder.CONTRACT_VERSION(); + vm.assertEq(version, bytes3(0x000001)); + + address currentOwner = shielder.owner(); + vm.assertEq(currentOwner, owner); + + bool paused = shielder.paused(); + vm.assertEq(paused, true); + + uint256 currentDepositLimit = shielder.depositLimit(); + vm.assertEq(currentDepositLimit, depositLimit); + + (, uint256 nextFreeLeafId, , ) = shielder.merkleTree(); + vm.assertNotEq(nextFreeLeafId, 0); + + // upgrade Shielder + CustomUpgrades.upgradeProxyWithErrors( + shielderProxy, + "ShielderV0_1_0.sol:ShielderV0_1_0", + abi.encodeWithSignature("initialize()"), + allowedErrors + ); + + ShielderV0_1_0 shielderV0_1_0 = ShielderV0_1_0(shielderProxy); + + bytes3 newVersion = shielderV0_1_0.CONTRACT_VERSION(); + vm.assertEq(newVersion, bytes3(0x000100)); + + address newOwner = shielderV0_1_0.owner(); + vm.assertEq(newOwner, currentOwner); + + bool newPaused = shielderV0_1_0.paused(); + vm.assertEq(newPaused, paused); + + uint256 newDepositLimit = shielderV0_1_0.depositLimit(); + vm.assertEq(newDepositLimit, currentDepositLimit); + + (, uint256 nextFreeLeafIdV0_1_0, , ) = shielderV0_1_0.merkleTree(); + vm.assertEq(nextFreeLeafId, nextFreeLeafIdV0_1_0); + + shielderV0_1_0.addTokenToList(address(1234)); + address tokenAddress = shielderV0_1_0.getTokenAddress(1); + vm.assertEq(tokenAddress, address(1234)); + + address[] memory tokens = shielderV0_1_0.getTokens(); + vm.assertEq(tokens.length, 2); + vm.assertEq(tokens[0], address(0)); // native token + vm.assertEq(tokens[1], address(1234)); // added token + + shielderV0_1_0.removeLastToken(); + tokenAddress = shielderV0_1_0.getTokenAddress(1); + vm.assertEq(tokenAddress, address(0)); + } +} From 4685ee47e3b011a6998bb05a482a3d2309f6c6c6 Mon Sep 17 00:00:00 2001 From: Maksym Zub Date: Wed, 22 Jan 2025 10:20:23 +0100 Subject: [PATCH 2/7] linter --- contracts/ShielderV0_1_0.sol | 1 + scripts/ShielderV0_1_0.s.sol | 3 +++ test/ShielderV0_1_0Upgrade.t.sol | 5 ++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/ShielderV0_1_0.sol b/contracts/ShielderV0_1_0.sol index dfef8c78..46132468 100644 --- a/contracts/ShielderV0_1_0.sol +++ b/contracts/ShielderV0_1_0.sol @@ -20,6 +20,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * @custom:oz-upgrades-from Shielder * @custom:oz-upgrades-unsafe-allow external-library-linking */ +// solhint-disable-next-line contract-name-camelcase contract ShielderV0_1_0 is Initializable, UUPSUpgradeable, diff --git a/scripts/ShielderV0_1_0.s.sol b/scripts/ShielderV0_1_0.s.sol index 0b95d955..f7f9c211 100644 --- a/scripts/ShielderV0_1_0.s.sol +++ b/scripts/ShielderV0_1_0.s.sol @@ -10,6 +10,7 @@ import { ShielderV0_1_0 } from "../contracts/ShielderV0_1_0.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; /* solhint-disable no-console */ +// solhint-disable-next-line contract-name-camelcase contract DeployShielderV0_1_0Script is Script { function run() external { uint256 privateKey = vm.envUint("PRIVATE_KEY"); @@ -34,6 +35,7 @@ contract DeployShielderV0_1_0Script is Script { address proxy = address(new ERC1967Proxy(shielderImplementation, data)); + // solhint-disable-next-line var-name-mixedcase address shielderV0_1_0Implementation = address(new ShielderV0_1_0()); console2.log( @@ -41,6 +43,7 @@ contract DeployShielderV0_1_0Script is Script { address(shielderV0_1_0Implementation) ); + // solhint-disable-next-line var-name-mixedcase bytes memory dataV0_1_0 = abi.encodeWithSignature("initialize()"); UUPSUpgradeable(proxy).upgradeToAndCall( diff --git a/test/ShielderV0_1_0Upgrade.t.sol b/test/ShielderV0_1_0Upgrade.t.sol index a408e088..4454217d 100644 --- a/test/ShielderV0_1_0Upgrade.t.sol +++ b/test/ShielderV0_1_0Upgrade.t.sol @@ -3,11 +3,12 @@ pragma solidity 0.8.26; import { Test } from "forge-std/Test.sol"; import { CustomUpgrades } from "./Utils.sol"; -import { Upgrades, Options } from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; import { Shielder } from "../contracts/Shielder.sol"; import { ShielderV0_1_0 } from "../contracts/ShielderV0_1_0.sol"; +// solhint-disable-next-line contract-name-camelcase contract ShielderV0_1_0Upgrade is Test { address public owner; uint256 public depositLimit = 100e18; @@ -49,6 +50,7 @@ contract ShielderV0_1_0Upgrade is Test { allowedErrors ); + // solhint-disable-next-line var-name-mixedcase ShielderV0_1_0 shielderV0_1_0 = ShielderV0_1_0(shielderProxy); bytes3 newVersion = shielderV0_1_0.CONTRACT_VERSION(); @@ -63,6 +65,7 @@ contract ShielderV0_1_0Upgrade is Test { uint256 newDepositLimit = shielderV0_1_0.depositLimit(); vm.assertEq(newDepositLimit, currentDepositLimit); + // solhint-disable-next-line var-name-mixedcase (, uint256 nextFreeLeafIdV0_1_0, , ) = shielderV0_1_0.merkleTree(); vm.assertEq(nextFreeLeafId, nextFreeLeafIdV0_1_0); From 7b4ad40f0825582a84d0a556ce4d34cff2bd3120 Mon Sep 17 00:00:00 2001 From: Maksym Zub Date: Wed, 22 Jan 2025 10:39:39 +0100 Subject: [PATCH 3/7] makefile generate --- Makefile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Makefile b/Makefile index a76aa1ad..0ad307e4 100644 --- a/Makefile +++ b/Makefile @@ -89,11 +89,7 @@ generate-verifier-contracts-v0_1_0: .PHONY: generate-contracts generate-contracts: # Generate poseidon & relation verifier contracts -generate-contracts: generate-poseidon-contracts generate-verifier-contracts - -.PHONY: generate-contracts-v0_1_0 -generate-contracts-v0_1_0: # Generate poseidon & relation verifier contracts -generate-contracts-v0_1_0: generate-poseidon-contracts generate-verifier-contracts-v0_1_0 +generate-contracts: generate-poseidon-contracts generate-verifier-contracts generate-verifier-contracts-v0_1_0 .PHONY: measure-gas measure-gas: # measure shielder gas usage From fb8e781f0b61c968e786a450de515ca86e3a4066 Mon Sep 17 00:00:00 2001 From: Maksym Zub Date: Fri, 24 Jan 2025 11:22:13 +0100 Subject: [PATCH 4/7] upd --- contracts/ShielderV0_1_0.sol | 62 +++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/contracts/ShielderV0_1_0.sol b/contracts/ShielderV0_1_0.sol index 46132468..741ae307 100644 --- a/contracts/ShielderV0_1_0.sol +++ b/contracts/ShielderV0_1_0.sol @@ -65,7 +65,7 @@ contract ShielderV0_1_0 is uint256 newNote, uint256 newNoteIndex ); - event DepositNative( + event Deposit( bytes3 contractVersion, uint256 idHiding, uint256 amount, @@ -193,37 +193,19 @@ contract ShielderV0_1_0 is fieldElement(newNote) { uint256 amount = msg.value; - if (amount == 0) revert ZeroAmount(); - if (nullifiers(oldNullifierHash) != 0) revert DuplicatedNullifier(); - if (!_merkleRootExists(merkleRoot)) revert MerkleRootDoesNotExist(); // `address(this).balance` already includes `msg.value`. if (address(this).balance > MAX_CONTRACT_BALANCE) { revert ContractBalanceLimitReached(); } - // @dev needs to match the order in the circuit - uint256[] memory publicInputs = new uint256[](5); - publicInputs[0] = idHiding; - publicInputs[1] = merkleRoot; - publicInputs[2] = oldNullifierHash; - publicInputs[3] = newNote; - publicInputs[4] = amount; - publicInputs[5] = 0; // token index - - bool success = DepositVerifier.verifyProof(proof, publicInputs); - - if (!success) revert DepositVerificationFailed(); - - uint256 newNoteIndex = _addNote(newNote); - _registerNullifier(oldNullifierHash); - - emit DepositNative( - CONTRACT_VERSION, + deposit( idHiding, - amount, + oldNullifierHash, newNote, - newNoteIndex, - 0 + merkleRoot, + amount, + 0, + proof ); } @@ -241,15 +223,13 @@ contract ShielderV0_1_0 is bytes calldata proof ) external + whenNotPaused restrictContractVersion(expectedContractVersion) fieldElement(idHiding) fieldElement(oldNullifierHash) fieldElement(newNote) { - if (amount == 0) revert ZeroAmount(); if (amount > depositLimit()) revert AmountOverDepositLimit(); - if (nullifiers(oldNullifierHash) != 0) revert DuplicatedNullifier(); - if (!_merkleRootExists(merkleRoot)) revert MerkleRootDoesNotExist(); address tokenAddress = getTokenAddress(tokenIndex); if (tokenAddress == address(0)) revert TokenDoesNotExist(); // transfer the tokens to the contract @@ -259,6 +239,30 @@ contract ShielderV0_1_0 is ) { revert ContractBalanceLimitReached(); } + + deposit( + idHiding, + oldNullifierHash, + newNote, + merkleRoot, + amount, + tokenIndex, + proof + ); + } + + function deposit( + uint256 idHiding, + uint256 oldNullifierHash, + uint256 newNote, + uint256 merkleRoot, + uint256 amount, + uint256 tokenIndex, + bytes calldata proof + ) internal { + if (amount == 0) revert ZeroAmount(); + if (nullifiers(oldNullifierHash) != 0) revert DuplicatedNullifier(); + if (!_merkleRootExists(merkleRoot)) revert MerkleRootDoesNotExist(); // @dev needs to match the order in the circuit uint256[] memory publicInputs = new uint256[](5); publicInputs[0] = idHiding; @@ -275,7 +279,7 @@ contract ShielderV0_1_0 is uint256 newNoteIndex = _addNote(newNote); _registerNullifier(oldNullifierHash); - emit DepositNative( + emit Deposit( CONTRACT_VERSION, idHiding, amount, From b50410d0cbecc9babad8591e2bdad5f1d216c97a Mon Sep 17 00:00:00 2001 From: Maksym Zub Date: Fri, 24 Jan 2025 16:34:02 +0100 Subject: [PATCH 5/7] upd --- Makefile | 7 +-- contracts/ShielderV0_1_0.sol | 20 ++----- contracts/ShielderV2Mock.sol | 2 +- contracts/TokenList.sol | 23 +++----- scripts/2_AddTokenSupport.sol | 41 ++++++++++++++ scripts/ShielderV0_1_0.s.sol | 65 ---------------------- scripts/deploy-all-migrations.sh | 40 ++++++++++++++ test/ShielderV0_1_0Upgrade.t.sol | 95 ++++++++++++++++++++------------ 8 files changed, 158 insertions(+), 135 deletions(-) create mode 100644 scripts/2_AddTokenSupport.sol delete mode 100644 scripts/ShielderV0_1_0.s.sol create mode 100755 scripts/deploy-all-migrations.sh diff --git a/Makefile b/Makefile index 0ad307e4..dbc96511 100644 --- a/Makefile +++ b/Makefile @@ -59,12 +59,7 @@ endif .PHONY: deploy-contracts-v0_1_0 deploy-contracts-v0_1_0: # Deploy solidity contracts deploy-contracts-v0_1_0: -ifeq ($(NETWORK),anvil) - $(eval PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80) \ - PRIVATE_KEY=$(PRIVATE_KEY) OWNER_ADDRESS=$(OWNER_ADDRESS) forge script DeployShielderV0_1_0Script --broadcast --rpc-url anvil --sender $(shell cast wallet address $(PRIVATE_KEY)) -else - PRIVATE_KEY=$(PRIVATE_KEY) OWNER_ADDRESS=$(OWNER_ADDRESS) forge script DeployShielderV0_1_0Script --broadcast --rpc-url $(NETWORK) --sender $(shell cast wallet address $(PRIVATE_KEY)) -endif + NETWORK=$(NETWORK) PRIVATE_KEY=$(PRIVATE_KEY) OWNER_ADDRESS=$(OWNER_ADDRESS) ./scripts/deploy-all-migrations.sh .PHONY: generate-poseidon-contracts generate-poseidon-contracts: # Generate Poseidon contract diff --git a/contracts/ShielderV0_1_0.sol b/contracts/ShielderV0_1_0.sol index 741ae307..a41129e6 100644 --- a/contracts/ShielderV0_1_0.sol +++ b/contracts/ShielderV0_1_0.sol @@ -15,13 +15,13 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** - * @title ShielderV0_1_0 + * @title Shielder * @author CardinalCryptography - * @custom:oz-upgrades-from Shielder + * @custom:oz-upgrades-from contracts/Shielder.sol:Shielder * @custom:oz-upgrades-unsafe-allow external-library-linking */ // solhint-disable-next-line contract-name-camelcase -contract ShielderV0_1_0 is +contract Shielder is Initializable, UUPSUpgradeable, Ownable2StepUpgradeable, @@ -386,17 +386,7 @@ contract ShielderV0_1_0 is _setDepositLimit(_depositLimit); } - /* - * Add a token to the list - */ - function addTokenToList(address _token) external onlyOwner { - _addTokenToList(_token); - } - - /* - * Remove the last token from the list - */ - function removeLastToken() external onlyOwner { - _removeLastToken(); + function setTokenList(address[] calldata _tokens) external onlyOwner { + _setTokenList(_tokens); } } diff --git a/contracts/ShielderV2Mock.sol b/contracts/ShielderV2Mock.sol index 68cbfa30..9ad3c811 100644 --- a/contracts/ShielderV2Mock.sol +++ b/contracts/ShielderV2Mock.sol @@ -14,7 +14,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils * @dev This is a mock version on ShielderV2 for testing purposes only. * Most of the functionality has been removed for clarity. * - * @custom:oz-upgrades-from Shielder + * @custom:oz-upgrades-from contracts/Shielder.sol:Shielder * @custom:oz-upgrades-unsafe-allow external-library-linking */ contract ShielderV2Mock is diff --git a/contracts/TokenList.sol b/contracts/TokenList.sol index f6b082ce..141ebb96 100644 --- a/contracts/TokenList.sol +++ b/contracts/TokenList.sol @@ -47,22 +47,17 @@ abstract contract TokenList is Initializable { return tokens; } - /* - * Add a token to the list - */ - function _addTokenToList(address _token) internal { + function _setTokenList(address[] memory _tokens) public { TokenListStorage storage $ = _getTokenListStorage(); - $.tokenAddressByIndex[$.tokensNumber] = _token; - $.tokensNumber++; - } - function _removeLastToken() internal { - TokenListStorage storage $ = _getTokenListStorage(); - if ($.tokensNumber == 1) { - // cannot remove the native token - return; + // clear the previous list + for (uint256 i = 1; i < $.tokensNumber; i++) { + delete $.tokenAddressByIndex[i]; + } + + $.tokensNumber = _tokens.length + 1; // reserve the first index for the native token + for (uint256 i = 0; i < _tokens.length; i++) { + $.tokenAddressByIndex[i + 1] = _tokens[i]; } - delete $.tokenAddressByIndex[$.tokensNumber - 1]; - $.tokensNumber--; } } diff --git a/scripts/2_AddTokenSupport.sol b/scripts/2_AddTokenSupport.sol new file mode 100644 index 00000000..e69a4844 --- /dev/null +++ b/scripts/2_AddTokenSupport.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.14; + +import { Script, console2 } from "forge-std/Script.sol"; + +import { Shielder } from "../contracts/ShielderV0_1_0.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +/* solhint-disable no-console */ +contract AddTokenSupport is Script { + function run() external { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + + address owner = vm.envAddress("OWNER_ADDRESS"); + address broadcaster = vm.addr(privateKey); + address proxy = vm.envAddress("SHIELDER_PROXY"); + console2.log("Using", broadcaster, "as broadcaster"); + + vm.startBroadcast(privateKey); + + address shielderImplementation = address(new Shielder()); + + console2.log( + "ShielderV0_1_0 Implementation deployed at:", + address(shielderImplementation) + ); + + bytes memory data = abi.encodeWithSignature("initialize()"); + + UUPSUpgradeable(proxy).upgradeToAndCall(shielderImplementation, data); + + Shielder shielder = Shielder(proxy); + + console2.log("Upgraded at proxy:", address(shielder)); + console2.logBytes3(shielder.CONTRACT_VERSION()); + console2.log("Owner:", shielder.owner()); + + vm.stopBroadcast(); + } +} diff --git a/scripts/ShielderV0_1_0.s.sol b/scripts/ShielderV0_1_0.s.sol deleted file mode 100644 index f7f9c211..00000000 --- a/scripts/ShielderV0_1_0.s.sol +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -pragma solidity ^0.8.14; - -import { Script, console2 } from "forge-std/Script.sol"; - -import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -import { Shielder } from "../contracts/Shielder.sol"; -import { ShielderV0_1_0 } from "../contracts/ShielderV0_1_0.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -/* solhint-disable no-console */ -// solhint-disable-next-line contract-name-camelcase -contract DeployShielderV0_1_0Script is Script { - function run() external { - uint256 privateKey = vm.envUint("PRIVATE_KEY"); - - address owner = vm.envAddress("OWNER_ADDRESS"); - address broadcaster = vm.addr(privateKey); - console2.log("Using", broadcaster, "as broadcaster"); - - vm.startBroadcast(privateKey); - - address shielderImplementation = address(new Shielder()); - - console2.log( - "Shielder Implementation deployed at:", - address(shielderImplementation) - ); - - bytes memory data = abi.encodeCall( - Shielder.initialize, - (owner, type(uint256).max) - ); - - address proxy = address(new ERC1967Proxy(shielderImplementation, data)); - - // solhint-disable-next-line var-name-mixedcase - address shielderV0_1_0Implementation = address(new ShielderV0_1_0()); - - console2.log( - "ShielderV0_1_0 Implementation deployed at:", - address(shielderV0_1_0Implementation) - ); - - // solhint-disable-next-line var-name-mixedcase - bytes memory dataV0_1_0 = abi.encodeWithSignature("initialize()"); - - UUPSUpgradeable(proxy).upgradeToAndCall( - shielderV0_1_0Implementation, - dataV0_1_0 - ); - - Shielder shielder = Shielder(proxy); - - console2.log("Shielder deployed at:", address(shielder)); - if (owner == broadcaster) { - shielder.unpause(); - } - console2.logBytes3(shielder.CONTRACT_VERSION()); - console2.log("Owner:", shielder.owner()); - - vm.stopBroadcast(); - } -} diff --git a/scripts/deploy-all-migrations.sh b/scripts/deploy-all-migrations.sh new file mode 100755 index 00000000..8cddf69c --- /dev/null +++ b/scripts/deploy-all-migrations.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# Default network is anvil +NETWORK=${NETWORK:-anvil} + +# Anvil default private key +ANVIL_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + +if [ "$NETWORK" = "anvil" ]; then + # Use default anvil private key if not provided + PRIVATE_KEY=$ANVIL_PRIVATE_KEY + OWNER_ADDRESS=$(cast wallet address "$PRIVATE_KEY") + + # Get the sender address from private key + SENDER=$(cast wallet address "$PRIVATE_KEY") + + # Deploy scripts for anvil + + SHIELDER_PROXY=$( + PRIVATE_KEY=$PRIVATE_KEY \ + OWNER_ADDRESS=$OWNER_ADDRESS \ + forge script DeployShielderScript --broadcast --rpc-url anvil --sender "$SENDER" \ + | grep 'Shielder deployed at:' | awk '{print $NF}' + ) + SHIELDER_PROXY=$SHIELDER_PROXY PRIVATE_KEY=$PRIVATE_KEY OWNER_ADDRESS=$OWNER_ADDRESS \ + forge script AddTokenSupport --broadcast --rpc-url anvil --sender "$SENDER" +else + # Get the sender address from private key + SENDER=$(cast wallet address "$PRIVATE_KEY") + + # Deploy scripts for other networks + SHIELDER_PROXY=$( + PRIVATE_KEY=$PRIVATE_KEY \ + OWNER_ADDRESS=$OWNER_ADDRESS \ + forge script DeployShielderScript --broadcast --rpc-url "$NETWORK" --sender "$SENDER" \ + | grep 'Shielder deployed at:' | awk '{print $NF}' + ) + SHIELDER_PROXY=$SHIELDER_PROXY PRIVATE_KEY=$PRIVATE_KEY OWNER_ADDRESS=$OWNER_ADDRESS \ + forge script DeployShielderV0_1_0Script --broadcast --rpc-url "$NETWORK" --sender "$SENDER" +fi diff --git a/test/ShielderV0_1_0Upgrade.t.sol b/test/ShielderV0_1_0Upgrade.t.sol index 4454217d..22a71fd3 100644 --- a/test/ShielderV0_1_0Upgrade.t.sol +++ b/test/ShielderV0_1_0Upgrade.t.sol @@ -5,8 +5,8 @@ import { Test } from "forge-std/Test.sol"; import { CustomUpgrades } from "./Utils.sol"; import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol"; -import { Shielder } from "../contracts/Shielder.sol"; -import { ShielderV0_1_0 } from "../contracts/ShielderV0_1_0.sol"; +import { Shielder as ShielderPrev } from "../contracts/Shielder.sol"; +import { Shielder as ShielderNext } from "../contracts/ShielderV0_1_0.sol"; // solhint-disable-next-line contract-name-camelcase contract ShielderV0_1_0Upgrade is Test { @@ -23,63 +23,90 @@ contract ShielderV0_1_0Upgrade is Test { // Deploy upgradeable Shielder address shielderProxy = Upgrades.deployUUPSProxy( "Shielder.sol:Shielder", - abi.encodeCall(Shielder.initialize, (owner, depositLimit)) + abi.encodeCall(ShielderPrev.initialize, (owner, depositLimit)) ); - Shielder shielder = Shielder(shielderProxy); + ShielderPrev shielderPrev = ShielderPrev(shielderProxy); - bytes3 version = shielder.CONTRACT_VERSION(); - vm.assertEq(version, bytes3(0x000001)); + bytes3 prevVersion = shielderPrev.CONTRACT_VERSION(); + vm.assertEq(prevVersion, bytes3(0x000001)); - address currentOwner = shielder.owner(); - vm.assertEq(currentOwner, owner); + address prevOwner = shielderPrev.owner(); + vm.assertEq(prevOwner, owner); - bool paused = shielder.paused(); - vm.assertEq(paused, true); + bool prevPaused = shielderPrev.paused(); + vm.assertEq(prevPaused, true); - uint256 currentDepositLimit = shielder.depositLimit(); - vm.assertEq(currentDepositLimit, depositLimit); + uint256 prevDepositLimit = shielderPrev.depositLimit(); + vm.assertEq(prevDepositLimit, depositLimit); - (, uint256 nextFreeLeafId, , ) = shielder.merkleTree(); - vm.assertNotEq(nextFreeLeafId, 0); + (, uint256 prevFreeLeafId, , ) = shielderPrev.merkleTree(); + vm.assertNotEq(prevFreeLeafId, 0); - // upgrade Shielder + // upgrade to v0.1.0 CustomUpgrades.upgradeProxyWithErrors( shielderProxy, - "ShielderV0_1_0.sol:ShielderV0_1_0", + "ShielderV0_1_0.sol:Shielder", abi.encodeWithSignature("initialize()"), allowedErrors ); - // solhint-disable-next-line var-name-mixedcase - ShielderV0_1_0 shielderV0_1_0 = ShielderV0_1_0(shielderProxy); + ShielderNext shielderNext = ShielderNext(shielderProxy); - bytes3 newVersion = shielderV0_1_0.CONTRACT_VERSION(); + bytes3 newVersion = shielderNext.CONTRACT_VERSION(); vm.assertEq(newVersion, bytes3(0x000100)); - address newOwner = shielderV0_1_0.owner(); - vm.assertEq(newOwner, currentOwner); + address newOwner = shielderNext.owner(); + vm.assertEq(newOwner, prevOwner); - bool newPaused = shielderV0_1_0.paused(); - vm.assertEq(newPaused, paused); + bool newPaused = shielderNext.paused(); + vm.assertEq(newPaused, prevPaused); - uint256 newDepositLimit = shielderV0_1_0.depositLimit(); - vm.assertEq(newDepositLimit, currentDepositLimit); + uint256 newDepositLimit = shielderNext.depositLimit(); + vm.assertEq(newDepositLimit, prevDepositLimit); - // solhint-disable-next-line var-name-mixedcase - (, uint256 nextFreeLeafIdV0_1_0, , ) = shielderV0_1_0.merkleTree(); - vm.assertEq(nextFreeLeafId, nextFreeLeafIdV0_1_0); + (, uint256 newFreeLeafId, , ) = shielderNext.merkleTree(); + vm.assertEq(newFreeLeafId, prevFreeLeafId); - shielderV0_1_0.addTokenToList(address(1234)); - address tokenAddress = shielderV0_1_0.getTokenAddress(1); + // test TokenList + + address[] memory tokensToSet = new address[](1); + tokensToSet[0] = address(1234); + + shielderNext.setTokenList(tokensToSet); + address tokenAddress = shielderNext.getTokenAddress(1); vm.assertEq(tokenAddress, address(1234)); - address[] memory tokens = shielderV0_1_0.getTokens(); + address[] memory tokens = shielderNext.getTokens(); vm.assertEq(tokens.length, 2); vm.assertEq(tokens[0], address(0)); // native token vm.assertEq(tokens[1], address(1234)); // added token - shielderV0_1_0.removeLastToken(); - tokenAddress = shielderV0_1_0.getTokenAddress(1); - vm.assertEq(tokenAddress, address(0)); + // reset tokens to other token + { + address[] memory tokensToSet = new address[](1); + tokensToSet[0] = address(5678); + + shielderNext.setTokenList(tokensToSet); + address tokenAddress = shielderNext.getTokenAddress(1); + vm.assertEq(tokenAddress, address(5678)); + + address[] memory tokens = shielderNext.getTokens(); + vm.assertEq(tokens.length, 2); + vm.assertEq(tokens[0], address(0)); // native token + vm.assertEq(tokens[1], address(5678)); // added token + } + + // reset tokens to empty + { + address[] memory tokensToSet = new address[](0); + + shielderNext.setTokenList(tokensToSet); + address tokenAddress = shielderNext.getTokenAddress(1); + vm.assertEq(tokenAddress, address(0)); + + address[] memory tokens = shielderNext.getTokens(); + vm.assertEq(tokens.length, 1); + vm.assertEq(tokens[0], address(0)); // native token + } } } From d51e18d199a23b8e18cd1b0af16fa1afcc41e727 Mon Sep 17 00:00:00 2001 From: Maksym Zub Date: Fri, 24 Jan 2025 16:38:22 +0100 Subject: [PATCH 6/7] upd --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index dbc96511..eccf4737 100644 --- a/Makefile +++ b/Makefile @@ -56,9 +56,9 @@ else PRIVATE_KEY=$(PRIVATE_KEY) OWNER_ADDRESS=$(OWNER_ADDRESS) forge script DeployShielderScript --broadcast --rpc-url $(NETWORK) --sender $(shell cast wallet address $(PRIVATE_KEY)) endif -.PHONY: deploy-contracts-v0_1_0 -deploy-contracts-v0_1_0: # Deploy solidity contracts -deploy-contracts-v0_1_0: +.PHONY: deploy-contracts-and-migrate +deploy-contracts-and-migrate: # Deploy solidity contracts +deploy-contracts-and-migrate: NETWORK=$(NETWORK) PRIVATE_KEY=$(PRIVATE_KEY) OWNER_ADDRESS=$(OWNER_ADDRESS) ./scripts/deploy-all-migrations.sh .PHONY: generate-poseidon-contracts From 723990b83c93f1148cfcfd3754bb2b588ea2edef Mon Sep 17 00:00:00 2001 From: Maksym Zub Date: Fri, 24 Jan 2025 19:03:01 +0100 Subject: [PATCH 7/7] lint --- scripts/2_AddTokenSupport.sol | 1 - scripts/deploy-all-migrations.sh | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/2_AddTokenSupport.sol b/scripts/2_AddTokenSupport.sol index e69a4844..a2c6cfb0 100644 --- a/scripts/2_AddTokenSupport.sol +++ b/scripts/2_AddTokenSupport.sol @@ -12,7 +12,6 @@ contract AddTokenSupport is Script { function run() external { uint256 privateKey = vm.envUint("PRIVATE_KEY"); - address owner = vm.envAddress("OWNER_ADDRESS"); address broadcaster = vm.addr(privateKey); address proxy = vm.envAddress("SHIELDER_PROXY"); console2.log("Using", broadcaster, "as broadcaster"); diff --git a/scripts/deploy-all-migrations.sh b/scripts/deploy-all-migrations.sh index 8cddf69c..186d5c12 100755 --- a/scripts/deploy-all-migrations.sh +++ b/scripts/deploy-all-migrations.sh @@ -22,7 +22,8 @@ if [ "$NETWORK" = "anvil" ]; then forge script DeployShielderScript --broadcast --rpc-url anvil --sender "$SENDER" \ | grep 'Shielder deployed at:' | awk '{print $NF}' ) - SHIELDER_PROXY=$SHIELDER_PROXY PRIVATE_KEY=$PRIVATE_KEY OWNER_ADDRESS=$OWNER_ADDRESS \ + SHIELDER_PROXY=$SHIELDER_PROXY \ + PRIVATE_KEY=$PRIVATE_KEY \ forge script AddTokenSupport --broadcast --rpc-url anvil --sender "$SENDER" else # Get the sender address from private key @@ -35,6 +36,7 @@ else forge script DeployShielderScript --broadcast --rpc-url "$NETWORK" --sender "$SENDER" \ | grep 'Shielder deployed at:' | awk '{print $NF}' ) - SHIELDER_PROXY=$SHIELDER_PROXY PRIVATE_KEY=$PRIVATE_KEY OWNER_ADDRESS=$OWNER_ADDRESS \ + SHIELDER_PROXY=$SHIELDER_PROXY \ + PRIVATE_KEY=$PRIVATE_KEY \ forge script DeployShielderV0_1_0Script --broadcast --rpc-url "$NETWORK" --sender "$SENDER" fi