From 0de232a1ab57e5e4a65f8f4541abe827a0ba51f3 Mon Sep 17 00:00:00 2001 From: Sabrina Ferguson Date: Mon, 19 Aug 2024 16:51:29 -0400 Subject: [PATCH] feat: add zksync-101 template --- templates/101/.env.example | 3 + templates/101/.gitignore | 33 +++ templates/101/.npmrc | 1 + templates/101/.vscode/extensions.json | 12 ++ templates/101/.vscode/settings.json | 11 + templates/101/LICENSE | 21 ++ templates/101/README.md | 66 ++++++ .../1-hello-zksync/CrowdfundingCampaign.sol | 48 +++++ .../CrowdfundingFactory.sol | 29 +++ .../beacon/BeaconCrowdfundingCampaign.sol | 53 +++++ .../beacon/V2_BeaconCrowdfundingCampaign.sol | 84 ++++++++ .../ProxyableCrowdfundingCampaign.sol | 54 +++++ .../V2_ProxyableCrowdfundingCampaign.sol | 82 +++++++ .../uups/UUPSCrowdfundingCampaign.sol | 64 ++++++ .../uups/V2_UUPSCrowdfundingCampaign.sol | 89 ++++++++ .../approval/ApprovalFlowPaymaster.sol | 142 +++++++++++++ .../4-paymaster/approval/CrownToken.sol | 16 ++ .../4-paymaster/gasless/GaslessPaymaster.sol | 86 ++++++++ templates/101/deploy/1-hello-zksync/deploy.ts | 10 + .../101/deploy/2-contract-factory/deploy.ts | 33 +++ .../deploy/3-proxy-contracts/beacon/deploy.ts | 28 +++ .../3-proxy-contracts/beacon/upgrade.ts | 51 +++++ .../3-proxy-contracts/transparent/deploy.ts | 27 +++ .../3-proxy-contracts/transparent/upgrade.ts | 39 ++++ .../deploy/3-proxy-contracts/uups/deploy.ts | 23 ++ .../deploy/3-proxy-contracts/uups/upgrade.ts | 34 +++ .../101/deploy/4-paymaster/approval/deploy.ts | 72 +++++++ .../deploy/4-paymaster/approval/interact.ts | 83 ++++++++ .../101/deploy/4-paymaster/gasless/deploy.ts | 50 +++++ .../deploy/4-paymaster/gasless/interact.ts | 80 +++++++ .../101/deploy/4-paymaster/general-deploy.ts | 0 templates/101/hardhat.config.ts | 36 ++++ templates/101/package.json | 63 ++++++ .../CrowdfundingCampaign.test.ts | 90 ++++++++ .../CrowdfundingFactory.test.ts | 50 +++++ .../3-proxy-contracts/BeaconProxy.test.ts | 131 ++++++++++++ .../TransparentProxy.test.ts | 123 +++++++++++ .../test/3-proxy-contracts/UupsProxy.test.ts | 120 +++++++++++ .../4-paymaster/ApprovalPaymaster.test.ts | 174 +++++++++++++++ .../test/4-paymaster/GaslessPaymaster.test.ts | 122 +++++++++++ templates/101/tsconfig.json | 12 ++ templates/101/utils/index.ts | 201 ++++++++++++++++++ 42 files changed, 2546 insertions(+) create mode 100644 templates/101/.env.example create mode 100644 templates/101/.gitignore create mode 100644 templates/101/.npmrc create mode 100644 templates/101/.vscode/extensions.json create mode 100644 templates/101/.vscode/settings.json create mode 100644 templates/101/LICENSE create mode 100644 templates/101/README.md create mode 100644 templates/101/contracts/1-hello-zksync/CrowdfundingCampaign.sol create mode 100644 templates/101/contracts/2-contract-factory/CrowdfundingFactory.sol create mode 100644 templates/101/contracts/3-proxy-contracts/beacon/BeaconCrowdfundingCampaign.sol create mode 100644 templates/101/contracts/3-proxy-contracts/beacon/V2_BeaconCrowdfundingCampaign.sol create mode 100644 templates/101/contracts/3-proxy-contracts/transparent/ProxyableCrowdfundingCampaign.sol create mode 100644 templates/101/contracts/3-proxy-contracts/transparent/V2_ProxyableCrowdfundingCampaign.sol create mode 100644 templates/101/contracts/3-proxy-contracts/uups/UUPSCrowdfundingCampaign.sol create mode 100644 templates/101/contracts/3-proxy-contracts/uups/V2_UUPSCrowdfundingCampaign.sol create mode 100644 templates/101/contracts/4-paymaster/approval/ApprovalFlowPaymaster.sol create mode 100644 templates/101/contracts/4-paymaster/approval/CrownToken.sol create mode 100644 templates/101/contracts/4-paymaster/gasless/GaslessPaymaster.sol create mode 100644 templates/101/deploy/1-hello-zksync/deploy.ts create mode 100644 templates/101/deploy/2-contract-factory/deploy.ts create mode 100644 templates/101/deploy/3-proxy-contracts/beacon/deploy.ts create mode 100644 templates/101/deploy/3-proxy-contracts/beacon/upgrade.ts create mode 100644 templates/101/deploy/3-proxy-contracts/transparent/deploy.ts create mode 100644 templates/101/deploy/3-proxy-contracts/transparent/upgrade.ts create mode 100644 templates/101/deploy/3-proxy-contracts/uups/deploy.ts create mode 100644 templates/101/deploy/3-proxy-contracts/uups/upgrade.ts create mode 100644 templates/101/deploy/4-paymaster/approval/deploy.ts create mode 100644 templates/101/deploy/4-paymaster/approval/interact.ts create mode 100644 templates/101/deploy/4-paymaster/gasless/deploy.ts create mode 100644 templates/101/deploy/4-paymaster/gasless/interact.ts create mode 100644 templates/101/deploy/4-paymaster/general-deploy.ts create mode 100644 templates/101/hardhat.config.ts create mode 100644 templates/101/package.json create mode 100644 templates/101/test/1-hello-zksync/CrowdfundingCampaign.test.ts create mode 100644 templates/101/test/2-deploy-crowdfunding/CrowdfundingFactory.test.ts create mode 100644 templates/101/test/3-proxy-contracts/BeaconProxy.test.ts create mode 100644 templates/101/test/3-proxy-contracts/TransparentProxy.test.ts create mode 100644 templates/101/test/3-proxy-contracts/UupsProxy.test.ts create mode 100644 templates/101/test/4-paymaster/ApprovalPaymaster.test.ts create mode 100644 templates/101/test/4-paymaster/GaslessPaymaster.test.ts create mode 100644 templates/101/tsconfig.json create mode 100644 templates/101/utils/index.ts diff --git a/templates/101/.env.example b/templates/101/.env.example new file mode 100644 index 0000000..00c696c --- /dev/null +++ b/templates/101/.env.example @@ -0,0 +1,3 @@ +# DO NOT PUT REAL PRIVATE KEYS INTO THIS FILE +# This is a local rich wallet private key for development and learning purposes only +WALLET_PRIVATE_KEY=0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 \ No newline at end of file diff --git a/templates/101/.gitignore b/templates/101/.gitignore new file mode 100644 index 0000000..1d4ddf4 --- /dev/null +++ b/templates/101/.gitignore @@ -0,0 +1,33 @@ +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local +.env.* +!.env.example + +# Finder (MacOS) folder config +.DS_Store + +node_modules/ + +era_test_node.log + +package-lock.json +node_modules +typechain-types +typechain + +**/.upgradable/ +build-test/ +dist +artifacts-zk/ +cache-zk/ +deployments-zk/ +deployments/ + +# Hardhat files +cache +artifacts diff --git a/templates/101/.npmrc b/templates/101/.npmrc new file mode 100644 index 0000000..e9ee3cb --- /dev/null +++ b/templates/101/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true \ No newline at end of file diff --git a/templates/101/.vscode/extensions.json b/templates/101/.vscode/extensions.json new file mode 100644 index 0000000..82d1574 --- /dev/null +++ b/templates/101/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "christian-kohler.npm-intellisense", + "christian-kohler.path-intellisense", + "dbaeumer.vscode-eslint", + "editorconfig.editor", + "esbenp.prettier-vscode", + "mikestead.dotenv", + "yoavbls.pretty-ts-errors" + ], + "unwantedRecommendations": [] + } diff --git a/templates/101/.vscode/settings.json b/templates/101/.vscode/settings.json new file mode 100644 index 0000000..0e99e20 --- /dev/null +++ b/templates/101/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.trimTrailingWhitespace": true, + "editor.formatOnType": false, + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "editor.tabSize": 2, + "editor.insertSpaces": true, + "files.eol": "\n", + } diff --git a/templates/101/LICENSE b/templates/101/LICENSE new file mode 100644 index 0000000..195fb6c --- /dev/null +++ b/templates/101/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Matter Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/templates/101/README.md b/templates/101/README.md new file mode 100644 index 0000000..8ba173b --- /dev/null +++ b/templates/101/README.md @@ -0,0 +1,66 @@ +# ZKsync 101 + +This project was scaffolded with [ZKsync CLI](https://github.com/matter-labs/zksync-cli). + +ZKsync 101 is a quick start template to help you learn how to +be a super developer using ZKsync CLI to build projects in the +ZKsync ecosystem! + +This project is a Nodejs project that uses hardhat, viem, and solidity. +You will learn how to build, deploy and test smart contracts with hardhat +onto the ZKsync in memory node and use ZKsync CLI to interact with the contracts. + +## Project Layout + +- `/contracts`: Contains solidity smart contracts. +- `/deploy`: Scripts for contract deployment and interaction. +- `/test`: Test files. +- `hardhat.config.ts`: Configuration settings, the default network is set to "inMemoryNode". + +## How to Use + +1. Install dependencies with `npm install --force`. + +2. Configure your ZKsync CLI to use the In memory node settings for dev. + + ```bash + zksync-cli dev config + ``` + +3. Start up a local in-memory node with ZKsync CLI with the following command: + + ```bash + zksync-cli dev start + ``` + +4. Follow along on ZKsync Docs in our [ZKsync 101](https://docs.zksync.io/build/start-coding/zksync-101)! + +### Environment Settings + +This project pulls in environment variables from `.env` files. + +Rename `.env.example` to `.env`, the provided private key is a local rich wallet +that is available in the local in-memory node. + +```txt +WALLET_PRIVATE_KEY=your_private_key_here... +``` + +### Local Tests + +Running `npm run test` by default runs the [ZKsync In-memory Node](https://docs.zksync.io/build/test-and-debug/in-memory-node) provided by the [@matterlabs/hardhat-zksync-node](https://docs.zksync.io/build/tooling/hardhat/hardhat-zksync-node) tool. + +Important: ZKsync In-memory Node currently supports only the L2 node. If contracts also need L1, use another testing environment like Dockerized Node. +Refer to [test documentation](https://docs.zksync.io/build/test-and-debug) for details. + +## Useful Links + +- [Docs](https://docs.zksync.io) +- [Official Site](https://zksync.io/) +- [GitHub](https://github.com/matter-labs) +- [Twitter](https://twitter.com/zksync) +- [Discord](https://join.zksync.dev/) + +## License + +This project is under the [MIT](./LICENSE) license. diff --git a/templates/101/contracts/1-hello-zksync/CrowdfundingCampaign.sol b/templates/101/contracts/1-hello-zksync/CrowdfundingCampaign.sol new file mode 100644 index 0000000..ce546a9 --- /dev/null +++ b/templates/101/contracts/1-hello-zksync/CrowdfundingCampaign.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract CrowdfundingCampaign { + address public owner; + uint256 private fundingGoal; + uint256 private totalFundsRaised; + mapping(address => uint256) private contributions; + + event ContributionReceived(address contributor, uint256 amount); + event GoalReached(uint256 totalFundsRaised); + + constructor(uint256 _fundingGoal) { + owner = msg.sender; + fundingGoal = _fundingGoal; + } + + function contribute() public payable { + require(msg.value > 0, "Contribution must be greater than 0"); + contributions[msg.sender] += msg.value; + totalFundsRaised += msg.value; + + emit ContributionReceived(msg.sender, msg.value); + + if (totalFundsRaised >= fundingGoal) { + emit GoalReached(totalFundsRaised); + } + } + + function withdrawFunds() public { + require(msg.sender == owner, "Only the owner can withdraw funds"); + require(totalFundsRaised >= fundingGoal, "Funding goal not reached"); + + uint256 amount = address(this).balance; + totalFundsRaised = 0; + + (bool success, ) = payable(owner).call{value: amount}(""); + require(success, "Transfer failed."); + } + + function getTotalFundsRaised() public view returns (uint256) { + return totalFundsRaised; + } + + function getFundingGoal() public view returns (uint256) { + return fundingGoal; + } +} diff --git a/templates/101/contracts/2-contract-factory/CrowdfundingFactory.sol b/templates/101/contracts/2-contract-factory/CrowdfundingFactory.sol new file mode 100644 index 0000000..16c791a --- /dev/null +++ b/templates/101/contracts/2-contract-factory/CrowdfundingFactory.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// Crowdfunding campaign contract +import "../1-hello-zksync/CrowdfundingCampaign.sol"; + +// Factory contract to create and manage crowdfunding campaigns +contract CrowdfundingFactory { + CrowdfundingCampaign[] private campaigns; + + event CampaignCreated(address campaignAddress, uint256 fundingGoal); + + function createCampaign(uint256 fundingGoal) public { + CrowdfundingCampaign newCampaign = new CrowdfundingCampaign( + fundingGoal + ); + campaigns.push(newCampaign); + + emit CampaignCreated(address(newCampaign), fundingGoal); + } + + function getCampaigns() + public + view + returns (CrowdfundingCampaign[] memory) + { + return campaigns; + } +} diff --git a/templates/101/contracts/3-proxy-contracts/beacon/BeaconCrowdfundingCampaign.sol b/templates/101/contracts/3-proxy-contracts/beacon/BeaconCrowdfundingCampaign.sol new file mode 100644 index 0000000..e159f60 --- /dev/null +++ b/templates/101/contracts/3-proxy-contracts/beacon/BeaconCrowdfundingCampaign.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +// The BeaconCrowdfundingCampaign contract implements +// the Initializable interface from OpenZeppelin +contract BeaconCrowdfundingCampaign is Initializable { + address public owner; + uint256 private fundingGoal; + uint256 private totalFundsRaised; + mapping(address => uint256) private contributions; + + event ContributionReceived(address contributor, uint256 amount); + event GoalReached(uint256 totalFundsRaised); + + // The `initialize` function replaces the constructor in upgradeable contracts + function initialize(uint256 _fundingGoal) public initializer { + owner = msg.sender; + fundingGoal = _fundingGoal; + } + + function contribute() public payable { + require(msg.value > 0, "Contribution must be greater than 0"); + contributions[msg.sender] += msg.value; + totalFundsRaised += msg.value; + + emit ContributionReceived(msg.sender, msg.value); + + if (totalFundsRaised >= fundingGoal) { + emit GoalReached(totalFundsRaised); + } + } + + function withdrawFunds() public { + require(msg.sender == owner, "Only the owner can withdraw funds"); + require(totalFundsRaised >= fundingGoal, "Funding goal not reached"); + + uint256 amount = address(this).balance; + totalFundsRaised = 0; + + (bool success, ) = payable(owner).call{value: amount}(""); + require(success, "Transfer failed."); + } + + function getTotalFundsRaised() public view returns (uint256) { + return totalFundsRaised; + } + + function getFundingGoal() public view returns (uint256) { + return fundingGoal; + } +} diff --git a/templates/101/contracts/3-proxy-contracts/beacon/V2_BeaconCrowdfundingCampaign.sol b/templates/101/contracts/3-proxy-contracts/beacon/V2_BeaconCrowdfundingCampaign.sol new file mode 100644 index 0000000..9d3da6e --- /dev/null +++ b/templates/101/contracts/3-proxy-contracts/beacon/V2_BeaconCrowdfundingCampaign.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +// The BeaconCrowdfundingCampaign contract implements +// the Initializable interface from OpenZeppelin +contract V2_BeaconCrowdfundingCampaign is Initializable { + address public owner; + uint256 private fundingGoal; + uint256 private totalFundsRaised; + mapping(address => uint256) private contributions; + + // In V2 we add a deadline for the campaign + uint256 public deadline; + // We also add a flag to check if the V2 contract has been initialized + bool private initializedV2; + + event ContributionReceived(address contributor, uint256 amount); + event GoalReached(uint256 totalFundsRaised); + + // Original initialization function for V1 + function initialize(uint256 _fundingGoal) public initializer { + owner = msg.sender; + fundingGoal = _fundingGoal; + } + + // Additional initialization function for V2 + function initializeV2(uint256 _duration) public { + require(!initializedV2, "V2 already initialized"); + require(msg.sender == owner, "Only the owner can initialize V2"); + + deadline = block.timestamp + _duration; + initializedV2 = true; + } + + // Modifier to check if the campaign is still within the deadline + // A modifier is a function that is executed + // before the function that uses it + modifier withinDeadline() { + require(block.timestamp <= deadline, "Funding period has ended"); + _; + } + + // We use the `withinDeadline` modifier to ensure + // that contributions are only accepted within the deadlin + function contribute() public payable withinDeadline { + require(msg.value > 0, "Contribution must be greater than 0"); + contributions[msg.sender] += msg.value; + totalFundsRaised += msg.value; + + emit ContributionReceived(msg.sender, msg.value); + + if (totalFundsRaised >= fundingGoal) { + emit GoalReached(totalFundsRaised); + } + } + + // We add a new function to extend the deadline + // only by the owner of the campaign + function extendDeadline(uint256 _newDuration) public { + require(msg.sender == owner, "Only the owner can extend the deadline"); + deadline = block.timestamp + _newDuration; + } + + function withdrawFunds() public { + require(msg.sender == owner, "Only the owner can withdraw funds"); + require(totalFundsRaised >= fundingGoal, "Funding goal not reached"); + + uint256 amount = address(this).balance; + totalFundsRaised = 0; + + (bool success, ) = payable(owner).call{value: amount}(""); + require(success, "Transfer failed."); + } + + function getTotalFundsRaised() public view returns (uint256) { + return totalFundsRaised; + } + + function getFundingGoal() public view returns (uint256) { + return fundingGoal; + } +} diff --git a/templates/101/contracts/3-proxy-contracts/transparent/ProxyableCrowdfundingCampaign.sol b/templates/101/contracts/3-proxy-contracts/transparent/ProxyableCrowdfundingCampaign.sol new file mode 100644 index 0000000..b373d74 --- /dev/null +++ b/templates/101/contracts/3-proxy-contracts/transparent/ProxyableCrowdfundingCampaign.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +// The proxyable contract implements the Initializable interface from OpenZeppelin +contract ProxyableCrowdfundingCampaign is Initializable { + address public owner; + uint256 private fundingGoal; + uint256 private totalFundsRaised; + mapping(address => uint256) private contributions; + + event ContributionReceived(address contributor, uint256 amount); + event GoalReached(uint256 totalFundsRaised); + + // Proxy'd contract do not use a constructor + // They implement the `initialize` function instead from OpenZeppelin's Initializable + // The rest of the logic for the contract stays the same! + function initialize(uint256 _fundingGoal) public initializer { + owner = msg.sender; + fundingGoal = _fundingGoal; + } + + function contribute() public payable { + require(msg.value > 0, "Contribution must be greater than 0"); + contributions[msg.sender] += msg.value; + totalFundsRaised += msg.value; + + emit ContributionReceived(msg.sender, msg.value); + + if (totalFundsRaised >= fundingGoal) { + emit GoalReached(totalFundsRaised); + } + } + + function withdrawFunds() public { + require(msg.sender == owner, "Only the owner can withdraw funds"); + require(totalFundsRaised >= fundingGoal, "Funding goal not reached"); + + uint256 amount = address(this).balance; + totalFundsRaised = 0; + + (bool success, ) = payable(owner).call{value: amount}(""); + require(success, "Transfer failed."); + } + + function getTotalFundsRaised() public view returns (uint256) { + return totalFundsRaised; + } + + function getFundingGoal() public view returns (uint256) { + return fundingGoal; + } +} diff --git a/templates/101/contracts/3-proxy-contracts/transparent/V2_ProxyableCrowdfundingCampaign.sol b/templates/101/contracts/3-proxy-contracts/transparent/V2_ProxyableCrowdfundingCampaign.sol new file mode 100644 index 0000000..489bef3 --- /dev/null +++ b/templates/101/contracts/3-proxy-contracts/transparent/V2_ProxyableCrowdfundingCampaign.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +contract V2_ProxyableCrowdfundingCampaign is Initializable { + address public owner; + uint256 private fundingGoal; + uint256 private totalFundsRaised; + mapping(address => uint256) private contributions; + + // In V2 we add a deadline for the campaign + uint256 public deadline; + // We also add a flag to check if the V2 contract has been initialized + bool private initializedV2; + + event ContributionReceived(address contributor, uint256 amount); + event GoalReached(uint256 totalFundsRaised); + + // Original initialization function for V1 + function initialize(uint256 _fundingGoal) public initializer { + owner = msg.sender; + fundingGoal = _fundingGoal; + } + + // Additional initialization function for V2 + function initializeV2(uint256 _duration) public { + require(!initializedV2, "V2 already initialized"); + require(msg.sender == owner, "Only the owner can initialize V2"); + + deadline = block.timestamp + _duration; + initializedV2 = true; + } + + // Modifier to check if the campaign is still within the deadline + // A modifier is a function that is executed + // before the function that uses it + modifier withinDeadline() { + require(block.timestamp <= deadline, "Funding period has ended"); + _; + } + + // We use the `withinDeadline` modifier to ensure + // that contributions are only accepted within the deadline + function contribute() public payable withinDeadline { + require(msg.value > 0, "Contribution must be greater than 0"); + contributions[msg.sender] += msg.value; + totalFundsRaised += msg.value; + + emit ContributionReceived(msg.sender, msg.value); + + if (totalFundsRaised >= fundingGoal) { + emit GoalReached(totalFundsRaised); + } + } + + // We add a new function to extend the deadline + // only by the owner of the campaign + function extendDeadline(uint256 _newDuration) public { + require(msg.sender == owner, "Only the owner can extend the deadline"); + deadline = block.timestamp + _newDuration; + } + + function withdrawFunds() public { + require(msg.sender == owner, "Only the owner can withdraw funds"); + require(totalFundsRaised >= fundingGoal, "Funding goal not reached"); + + uint256 amount = address(this).balance; + totalFundsRaised = 0; + + (bool success, ) = payable(owner).call{value: amount}(""); + require(success, "Transfer failed."); + } + + function getTotalFundsRaised() public view returns (uint256) { + return totalFundsRaised; + } + + function getFundingGoal() public view returns (uint256) { + return fundingGoal; + } +} diff --git a/templates/101/contracts/3-proxy-contracts/uups/UUPSCrowdfundingCampaign.sol b/templates/101/contracts/3-proxy-contracts/uups/UUPSCrowdfundingCampaign.sol new file mode 100644 index 0000000..88caeb2 --- /dev/null +++ b/templates/101/contracts/3-proxy-contracts/uups/UUPSCrowdfundingCampaign.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +// The UUPSCrowdfundingCampaign contract is an upgradeable version of the CrowdfundingCampaign contract +// It uses the UUPS upgrade pattern and inherits from Initializable, UUPSUpgradeable, and OwnableUpgradeable +contract UUPSCrowdfundingCampaign is + Initializable, + UUPSUpgradeable, + OwnableUpgradeable +{ + uint256 private fundingGoal; + uint256 private totalFundsRaised; + mapping(address => uint256) private contributions; + + event ContributionReceived(address contributor, uint256 amount); + event GoalReached(uint256 totalFundsRaised); + + // The `initialize` function replaces the constructor in upgradeable contracts + function initialize(uint256 _fundingGoal) public initializer { + __Ownable_init(); // Initialize ownership to the deployer + __UUPSUpgradeable_init(); // Initialize UUPS upgradeability + + fundingGoal = _fundingGoal; + } + + // Ensure that only the owner can upgrade the contract + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + function contribute() public payable { + require(msg.value > 0, "Contribution must be greater than 0"); + contributions[msg.sender] += msg.value; + totalFundsRaised += msg.value; + + emit ContributionReceived(msg.sender, msg.value); + + if (totalFundsRaised >= fundingGoal) { + emit GoalReached(totalFundsRaised); + } + } + + function withdrawFunds() public onlyOwner { + require(totalFundsRaised >= fundingGoal, "Funding goal not reached"); + + uint256 amount = address(this).balance; + totalFundsRaised = 0; + + (bool success, ) = payable(owner()).call{value: amount}(""); + require(success, "Transfer failed."); + } + + function getTotalFundsRaised() public view returns (uint256) { + return totalFundsRaised; + } + + function getFundingGoal() public view returns (uint256) { + return fundingGoal; + } +} diff --git a/templates/101/contracts/3-proxy-contracts/uups/V2_UUPSCrowdfundingCampaign.sol b/templates/101/contracts/3-proxy-contracts/uups/V2_UUPSCrowdfundingCampaign.sol new file mode 100644 index 0000000..06e512f --- /dev/null +++ b/templates/101/contracts/3-proxy-contracts/uups/V2_UUPSCrowdfundingCampaign.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; + +contract V2_UUPSCrowdfundingCampaign is + Initializable, + UUPSUpgradeable, + OwnableUpgradeable +{ + uint256 private fundingGoal; + uint256 private totalFundsRaised; + mapping(address => uint256) private contributions; + + // In V2 we add a deadline for the campaign + uint256 public deadline; + // We add a flag to check if the V2 contract has been initialized + bool private initializedV2; + + event ContributionReceived(address contributor, uint256 amount); + event GoalReached(uint256 totalFundsRaised); + + // The original initialize function remains unchanged + function initialize(uint256 _fundingGoal) public initializer { + fundingGoal = _fundingGoal; + } + + // The upgrade initialization function for V2 + function initializeV2(uint256 _duration) public onlyOwner { + require(!initializedV2, "V2 already initialized"); + + deadline = block.timestamp + _duration; + initializedV2 = true; + } + + // Modifier to check if the campaign is still within the deadline + // A modifier is a function that is executed + // before the function that uses it + modifier withinDeadline() { + require(block.timestamp <= deadline, "Funding period has ended"); + _; + } + + // We use the `withinDeadline` modifier to ensure + // that contributions are only accepted within the deadline + function contribute() public payable withinDeadline { + require(msg.value > 0, "Contribution must be greater than 0"); + contributions[msg.sender] += msg.value; + totalFundsRaised += msg.value; + + emit ContributionReceived(msg.sender, msg.value); + + if (totalFundsRaised >= fundingGoal) { + emit GoalReached(totalFundsRaised); + } + } + + // We override the _authorizeUpgrade of the UUPSUpgradeable contract + // to ensure that only the owner can upgrade the contract + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + // We add a new function to extend the deadline + // only by the owner of the campaign + function extendDeadline(uint256 _newDuration) public onlyOwner { + deadline = block.timestamp + _newDuration; + } + + function withdrawFunds() public onlyOwner { + require(totalFundsRaised >= fundingGoal, "Funding goal not reached"); + + uint256 amount = address(this).balance; + totalFundsRaised = 0; + + (bool success, ) = payable(owner()).call{value: amount}(""); + require(success, "Transfer failed."); + } + + function getTotalFundsRaised() public view returns (uint256) { + return totalFundsRaised; + } + + function getFundingGoal() public view returns (uint256) { + return fundingGoal; + } +} diff --git a/templates/101/contracts/4-paymaster/approval/ApprovalFlowPaymaster.sol b/templates/101/contracts/4-paymaster/approval/ApprovalFlowPaymaster.sol new file mode 100644 index 0000000..fc05d2d --- /dev/null +++ b/templates/101/contracts/4-paymaster/approval/ApprovalFlowPaymaster.sol @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; +import {IPaymasterFlow} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; +import {TransactionHelper, Transaction} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; + +import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol"; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +import "hardhat/console.sol"; + +// This smart contract pays the gas fees for accounts +// with balance of a specific ERC20 token. +// It makes use of the approval-based flow paymaster. +contract ApprovalFlowPaymaster is IPaymaster, Ownable { + uint256 constant PRICE_FOR_PAYING_FEES = 1; + + address public allowedToken; + + event PaymentCovered(address user, uint256 amount); + + // This is a modifier that restricts the access to the bootloader + // to the formal address of the bootloader. + // This adds a layer of security to the contract. + modifier onlyBootloader() { + require( + msg.sender == BOOTLOADER_FORMAL_ADDRESS, + "Only bootloader can call this method" + ); + _; + } + + // We set the token we will accept for paying the fees + constructor(address _allowedToken) { + allowedToken = _allowedToken; + } + + // This function is called by the bootloader + // to validate whether the transaction + // can be paid for by the paymaster. + // + // It uses the onlyBootloader modifier to ensure that only the bootloader + // can call this method. + function validateAndPayForPaymasterTransaction( + bytes32, + bytes32, + Transaction calldata _transaction + ) + external + payable + onlyBootloader + returns (bytes4 magic, bytes memory context) + { + // Default to transaction acceptance + magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC; + require( + _transaction.paymasterInput.length >= 4, + "The standard paymaster input must be at least 4 bytes long" + ); + + bytes4 paymasterInputSelector = bytes4( + _transaction.paymasterInput[0:4] + ); + // Check if it's approval-based flow + if (paymasterInputSelector == IPaymasterFlow.approvalBased.selector) { + (address token, uint256 amount, ) = abi.decode( + _transaction.paymasterInput[4:], + (address, uint256, bytes) + ); + + // Ensure the token is the allowed one + require(token == allowedToken, "Invalid token"); + + // Check user's allowance + address userAddress = address(uint160(_transaction.from)); + address paymasterAddress = address(this); + uint256 providedAllowance = IERC20(token).allowance( + userAddress, + paymasterAddress + ); + require( + providedAllowance >= PRICE_FOR_PAYING_FEES, + "Min allowance too low" + ); + + uint256 requiredETH = _transaction.gasLimit * + _transaction.maxFeePerGas; + try + IERC20(token).transferFrom( + userAddress, + paymasterAddress, + amount + ) + {} catch (bytes memory revertReason) { + if (revertReason.length <= 4) { + revert("Failed to transferFrom from user's account"); + } else { + assembly { + revert(add(0x20, revertReason), mload(revertReason)) + } + } + } + (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ + value: requiredETH + }(""); + require(success, "Failed to transfer tx fee to bootloader."); + + // This uses hardhat console log to print an event. + // You can see the output in the log of the in-memory-node + console.log("************************************************"); + console.log("PAYING CONTRIBUTION FOR ADDRESS: ", userAddress); + console.log("************************************************"); + emit PaymentCovered(userAddress, _transaction.value); + } else { + revert("Unsupported paymaster flow"); + } + } + + // An optional method that can be used to post-process the transaction + // after it has been executed. + // It uses the onlyBootloader modifier to ensure that only the bootloader + // can call this method. + // This method is not used in this example. + function postTransaction( + bytes calldata _context, + Transaction calldata _transaction, + bytes32, + bytes32, + ExecutionResult _txResult, + uint256 _maxRefundedGas + ) external payable override onlyBootloader {} + + function withdraw(address _to) external onlyOwner { + (bool success, ) = payable(_to).call{value: address(this).balance}(""); + require(success, "Failed to withdraw funds from paymaster."); + } + + receive() external payable {} +} diff --git a/templates/101/contracts/4-paymaster/approval/CrownToken.sol b/templates/101/contracts/4-paymaster/approval/CrownToken.sol new file mode 100644 index 0000000..6a702db --- /dev/null +++ b/templates/101/contracts/4-paymaster/approval/CrownToken.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; + +contract CrownToken is ERC20, Ownable, ERC20Burnable { + constructor() ERC20("Crown Token", "CROWN") { + _mint(msg.sender, 100 * 10 ** decimals()); + } + + function mint(address to, uint256 amount) public onlyOwner { + _mint(to, amount); + } +} diff --git a/templates/101/contracts/4-paymaster/gasless/GaslessPaymaster.sol b/templates/101/contracts/4-paymaster/gasless/GaslessPaymaster.sol new file mode 100644 index 0000000..246d54c --- /dev/null +++ b/templates/101/contracts/4-paymaster/gasless/GaslessPaymaster.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol"; +import {IPaymasterFlow} from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol"; +import {TransactionHelper, Transaction} from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol"; + +import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol"; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +/// @author Matter Labs +/// @notice This contract does not include any validations other than using the paymaster general flow. +contract GaslessPaymaster is IPaymaster, Ownable { + // This is a modifier that restricts the access to the bootloader + // This adds a layer of security to the contract. + modifier onlyBootloader() { + require( + msg.sender == BOOTLOADER_FORMAL_ADDRESS, + "Only bootloader can call this method" + ); + // Continue execution if called from the bootloader. + _; + } + + // This required method is called by the bootloader + // to validate and pay for the transaction fees. + function validateAndPayForPaymasterTransaction( + bytes32, + bytes32, + Transaction calldata _transaction + ) + external + payable + onlyBootloader + returns (bytes4 magic, bytes memory context) + { + // By default we consider the transaction as accepted. + magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC; + require( + _transaction.paymasterInput.length >= 4, + "The standard paymaster input must be at least 4 bytes long" + ); + + bytes4 paymasterInputSelector = bytes4( + _transaction.paymasterInput[0:4] + ); + if (paymasterInputSelector == IPaymasterFlow.general.selector) { + // Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit, + // neither paymaster nor account are allowed to access this context variable. + uint256 requiredETH = _transaction.gasLimit * + _transaction.maxFeePerGas; + + // The bootloader never returns any data, so it can safely be ignored here. + (bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ + value: requiredETH + }(""); + require( + success, + "Failed to transfer tx fee to the Bootloader. Paymaster balance might not be enough." + ); + } else { + revert("Unsupported paymaster flow in paymasterParams."); + } + } + + function postTransaction( + bytes calldata _context, + Transaction calldata _transaction, + bytes32, + bytes32, + ExecutionResult _txResult, + uint256 _maxRefundedGas + ) external payable override onlyBootloader { + // Refunds are not supported yet. + } + + function withdraw(address payable _to) external onlyOwner { + // send paymaster funds to the owner + uint256 balance = address(this).balance; + (bool success, ) = _to.call{value: balance}(""); + require(success, "Failed to withdraw funds from paymaster."); + } + + receive() external payable {} +} diff --git a/templates/101/deploy/1-hello-zksync/deploy.ts b/templates/101/deploy/1-hello-zksync/deploy.ts new file mode 100644 index 0000000..a543a83 --- /dev/null +++ b/templates/101/deploy/1-hello-zksync/deploy.ts @@ -0,0 +1,10 @@ +import { deployContract } from "../../utils"; + +import { ethers } from "ethers"; + +// Deploy a CrowdfundingCampaign contract +export default async function () { + const contractArtifactName = "CrowdfundingCampaign"; + const constructorArguments = [ethers.parseEther(".02").toString()]; + await deployContract(contractArtifactName, constructorArguments); +} diff --git a/templates/101/deploy/2-contract-factory/deploy.ts b/templates/101/deploy/2-contract-factory/deploy.ts new file mode 100644 index 0000000..67240bb --- /dev/null +++ b/templates/101/deploy/2-contract-factory/deploy.ts @@ -0,0 +1,33 @@ +import { deployContract, getWallet } from "../utils"; +import { ethers } from "ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +export default async function (hre: HardhatRuntimeEnvironment) { + const contractArtifactName = "CrowdfundingFactory"; + const crowdfundingFactory = await deployContract(contractArtifactName); + + console.log(`🏭 CrowdfundingFactory address: ${crowdfundingFactory.target}`); + + const contractArtifact = await hre.artifacts.readArtifact( + "CrowdfundingFactory" + ); + const factoryContract = new ethers.Contract( + crowdfundingFactory.target, + contractArtifact.abi, + getWallet() + ); + + // Define funding goal for the campaign, e.g., 0.1 ether + const fundingGoalInWei = ethers.parseEther("0.1").toString(); + + // Use the factory to create a new CrowdfundingCampaign + const createTx = await factoryContract.createCampaign(fundingGoalInWei); + await createTx.wait(); + + // Retrieve the address of the newly created CrowdfundingCampaign + const campaigns = await factoryContract.getCampaigns(); + const newCampaignAddress = campaigns[campaigns.length - 1]; + + console.log(`🚀 New CrowdfundingCampaign deployed at: ${newCampaignAddress}`); + console.log("✅ Deployment and campaign creation complete!"); +} diff --git a/templates/101/deploy/3-proxy-contracts/beacon/deploy.ts b/templates/101/deploy/3-proxy-contracts/beacon/deploy.ts new file mode 100644 index 0000000..76ca8d9 --- /dev/null +++ b/templates/101/deploy/3-proxy-contracts/beacon/deploy.ts @@ -0,0 +1,28 @@ +import { getWallet } from "../../../utils"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import { ethers } from "ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +export default async function (hre: HardhatRuntimeEnvironment) { + const wallet = getWallet(); + const deployer = new Deployer(hre, wallet); + + const contractArtifact = await deployer.loadArtifact( + "BeaconCrowdfundingCampaign" + ); + const fundingGoalInWei = ethers.parseEther("0.1").toString(); + + const beacon = await hre.zkUpgrades.deployBeacon( + getWallet(), + contractArtifact + ); + await beacon.waitForDeployment(); + + const crowdfunding = await hre.zkUpgrades.deployBeaconProxy( + deployer.zkWallet, + await beacon.getAddress(), + contractArtifact, + [fundingGoalInWei] + ); + await crowdfunding.waitForDeployment(); +} diff --git a/templates/101/deploy/3-proxy-contracts/beacon/upgrade.ts b/templates/101/deploy/3-proxy-contracts/beacon/upgrade.ts new file mode 100644 index 0000000..45e6371 --- /dev/null +++ b/templates/101/deploy/3-proxy-contracts/beacon/upgrade.ts @@ -0,0 +1,51 @@ +import { getWallet } from "../../../utils"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import * as zk from "zksync-ethers"; +import { Contract } from "ethers"; + +// Update with the address for your beacon contract +// and the beacon proxy address +const beaconAddress = "YOUR_BEACON_ADDRESS_HERE"; +const proxyAddress = "YOUR_PROXY_ADDRESS_HERE"; + +export default async function (hre: HardhatRuntimeEnvironment) { + const wallet = getWallet(); + const deployer = new Deployer(hre, wallet); + + const contractV2Artifact = await deployer.loadArtifact( + "V2_BeaconCrowdfundingCampaign" + ); + + // Upgrade the proxy to V2 + await hre.zkUpgrades.upgradeBeacon( + deployer.zkWallet, + beaconAddress, + contractV2Artifact + ); + + console.log( + "Successfully upgraded BeaconCrowdfundingCampaign to V2_BeaconCrowdfundingCampaign" + ); + + const attachTo = new zk.ContractFactory( + contractV2Artifact.abi, + contractV2Artifact.bytecode, + deployer.zkWallet + ); + + const upgradedContract = attachTo.attach(proxyAddress); + + upgradedContract.connect(deployer.zkWallet); + // wait some time before the next call + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Initialize V2 with a new campaign duration + const durationInSeconds = 30 * 24 * 60 * 60; // For example, setting a 30-day duration + const initTx = await upgradedContract.initializeV2(durationInSeconds); + const receipt = await initTx.wait(); + + console.log( + `V2_BeaconCrowdfundingCampaign initialized. Transaction Hash: ${receipt.hash}` + ); +} diff --git a/templates/101/deploy/3-proxy-contracts/transparent/deploy.ts b/templates/101/deploy/3-proxy-contracts/transparent/deploy.ts new file mode 100644 index 0000000..239abf8 --- /dev/null +++ b/templates/101/deploy/3-proxy-contracts/transparent/deploy.ts @@ -0,0 +1,27 @@ +import { getWallet } from "../../../utils"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import { ethers } from "ethers"; +import * as hre from "hardhat"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +export default async function (hre: HardhatRuntimeEnvironment) { + const wallet = getWallet(); + const deployer = new Deployer(hre, wallet); + const fundingGoal = "0.1"; + + console.log("Deploying with funding goal:", fundingGoal); + + const contractArtifact = await deployer.loadArtifact( + "ProxyableCrowdfundingCampaign" + ); + const fundingGoalInWei = ethers.parseEther(fundingGoal).toString(); + // Deploy the contract using a transparent proxy + const crowdfunding = await hre.zkUpgrades.deployProxy( + getWallet(), + contractArtifact, + [ethers.parseEther(fundingGoal).toString()], + { initializer: "initialize" } + ); + + return await crowdfunding.waitForDeployment(); +} diff --git a/templates/101/deploy/3-proxy-contracts/transparent/upgrade.ts b/templates/101/deploy/3-proxy-contracts/transparent/upgrade.ts new file mode 100644 index 0000000..0d8f1ce --- /dev/null +++ b/templates/101/deploy/3-proxy-contracts/transparent/upgrade.ts @@ -0,0 +1,39 @@ +import { getWallet } from "../../../utils"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import * as hre from "hardhat"; + +// Replace with your deployed transparent proxy address +const proxyAddress = "YOUR_PROXY_ADDRESS_HERE"; + +export default async function () { + const wallet = getWallet(); + const deployer = new Deployer(hre, wallet); + + const contractV2Artifact = await deployer.loadArtifact( + "V2_ProxyableCrowdfundingCampaign" + ); + + // Upgrade the proxy to V2 + const upgradedContract = await hre.zkUpgrades.upgradeProxy( + deployer.zkWallet, + proxyAddress, + contractV2Artifact + ); + + console.log( + "Successfully upgraded ProxyableCrowdfundingCampaign to V2_ProxyableCrowdfundingCampaign" + ); + + upgradedContract.connect(deployer.zkWallet); + // wait some time before the next call + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Initialize V2 with a new campaign duration + const durationInSeconds = 30 * 24 * 60 * 60; // For example, setting a 30-day duration + const initTx = await upgradedContract.initializeV2(durationInSeconds); + const receipt = await initTx.wait(); + + console.log( + `V2_ProxyableCrowdfundingCampaign initialized. Transaction Hash: ${receipt.hash}` + ); +} diff --git a/templates/101/deploy/3-proxy-contracts/uups/deploy.ts b/templates/101/deploy/3-proxy-contracts/uups/deploy.ts new file mode 100644 index 0000000..1b99a2c --- /dev/null +++ b/templates/101/deploy/3-proxy-contracts/uups/deploy.ts @@ -0,0 +1,23 @@ +import { getWallet } from "../../../utils"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import { ethers } from "ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +export default async function (hre: HardhatRuntimeEnvironment) { + const wallet = getWallet(); + const deployer = new Deployer(hre, wallet); + + const contractArtifact = await deployer.loadArtifact( + "UUPSCrowdfundingCampaign" + ); + const fundingGoalInWei = ethers.parseEther("0.1").toString(); + + const crowdfunding = await hre.zkUpgrades.deployProxy( + getWallet(), + contractArtifact, + [fundingGoalInWei], + { initializer: "initialize" } + ); + + await crowdfunding.waitForDeployment(); +} diff --git a/templates/101/deploy/3-proxy-contracts/uups/upgrade.ts b/templates/101/deploy/3-proxy-contracts/uups/upgrade.ts new file mode 100644 index 0000000..52aeb7c --- /dev/null +++ b/templates/101/deploy/3-proxy-contracts/uups/upgrade.ts @@ -0,0 +1,34 @@ +import { getWallet } from "../../../utils"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; + +// Replace with the address of the proxy contract you want to upgrade +const proxyAddress = "0x60Aa68f9D0D736B9a0a716d04323Ba3b22602840"; + +export default async function (hre: HardhatRuntimeEnvironment) { + const wallet = getWallet(); + const deployer = new Deployer(hre, wallet); + + const contractV2Artifact = await deployer.loadArtifact( + "V2_UUPSCrowdfundingCampaign" + ); + const upgradedContract = await hre.zkUpgrades.upgradeProxy( + deployer.zkWallet, + proxyAddress, + contractV2Artifact + ); + console.log( + "Successfully upgraded UUPSCrowdfundingCampaign to V2_UUPSCrowdfundingCampaign" + ); + + upgradedContract.connect(deployer.zkWallet); + // wait some time before the next call + await new Promise((resolve) => setTimeout(resolve, 0)); + + const durationInSeconds = 30 * 24 * 60 * 60; // For example, setting a 30-day duration + + const initTx = await upgradedContract.initializeV2(durationInSeconds); + const receipt = await initTx.wait(); + + console.log("V2_UUPSCrowdfundingCampaign initialized!", receipt.hash); +} diff --git a/templates/101/deploy/4-paymaster/approval/deploy.ts b/templates/101/deploy/4-paymaster/approval/deploy.ts new file mode 100644 index 0000000..e4ec82f --- /dev/null +++ b/templates/101/deploy/4-paymaster/approval/deploy.ts @@ -0,0 +1,72 @@ +import { Wallet } from "zksync-ethers"; +import { + deployContract, + getWallet, + getProvider, + LOCAL_RICH_WALLETS, +} from "../../../utils"; +import { BaseContract, ethers } from "ethers"; +import { ApprovalFlowPaymaster, CrownToken } from "../../../typechain-types"; + +export default async function () { + const wallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + const provider = getProvider(); + + const crownToken = await deployCrownToken(wallet); + + const paymaster = await deployPaymaster(wallet, crownToken); + + await ( + await wallet.sendTransaction({ + to: paymaster.target, + value: ethers.parseEther("0.005"), + }) + ).wait(); + + let paymasterBalance = await provider.getBalance(paymaster.target.toString()); + console.log( + `\nPaymaster ETH balance is now ${ethers.formatEther( + paymasterBalance.toString() + )}` + ); +} + +export async function deployCrownToken( + wallet: Wallet, + quiet = false +): Promise { + const contractArtifactName = "CrownToken"; + const contract = (await deployContract(contractArtifactName, [], { + wallet, + silent: true, + })) as unknown as CrownToken; + + !quiet + ? console.log( + `\nCrownToken contract deployed at ${await contract.getAddress()}` + ) + : null; + + return contract; +} + +export async function deployPaymaster( + wallet: Wallet, + _tokenAddress: BaseContract, + quiet = false +): Promise { + const contractArtifactName = "ApprovalFlowPaymaster"; + const tokenAddress = await _tokenAddress.getAddress(); + const contract = (await deployContract(contractArtifactName, [tokenAddress], { + wallet, + silent: true, + })) as unknown as ApprovalFlowPaymaster; + + !quiet + ? console.log( + `ApprovalFlowPaymaster contract deployed at ${await contract.getAddress()}` + ) + : null; + + return contract; +} diff --git a/templates/101/deploy/4-paymaster/approval/interact.ts b/templates/101/deploy/4-paymaster/approval/interact.ts new file mode 100644 index 0000000..efa021a --- /dev/null +++ b/templates/101/deploy/4-paymaster/approval/interact.ts @@ -0,0 +1,83 @@ +import * as hre from "hardhat"; +import { + getWallet, + getProvider, + deployContract, + LOCAL_RICH_WALLETS, +} from "../../../utils"; +import { ethers } from "ethers"; +import { utils } from "zksync-ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { CrowdfundingCampaign } from "../../../typechain-types"; + +// Update with the addresses for your paymaster contract +// and token contract +const TOKEN_ADDRESS = "YOUR_TOKEN_ADDRESS"; +const PAYMASTER_ADDRESS = "YOUR_PAYMASTER_ADDRESS"; + +export default async function (hre: HardhatRuntimeEnvironment) { + const provider = getProvider(); + console.log("Deploying a CrowdfundingCampaign contract..."); + + // Deploy a crowdfund contract for this example. + // We will use the paymaster to cover funds when + // the user contributes to the crowdfund + const deployedContract = await deployCrowdfundContract(); + const contractArtifact = await hre.artifacts.readArtifact( + "CrowdfundingCampaign" + ); + const contract = new ethers.Contract( + await deployedContract.getAddress(), + contractArtifact.abi, + getWallet(LOCAL_RICH_WALLETS[1].privateKey) + ); + const contributionAmount = ethers.parseEther("0.0001"); + + // Get paymaster params for the ApprovalBased paymaster + const paymasterParams = utils.getPaymasterParams(PAYMASTER_ADDRESS, { + type: "ApprovalBased", + token: TOKEN_ADDRESS, + minimalAllowance: 1n, + innerInput: new Uint8Array(), + }); + // Determine the gas limit for the contribution transaction + const gasLimit = await contract.contribute.estimateGas({ + value: contributionAmount, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams: paymasterParams, + }, + }); + + // Contribute to the crowdfund contract + // and have the paymaster cover the funds + const transaction = await contract.contribute({ + value: contributionAmount, + maxPriorityFeePerGas: 0n, + maxFeePerGas: await provider.getGasPrice(), + gasLimit, + // Pass the paymaster params as custom data + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams, + }, + }); + + console.log( + `Contributing ${ethers.formatEther( + contributionAmount.toString() + )} to the crowdfund contract...` + ); + console.log(`Transaction hash: ${transaction.hash}`); + + await transaction.wait(); + console.log("Contribution successful!"); +} + +async function deployCrowdfundContract(): Promise { + const contractArtifactName = "CrowdfundingCampaign"; + const constructorArguments = [ethers.parseEther(".02").toString()]; + return (await deployContract(contractArtifactName, constructorArguments, { + silent: true, + })) as unknown as CrowdfundingCampaign; +} diff --git a/templates/101/deploy/4-paymaster/gasless/deploy.ts b/templates/101/deploy/4-paymaster/gasless/deploy.ts new file mode 100644 index 0000000..eaa1d00 --- /dev/null +++ b/templates/101/deploy/4-paymaster/gasless/deploy.ts @@ -0,0 +1,50 @@ +import { Wallet } from "zksync-ethers"; +import { + deployContract, + getWallet, + getProvider, + LOCAL_RICH_WALLETS, +} from "../../../utils"; +import { ethers } from "ethers"; +import { GaslessPaymaster } from "../../../typechain-types"; + +export default async function () { + const wallet = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + const provider = getProvider(); + + const paymaster = await deployPaymaster(wallet); + + // Supplying paymaster with ETH + await ( + await wallet.sendTransaction({ + to: paymaster.target, + value: ethers.parseEther("0.005"), + }) + ).wait(); + + let paymasterBalance = await provider.getBalance(paymaster.target.toString()); + console.log( + `\nPaymaster ETH balance is now ${ethers.formatEther( + paymasterBalance.toString() + )}` + ); +} + +export async function deployPaymaster( + wallet: Wallet, + silent = false +): Promise { + const contractArtifactName = "GaslessPaymaster"; + const contract = (await deployContract(contractArtifactName, [], { + wallet, + silent: true, + })) as unknown as GaslessPaymaster; + + !silent + ? console.log( + `GaslessPaymaster contract deployed at ${await contract.getAddress()}` + ) + : null; + + return contract; +} diff --git a/templates/101/deploy/4-paymaster/gasless/interact.ts b/templates/101/deploy/4-paymaster/gasless/interact.ts new file mode 100644 index 0000000..1cd02fb --- /dev/null +++ b/templates/101/deploy/4-paymaster/gasless/interact.ts @@ -0,0 +1,80 @@ +import { + getProvider, + deployContract, + getWallet, + LOCAL_RICH_WALLETS, +} from "../../../utils"; +import { ethers } from "ethers"; +import { utils, Wallet } from "zksync-ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { CrowdfundingCampaign } from "../../../typechain-types"; + +// Update with the address for your paymaster contract +const PAYMASTER_ADDRESS = "YOUR_PAYMASTER_ADDRESS"; + +export default async function (hre: HardhatRuntimeEnvironment) { + const provider = getProvider(); + console.log("Deploying a CrowdfundingCampaign contract..."); + + // Deploy a crowdfund contract for this example. + // We will use the paymaster to cover funds when + // the user contributes to the crowdfund + const deployedContract = await deployCrowdfundContract(); + const contractArtifact = await hre.artifacts.readArtifact( + "CrowdfundingCampaign" + ); + const contract = new ethers.Contract( + await deployedContract.getAddress(), + contractArtifact.abi, + getWallet(LOCAL_RICH_WALLETS[1].privateKey) + ); + const contributionAmount = ethers.parseEther("0.01"); + + // Get paymaster params for the Gasless paymaster + const paymasterParams = utils.getPaymasterParams(PAYMASTER_ADDRESS, { + type: "General", + innerInput: new Uint8Array(), + }); + // Determine the gas limit for the contribution transaction + const gasLimit = await contract.contribute.estimateGas({ + value: contributionAmount, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams: paymasterParams, + }, + }); + // Contribute to the crowdfund contract + // and have the paymaster cover the funds + const transaction = await contract.contribute({ + value: contributionAmount, + maxPriorityFeePerGas: 0n, + maxFeePerGas: await provider.getGasPrice(), + gasLimit, + // Pass the paymaster params as custom data + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams, + }, + }); + + console.log( + `Contributing ${ethers.formatEther( + contributionAmount.toString() + )} to the crowdfund contract...` + ); + console.log(`Transaction hash: ${transaction.hash}`); + + await transaction.wait(); + console.log("Contribution successful!"); +} + +export async function deployCrowdfundContract( + wallet?: Wallet +): Promise { + const contractArtifactName = "CrowdfundingCampaign"; + const constructorArguments = [ethers.parseEther(".02").toString()]; + return (await deployContract(contractArtifactName, constructorArguments, { + wallet, + silent: true, + })) as unknown as CrowdfundingCampaign; +} diff --git a/templates/101/deploy/4-paymaster/general-deploy.ts b/templates/101/deploy/4-paymaster/general-deploy.ts new file mode 100644 index 0000000..e69de29 diff --git a/templates/101/hardhat.config.ts b/templates/101/hardhat.config.ts new file mode 100644 index 0000000..c57e599 --- /dev/null +++ b/templates/101/hardhat.config.ts @@ -0,0 +1,36 @@ +import type { HardhatUserConfig } from "hardhat/config"; +// import "@nomicfoundation/hardhat-chai-matchers"; +import "@nomicfoundation/hardhat-toolbox"; +import "@matterlabs/hardhat-zksync"; +// import "@typechain/hardhat"; + +const config: HardhatUserConfig = { + solidity: "0.8.24", + defaultNetwork: "inMemoryNode", + networks: { + inMemoryNode: { + url: "http://127.0.0.1:8011", + ethNetwork: "", // in-memory node doesn't support eth node; removing this line will cause an error + zksync: true, + }, + zkSyncSepoliaTestnet: { + url: "https://sepolia.era.zksync.dev", + ethNetwork: "sepolia", + zksync: true, + verifyURL: + "https://explorer.sepolia.era.zksync.dev/contract_verification", + }, + zkSyncMainnet: { + url: "https://mainnet.era.zksync.io", + ethNetwork: "mainnet", + zksync: true, + verifyURL: + "https://zksync2-mainnet-explorer.zksync.io/contract_verification", + }, + hardhat: { + zksync: true, + }, + }, +}; + +export default config; diff --git a/templates/101/package.json b/templates/101/package.json new file mode 100644 index 0000000..a5e6930 --- /dev/null +++ b/templates/101/package.json @@ -0,0 +1,63 @@ +{ + "name": "zksync-101", + "version": "1.0.0", + "description": "A project to learn how to build with zksync-cli", + "private": true, + "author": "Matter Labs", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/matter-labs/zksync-contract-templates.git" + }, + "keywords": [ + "zksync", + "zksync-cli", + "hardhat" + ], + "scripts": { + "deploy:hello-zksync": "hardhat deploy-zksync --script 1-hello-zksync/deploy.ts", + "deploy:crowdfunding-factory": "hardhat deploy-zksync --script 2-crowdfunding-factory/deploy.ts", + "test:crowdfunding-campaign": "hardhat test --network hardhat test/1-hello-zksync/CrowdfundingCampaign.test.ts", + "deploy:transparent-proxy": "hardhat deploy-zksync --script 3-proxy-contracts/transparent/deploy.ts", + "upgrade:transparent-proxy": "hardhat deploy-zksync --script 3-proxy-contracts/transparent/upgrade.ts", + "deploy:uups-proxy": "hardhat deploy-zksync --script 3-proxy-contracts/uups/deploy.ts", + "upgrade:uups-proxy": "hardhat deploy-zksync --script 3-proxy-contracts/uups/upgrade.ts", + "deploy:beacon-proxy": "hardhat deploy-zksync --script 3-proxy-contracts/beacon/deploy.ts", + "upgrade:beacon-proxy": "hardhat deploy-zksync --script 3-proxy-contracts/beacon/upgrade.ts", + "deploy:approval-paymaster": "hardhat deploy-zksync --script 4-paymaster/approval/deploy.ts", + "interact:approval-paymaster": "hardhat deploy-zksync --script 4-paymaster/approval/interact.ts", + "deploy:gasless-paymaster": "hardhat deploy-zksync --script 4-paymaster/gasless/deploy.ts", + "interact:gasless-paymaster": "hardhat deploy-zksync --script 4-paymaster/gasless/interact.ts", + "compile": "hardhat compile", + "clean": "hardhat clean", + "test": "hardhat test --network hardhat" + }, + "bugs": { + "url": "https://github.com/matter-labs/zksync-contract-templates/issues" + }, + "homepage": "https://github.com/matter-labs/zksync-contract-templates#readme", + "devDependencies": { + "@matterlabs/hardhat-zksync": "^1.0.0", + "@matterlabs/zksync-contracts": "^0.6.1", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.7", + "@nomicfoundation/hardhat-ignition": "^0.15.5", + "@nomicfoundation/hardhat-ignition-ethers": "^0.15.5", + "@nomicfoundation/hardhat-network-helpers": "^1.0.11", + "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "@nomicfoundation/ignition-core": "^0.15.5", + "@openzeppelin/contracts": "^4.9.5", + "@openzeppelin/contracts-upgradeable": "^4.9.5", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.17", + "@types/mocha": "^10.0.7", + "dotenv": "^16.4.5", + "hardhat": "^2.22.8", + "hardhat-gas-reporter": "^1.0.10", + "solidity-coverage": "^0.8.12", + "ts-node": "^10.9.2", + "typechain": "^8.3.2", + "typescript": "^4.9.5", + "zksync-ethers": "^6.11.1" + } +} diff --git a/templates/101/test/1-hello-zksync/CrowdfundingCampaign.test.ts b/templates/101/test/1-hello-zksync/CrowdfundingCampaign.test.ts new file mode 100644 index 0000000..de6e68d --- /dev/null +++ b/templates/101/test/1-hello-zksync/CrowdfundingCampaign.test.ts @@ -0,0 +1,90 @@ +import { expect } from "chai"; +import { ethers } from "ethers"; +import { getWallet, LOCAL_RICH_WALLETS, deployContract } from "../../utils"; +import { CrowdfundingCampaign } from "../../typechain-types"; +import { Wallet } from "zksync-ethers"; + +describe("CrowdfundingCampaign", function () { + let campaign: CrowdfundingCampaign; + let owner: Wallet, addr1: Wallet, addr2: Wallet; + + beforeEach(async function () { + owner = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + addr1 = getWallet(LOCAL_RICH_WALLETS[1].privateKey); + addr2 = getWallet(LOCAL_RICH_WALLETS[2].privateKey); + const fundingGoalInWei = ethers.parseEther("1").toString(); + campaign = (await deployContract( + "CrowdfundingCampaign", + [fundingGoalInWei], + { wallet: owner, silent: true } + )) as unknown as CrowdfundingCampaign; + }); + + describe("contribute", function () { + it("should reject contributions of 0", async function () { + await expect( + campaign.connect(addr1).contribute({ value: ethers.parseEther("0") }) + ).to.be.revertedWith("Contribution must be greater than 0"); + }); + + it("should aggregate contributions in totalFundsRaised", async function () { + await campaign + .connect(addr1) + .contribute({ value: ethers.parseEther("0.5") }); + await campaign + .connect(addr2) + .contribute({ value: ethers.parseEther("0.3") }); + expect(await campaign.getTotalFundsRaised()).to.equal( + ethers.parseEther("0.8") + ); + }); + + it("should emit GoalReached event when funding goal is met", async function () { + await expect( + campaign.connect(addr1).contribute({ value: ethers.parseEther("1") }) + ) + .to.emit(campaign, "GoalReached") + .withArgs(ethers.parseEther("1")); + }); + }); + + describe("withdrawFunds", function () { + it("should revert if called by a non-owner", async function () { + await expect(campaign.connect(addr1).withdrawFunds()).to.be.revertedWith( + "Only the owner can withdraw funds" + ); + }); + + it("should revert if funding goal hasn't been reached", async function () { + await expect(campaign.connect(owner).withdrawFunds()).to.be.revertedWith( + "Funding goal not reached" + ); + }); + + it("should transfer the funds to the owner when funds have been raised", async function () { + await campaign.connect(addr1).contribute({ value: 500000000000000000n }); + await campaign.connect(addr2).contribute({ value: 500000000000000000n }); + + const initialBalance = await owner.getBalance(); + await campaign.connect(owner).withdrawFunds(); + const finalBalance = await owner.getBalance(); + expect(ethers.formatEther(finalBalance - initialBalance)).to.match( + /0\.999/ + ); + }); + }); + + describe("getFundingGoal", function () { + it("should return the correct funding goal", async function () { + const fundingGoal = await campaign.getFundingGoal(); + expect(fundingGoal).to.equal(ethers.parseEther("1")); + }); + }); + + describe("getTotalFundsRaised", function () { + it("should return 0 when no contributions have been made", async function () { + const totalFundsRaised = await campaign.getTotalFundsRaised(); + expect(totalFundsRaised).to.equal(ethers.parseEther("0")); + }); + }); +}); diff --git a/templates/101/test/2-deploy-crowdfunding/CrowdfundingFactory.test.ts b/templates/101/test/2-deploy-crowdfunding/CrowdfundingFactory.test.ts new file mode 100644 index 0000000..e031d7c --- /dev/null +++ b/templates/101/test/2-deploy-crowdfunding/CrowdfundingFactory.test.ts @@ -0,0 +1,50 @@ +import "@nomicfoundation/hardhat-chai-matchers"; +import { expect } from "chai"; +import { ethers } from "ethers"; +import { getWallet, LOCAL_RICH_WALLETS, deployContract } from "../../utils"; +import { CrowdfundingFactory } from "../../typechain-types"; +import { Wallet } from "zksync-ethers"; + +describe("CrowdfundingFactory", function () { + let factory: CrowdfundingFactory; + let owner: Wallet, addr1: Wallet; + + beforeEach(async function () { + owner = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + addr1 = getWallet(LOCAL_RICH_WALLETS[1].privateKey); + factory = (await deployContract("CrowdfundingFactory", [], { + wallet: owner, + silent: true, + })) as unknown as CrowdfundingFactory; + }); + + describe("createCampaign", function () { + it("should create a new CrowdfundingCampaign", async function () { + const fundingGoalInWei = ethers.parseEther("1").toString(); + const createTx = await factory.createCampaign(fundingGoalInWei); + await createTx.wait(); + + const [event] = await factory.queryFilter( + factory.filters.CampaignCreated() + ); + + const campaigns = await factory.getCampaigns(); + const newCampaignAddress = campaigns[campaigns.length - 1]; + + expect(newCampaignAddress).to.equal(event.args[0]); + }); + + it("should emit a CampaignCreated event", async function () { + const fundingGoalInWei = ethers.parseEther("1").toString(); + const createTx = await factory.createCampaign(fundingGoalInWei); + await createTx.wait(); + + const [event] = await factory.queryFilter( + factory.filters.CampaignCreated() + ); + + expect(event.args[0]).to.be.properAddress; + expect(event.args[1]).to.equal(fundingGoalInWei); + }); + }); +}); diff --git a/templates/101/test/3-proxy-contracts/BeaconProxy.test.ts b/templates/101/test/3-proxy-contracts/BeaconProxy.test.ts new file mode 100644 index 0000000..b39c0ab --- /dev/null +++ b/templates/101/test/3-proxy-contracts/BeaconProxy.test.ts @@ -0,0 +1,131 @@ +import { expect } from "chai"; +import { Contract, ethers } from "ethers"; +import { getWallet, LOCAL_RICH_WALLETS } from "../../utils"; +import { Wallet } from "zksync-ethers"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import * as hre from "hardhat"; +import * as zk from "zksync-ethers"; +import fs from "fs"; +import path from "path"; +import { + BeaconCrowdfundingCampaign, + V2_BeaconCrowdfundingCampaign, +} from "../../typechain-types"; + +describe("Beacon Proxy Campaign", function () { + let campaign: BeaconCrowdfundingCampaign; + let owner: Wallet, addr1: Wallet; + let beacon: ethers.Contract; + + beforeEach(async () => { + owner = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + addr1 = getWallet(LOCAL_RICH_WALLETS[1].privateKey); + const deployer = new Deployer(hre, owner); + const contractArtifact = await deployer.loadArtifact( + "BeaconCrowdfundingCampaign" + ); + + // delete the .upgradable/ZKsync-era-test-node.json file + // to ensure that the contract is deployed + // from scratch + const filePath = path.join( + __dirname, + "../../", + ".upgradable/ZKsync-era-test-node.json" + ); + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + const fundingGoalInWei = ethers.parseEther("0.777").toString(); + beacon = await hre.zkUpgrades.deployBeacon( + deployer.zkWallet, + contractArtifact, + {}, + true + ); + await beacon.waitForDeployment(); + campaign = (await hre.zkUpgrades.deployBeaconProxy( + deployer.zkWallet, + await beacon.getAddress(), + contractArtifact, + [fundingGoalInWei], + {}, + true + )) as unknown as BeaconCrowdfundingCampaign; + campaign.waitForDeployment(); + }); + + describe("V1", function () { + it("has a funding goal of 0.777 ETH", async function () { + expect(await campaign.getFundingGoal()).to.equal( + ethers.parseEther("0.777").toString() + ); + }); + + it("does not allow initialize to be called more than once", async function () { + await expect( + campaign.initialize(ethers.parseEther("1").toString()) + ).to.be.revertedWith("Initializable: contract is already initialized"); + }); + }); + + describe("V2", function () { + let v2Campaign: V2_BeaconCrowdfundingCampaign; + let blockTimestamp: number; + const durationInSeconds = 1800; // 5 hours + + beforeEach(async () => { + const campaignAddress = await campaign.getAddress(); + const deployer = new Deployer(hre, owner); + + const contractV2Artifact = await deployer.loadArtifact( + "V2_BeaconCrowdfundingCampaign" + ); + + beacon = await hre.zkUpgrades.upgradeBeacon( + deployer.zkWallet, + await beacon.getAddress(), + contractV2Artifact, + {}, + true + ); + + const attachTo = new zk.ContractFactory( + contractV2Artifact.abi, + contractV2Artifact.bytecode, + deployer.zkWallet + ); + const attachment = attachTo.attach(campaignAddress); + v2Campaign = attachment.connect( + deployer.zkWallet + ) as unknown as V2_BeaconCrowdfundingCampaign; + // wait some time before the next call + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Initialize V2 with a new campaign duration + const transaction = await v2Campaign.initializeV2(durationInSeconds); + const receipt = await transaction.wait(); + blockTimestamp = (await receipt?.getBlock())?.timestamp || 0; + }); + + it("does not allow initializeV2 to be run more than once", async () => { + await expect( + v2Campaign.initializeV2(durationInSeconds) + ).to.be.revertedWith("V2 already initialized"); + }); + + it("has a deadline", async () => { + expect(await v2Campaign.deadline()).to.equal( + blockTimestamp + durationInSeconds + ); + }); + + it("only allows the owner to update the deadline", async () => { + await expect( + v2Campaign.connect(addr1).extendDeadline(durationInSeconds) + ).to.be.revertedWith("Only the owner can extend the deadline"); + }); + }); +}); diff --git a/templates/101/test/3-proxy-contracts/TransparentProxy.test.ts b/templates/101/test/3-proxy-contracts/TransparentProxy.test.ts new file mode 100644 index 0000000..dd1492f --- /dev/null +++ b/templates/101/test/3-proxy-contracts/TransparentProxy.test.ts @@ -0,0 +1,123 @@ +import { expect } from "chai"; +import { ethers } from "ethers"; +import { getWallet, LOCAL_RICH_WALLETS } from "../../utils"; +import { Wallet } from "zksync-ethers"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import * as hre from "hardhat"; +import { + ProxyableCrowdfundingCampaign, + V2ProxyableCrowdfundingCampaign, +} from "../../typechain-types"; +import fs from "fs"; +import path from "path"; + +describe("Transparent Proxy Campaign", function () { + let campaign: ProxyableCrowdfundingCampaign; + let owner: Wallet, addr1: Wallet; + + beforeEach(async () => { + owner = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + addr1 = getWallet(LOCAL_RICH_WALLETS[1].privateKey); + const deployer = new Deployer(hre, owner); + const contractArtifact = await deployer.loadArtifact( + "ProxyableCrowdfundingCampaign" + ); + + // delete the .upgradable/ZKsync-era-test-node.json file + // to ensure that the contract is deployed + // from scratch + const filePath = path.join( + __dirname, + "../../", + ".upgradable/ZKsync-era-test-node.json" + ); + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + const fundingGoalInWei = ethers.parseEther("1").toString(); + campaign = (await hre.zkUpgrades.deployProxy( + deployer.zkWallet, + contractArtifact, + [fundingGoalInWei], + { initializer: "initialize" }, + true + )) as unknown as ProxyableCrowdfundingCampaign; + campaign.waitForDeployment(); + }); + + describe("V1", function () { + it("has a funding goal of 1 ETH", async function () { + expect(await campaign.getFundingGoal()).to.equal( + ethers.parseEther("1").toString() + ); + }); + + it("does not allow initialize to be called more than once", async function () { + await expect( + campaign.initialize(ethers.parseEther("1").toString()) + ).to.be.revertedWith("Initializable: contract is already initialized"); + }); + }); + + describe("V2", function () { + let v2campaign: V2ProxyableCrowdfundingCampaign; + let blockTimestamp: number; + const durationInSeconds = 1800; // 5 hours + + beforeEach(async () => { + const campaignAddress = await campaign.getAddress(); + const deployer = new Deployer(hre, owner); + + const contractV2Artifact = await deployer.loadArtifact( + "V2_ProxyableCrowdfundingCampaign" + ); + + v2campaign = (await hre.zkUpgrades.upgradeProxy( + deployer.zkWallet, + campaignAddress, + contractV2Artifact, + {}, + true + )) as unknown as V2ProxyableCrowdfundingCampaign; + + v2campaign.connect(deployer.zkWallet); + // wait some time before the next call + await new Promise((resolve) => setTimeout(resolve, 0)); + + // Initialize V2 with a new campaign duration + + const transaction = await v2campaign.initializeV2(durationInSeconds); + const receipt = await transaction.wait(); + blockTimestamp = (await receipt?.getBlock())?.timestamp || 0; + }); + + it("has a deadline", async () => { + expect(await v2campaign.deadline()).to.equal( + blockTimestamp + durationInSeconds + ); + }); + + it("only allows the owner to update the deadline", async () => { + await expect( + v2campaign.connect(addr1).extendDeadline(durationInSeconds) + ).to.be.revertedWith("Only the owner can extend the deadline"); + }); + + it("does not allow initializeV2 to be run more than once", async () => { + await expect( + v2campaign.initializeV2(durationInSeconds) + ).to.be.revertedWith("V2 already initialized"); + }); + + // in-memory-node does not support time manipulation yet + // + // it("only allows contributions before the deadline", async () => { + // await time.increase(durationInSeconds - 1); + // await expect( + // v2campaign.connect(addr1).contribute({ value: ethers.parseEther("1") }) + // ).to.be.revertedWith("The deadline has passed"); + // }); + }); +}); diff --git a/templates/101/test/3-proxy-contracts/UupsProxy.test.ts b/templates/101/test/3-proxy-contracts/UupsProxy.test.ts new file mode 100644 index 0000000..081a967 --- /dev/null +++ b/templates/101/test/3-proxy-contracts/UupsProxy.test.ts @@ -0,0 +1,120 @@ +import { expect } from "chai"; +import { ethers } from "ethers"; +import { getWallet, LOCAL_RICH_WALLETS } from "../../utils"; +import { Wallet } from "zksync-ethers"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import * as hre from "hardhat"; +import { + UUPSCrowdfundingCampaign, + V2_UUPSCrowdfundingCampaign, +} from "../../typechain-types"; +import fs from "fs"; +import path from "path"; + +describe("UUPS Proxy Campaign", function () { + let campaign: UUPSCrowdfundingCampaign; + let owner: Wallet, addr1: Wallet; + + beforeEach(async () => { + owner = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + addr1 = getWallet(LOCAL_RICH_WALLETS[1].privateKey); + const deployer = new Deployer(hre, owner); + const contractArtifact = await deployer.loadArtifact( + "UUPSCrowdfundingCampaign" + ); + + // delete the .upgradable/ZKsync-era-test-node.json file + // to ensure that the contract is deployed + // from scratch + const filePath = path.join( + __dirname, + "../../", + ".upgradable/ZKsync-era-test-node.json" + ); + + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } + + const fundingGoalInWei = ethers.parseEther("1").toString(); + campaign = (await hre.zkUpgrades.deployProxy( + deployer.zkWallet, + contractArtifact, + [fundingGoalInWei], + { initializer: "initialize" }, + true + )) as unknown as UUPSCrowdfundingCampaign; + campaign.waitForDeployment(); + }); + + describe("V1", function () { + it("has a funding goal of 1 ETH", async function () { + expect(await campaign.getFundingGoal()).to.equal( + ethers.parseEther("1").toString() + ); + }); + + it("does not allow initialize to be called more than once", async function () { + await expect( + campaign.initialize(ethers.parseEther("1").toString()) + ).to.be.revertedWith("Initializable: contract is already initialized"); + }); + }); + + describe("V2", function () { + let v2campaign: V2_UUPSCrowdfundingCampaign; + let blockTimestamp: number; + const durationInSeconds = 1800; // 5 hours + + beforeEach(async () => { + const deployer = new Deployer(hre, owner); + const contractV2Artifact = await deployer.loadArtifact( + "V2_UUPSCrowdfundingCampaign" + ); + + v2campaign = (await hre.zkUpgrades.upgradeProxy( + deployer.zkWallet, + await campaign.getAddress(), + contractV2Artifact, + {}, + true + )) as unknown as V2_UUPSCrowdfundingCampaign; + + // wait some time before the next call + await new Promise((resolve) => setTimeout(resolve, 0)); + v2campaign.connect(deployer.zkWallet); + + // Initialize V2 with a new campaign duration + const transaction = await v2campaign.initializeV2(durationInSeconds); + const receipt = await transaction.wait(); + blockTimestamp = (await receipt?.getBlock())?.timestamp || 0; + }); + + it("has a deadline", async () => { + expect(await v2campaign.deadline()).to.equal( + blockTimestamp + durationInSeconds + ); + }); + + it("only allows the owner to update the deadline", async () => { + await expect( + v2campaign.connect(addr1).extendDeadline(durationInSeconds) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + it("does not allow initializeV2 to be run more than once", async () => { + await expect( + v2campaign.initializeV2(durationInSeconds) + ).to.be.revertedWith("V2 already initialized"); + }); + + // in-memory-node does not support time manipulation yet + // + // it("only allows contributions before the deadline", async () => { + // await time.increase(durationInSeconds - 1); + // await expect( + // v2campaign.connect(addr1).contribute({ value: ethers.parseEther("1") }) + // ).to.be.revertedWith("The deadline has passed"); + // }); + }); +}); diff --git a/templates/101/test/4-paymaster/ApprovalPaymaster.test.ts b/templates/101/test/4-paymaster/ApprovalPaymaster.test.ts new file mode 100644 index 0000000..aa19377 --- /dev/null +++ b/templates/101/test/4-paymaster/ApprovalPaymaster.test.ts @@ -0,0 +1,174 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { getWallet, LOCAL_RICH_WALLETS, getProvider } from "../../utils"; +import { + ApprovalFlowPaymaster, + CrowdfundingCampaign, + CrownToken, +} from "../../typechain-types"; +import { Contract, Provider, utils, Wallet } from "zksync-ethers"; +import { deployCrowdfundContract } from "../../deploy/4-paymaster/gasless/interact"; +import * as hre from "hardhat"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import { + deployCrownToken, + deployPaymaster, +} from "../../deploy/4-paymaster/approval/deploy"; + +describe("ApprovalPaymaster", function () { + let campaign: CrowdfundingCampaign; + let token: CrownToken; + let paymaster: ApprovalFlowPaymaster; + let owner: Wallet, addr1: Wallet; + let provider: Provider; + let contributionAmount: bigint; + let deployer: Deployer; + let startingBalance: bigint; + + beforeEach(async function () { + owner = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + addr1 = getWallet(LOCAL_RICH_WALLETS[1].privateKey); + contributionAmount = ethers.parseEther("0.01"); + startingBalance = await addr1.getBalance(); + + provider = getProvider(); + deployer = new Deployer(hre, owner); + + token = await deployCrownToken(owner, true); + paymaster = await deployPaymaster(owner, token, true); + campaign = await deployCrowdfundContract(owner); + + await owner.sendTransaction({ + to: paymaster.target, + value: ethers.parseEther("0.005"), + }); + await token.connect(owner).mint(await addr1.getAddress(), "1"); + await token.connect(owner).mint(await paymaster.getAddress(), "3"); + }); + + it("paymaster has a balance of 0.005 ETH", async function () { + const balance = await provider.getBalance(await paymaster.getAddress()); + expect(balance).to.equal(ethers.parseEther("0.005")); + }); + + it("paymaster has a balance of 3 CROWN", async function () { + const balance = await provider.getBalance( + await paymaster.getAddress(), + "latest", + await token.getAddress() + ); + expect(balance).to.equal(3); + }); + + it("contributor has a balance of 1 CROWN", async function () { + const balance = await provider.getBalance( + await addr1.getAddress(), + "latest", + await token.getAddress() + ); + expect(balance).to.equal(1); + }); + + describe("contributing with paymaster", function () { + let contract: Contract; + + beforeEach(async function () { + const contractArtifact = await deployer.loadArtifact( + "CrowdfundingCampaign" + ); + contract = new ethers.Contract( + await campaign.getAddress(), + contractArtifact.abi, + addr1 + ); + }); + + it("pays for contributions", async function () { + await contribute( + contract, + await paymaster.getAddress(), + await token.getAddress(), + contributionAmount, + provider + ); + + const balance = await provider.getBalance( + await paymaster.getAddress(), + "latest", + await token.getAddress() + ); + expect(balance).to.equal(4); + expect( + (await provider.getBalance(await campaign.getAddress())).toString() + ).to.equal(contributionAmount); + }); + + it("contributor only spends for contribution", async function () { + await contribute( + contract, + await paymaster.getAddress(), + await token.getAddress(), + contributionAmount, + provider + ); + const updatedBalance = startingBalance - contributionAmount; + expect(await addr1.getBalance()).to.equal(updatedBalance.toString()); + }); + + it("contributor pays gas with CROWN", async function () { + await contribute( + contract, + await paymaster.getAddress(), + await token.getAddress(), + contributionAmount, + provider + ); + const balance = await provider.getBalance( + await addr1.getAddress(), + "latest", + await token.getAddress() + ); + expect(balance).to.equal(0); + }); + }); +}); + +async function contribute( + campaign: Contract, + paymasterAddress: string, + tokenAddress: string, + contributionAmount: bigint, + provider: Provider +) { + // Get paymaster params for the ApprovalBased paymaster + const paymasterParams = utils.getPaymasterParams(paymasterAddress, { + type: "ApprovalBased", + token: tokenAddress, + minimalAllowance: 1n, + innerInput: new Uint8Array(), + }); + // Determine the gas limit for the contribution transaction + const gasLimit = await campaign.contribute.estimateGas({ + value: contributionAmount, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams: paymasterParams, + }, + }); + + // Contribute to the crowdfund contract + // and have the paymaster cover the funds + const transaction = await campaign.contribute({ + value: contributionAmount, + maxPriorityFeePerGas: 0n, + maxFeePerGas: await provider.getGasPrice(), + gasLimit, + // Pass the paymaster params as custom data + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams, + }, + }); + + return await transaction.wait(); +} diff --git a/templates/101/test/4-paymaster/GaslessPaymaster.test.ts b/templates/101/test/4-paymaster/GaslessPaymaster.test.ts new file mode 100644 index 0000000..f61597c --- /dev/null +++ b/templates/101/test/4-paymaster/GaslessPaymaster.test.ts @@ -0,0 +1,122 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { getWallet, LOCAL_RICH_WALLETS, getProvider } from "../../utils"; +import { CrowdfundingCampaign, GaslessPaymaster } from "../../typechain-types"; +import { Contract, Provider, utils, Wallet } from "zksync-ethers"; +import { deployPaymaster } from "../../deploy/4-paymaster/gasless/deploy"; +import { deployCrowdfundContract } from "../../deploy/4-paymaster/gasless/interact"; +import * as hre from "hardhat"; +import { Deployer } from "@matterlabs/hardhat-zksync"; + +describe("GaslessPaymaster", function () { + let campaign: CrowdfundingCampaign; + let paymaster: GaslessPaymaster; + let owner: Wallet, addr1: Wallet; + let provider: Provider; + let contributionAmount: bigint; + let deployer: Deployer; + let startingBalance: bigint; + + beforeEach(async function () { + owner = getWallet(LOCAL_RICH_WALLETS[0].privateKey); + addr1 = getWallet(LOCAL_RICH_WALLETS[1].privateKey); + provider = getProvider(); + contributionAmount = ethers.parseEther("0.01"); + deployer = new Deployer(hre, owner); + startingBalance = await addr1.getBalance(); + paymaster = await deployPaymaster(owner, true); + campaign = await deployCrowdfundContract(owner); + + // fund the paymaster + await owner.sendTransaction({ + to: await paymaster.getAddress(), + value: ethers.parseEther("0.077"), + }); + }); + + it("has a balance of 0.077 ETH", async function () { + const balance = await provider.getBalance(await paymaster.getAddress()); + expect(balance).to.equal(ethers.parseEther("0.077")); + }); + + it("contributor has a starting balance", async function () { + expect(await addr1.getBalance()).to.equal(startingBalance.toString()); + }); + + describe("contributing with paymaster", function () { + let contract: Contract; + + beforeEach(async function () { + const contractArtifact = await deployer.loadArtifact( + "CrowdfundingCampaign" + ); + contract = new ethers.Contract( + await campaign.getAddress(), + contractArtifact.abi, + addr1 + ); + }); + + it("pays for contributions", async function () { + await contribute( + contract, + await paymaster.getAddress(), + contributionAmount, + provider + ); + expect( + await provider.getBalance(await paymaster.getAddress()) + ).to.be.lessThan(ethers.parseEther("0.077")); + expect( + (await provider.getBalance(await campaign.getAddress())).toString() + ).to.equal(contributionAmount); + }); + + it("contributor only pays contribution amount", async function () { + await contribute( + contract, + await paymaster.getAddress(), + contributionAmount, + provider + ); + const updatedBalance = startingBalance - contributionAmount; + expect(await addr1.getBalance()).to.equal(updatedBalance.toString()); + }); + }); +}); + +async function contribute( + campaign: Contract, + paymasterAddress: string, + contributionAmount: bigint, + provider: Provider +) { + // Get paymaster params for the Gasless paymaster + const paymasterParams = utils.getPaymasterParams(paymasterAddress, { + type: "General", + innerInput: new Uint8Array(), + }); + // Determine the gas limit for the contribution transaction + const gasLimit = await campaign.contribute.estimateGas({ + value: contributionAmount, + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams: paymasterParams, + }, + }); + // Contribute to the crowdfund contract + // and have the paymaster cover the funds + const transaction = await campaign.contribute({ + value: contributionAmount, + maxPriorityFeePerGas: 0n, + maxFeePerGas: await provider.getGasPrice(), + gasLimit, + // Pass the paymaster params as custom data + customData: { + gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, + paymasterParams, + }, + }); + + return await transaction.wait(); +} diff --git a/templates/101/tsconfig.json b/templates/101/tsconfig.json new file mode 100644 index 0000000..6cf7332 --- /dev/null +++ b/templates/101/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node"], + } +} diff --git a/templates/101/utils/index.ts b/templates/101/utils/index.ts new file mode 100644 index 0000000..d85b6a3 --- /dev/null +++ b/templates/101/utils/index.ts @@ -0,0 +1,201 @@ +import { Provider, Wallet } from "zksync-ethers"; +import * as hre from "hardhat"; +import { Deployer } from "@matterlabs/hardhat-zksync-deploy"; +import dotenv from "dotenv"; +import { ethers } from "ethers"; + +import "@matterlabs/hardhat-zksync-node/dist/type-extensions"; +import "@matterlabs/hardhat-zksync-verify/dist/src/type-extensions"; + +// Load env file +dotenv.config(); + +export const getProvider = () => { + const rpcUrl = hre.network.config.url; + if (!rpcUrl) + throw `⛔️ RPC URL wasn't found in "${hre.network.name}"! Please add a "url" field to the network config in hardhat.config.ts`; + + // Initialize ZKsync Provider + const provider = new Provider(rpcUrl); + + return provider; +}; + +export const getWallet = (privateKey?: string) => { + if (!privateKey) { + // Get wallet private key from .env file + if (!process.env.WALLET_PRIVATE_KEY) + throw "⛔️ Wallet private key wasn't found in .env file!"; + } + + const provider = getProvider(); + + // Initialize ZKsync Wallet + const wallet = new Wallet( + privateKey ?? process.env.WALLET_PRIVATE_KEY!, + provider + ); + + return wallet; +}; + +export const verifyEnoughBalance = async (wallet: Wallet, amount: bigint) => { + // Check if the wallet has enough balance + const balance = await wallet.getBalance(); + if (balance < amount) + throw `⛔️ Wallet balance is too low! Required ${ethers.formatEther( + amount + )} ETH, but current ${wallet.address} balance is ${ethers.formatEther( + balance + )} ETH`; +}; + +/** + * @param {string} data.contract The contract's path and name. E.g., "contracts/Greeter.sol:Greeter" + */ +export const verifyContract = async (data: { + address: string; + contract: string; + constructorArguments: string; + bytecode: string; +}) => { + const verificationRequestId: number = await hre.run("verify:verify", { + ...data, + noCompile: true, + }); + return verificationRequestId; +}; + +type DeployContractOptions = { + /** + * If true, the deployment process will not print any logs + */ + silent?: boolean; + /** + * If true, the contract will not be verified on Block Explorer + */ + noVerify?: boolean; + /** + * If specified, the contract will be deployed using this wallet + */ + wallet?: Wallet; +}; +export const deployContract = async ( + contractArtifactName: string, + constructorArguments?: any[], + options?: DeployContractOptions +) => { + const log = (message: string) => { + if (!options?.silent) console.log(message); + }; + + log(`\nStarting deployment process of "${contractArtifactName}"...`); + + const wallet = options?.wallet ?? getWallet(); + const deployer = new Deployer(hre, wallet); + const artifact = await deployer + .loadArtifact(contractArtifactName) + .catch((error) => { + if ( + error?.message?.includes( + `Artifact for contract "${contractArtifactName}" not found.` + ) + ) { + console.error(error.message); + throw `⛔️ Please make sure you have compiled your contracts or specified the correct contract name!`; + } else { + throw error; + } + }); + + // Estimate contract deployment fee + const deploymentFee = await deployer.estimateDeployFee( + artifact, + constructorArguments || [] + ); + log(`Estimated deployment cost: ${ethers.formatEther(deploymentFee)} ETH`); + + // Check if the wallet has enough balance + await verifyEnoughBalance(wallet, deploymentFee); + + // Deploy the contract to ZKsync + const contract = await deployer.deploy(artifact, constructorArguments); + const address = await contract.getAddress(); + const constructorArgs = contract.interface.encodeDeploy(constructorArguments); + const fullContractSource = `${artifact.sourceName}:${artifact.contractName}`; + + // Display contract deployment info + log(`\n"${artifact.contractName}" was successfully deployed:`); + log(` - Contract address: ${address}`); + log(` - Contract source: ${fullContractSource}`); + log(` - Encoded constructor arguments: ${constructorArgs}\n`); + + if (!options?.noVerify && hre.network.config.verifyURL) { + log(`Requesting contract verification...`); + await verifyContract({ + address, + contract: fullContractSource, + constructorArguments: constructorArgs, + bytecode: artifact.bytecode, + }); + } + + return contract; +}; + +/** + * Rich wallets can be used for testing purposes. + * Available on ZKsync In-memory node and Dockerized node. + */ +export const LOCAL_RICH_WALLETS = [ + { + address: "0x36615Cf349d7F6344891B1e7CA7C72883F5dc049", + privateKey: + "0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110", + }, + { + address: "0xa61464658AfeAf65CccaaFD3a512b69A83B77618", + privateKey: + "0xac1e735be8536c6534bb4f17f06f6afc73b2b5ba84ac2cfb12f7461b20c0bbe3", + }, + { + address: "0x0D43eB5B8a47bA8900d84AA36656c92024e9772e", + privateKey: + "0xd293c684d884d56f8d6abd64fc76757d3664904e309a0645baf8522ab6366d9e", + }, + { + address: "0xA13c10C0D5bd6f79041B9835c63f91de35A15883", + privateKey: + "0x850683b40d4a740aa6e745f889a6fdc8327be76e122f5aba645a5b02d0248db8", + }, + { + address: "0x8002cD98Cfb563492A6fB3E7C8243b7B9Ad4cc92", + privateKey: + "0xf12e28c0eb1ef4ff90478f6805b68d63737b7f33abfa091601140805da450d93", + }, + { + address: "0x4F9133D1d3F50011A6859807C837bdCB31Aaab13", + privateKey: + "0xe667e57a9b8aaa6709e51ff7d093f1c5b73b63f9987e4ab4aa9a5c699e024ee8", + }, + { + address: "0xbd29A1B981925B94eEc5c4F1125AF02a2Ec4d1cA", + privateKey: + "0x28a574ab2de8a00364d5dd4b07c4f2f574ef7fcc2a86a197f65abaec836d1959", + }, + { + address: "0xedB6F5B4aab3dD95C7806Af42881FF12BE7e9daa", + privateKey: + "0x74d8b3a188f7260f67698eb44da07397a298df5427df681ef68c45b34b61f998", + }, + { + address: "0xe706e60ab5Dc512C36A4646D719b889F398cbBcB", + privateKey: + "0xbe79721778b48bcc679b78edac0ce48306a8578186ffcb9f2ee455ae6efeace1", + }, + { + address: "0xE90E12261CCb0F3F7976Ae611A29e84a6A85f424", + privateKey: + "0x3eb15da85647edd9a1159a4a13b9e7c56877c4eb33f614546d4db06a51868b1c", + }, +];