diff --git a/bun.lock b/bun.lock index 9cdcefee7..588a7a898 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "yearn-devdocs", @@ -25,11 +24,11 @@ "recharts": "^2.15.4", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0", - "solc": "^0.8.31", + "solc": "^0.8.34", "solidity-docgen": "^0.5.17", - "turndown": "^7.1.2", + "turndown": "^7.2.2", "turndown-plugin-gfm": "^1.0.2", - "viem": "^2.41.2", + "viem": "^2.46.3", }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", @@ -1008,7 +1007,7 @@ "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], - "abitype": ["abitype@1.1.0", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A=="], + "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -2228,7 +2227,7 @@ "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], - "ox": ["ox@0.9.6", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.9", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg=="], + "ox": ["ox@0.12.4", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-+P+C7QzuwPV8lu79dOwjBKfB2CbnbEXe/hfyyrff1drrO1nOOj3Hc87svHfcW1yneRr3WXaKr6nz11nq+/DF9Q=="], "p-cancelable": ["p-cancelable@1.1.0", "", {}, "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw=="], @@ -2712,7 +2711,7 @@ "sockjs": ["sockjs@0.3.24", "", { "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^8.3.2", "websocket-driver": "^0.7.4" } }, "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ=="], - "solc": ["solc@0.8.31", "", { "dependencies": { "command-exists": "^1.2.8", "commander": "^8.1.0", "follow-redirects": "^1.12.1", "js-sha3": "0.8.0", "memorystream": "^0.3.1", "semver": "^5.5.0", "tmp": "0.0.33" }, "bin": { "solcjs": "solc.js" } }, "sha512-wpccgDgu/aE/rRcF2F/LeN+4knK0734XTcjppyaQOticjYd/Giq1AJE3XPQZKEViAsY3sNaFKl7QpMRYrK35vg=="], + "solc": ["solc@0.8.34", "", { "dependencies": { "command-exists": "^1.2.8", "commander": "^8.1.0", "follow-redirects": "^1.12.1", "js-sha3": "0.8.0", "memorystream": "^0.3.1", "semver": "^5.5.0", "tmp": "0.0.33" }, "bin": { "solcjs": "solc.js" } }, "sha512-qf8HajA1sHhXRV0hMSDXLjVbc4v3Q+SQbL9zok+1WmgVj7Z4oMjMHxaysCzfGtFVqjZdfDDJWyZI+tcx5bO7Dw=="], "solidity-docgen": ["solidity-docgen@0.5.17", "", { "dependencies": { "@oclif/command": "^1.8.0", "@oclif/config": "^1.17.0", "@oclif/errors": "^1.3.3", "@oclif/plugin-help": "^5.0.0", "globby": "^11.0.0", "handlebars": "^4.7.6", "json5": "^2.1.3", "lodash": "^4.17.15", "micromatch": "^4.0.2", "minimatch": "^5.0.0", "semver": "^7.3.2", "solc": "^0.6.7" }, "bin": { "solidity-docgen": "dist/cli.js" } }, "sha512-RX5SPLFL9z0ZVBcZ/o5l/TKXMgSjNhWdumLuuv+Dy1O/66sThpHYd0HVpzdwAjVff0Ajk76bYM2zZYiMnqBfng=="], @@ -2928,7 +2927,7 @@ "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], - "viem": ["viem@2.41.2", "", { "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" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-LYliajglBe1FU6+EH9mSWozp+gRA/QcHfxeD9Odf83AdH5fwUS7DroH4gHvlv6Sshqi1uXrYFA2B/EOczxd15g=="], + "viem": ["viem@2.46.3", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.12.4", "ws": "8.18.3" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-2LJS+Hyh2sYjHXQtzfv1kU9pZx9dxFzvoU/ZKIcn0FNtOU0HQuIICuYdWtUDFHaGXbAdVo8J1eCvmjkL9JVGwg=="], "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], diff --git a/package.json b/package.json index 96b4f478f..34b019f7c 100644 --- a/package.json +++ b/package.json @@ -43,11 +43,11 @@ "recharts": "^2.15.4", "rehype-katex": "^7.0.1", "remark-math": "^6.0.0", - "solc": "^0.8.31", + "solc": "^0.8.34", "solidity-docgen": "^0.5.17", - "turndown": "^7.1.2", + "turndown": "^7.2.2", "turndown-plugin-gfm": "^1.0.2", - "viem": "^2.41.2" + "viem": "^2.46.3" }, "browserslist": { "production": [ diff --git a/scripts/fetchedAddressData.json b/scripts/fetchedAddressData.json index 203de6e16..988be8834 100644 --- a/scripts/fetchedAddressData.json +++ b/scripts/fetchedAddressData.json @@ -1,5 +1,5 @@ { - "timeLastChecked": 1770347214, + "timeLastChecked": 1771960246, "addressesData": { "v3ContractAddresses": { "topLevel": { @@ -71,11 +71,38 @@ "yGauge DAI-2 yVault": "0x38E3d865e34f7367a69f096C80A4fc329DB38BF4", "yGauge WETH-2 yVault": "0x8E2485942B399EA41f3C910c1Bb8567128f79859", "yGauge crvUSD-2 yVault": "0x71c3223D6f836f84cAA7ab5a68AAb6ECe21A9f3b" + }, + "yearnMultisigMembers": { + "multisigAddress": "0xFEB4acf3df3cDEA7399794D0869ef76A6EfAff52", + "docsMemberAddresses": [ + "0x6F2A8Ee9452ba7d336b3fba03caC27f7818AeAD6", + "0xeA6c0837fef621E77329f85820F503cA09f2B3a9", + "0x70aF5a3368606c6557D2B3ce2EEC8796B914EAa3", + "0xf5D3dbda5F41A0E26D71B948e29522398e71cFaE", + "0x5Db9926c93085a92F14A85daBF6FF27b07362Cae", + "0x2B888954421b424C5D3D9Ce9bB67c9bD47537d12", + "0xFe45baf0F18c207152A807c1b05926583CFE2e4b", + "0x962228a90eaC69238c7D1F216d80037e61eA9255", + "0x700F1a984C962b447CcDb95c4c2D8074C65098a3" + ], + "onChainOwners": [ + "0xf5D3dbda5F41A0E26D71B948e29522398e71cFaE", + "0xeA6c0837fef621E77329f85820F503cA09f2B3a9", + "0x6F2A8Ee9452ba7d336b3fba03caC27f7818AeAD6", + "0x2B888954421b424C5D3D9Ce9bB67c9bD47537d12", + "0xFe45baf0F18c207152A807c1b05926583CFE2e4b", + "0x962228a90eaC69238c7D1F216d80037e61eA9255", + "0x70aF5a3368606c6557D2B3ce2EEC8796B914EAa3", + "0x5Db9926c93085a92F14A85daBF6FF27b07362Cae", + "0x700F1a984C962b447CcDb95c4c2D8074C65098a3" + ], + "docsSourcePath": "docs/developers/security/multisig.md" } }, "addressChecks": { "allV3ChecksPassed": true, "allVeYfiChecksPassed": true, + "allMultisigChecksPassed": true, "failedChecks": [], "v3Checks": { "topLevel": { @@ -128,6 +155,14 @@ "yGauge DAI-2 yVault": true, "yGauge WETH-2 yVault": true, "yGauge crvUSD-2 yVault": true + }, + "multisigChecks": { + "docsMembersSectionParsed": true, + "docsAddressesValid": true, + "docsOwnerCountMatch": true, + "docsUniqueOwnersCheck": true, + "onChainUniqueOwnersCheck": true, + "exactMembersMatch": true } } } \ No newline at end of file diff --git a/scripts/runAddressChecks.ts b/scripts/runAddressChecks.ts index f63f988f7..9d3a258fd 100644 --- a/scripts/runAddressChecks.ts +++ b/scripts/runAddressChecks.ts @@ -8,6 +8,7 @@ import { fetchAndCheckYearnV3Addresses, } from '../src/ethereum/v3Checks' import { veYfiChecks } from '../src/ethereum/veYfiChecks' +import { checkYearnMultisigMembers } from '../src/ethereum/multisigChecks' import { yfiContracts, veYfiContracts } from '../src/ethereum/constants' import { ContractAddresses, @@ -18,7 +19,13 @@ import { mainnet } from 'viem/chains' dotenv.config() -const alchemyKey = process.env.ALCHEMY_API_KEY +const alchemyKey = process.env.ALCHEMY_API_KEY?.trim() +const invalidAlchemyValues = new Set(['', 'undefined', 'null', 'yourApiKeyHere']) + +if (!alchemyKey || invalidAlchemyValues.has(alchemyKey)) { + console.error('Environment vars not set properly') + process.exit(1) +} const publicClient = createPublicClient({ batch: { @@ -90,16 +97,29 @@ const fetchAddresses = async () => { veYfiCheckFlag = veYfiData?.checkFlag if (!veYfiData) throw new Error('Failed to fetch veYFI gauge addresses') + let multisigCheckFlag: boolean | undefined + multisigCheckFlag = true + const multisigData = await checkYearnMultisigMembers( + yearnV3Data.addresses.yearnDaddy, + publicClient, + multisigCheckFlag, + failedChecks + ) + multisigCheckFlag = multisigData?.checkFlag + if (!multisigData) throw new Error('Failed to fetch multisig owners') + const addressesData: ContractAddresses = { v3ContractAddresses: v3AddressData, yfiTokenContracts: yfiContracts, veYfiContracts: veYfiContracts, veYfiGaugeAddresses: veYfiData.veYfiGaugeAddresses, + yearnMultisigMembers: multisigData.addresses, } const addressChecks: AddressChecks = { allV3ChecksPassed: v3CheckFlag, allVeYfiChecksPassed: veYfiCheckFlag, + allMultisigChecksPassed: multisigCheckFlag, failedChecks, v3Checks: { topLevel: topLevelData.checks, @@ -108,12 +128,15 @@ const fetchAddresses = async () => { yearnV3: yearnV3Data.checks, }, veYfiChecks: veYfiData.veYfiGaugeChecks, + multisigChecks: multisigData.checks, } if ( v3CheckFlag === false || v3CheckFlag === undefined || veYfiCheckFlag === false || - veYfiCheckFlag === undefined + veYfiCheckFlag === undefined || + multisigCheckFlag === false || + multisigCheckFlag === undefined ) { console.log('Addresses:', addressesData) console.log('Checks:', addressChecks) @@ -141,7 +164,9 @@ async function runAddressCheck() { } const allChecksPassed = - addressChecks.allV3ChecksPassed && addressChecks.allVeYfiChecksPassed + addressChecks.allV3ChecksPassed && + addressChecks.allVeYfiChecksPassed && + addressChecks.allMultisigChecksPassed process.env.ALL_CHECKS_PASSED = allChecksPassed ? 'true' : 'false' console.log('allChecksPassed: ', process.env.ALL_CHECKS_PASSED) @@ -156,7 +181,7 @@ async function runAddressCheck() { issueContent += `- ${check}\n` }) issueContent += - '\nThe addresses shown above should be the updated, correct addresses. Please review and change the values in `src/ethereum/constants.ts`.\n' + '\nThe addresses shown above should be the updated, correct addresses. Please review and update the relevant source (`src/ethereum/constants.ts` and/or `docs/developers/security/multisig.md`).\n' fs.writeFileSync('issue_body.md', issueContent) console.log('Issue content generated.') diff --git a/src/ethereum/ABIs/gnosisSafeABI.ts b/src/ethereum/ABIs/gnosisSafeABI.ts new file mode 100644 index 000000000..9cca1cf6d --- /dev/null +++ b/src/ethereum/ABIs/gnosisSafeABI.ts @@ -0,0 +1,9 @@ +export const gnosisSafeABI = [ + { + stateMutability: 'view', + type: 'function', + name: 'getOwners', + inputs: [], + outputs: [{ name: '', type: 'address[]' }], + }, +] diff --git a/src/ethereum/ABIs/index.ts b/src/ethereum/ABIs/index.ts index cf8c223a9..66d005569 100644 --- a/src/ethereum/ABIs/index.ts +++ b/src/ethereum/ABIs/index.ts @@ -14,3 +14,4 @@ export * from './v3VaultFactoryBlueprintABI' export * from './veyfiABI' export * from './yPoolsGenericGovernorABI' export * from './yPoolsInclusionVoteABI' +export * from './gnosisSafeABI' diff --git a/src/ethereum/multisigCalls.ts b/src/ethereum/multisigCalls.ts new file mode 100644 index 000000000..3f0eb7489 --- /dev/null +++ b/src/ethereum/multisigCalls.ts @@ -0,0 +1,18 @@ +import { Address, PublicClient, getContract } from 'viem' +import { gnosisSafeABI } from './ABIs' + +export const readSafeOwners = async ( + safeAddress: Address, + publicClient: PublicClient +) => { + const contract = getContract({ + address: safeAddress, + abi: gnosisSafeABI, + client: publicClient, + }) + + console.log('Fetching Gnosis Safe owners...') + const owners = await contract.read.getOwners() + console.log('Gnosis Safe owners fetched.') + return owners +} diff --git a/src/ethereum/multisigChecks.ts b/src/ethereum/multisigChecks.ts new file mode 100644 index 000000000..977951233 --- /dev/null +++ b/src/ethereum/multisigChecks.ts @@ -0,0 +1,192 @@ +import fs from 'fs' +import path from 'path' +import { Address, PublicClient, getAddress } from 'viem' +import { readSafeOwners } from './multisigCalls' + +const MULTISIG_DOCS_PATH = 'docs/developers/security/multisig.md' +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' + +const extractMembersSection = (markdown: string) => { + const parts = markdown.split(/^## Members\s*$/m) + const afterMembersHeading = parts[1] + if (!afterMembersHeading) { + throw new Error('Failed to find "## Members" section in multisig docs') + } + const nextHeadingIndex = afterMembersHeading.search(/^##\s/m) + if (nextHeadingIndex === -1) { + return afterMembersHeading + } + return afterMembersHeading.slice(0, nextHeadingIndex) +} + +const extractAddressesFromMembersTable = (section: string) => { + const addresses: string[] = [] + for (const line of section.split(/\r?\n/)) { + if (!line.trim().startsWith('|')) continue + const matches = line.match(/0x[a-fA-F0-9]{40}/g) + if (!matches || matches.length === 0) continue + addresses.push(matches[0]) + } + return addresses +} + +const getDuplicates = (addresses: Address[]) => { + const seen = new Set
() + const duplicates = new Set() + for (const address of addresses) { + if (seen.has(address)) { + duplicates.add(address) + continue + } + seen.add(address) + } + return [...duplicates] +} + +export const checkYearnMultisigMembers = async ( + multisigAddressFromRoleManager: Address | undefined, + publicClient: PublicClient, + checkFlag: boolean | undefined, + failedChecks: string[] +) => { + const multisigAddress = getAddress( + multisigAddressFromRoleManager ?? ZERO_ADDRESS + ) + const docsPath = path.resolve(MULTISIG_DOCS_PATH) + + console.log('validating Yearn multisig members...') + + let docsMembersSectionParsed = true + let docsAddressesValid = true + let docsOwnerCountMatch = true + let docsUniqueOwnersCheck = true + let onChainUniqueOwnersCheck = true + let exactMembersMatch = true + let onChainOwnersRead = true + + let docsMemberAddressesRaw: string[] = [] + let docsMemberAddresses: Address[] = [] + + try { + const markdown = fs.readFileSync(docsPath, 'utf8') + const membersSection = extractMembersSection(markdown) + docsMemberAddressesRaw = extractAddressesFromMembersTable(membersSection) + } catch (error) { + docsMembersSectionParsed = false + exactMembersMatch = false + failedChecks.push('yearnMultisig docs members section parse failed') + console.error(error) + } + + if (docsMembersSectionParsed) { + try { + docsMemberAddresses = docsMemberAddressesRaw.map((address) => + getAddress(address) + ) + } catch (error) { + docsAddressesValid = false + exactMembersMatch = false + failedChecks.push('yearnMultisig docs contains invalid address') + console.error(error) + } + } + + let onChainOwners: Address[] = [] + + if (multisigAddress === getAddress(ZERO_ADDRESS)) { + onChainOwnersRead = false + docsOwnerCountMatch = false + onChainUniqueOwnersCheck = false + exactMembersMatch = false + checkFlag = false + failedChecks.push('yearnMultisig daddy address missing from role manager') + } else { + try { + const safeOwners = (await readSafeOwners( + multisigAddress, + publicClient + )) as Address[] + onChainOwners = safeOwners.map((address) => getAddress(address)) + } catch (error) { + onChainOwnersRead = false + docsOwnerCountMatch = false + onChainUniqueOwnersCheck = false + exactMembersMatch = false + checkFlag = false + failedChecks.push( + `yearnMultisig failed to read onchain owners for ${multisigAddress}` + ) + console.error(error) + } + } + + if (docsAddressesValid && onChainOwnersRead) { + docsOwnerCountMatch = docsMemberAddresses.length === onChainOwners.length + if (!docsOwnerCountMatch) { + failedChecks.push( + `yearnMultisig owner count mismatch (docs=${docsMemberAddresses.length}, onchain=${onChainOwners.length})` + ) + } + + const docsDuplicates = getDuplicates(docsMemberAddresses) + const onChainDuplicates = getDuplicates(onChainOwners) + docsUniqueOwnersCheck = docsDuplicates.length === 0 + onChainUniqueOwnersCheck = onChainDuplicates.length === 0 + + for (const duplicate of docsDuplicates) { + failedChecks.push(`yearnMultisig duplicate docs member: ${duplicate}`) + } + for (const duplicate of onChainDuplicates) { + failedChecks.push(`yearnMultisig duplicate onchain owner: ${duplicate}`) + } + + const docsSet = new Set(docsMemberAddresses) + const onChainSet = new Set(onChainOwners) + const missingInDocs = onChainOwners.filter((owner) => !docsSet.has(owner)) + const extraInDocs = docsMemberAddresses.filter( + (owner) => !onChainSet.has(owner) + ) + + if (missingInDocs.length > 0 || extraInDocs.length > 0) { + exactMembersMatch = false + } + + for (const owner of missingInDocs) { + failedChecks.push(`yearnMultisig missing in docs: ${owner}`) + } + for (const owner of extraInDocs) { + failedChecks.push(`yearnMultisig extra in docs: ${owner}`) + } + } + + if ( + !docsMembersSectionParsed || + !docsAddressesValid || + !docsOwnerCountMatch || + !docsUniqueOwnersCheck || + !onChainUniqueOwnersCheck || + !exactMembersMatch + ) { + checkFlag = false + } + + console.log('Yearn multisig member validation complete. \n') + + return { + addresses: { + multisigAddress, + docsMemberAddresses, + onChainOwners, + docsSourcePath: MULTISIG_DOCS_PATH, + }, + checks: { + docsMembersSectionParsed, + docsAddressesValid, + docsOwnerCountMatch, + docsUniqueOwnersCheck, + onChainUniqueOwnersCheck, + exactMembersMatch, + }, + checkFlag, + } +} diff --git a/src/ethereum/types.ts b/src/ethereum/types.ts index 191ef47fa..e18d7b2f1 100644 --- a/src/ethereum/types.ts +++ b/src/ethereum/types.ts @@ -3,6 +3,7 @@ export type ContractAddresses = { yfiTokenContracts: YfiTokenContracts veYfiContracts: VeYfiContracts veYfiGaugeAddresses: GaugeAddressRecord + yearnMultisigMembers: YearnMultisigMembers } export type V3ContractAddresses = { @@ -60,6 +61,7 @@ export type VeYfiContracts = { export type GaugeAddressRecord = Record