diff --git a/contracts/orchestration/ZeroTreasuryHub.sol b/contracts/orchestration/ZeroTreasuryHub.sol new file mode 100644 index 0000000..2865c75 --- /dev/null +++ b/contracts/orchestration/ZeroTreasuryHub.sol @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import { Safe } from "@safe-global/safe-contracts/contracts/Safe.sol"; +import { SafeProxyFactory } from "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol"; +import { SafeProxy } from "@safe-global/safe-contracts/contracts/proxies/SafeProxy.sol"; + + +/** + * @title ZeroTreasuryHub v0. + * @dev A contract that serves as a factory for treasury deployments based on user configs. + * Also works as a registry to keep track of topologies of every treasury system deployed. + * TODO proto: how do we keep track of runtime changes of addresses and such once each treasury is modified by users ?? we shouldn't wire all treasury/dao/safe calls through this contract + */ +contract ZeroTreasuryHub { + + // <--- Errors ---> + error ZeroAddressPassed(); + error TreasuryExistsForDomain(bytes32 domain); + error InvalidSafeParams(); + + // <--- Events ---> + event SafeSystemSet( + address singleton, + address proxyFactory, + address fallbackHandler + ); + event SafeTreasuryInstanceCreated( + bytes32 indexed domain, + address indexed safe + ); + + /** + * @dev All available modules to be installed for any treasury. + * Lists all predeployed preset contracts to be cloned. + */ + // TODO proto: change this to be a mapping where the key = keccak256(abi.encodePacked(namespace, ":", name, ":", versionString)) + // e.g.: "OZ:Governor_V1:v1", "ZODIAC:Roles:v4", etc. think on this and make it better. + // this way we don't need to upgrade and we can easily add new modules over time. + // if doing so, we need to store all available keys in an array. + // Another way would be to store a struct with metadata on the end of the mapping instead of just plain address + // Also need to write a deterministic helper that can create and acquire these keys for apps and such. Readable names for modules could help in events. + struct ModuleCatalog { + address safe; +// address governor; +// address timelock; + } + + /** + * @dev Addresses of components that make up a deployed treasury system. + */ + struct TreasuryComponents { + address safe; +// address governor; +// address timelock; + } + + // TODO proto: figure these proper ones out for ZChain! + struct SafeSystem { + // Safe contract used + address singleton; + // Proxy factory used to deploy new safes + address proxyFactory; + // Fallback handler for the safe + address fallbackHandler; + } + + SafeSystem public safeSystem; + ModuleCatalog public moduleCatalog; + + /** + * @dev Mapping from ZNS domain hash to the addresses of components for each treasury. + */ + mapping(bytes32 => TreasuryComponents) public treasuries; + + // TODO proto: should we add ZNS registry address here in state to verify domain ownership/existence on treasury creation? + + // TODO proto: change this to initialize() if decided to make upgradeable + constructor( + address _safeSingleton, + address _safeProxyFactory, + address _safeFallbackHandler + ) { + if ( + _safeSingleton == address(0) || + _safeProxyFactory == address(0) || + _safeFallbackHandler == address(0) + ) { + revert ZeroAddressPassed(); + } + + _setSafeSystem( + _safeSingleton, + _safeProxyFactory, + _safeFallbackHandler + ); + } + + // <--- Treasury Creation ---> + // TODO proto: should these be composable contracts we can evolve over time? Also separate from registry?? + + function createSafe( + bytes32 domain, + address[] calldata owners, + uint256 threshold, + // TODO proto: make these better if possible. need to avoid errors and collisions. do we need it (adds complexity. including storage) ?? + // this outline Safe's purpose/role in the Treasury, so we can deploy multiple Safes if needed + // Optional, only for additional Safes. pass "" for "main" + string memory purpose + ) external returns (address) { + if (treasuries[domain].safe != address(0)) revert TreasuryExistsForDomain(domain); + // TODO proto: verify domain ownership!!! + + // TODO proto: should we store length in a memory var? does it save gas? + if (owners.length == 0 || threshold == 0 || threshold > owners.length) revert InvalidSafeParams(); + + // TODO proto: figure out if we ever need to set to/data/payment stuff ? + bytes memory setup = abi.encodeWithSelector( + Safe.setup.selector, + owners, + threshold, + // to + address(0), + // data + bytes(""), + safeSystem.fallbackHandler, + // paymentToken + address(0), + // payment + 0, + // paymentReceiver + payable(address(0)) + ); + + SafeProxy safe = SafeProxyFactory(safeSystem.proxyFactory).createProxyWithNonce( + safeSystem.singleton, + setup, + _getSaltNonce( + domain, + purpose + ) + ); + + address safeAddress = address(safe); + + treasuries[domain] = TreasuryComponents({ safe: safeAddress }); + // TODO proto: extend this event to inclide function parameters for Safe + emit SafeTreasuryInstanceCreated(domain, safeAddress); + + return safeAddress; + } + + function createDao() external {} + + function createHybrid() external {} + + // <--- Treasury Management ---> + + function addModule() external {} + + function removeModule() external {} + + // <--- Utilities ---> + function _getSaltNonce(bytes32 domain, string memory purpose) internal pure returns (uint256) { + string memory actualPurpose = bytes(purpose).length == 0 ? "main" : purpose; + + return uint256(keccak256(abi.encodePacked(domain, ":", actualPurpose))); + } + + // <--- Setters ---> + + function setSafeSystem( + address _singleton, + address _proxyFactory, + address _fallbackHandler + ) external { + // TODO proto: add access control! + _setSafeSystem( + _singleton, + _proxyFactory, + _fallbackHandler + ); + } + + function _setSafeSystem( + address _singleton, + address _proxyFactory, + address _fallbackHandler + ) internal { + safeSystem = SafeSystem({ + singleton: _singleton, + proxyFactory: _proxyFactory, + fallbackHandler: _fallbackHandler + }); + + emit SafeSystemSet( + _singleton, + _proxyFactory, + _fallbackHandler + ); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index a069b83..ed1a67d 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,18 +1,18 @@ import type { HardhatUserConfig } from "hardhat/config"; +// eslint-disable-next-line no-duplicate-imports import { configVariable } from "hardhat/config"; import hardhatViem from "@nomicfoundation/hardhat-viem"; import hardhatToolboxViem from "@nomicfoundation/hardhat-toolbox-viem"; import hardhatMocha from "@nomicfoundation/hardhat-mocha"; -const config: HardhatUserConfig = { +const config : HardhatUserConfig = { plugins: [ hardhatToolboxViem, hardhatViem, - hardhatMocha + hardhatMocha, ], solidity: { - npmFilesToBuild: ["@openzeppelin/contracts/governance/TimelockController.sol"], compilers: [ { version: "0.8.30", @@ -24,6 +24,16 @@ const config: HardhatUserConfig = { }, }, ], + npmFilesToBuild: [ + "@safe-global/safe-contracts/contracts/SafeL2.sol", + "@safe-global/safe-contracts/contracts/proxies/SafeProxyFactory.sol", + "@safe-global/safe-contracts/contracts/libraries/MultiSend.sol", + "@safe-global/safe-contracts/contracts/libraries/MultiSendCallOnly.sol", + "@safe-global/safe-contracts/contracts/libraries/SignMessageLib.sol", + "@safe-global/safe-contracts/contracts/libraries/CreateCall.sol", + "@safe-global/safe-contracts/contracts/handler/CompatibilityFallbackHandler.sol", + "@openzeppelin/contracts/governance/TimelockController.sol", + ], }, networks: { hardhatMainnet: { diff --git a/package.json b/package.json index 9b61dbe..34ffefc 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,16 @@ "scripts": { "typechain": "hardhat typechain", "compile": "hardhat compile", + "lint-ts": "yarn eslint ./test/** ./src/**", + "lint": "yarn lint-ts", "build": "yarn run clean && yarn run compile", "postbuild": "yarn save-tag", "clean": "hardhat clean", "test": "hardhat test mocha" }, "devDependencies": { - "@openzeppelin/contracts": "5.4.0", - "@openzeppelin/contracts-upgradeable": "5.4.0", - "@safe-global/safe-deployments": "1.37.46", - "@safe-global/safe-contracts": "1.4.1-2", - "@gnosis-guild/zodiac-core": "3.0.1", "@gnosis-guild/zodiac": "4.2.1", + "@gnosis-guild/zodiac-core": "3.0.1", "@nomicfoundation/hardhat-ethers": "^4.0.2", "@nomicfoundation/hardhat-ethers-chai-matchers": "^3.0.0", "@nomicfoundation/hardhat-ignition": "^3.0.0", @@ -27,19 +25,24 @@ "@nomicfoundation/hardhat-mocha": "^3.0.0", "@nomicfoundation/hardhat-network-helpers": "^3.0.1", "@nomicfoundation/hardhat-node-test-runner": "^3.0.3", - "@nomicfoundation/hardhat-toolbox-mocha-ethers": "^3.0.0", "@nomicfoundation/hardhat-toolbox-viem": "^5.0.0", "@nomicfoundation/hardhat-typechain": "^3.0.0", "@nomicfoundation/hardhat-verify": "^3.0.3", "@nomicfoundation/hardhat-viem": "^3.0.0", "@nomicfoundation/hardhat-viem-assertions": "^3.0.2", "@nomicfoundation/ignition-core": "^3.0.0", - "@safe-global/protocol-kit": "6.1.1", + "@openzeppelin/contracts": "5.4.0", + "@openzeppelin/contracts-upgradeable": "5.4.0", "@openzeppelin/hardhat-upgrades": "^3.9.1", + "@safe-global/protocol-kit": "6.1.1", + "@safe-global/safe-contracts": "1.4.1-2", + "@safe-global/safe-deployments": "1.37.46", "@types/chai": "^4.2.0", "@types/chai-as-promised": "^8.0.1", "@types/mocha": ">=10.0.10", "@types/node": "^22.8.5", + "@wagmi/cli": "^2.7.1", + "@zero-tech/eslint-config-cpt": "^0.2.8", "chai": "^5.1.2", "ethers": "^6.14.0", "forge-std": "foundry-rs/forge-std#v1.9.4", @@ -48,8 +51,7 @@ "ts-node": "^10.9.2", "typescript": "^5.9.3", "viem": "^2.38.3", - "eslint": "^8.37.0", - "@zero-tech/eslint-config-cpt": "0.2.8" + "eslint": "^8.37.0" }, "type": "module", "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..26792b6 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; \ No newline at end of file diff --git a/test/ZeroTreasuryHub.test.ts b/test/ZeroTreasuryHub.test.ts new file mode 100644 index 0000000..3b123b5 --- /dev/null +++ b/test/ZeroTreasuryHub.test.ts @@ -0,0 +1,63 @@ +import { type HardhatViemHelpers } from "@nomicfoundation/hardhat-viem/types"; +import { keccak256 } from "viem"; +import { type Contract, setupViem, type Wallet } from "./helpers/viem"; + + +describe("ZeroTreasuryHub Smoke Tests", () => { + let viem : HardhatViemHelpers; + + let admin : Wallet; + let user1 : Wallet; + let user2 : Wallet; + let user3 : Wallet; + + // TODO proto: is this really the best way ?! + let theHub : Contract<"ZeroTreasuryHub">; + let safeSingleton : Contract<"SafeL2">; + let proxyFactory : Contract<"SafeProxyFactory">; + let fallbackHandler : Contract<"CompatibilityFallbackHandler">; + + before(async () => { + ({ viem, wallets: [ admin, user1, user2, user3 ] } = await setupViem()); + + // Deploy the Safe singleton (use 'Safe' instead for L1-style) + safeSingleton = await viem.deployContract("SafeL2"); + + // Proxy Factory + proxyFactory = await viem.deployContract("SafeProxyFactory"); + + // Libs & handler frequently used by Protocol Kit + const multiSend = await viem.deployContract("MultiSend"); + const multiSendCallOnly = await viem.deployContract("MultiSendCallOnly"); + const signMessageLib = await viem.deployContract("SignMessageLib"); + const createCall = await viem.deployContract("CreateCall"); + fallbackHandler = await viem.deployContract("CompatibilityFallbackHandler"); + + // Deploy the Hub + theHub = await viem.deployContract( + "ZeroTreasuryHub", + [ + safeSingleton.address, + proxyFactory.address, + fallbackHandler.address, + ]); + }); + + it("should deploy Safe from the hub", async () => { + await theHub.write.createSafe([ + keccak256("0xmydomain"), + [user2.account.address, user3.account.address], + 1n, + "main", + ]); + + const { + args: { + domain, + safe, + }, + } = (await theHub.getEvents.SafeTreasuryInstanceCreated())[0]; + + console.log(`Domain: ${domain}. Safe ${safe}`); + }); +}); diff --git a/test/helpers/viem.ts b/test/helpers/viem.ts new file mode 100644 index 0000000..e27af4c --- /dev/null +++ b/test/helpers/viem.ts @@ -0,0 +1,31 @@ +import hre from "hardhat"; +import type { Account, WalletClient } from "viem"; +import type { ContractReturnType } from "@nomicfoundation/hardhat-viem/types"; + +// <-- TYPES --> +// exact helper shape returned by hardhat-viem +export type Viem = Awaited>["viem"]; +// valid compiled contract names inferred from viem.deployContract +export type ContractName = Parameters[0]; +// instance type returned by viem.deployContract for a given name +export type Contract = ContractReturnType; +export type Wallet = WalletClient & { account : Account; }; + +// <-- HELPERS --> +// ensure a WalletClient definitely has an account (narrow once, use everywhere) +export const withAccount = (w : T) : T & { account : Account; } => { + if (!w.account) throw new Error("WalletClient has no account"); + return w as T & { account : Account; }; +}; + +// init viem, connect to Hardhat, get `walletCount` amount of wallets, 4 by default +export const setupViem = async (walletCount = 4) => { + const { viem } = await hre.network.connect(); + const all = (await viem.getWalletClients()).map(withAccount); + if (walletCount < 0) throw new Error("count must be >= 0"); + if (walletCount > all.length) { + throw new Error(`Requested ${walletCount} wallets, but only ${all.length} are available`); + } + const wallets = all.slice(0, walletCount) as Array; + return { viem, wallets } as const; +}; diff --git a/test/types.ts b/test/types.ts new file mode 100644 index 0000000..7f1d8b7 --- /dev/null +++ b/test/types.ts @@ -0,0 +1,4 @@ +import { WalletClient, Transport, Chain, Account, RpcSchema } from "viem"; + + +export type DaoTestWallet = WalletClient; diff --git a/test/zDAO.test.ts b/test/zDAO.test.ts index a2cc1af..1c47979 100644 --- a/test/zDAO.test.ts +++ b/test/zDAO.test.ts @@ -1,136 +1,712 @@ import hre from "hardhat"; -import { expect } from "chai"; import { ethers } from "ethers"; -import { ZeroVotingERC20 } from "../types/ethers-contracts/voting/ZVoting20.sol/ZeroVotingERC20.js"; -import { ZDAO } from "../types/ethers-contracts/ZDAO.js"; +import { HardhatViemHelpers } from "@nomicfoundation/hardhat-viem/types"; +import { encodeFunctionData, Hex, zeroAddress } from "viem"; +import { type Contract, type Wallet } from "./helpers/viem"; +import { NetworkHelpers } from "@nomicfoundation/hardhat-network-helpers/types"; +import * as chai from "chai"; +import { expect } from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { DEFAULT_ADMIN_ROLE } from "../src/constants.js"; +chai.use(chaiAsPromised); -let admin; -let user1; -let user2; -let votingERC20 : ZeroVotingERC20; -let governance20 : ZDAO; +describe("Deploy restrictions", () => { + let viem : HardhatViemHelpers; + let networkHelpers : NetworkHelpers; -const delay = 1; -const votingPeriod = 10; -const proposalThreshold20 = ethers.parseUnits("100"); -const proposalThreshold721 = 1; -const quorumPercentage = 10n; -const voteExtension = 5; + let admin : Wallet; -describe("ZDAO", () => { - const fixture = async () => { - const { viem, networkHelpers } = await hre.network.connect(); - [ admin, user1, user2 ] = await viem.getWalletClients(); - }; + let voting : Contract<"ZeroVotingERC20">; + let timelock : Contract<"TimelockController">; - it("should deploy Voting20 with DAO using viem", async () => { - const { - viem, - // networkHelpers - } = await hre.network.connect(); + let voting20Params : [ + string, + string, + string, + string, + Hex + ]; + let timelockParams : [ + bigint, + Array, + Array, + Hex + ]; + let govParams : [ + bigint, + string, + Hex, + Hex, + number, + number, + bigint, + bigint, + number + ]; - // networkHelpers.loadFixture(fixture); - await fixture(); + beforeEach(async () => { + ({ viem, networkHelpers } = await hre.network.connect()); + [ admin ] = await viem.getWalletClients(); - const voting20Params = [ + voting20Params = [ "ZeroVotingERC20", "ZV", "ZERO DAO", "1", - admin.account.address - ] + admin.account.address, + ]; - const votingERC20 = await viem.deployContract( + timelockParams = [ + 1n, + [], + [], + admin.account.address, + ]; + + voting = await viem.deployContract( "ZeroVotingERC20", + voting20Params + ); + + timelock = await viem.deployContract( + "TimelockController", + timelockParams + ); + + govParams = [ + 1n, + "ZDAO", + voting.address, + timelock.address, + 1, + 10, + ethers.parseUnits("10"), + 10n, + 5, + ]; + + // Give minter role to admin + await voting.write.grantRole([ + await voting.read.MINTER_ROLE(), + admin.account.address, + ]); + + // Mint tokens to users + await voting.write.mint([admin.account.address, ethers.parseUnits("10000000000")]); + + // Delegate tokens to themselves for voting power + await voting.write.delegate([admin.account.address], { account: admin.account.address }); + }); + + it("Should deploy Voting20 with DAO using viem with right params", async () => { + const governance = await viem.deployContract( + "ZDAO", + govParams + ); + + expect(voting.address).to.exist; + expect(await voting.read.name()).to.equal("ZeroVotingERC20"); + expect(await voting.read.symbol()).to.equal("ZV"); + + expect(timelock).to.exist; + expect(await timelock.read.getMinDelay()).to.equal(timelockParams[0]); + expect( + await timelock.read.hasRole([ + DEFAULT_ADMIN_ROLE, + admin.account.address, + ]) + ).to.equal(true); + + expect(governance).to.exist; + expect(await governance.read.name()).to.equal(govParams[1]); + expect( + (await governance.read.token()).toLowerCase() + ).to.equal( + govParams[2].toLowerCase() + ); + expect( + (await governance.read.timelock()).toLowerCase() + ).to.equal( + govParams[3].toLowerCase() + ); + expect( + await governance.read.votingPeriod() + ).to.equal( + BigInt(govParams[5]) + ); + expect( + await governance.read.proposalThreshold() + ).to.equal( + BigInt(govParams[6]) + ); + }); + + it("Should have attached correct voting token and timelock", async () => { + const governance = await viem.deployContract( + "ZDAO", + govParams + ); + + expect( + (await governance.read.token()).toLowerCase() + ).to.equal( + voting.address.toLowerCase() + ); + expect( + (await governance.read.timelock()).toLowerCase() + ).to.equal( + timelock.address.toLowerCase() + ); + }); + + it("Should revert when deploying gov without voting token", async () => { + await expect( + viem.deployContract( + "ZDAO", + [ + 1n, + "ZDAO", + zeroAddress, // no voting token + "0x0000000000000000000000000000000000000002", + 1, + 10, + ethers.parseUnits("10"), + 10n, + 5, + ]) + ).to.be.rejectedWith("Transaction reverted"); + }); + + it("Should let deploy gov without TimelockController", async () => { + await expect( + viem.deployContract( + "ZDAO", + [ + 1n, + "ZDAO", + voting.address, + zeroAddress, + 1, + 10, + ethers.parseUnits("10"), + 10n, + 5, + ] + )).to.be.fulfilled; + }); + + it("Should revert when calling gov functions that require TimelockController when none is set", async () => { + const governance = await viem.deployContract( + "ZDAO", [ - "ZeroVotingERC20", - "ZV", - "ZERO DAO", - "1", - admin.account.address + 1n, + "ZDAO", + voting.address, + zeroAddress, // no timelock + 1, + 10, + ethers.parseUnits("10"), + 10n, + 5, ] ); - expect(votingERC20).to.exist; - expect(votingERC20.address).to.exist; - - // const timelock = await viem.deployContract( - // "TimelockController", - // [ - // 1, - // [], - // [], - // admin.account.address - // ] - // ); - // expect(timelock).to.exist; - - // const governance20 = await viem.deployContract( - // "ZDAO", - // [ - // 1n, - // "ZDAO", - // votingERC20.address, - // timelock.address, - // delay, - // votingPeriod, - // proposalThreshold20, - // quorumPercentage, - // voteExtension, - // ]); - // expect(governance20).to.exist; + + const proposalDescription = "Mint 1 token to admin"; + const proposalDescriptionHash = ethers.keccak256( + ethers.toUtf8Bytes(proposalDescription) + ) as Hex; + const calldata = encodeFunctionData({ + abi: voting.abi, + functionName: "mint", + args: [admin.account.address, ethers.parseUnits("1")], + }); + + await governance.write.propose( + [ + [ voting.address ], + [ 0n ], + [ calldata], + proposalDescription, + ], + { + account: admin.account.address, + } + ); + + await networkHelpers.mine(2); + + await governance.write.castVote( + [ + await governance.read.getProposalId( + [ + [ voting.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ] + ), + 1, + ], + { account: admin.account.address, + } + ); + + await networkHelpers.mine(11); + + await expect( + governance.write.queue( + [ + [ voting.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ], { + account: admin.account.address, + } + ) + ).to.be.rejectedWith("Transaction reverted: function returned an unexpected amount of data"); + }); +}); + +describe("ZDAO main features flow test", () => { + let viem : HardhatViemHelpers; + let networkHelpers : NetworkHelpers; + + let admin : Wallet; + let user1 : Wallet; + let user2 : Wallet; + let empty : Wallet; + + let voting20Params : [ + string, + string, + string, + string, + Hex + ]; + + let timelockParams : [ + bigint, + Array, + Array, + Hex + ]; + + const delay = 1; + const votingPeriod = 10; + const proposalThreshold20 = ethers.parseUnits("10"); + const quorumPercentage = 10n; + const voteExtension = 5; + + let govParams : [ + bigint, + string, + Hex, + Hex, + number, + number, + bigint, + bigint, + number + ]; + + const initialAdminBalance = ethers.parseUnits("100000000000"); + const initialUser1Balance = ethers.parseUnits("1000"); + const initialUser2Balance = ethers.parseUnits("200"); + + let votingERC20 : Contract<"ZeroVotingERC20">; + let governance20 : Contract<"ZDAO">; + + // eslint-disable-next-line prefer-arrow/prefer-arrow-functions + async function fixture () { + ({ viem, networkHelpers } = await hre.network.connect()); + [ admin, user1, user2, empty ] = await viem.getWalletClients(); + + voting20Params = [ + "ZeroVotingERC20", + "ZV", + "ZERO DAO", + "1", + admin.account.address, + ]; + + timelockParams = [ + 1n, + [], + [], + admin.account.address, + ]; + + // Deploy VotingERC20 + const votingERC20 = await viem.deployContract( + "ZeroVotingERC20", + voting20Params + ); + + // Deploy Timelock + const timelock = await viem.deployContract( + "TimelockController", + timelockParams + ); + + govParams = [ + 1n, + "ZDAO", + votingERC20.address, + timelock.address, + delay, + votingPeriod, + proposalThreshold20, + quorumPercentage, + voteExtension, + ]; + + // Deploy Governance + const governance20 = await viem.deployContract( + "ZDAO", + govParams + ); + + // Grant proposer and executor role to the gov contract to use proposals + await timelock.write.grantRole([ + await timelock.read.PROPOSER_ROLE(), + governance20.address, + ]); + await timelock.write.grantRole([ + await timelock.read.EXECUTOR_ROLE(), + governance20.address, + ]); + + // Grant minter role to the timelock to let it execute proposal on mint + await votingERC20.write.grantRole([ + await votingERC20.read.MINTER_ROLE(), + timelock.address, + ]); + + // Give minter role to admin + await votingERC20.write.grantRole([ + await votingERC20.read.MINTER_ROLE(), + admin.account.address, + ]); + + // Mint tokens to users + await votingERC20.write.mint([admin.account.address, initialAdminBalance]); + await votingERC20.write.mint([user1.account.address, initialUser1Balance]); + await votingERC20.write.mint([user2.account.address, initialUser2Balance]); + + // Delegate tokens to themselves for voting power + await votingERC20.write.delegate([admin.account.address], { account: admin.account.address }); + await votingERC20.write.delegate([user1.account.address], { account: user1.account.address }); + await votingERC20.write.delegate([user2.account.address], { account: user2.account.address }); + + // mine 2 blocks so info about delegations can be updated + await networkHelpers.mine(2); + + return ({ + votingERC20, + timelock, + governance20, + }); + } + + beforeEach(async () => { + ({ viem, networkHelpers } = await hre.network.connect()); + ({ + votingERC20, + timelock, + governance20, + } = await networkHelpers.loadFixture(fixture)); + }); + + it("Should #updateQuorumNumerator using governon voting process", async () => { + const currentQuorumNumerator = await governance20.read.quorumNumerator(); + const newQuorumNumerator = currentQuorumNumerator + 1n; + + const calldata = encodeFunctionData({ + abi: governance20.abi, + functionName: "updateQuorumNumerator", + args: [newQuorumNumerator], + }); + + const proposalDescription = "Increase quorum percentage by 1"; + const proposalDescriptionHash = ethers.keccak256( + ethers.toUtf8Bytes(proposalDescription) + ) as Hex; + + await governance20.write.propose([ + [ governance20.address ], + [ 0n ], + [ calldata ], + proposalDescription, + ], { + account: admin.account.address, + }); + + const proposalId = await governance20.read.getProposalId([ + [ governance20.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ]); + + await networkHelpers.mine(delay + 1); + + await governance20.write.castVote([ + proposalId, + 1, + ], { + account: admin.account.address, + }); + + await networkHelpers.mine(votingPeriod + 1); + + await governance20.write.queue([ + [ governance20.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ], { + account: admin.account.address, + }); + + await governance20.write.execute([ + [governance20.address], + [0n], + [ + calldata, + ], + proposalDescriptionHash, + ], { + account: admin.account.address, + }); + + expect( + await governance20.read.quorumNumerator() + ).to.equal( + newQuorumNumerator + ); + }); + + it("Delegates may vote on behalf of multiple token holders", async () => { + const user1InitialBalance = await votingERC20.read.balanceOf([user1.account.address]); + const user2InitialBalance = await votingERC20.read.balanceOf([user2.account.address]); + const adminInitialBalance = await votingERC20.read.balanceOf([admin.account.address]); + + // user2 delegates to user1 + await votingERC20.write.delegate([user1.account.address], { + account: user2.account.address, + }); + // admin delegates to user1 + await votingERC20.write.delegate([user1.account.address], { + account: admin.account.address, + }); + // mine 1 block so info about delegations can be updated + await networkHelpers.mine(2); + + const calldata = encodeFunctionData({ + abi: votingERC20.abi, + functionName: "mint", + args: [user1.account.address, ethers.parseUnits("1")], + }); + const proposalDescription = "Mint 1 token to user1"; + const proposalDescriptionHash = ethers.keccak256( + ethers.toUtf8Bytes(proposalDescription) + ) as Hex; + + await governance20.write.propose([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescription, + ], { + account: user1.account.address, + }); + + const proposalId = await governance20.read.getProposalId([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ]); + + await networkHelpers.mine(delay + 1); + + await governance20.write.castVote([ + proposalId, + 1, + ], { + account: user1.account.address, + }); + + await networkHelpers.mine(votingPeriod + 1); + + const currentVotes = await governance20.read.getVotes([ + user1.account.address, + BigInt(await networkHelpers.time.latestBlock() - 1), + ], { + account: user1.account.address, + }); + + expect(currentVotes).to.equal( + user1InitialBalance + + user2InitialBalance + + adminInitialBalance + ); + }); + + it("Should fail when #propose with insufficient votes", async () => { + expect( + Number(await governance20.read.getVotes([ + empty.account.address, + BigInt(await networkHelpers.time.latestBlock() - 1), + ])) + ).to.be.lessThan( + Number(await governance20.read.proposalThreshold()) + ); + + const calldata = encodeFunctionData({ + abi: votingERC20.abi, + functionName: "mint", + args: [empty.account.address, ethers.parseUnits("1")], + }); + + const proposalDescription = "Mint 1 token to `empty` wallet"; + + await expect( + governance20.write.propose([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescription, + ], { + account: empty.account.address, + }) + ).to.be.rejectedWith("GovernorInsufficientProposerVotes"); + }); + + it("Should have correct proposal state transitions", async () => { + const calldata = encodeFunctionData({ + abi: votingERC20.abi, + functionName: "mint", + args: [user1.account.address, ethers.parseUnits("1")], + }); + + const proposalDescription = "Mint 1 token to user1"; + const proposalDescriptionHash = ethers.keccak256( + ethers.toUtf8Bytes(proposalDescription) + ) as Hex; + + await governance20.write.propose([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescription, + ], { + account: admin.account.address, + }); + + const proposalId = await governance20.read.getProposalId([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ]); + expect( + await governance20.read.state([proposalId]) + ).to.equal(0); // Pending + + await networkHelpers.mine(delay + 1); + + expect( + await governance20.read.state([proposalId]) + ).to.equal(1); // Active + + await governance20.write.castVote([ + proposalId, + 1, + ], { + account: admin.account.address, + }); + + await networkHelpers.mine(votingPeriod + 1); + + expect( + await governance20.read.state([proposalId]) + ).to.equal(4); // Succeeded + + await governance20.write.queue([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ], { + account: admin.account.address, + }); + + expect( + await governance20.read.state([proposalId]) + ).to.equal(5); // Queued + + await networkHelpers.time.increase(delay + 1); + + await governance20.write.execute([ + [votingERC20.address], + [0n], + [ + calldata, + ], + proposalDescriptionHash, + ], { + account: admin.account.address, + }); + + expect( + await governance20.read.state([proposalId]) + ).to.equal(7); // Executed + }); + + it("Should allow to cancel proposal", async () => { + const calldata = encodeFunctionData({ + abi: votingERC20.abi, + functionName: "mint", + args: [user1.account.address, ethers.parseUnits("1")], + }); + + const proposalDescription = "Mint 1 token to user1"; + const proposalDescriptionHash = ethers.keccak256( + ethers.toUtf8Bytes(proposalDescription) + ) as Hex; + + await governance20.write.propose([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescription, + ], { + account: admin.account.address, + }); + + const proposalId = await governance20.read.getProposalId([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ]); + + await governance20.write.cancel([ + [ votingERC20.address ], + [ 0n ], + [ calldata ], + proposalDescriptionHash, + ], { + account: admin.account.address, + }); + + expect( + await governance20.read.state([proposalId]) + ).to.equal(2); // Canceled }); }); -// const { viem } = await network.connect(); -// votingERC20 = await viem.deployContract( -// "ZeroVotingERC20", -// [ -// "ZV", -// "domname", -// "ZERO DAO", -// "1", -// "0x1234567890123456789012345678901234567890" -// ] -// ); - -// describe("ZDAO", () => { -// let dao; - -// it("load fixture", async () => { -// const { viem } = await network.connect(); -// votingERC20 = await viem.deployContract("ZeroVotingERC20"); - -// expect(votingERC20).to.exist; -// expect(votingERC20).to.not.be.undefined; -// }); - - -// // beforeEach(async function () { - -// // const voting20 = await viem.deployContract("ZeroVotingERC20"); -// // // const voting20 = await viem.deployContract("ZeroVotingERC20"); -// // const token = await voting20.deploy( -// // "ZeroVotingERC20", -// // "ZV", -// // "ZERO DAO", -// // "1", -// // (await ethers.getSigners())[0] -// // ); -// // await token.waitForDeployment(); - -// // const ZDAO = await ethers.getContractFactory("ZDAO"); -// // dao = await ZDAO.deploy( -// // 1n, -// // "ZDAO", -// // token, -// // delay, -// // votingPeriod, -// // proposalThreshold20, -// // quorumPercentage, -// // voteExtension, -// // ); -// // await dao.waitForDeployment(); -// // }); -// }); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9b1380c..6d8a5c1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,12 +2,18 @@ { "compilerOptions": { "lib": ["es2023"], - "module": "node16", + "module": "ESNext", "target": "es2022", "strict": true, + "noEmit": true, + "resolveJsonModule": true, "esModuleInterop": true, "skipLibCheck": true, - "moduleResolution": "node16", - "outDir": "dist" + "sourceMap": true, + "inlineSourceMap": false, + "inlineSources": false, + "moduleResolution": "bundler", + "outDir": "dist", + "types": ["hardhat", "hardhat-viem", "viem"] } } diff --git a/wagmi.config.ts b/wagmi.config.ts new file mode 100644 index 0000000..1879a85 --- /dev/null +++ b/wagmi.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@wagmi/cli'; +import { hardhat } from '@wagmi/cli/plugins'; + +// TODO proto: do we need this wagmi here at all ?! + +export default defineConfig({ + out: 'src/abi/generated.ts', + plugins: [hardhat({ + project: ".", + commands: { build: 'yarn hardhat compile' }, + })], +}); diff --git a/yarn.lock b/yarn.lock index 3ecab10..42283db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -662,9 +662,9 @@ eslint-visitor-keys "^3.4.3" "@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": - version "4.12.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" - integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== "@eslint/eslintrc@^2.1.4": version "2.1.4" @@ -2247,7 +2247,31 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== -"@zero-tech/eslint-config-cpt@0.2.8": +"@wagmi/cli@^2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@wagmi/cli/-/cli-2.7.1.tgz#de76a16005fdc450127f7b665242fadc2c22b6ee" + integrity sha512-yuhTvbNkOAxMVDigO2IPySmV8/2i5KeU0gQs4XoGNL+hIoH/2tYlJ2BzbC/ylC8LWatWHlNMYspWKw7rE+izqw== + dependencies: + abitype "^1.0.4" + bundle-require "^5.1.0" + cac "^6.7.14" + change-case "^5.4.4" + chokidar "4.0.1" + dedent "^0.7.0" + dotenv "^16.3.1" + dotenv-expand "^10.0.0" + esbuild "~0.25.4" + escalade "3.2.0" + fdir "^6.1.1" + nanospinner "1.2.2" + pathe "^1.1.2" + picocolors "^1.0.0" + picomatch "^3.0.0" + prettier "^3.0.3" + viem "2.x" + zod "^4.1.11" + +"@zero-tech/eslint-config-cpt@^0.2.8": version "0.2.8" resolved "https://registry.yarnpkg.com/@zero-tech/eslint-config-cpt/-/eslint-config-cpt-0.2.8.tgz#f4b69187e65f61d519c77755f5ae0963efeb5c9d" integrity sha512-i5v/tl6Nv23gM8HGXJiiYh5NaL1guARDtka2cx7T6K7g41zd9NZPynHQeGHHtv3zvcFG/hP5J8uS7O3k4DpplA== @@ -2267,7 +2291,7 @@ abitype@1.1.0: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.1.0.tgz#510c5b3f92901877977af5e864841f443bf55406" integrity sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A== -abitype@^1.0.2, abitype@^1.0.9: +abitype@^1.0.2, abitype@^1.0.4, abitype@^1.0.9: version "1.1.1" resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.1.1.tgz#b50ed400f8bfca5452eb4033445c309d3e1117c8" integrity sha512-Loe5/6tAgsBukY95eGaPSDmQHIjRZYQq8PB1MpsNccDIK8WiV+Uw6WzaIXipvaxTEL2yEB0OpEaQv3gs8pkS9Q== @@ -2620,6 +2644,18 @@ buffer@4.9.2: ieee754 "^1.1.4" isarray "^1.0.0" +bundle-require@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-5.1.0.tgz#8db66f41950da3d77af1ef3322f4c3e04009faee" + integrity sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA== + dependencies: + load-tsconfig "^0.2.3" + +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" @@ -2708,11 +2744,23 @@ chalk@^5.3.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== +change-case@^5.4.4: + version "5.4.4" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" + integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== + check-error@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== +chokidar@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.1.tgz#4a6dff66798fb0f72a94f616abbd7e1a19f31d41" + integrity sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA== + dependencies: + readdirp "^4.0.1" + chokidar@^4.0.1, chokidar@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" @@ -2897,6 +2945,11 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + deep-eql@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" @@ -2971,6 +3024,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dotenv-expand@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" + integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== + +dotenv@^16.3.1: + version "16.6.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.6.1.tgz#773f0e69527a8315c7285d5ee73c4459d20a8020" + integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow== + dunder-proto@^1.0.0, dunder-proto@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" @@ -3124,7 +3187,7 @@ es-to-primitive@^1.3.0: is-date-object "^1.0.5" is-symbol "^1.0.4" -esbuild@~0.25.0: +esbuild@~0.25.0, esbuild@~0.25.4: version "0.25.11" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.11.tgz#0f31b82f335652580f75ef6897bba81962d9ae3d" integrity sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q== @@ -3156,7 +3219,7 @@ esbuild@~0.25.0: "@esbuild/win32-ia32" "0.25.11" "@esbuild/win32-x64" "0.25.11" -escalade@^3.1.1: +escalade@3.2.0, escalade@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== @@ -3487,6 +3550,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fdir@^6.1.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -4258,6 +4326,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +load-tsconfig@^0.2.3: + version "0.2.5" + resolved "https://registry.yarnpkg.com/load-tsconfig/-/load-tsconfig-0.2.5.tgz#453b8cd8961bfb912dea77eb6c168fe8cca3d3a1" + integrity sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg== + locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -4434,6 +4507,13 @@ ms@^2.1.1, ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +nanospinner@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/nanospinner/-/nanospinner-1.2.2.tgz#5a38f4410b5bf7a41585964bee74d32eab3e040b" + integrity sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA== + dependencies: + picocolors "^1.1.1" + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -4661,6 +4741,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + pathval@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.1.tgz#8855c5a2899af072d6ac05d11e46045ad0dc605d" @@ -4678,7 +4763,7 @@ pbkdf2@^3.0.17: sha.js "^2.4.12" to-buffer "^1.2.1" -picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -4688,6 +4773,11 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-3.0.1.tgz#817033161def55ec9638567a2f3bbc876b3e7516" + integrity sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag== + possible-typed-array-names@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" @@ -4703,6 +4793,11 @@ prettier@^2.3.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.0.3: + version "3.6.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393" + integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== + pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" @@ -5549,6 +5644,20 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== +viem@2.x: + version "2.38.4" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.38.4.tgz#1523aceb2ae2d8ae67a11cbc44f3f984058659fe" + integrity sha512-qnyPNg6Lz1EEC86si/1dq7GlOyZVFHSgAW+p8Q31R5idnAYCOdTM2q5KLE4/ykMeMXzY0bnp5MWTtR/wjCtWmQ== + dependencies: + "@noble/curves" "1.9.1" + "@noble/hashes" "1.8.0" + "@scure/bip32" "1.7.0" + "@scure/bip39" "1.6.0" + abitype "1.1.0" + isows "1.0.7" + ox "0.9.6" + ws "8.18.3" + viem@^2.21.8, viem@^2.38.3: version "2.38.3" resolved "https://registry.yarnpkg.com/viem/-/viem-2.38.3.tgz#316905584d60188d3a921fc3960e55e9e46cb3f4" @@ -5743,3 +5852,8 @@ zod@^3.23.8: version "3.25.76" resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== + +zod@^4.1.11: + version "4.1.12" + resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0" + integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==