From 59216b2d7669bd553dfb273330b187df154db9a5 Mon Sep 17 00:00:00 2001 From: Will Kim Date: Fri, 29 Apr 2022 14:52:04 -0400 Subject: [PATCH 1/3] fix: update super/sub proposal logic, add test scripts --- .gitignore | 1 + env-example.json | 9 + package-lock.json | 80 +++----- package.json | 1 + scripts/approve-proposal.ts | 34 ++++ scripts/approve-superproposal.ts | 56 ++++++ scripts/reject-proposal.ts | 33 ++++ scripts/reject-superproposal.ts | 63 ++++++ scripts/utils.ts | 19 ++ src/Pod.ts | 69 +++++-- src/Proposal.ts | 41 +++- src/lib/services/create-safe-transaction.ts | 136 ++++++++++++- src/lib/services/super-proposal.ts | 0 src/lib/services/transaction-service.ts | 44 ++++- src/lib/utils.ts | 5 +- test/fixtures/index.ts | 204 ++++++++++++++++++++ test/proposal.test.ts | 14 +- tsconfig.json | 2 +- 18 files changed, 735 insertions(+), 76 deletions(-) create mode 100644 env-example.json create mode 100644 scripts/approve-proposal.ts create mode 100644 scripts/approve-superproposal.ts create mode 100644 scripts/reject-proposal.ts create mode 100644 scripts/reject-superproposal.ts create mode 100644 scripts/utils.ts create mode 100644 src/lib/services/super-proposal.ts diff --git a/.gitignore b/.gitignore index 2b0a9c3..148b492 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .nvmrc dist +env.json diff --git a/env-example.json b/env-example.json new file mode 100644 index 0000000..575a4b7 --- /dev/null +++ b/env-example.json @@ -0,0 +1,9 @@ +{ + "accountOne": "0xf0C7d25c942264D6F21871c05d3dB3b98344b499", + "accountOnePrivateKey": "", + "accountTwo": "0xA56b297065D814988080b8E765D953651059539c", + "accountTwoPrivateKey": "", + "dummyAccount": "0x1cC62cE7cb56ed99513823064295761f9b7C856e", + "adminPodAddress": "0xBe71ECaA104645ab78ed62A52763b2854e6DaD2E", + "subPodAddress": "0x4d3ba1AdabA15796CC3d11E48e8EC28e3A5F7C41" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cb85f61..93c587b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "babel-jest": "^27.4.6", "cod-scripts": "^10.0.0", "jest": "^27.4.7", + "ts-node": "^10.7.0", "typedoc": "^0.22.15" } }, @@ -2367,8 +2368,6 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">= 12" } @@ -2378,8 +2377,6 @@ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@cspotcode/source-map-consumer": "0.8.0" }, @@ -5849,33 +5846,25 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@typechain/ethers-v5": { "version": "2.0.0", @@ -34657,12 +34646,10 @@ } }, "node_modules/ts-node": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", - "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", @@ -34675,11 +34662,13 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", "yn": "3.1.1" }, "bin": { "ts-node": "dist/bin.js", "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", "ts-node-transpile-only": "dist/bin-transpile.js", "ts-script": "dist/bin-script-deprecated.js" @@ -34704,8 +34693,6 @@ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true, - "optional": true, - "peer": true, "engines": { "node": ">=0.4.0" } @@ -35437,6 +35424,12 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", @@ -39278,17 +39271,13 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@cspotcode/source-map-support": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz", "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==", "dev": true, - "optional": true, - "peer": true, "requires": { "@cspotcode/source-map-consumer": "0.8.0" } @@ -42051,33 +42040,25 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@tsconfig/node12": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@tsconfig/node14": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@tsconfig/node16": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "@typechain/ethers-v5": { "version": "2.0.0", @@ -64459,12 +64440,10 @@ } }, "ts-node": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz", - "integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", + "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", "dev": true, - "optional": true, - "peer": true, "requires": { "@cspotcode/source-map-support": "0.7.0", "@tsconfig/node10": "^1.0.7", @@ -64477,6 +64456,7 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.0", "yn": "3.1.1" }, "dependencies": { @@ -64484,9 +64464,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "optional": true, - "peer": true + "dev": true } } }, @@ -65081,6 +65059,12 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "v8-to-istanbul": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", diff --git a/package.json b/package.json index 01a7545..9828597 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "babel-jest": "^27.4.6", "cod-scripts": "^10.0.0", "jest": "^27.4.7", + "ts-node": "^10.7.0", "typedoc": "^0.22.15" }, "dependencies": { diff --git a/scripts/approve-proposal.ts b/scripts/approve-proposal.ts new file mode 100644 index 0000000..5271036 --- /dev/null +++ b/scripts/approve-proposal.ts @@ -0,0 +1,34 @@ +import { getPod } from '../src'; +import { adminPodAddress, dummyAccount } from '../env.json'; +import { setup, sleep } from './utils'; + +async function main() { + const { walletOne, walletTwo } = setup(); + + // Get the pod we're working with + + const adminTest = await getPod(adminPodAddress); + + // We mint/burn the dummy account based on whether its a member or not. + const isMember = await adminTest.isMember(dummyAccount); + if (isMember) { + await adminTest.proposeBurnMember(dummyAccount, walletOne); + } else { + await adminTest.proposeMintMember(dummyAccount, walletOne); + } + + const proposal = (await adminTest.getProposals())[0]; + + await proposal.approve(walletTwo); + await proposal.executeApprove(walletTwo); + + // Let gnosis catch up. + await sleep(5000); + + if (proposal.status !== 'executed') throw new Error('Proposal status not right'); + const refetchProposal = (await adminTest.getProposals())[0]; + if (!refetchProposal.safeTransaction.isExecuted) + throw new Error('Proposal not executed according to gnosis'); +} + +main(); diff --git a/scripts/approve-superproposal.ts b/scripts/approve-superproposal.ts new file mode 100644 index 0000000..280c5ea --- /dev/null +++ b/scripts/approve-superproposal.ts @@ -0,0 +1,56 @@ +import { getPod } from '../src'; +import { adminPodAddress, dummyAccount, subPodAddress } from '../env.json'; +import { setup, sleep } from './utils'; + +async function main() { + const { walletOne, walletTwo } = setup(); + + const adminPod = await getPod(adminPodAddress); + const subPod = await getPod(subPodAddress); + + if ( + (await adminPod.getProposals({ queued: true }))[0].status !== 'executed' || + (await subPod.getProposals({ queued: true }))[0].status !== 'executed' + ) { + throw new Error( + 'Admin or sub pod had an active/queued transaction. This script expects no enqueued transactions', + ); + } + + // We mint/burn the dummy account based on whether its a member or not. + const isMember = await adminPod.isMember(dummyAccount); + try { + if (isMember) { + console.log('Burning'); + await adminPod.proposeBurnMemberFromPod(subPod, dummyAccount, walletOne); + } else { + console.log('Minting'); + await adminPod.proposeMintMemberFromPod(subPod, dummyAccount, walletOne); + } + } catch (err) { + console.log(err); + throw new Error('Error creating proposal on subpod'); + } + + console.log('Executing subproposal'); + const subProposal = (await subPod.getProposals())[0]; + await subProposal.executeApprove(walletOne); + + // Let the tx service catch up + console.log('Letting tx service catch up...'); + await sleep(40000); + + console.log('Approving + executing superproposal'); + let superProposal = (await adminPod.getProposals())[0]; + + console.log('superProposal', superProposal); + await superProposal.approve(walletOne); + // Refetch + [superProposal] = await adminPod.getProposals(); + await superProposal.executeApprove(walletOne); + + console.log('We did it! 🎉'); + console.log('At least I think so. Check the pod page to be sure lol'); +} + +main(); diff --git a/scripts/reject-proposal.ts b/scripts/reject-proposal.ts new file mode 100644 index 0000000..c6bbe9d --- /dev/null +++ b/scripts/reject-proposal.ts @@ -0,0 +1,33 @@ +import { getPod } from '../src'; +import { adminTestPod } from '../env.json'; +import { setup, sleep } from './utils'; + +async function main() { + const { walletOne, walletTwo } = setup(); + + const adminTest = await getPod(adminTestPod); + + // We mint/burn the dummy account based on whether its a member or not. + const isMember = await adminTest.isMember(dummyAccount); + if (isMember) { + await adminTest.proposeBurnMember(dummyAccount, walletOne); + } else { + await adminTest.proposeMintMember(dummyAccount, walletOne); + } + + const proposal = (await adminTest.getProposals())[0]; + + // await proposal.reject(walletOne); + // await proposal.reject(walletTwo); + await proposal.executeReject(walletTwo); + + // Let gnosis catch up. + await sleep(5000); + + if (proposal.status !== 'executed') throw new Error('Proposal status not right'); + const refetchProposal = (await adminTest.getProposals())[0]; + if (!refetchProposal.safeTransaction.isExecuted) + throw new Error('Proposal not executed according to gnosis'); +} + +main(); diff --git a/scripts/reject-superproposal.ts b/scripts/reject-superproposal.ts new file mode 100644 index 0000000..35f4de1 --- /dev/null +++ b/scripts/reject-superproposal.ts @@ -0,0 +1,63 @@ +import { getPod } from '../src'; +import { adminPodAddress, dummyAccount, subPodAddress } from '../env.json'; +import { setup, sleep } from './utils'; + +async function main() { + const { walletOne, walletTwo } = setup(); + + const adminPod = await getPod(adminPodAddress); + const subPod = await getPod(subPodAddress); + + if ( + (await adminPod.getProposals({ queued: true }))[0].status !== 'executed' || + (await subPod.getProposals({ queued: true }))[0].status !== 'executed' + ) { + throw new Error( + 'Admin or sub pod had an active/queued transaction. This script expects no enqueued transactions', + ); + } + + // We mint/burn the dummy account based on whether its a member or not. + const isMember = await adminPod.isMember(dummyAccount); + console.log('Creating '); + try { + if (isMember) { + console.log('Burning'); + await adminPod.proposeBurnMemberFromPod(subPod, dummyAccount, walletOne); + } else { + console.log('Minting'); + await adminPod.proposeMintMemberFromPod(subPod, dummyAccount, walletOne); + } + } catch (err) { + console.log(err); + throw new Error('Error creating proposal on subpod'); + } + + console.log('Rejecting the subproposal'); + const [subProposal] = await subPod.getProposals(); + // await subProposal.executeApprove(walletOne); + await subProposal.reject(walletOne); + + await subProposal.executeReject(walletOne); + + console.log('Letting tx service catch up...'); + await sleep(40000); + + console.log('Rejecting the super proposal'); + let [superProposal] = await adminPod.getProposals(); + console.log('superProposal', superProposal); + await superProposal.reject(walletOne); + await sleep(40000); + [superProposal] = await adminPod.getProposals(); + await superProposal.executeReject(walletOne); + + console.log('Rejection seems to have worked, now waiting to refetch from Gnosis'); + + await sleep(30000); + [superProposal] = await adminPod.getProposals(); + + console.log(`Gnosis says the proposal status is ${superProposal.status}`); + console.log('If that seems wrong, it (probably) is slow, check the pod page to be sure'); +} + +main(); diff --git a/scripts/utils.ts b/scripts/utils.ts new file mode 100644 index 0000000..b5c7886 --- /dev/null +++ b/scripts/utils.ts @@ -0,0 +1,19 @@ +import { ethers } from 'ethers'; +import { init } from '../src'; +import { accountOnePrivateKey, accountTwoPrivateKey } from '../env.json'; + +export function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +export function setup() { + const provider = new ethers.providers.InfuraProvider('rinkeby', { + infura: '69ecf3b10bc24c6a972972666fe950c8', + }); + init({ provider, network: 4 }); + + // Get two accounts + const walletOne = new ethers.Wallet(accountOnePrivateKey, provider); + const walletTwo = new ethers.Wallet(accountTwoPrivateKey, provider); + return { walletOne, walletTwo }; +} diff --git a/src/Pod.ts b/src/Pod.ts index d6548d3..d8edaf4 100644 --- a/src/Pod.ts +++ b/src/Pod.ts @@ -16,7 +16,10 @@ import { getSafeTransactionsBySafe, populateDataDecoded, } from './lib/services/transaction-service'; -import { createSafeTransaction } from './lib/services/create-safe-transaction'; +import { + createSafeTransaction, + createNestedProposal, +} from './lib/services/create-safe-transaction'; import Proposal from './Proposal'; /** @@ -200,27 +203,55 @@ export default class Pod { const normalTransactions = []; // All the reject transactions, we need to combine this with the filtered transaction in the Proposal constructor. const rejectTransactions = []; + // Sub proposal transactions need to be handled differently. + const pairedSubTxs = {}; safeTransactions.forEach(tx => { if (tx.data === null && tx.to === this.safe) { - return rejectTransactions.push(tx); + rejectTransactions.push(tx); + return; + } + if (tx.dataDecoded?.method === 'approveHash') { + // Pair approve/reject sub transactions together + // Sub transactions always have an approve, but do not always have a reject + if (Array.isArray(pairedSubTxs[tx.nonce])) { + pairedSubTxs[tx.nonce].push(tx); + return; + } + pairedSubTxs[tx.nonce] = [tx]; + return; } - return normalTransactions.push(tx); + normalTransactions.push(tx); }); const rejectNonces = rejectTransactions.map(tx => tx.nonce); - return Promise.all( - normalTransactions.map(tx => { - // Check to see if there is a corresponding reject nonce. - const rejectNonceIndex = rejectNonces.indexOf(tx.nonce); - // If there is, we package that together with the regular transaction. - if (rejectNonceIndex >= 0) { - return new Proposal(this, nonce, tx, rejectTransactions[rejectNonceIndex]); - } - // Otherwise, just handle it normally. - return new Proposal(this, nonce, tx); - }), - ); + const subProposals = Object.keys(pairedSubTxs).map(subTxNonce => { + // subTxPair is an array of length 1 or 2, depending on if there's a reject or not + const subTxPair = pairedSubTxs[subTxNonce]; + if (subTxPair.length === 1) { + return new Proposal(this, nonce, subTxPair[0]); + } + // If the length is 2, that means there is a paired reject transaction. + // The reject transaction is always created after the approve transaction + // Because safeTx comes in reverse chron order, subTxPair[1] is the approve, [0] is the reject + return new Proposal(this, nonce, subTxPair[1], subTxPair[0]); + }); + + const normalProposals = normalTransactions.map(tx => { + // Check to see if there is a corresponding reject nonce. + const rejectNonceIndex = rejectNonces.indexOf(tx.nonce); + // If there is, we package that together with the regular transaction. + if (rejectNonceIndex >= 0) { + return new Proposal(this, nonce, tx, rejectTransactions[rejectNonceIndex]); + } + // Otherwise, just handle it normally. + return new Proposal(this, nonce, tx); + }); + + return subProposals.concat(normalProposals).sort((a, b) => { + // Sort in descending order/reverse chron based on nonce/id. + return b.id - a.id; + }); }; /** @@ -511,14 +542,15 @@ export default class Pod { const { address: memberTokenAddress } = getContract('MemberToken', signer); try { - // Create a safe transaction on this pod, sent from the admin pod - await createSafeTransaction( + // Create a safe transaction on this pod, sent from the signer + await createNestedProposal( { sender: externalPod.safe, safe: this.safe, to: memberTokenAddress, data, }, + externalPod, signer, ); } catch (err) { @@ -608,13 +640,14 @@ export default class Pod { const { address: memberTokenAddress } = getContract('MemberToken', signer); try { // Create a safe transaction on this pod, sent from the admin pod - await createSafeTransaction( + await createNestedProposal( { sender: externalPod.safe, safe: this.safe, to: memberTokenAddress, data, }, + externalPod, signer, ); } catch (err) { diff --git a/src/Proposal.ts b/src/Proposal.ts index 2a73c00..6f2b44d 100644 --- a/src/Proposal.ts +++ b/src/Proposal.ts @@ -4,6 +4,7 @@ import { approveSafeTransaction, createRejectTransaction, executeSafeTransaction, + rejectSuperProposal, SafeTransaction, } from './lib/services/transaction-service'; import { checkAddress } from './lib/utils'; @@ -26,6 +27,11 @@ export default class Proposal { /** @property Proposal status, i.e., 'active', 'executed', or 'queued', */ status: ProposalStatus; + /** + * @property Whether or not this proposal corresponds to a superproposal + */ + isSubProposal: boolean; + /** @property Array of addresses that approved */ approvals: string[]; @@ -97,6 +103,10 @@ export default class Proposal { this.method = null; this.parameters = null; } + + if (this.method === 'approveHash') { + this.isSubProposal = true; + } } /** @@ -132,6 +142,13 @@ export default class Proposal { */ reject = async (signer: ethers.Signer) => { const signerAddress = checkAddress(await signer.getAddress()); + + if (this.isSubProposal) { + // parameters[0].value is the superProposalTxHash + await rejectSuperProposal(this.parameters[0].value, this, signer); + return; + } + if (this.rejections.includes(signerAddress)) { throw new Error('Signer has already rejected this proposal'); } @@ -160,12 +177,34 @@ export default class Proposal { */ executeApprove = async (signer: ethers.Signer) => { const signerAddress = checkAddress(await signer.getAddress()); - if (this.approvals.length !== this.threshold) { + if (this.approvals.length < this.threshold) { throw new Error('Not enough approvals to execute'); } if (!(await this.pod.isMember(signerAddress))) { throw new Error('Signer was not part of this pod'); } + this.status = 'executed'; return executeSafeTransaction(this.safeTransaction, signer); }; + + /** + * Executes the rejection of proposal + * @param signer - Signer of pod member + * @throws If not enough rejections to execute + * @throws If signer was not part of the pod + */ + executeReject = async (signer: ethers.Signer) => { + const signerAddress = checkAddress(await signer.getAddress()); + if (this.isSubProposal) { + return executeRejectSuperProposal(this.parameters[0].value, this, signer); + } + if (this.rejections.length < this.threshold) { + throw new Error('Not enough rejections to execute'); + } + if (!(await this.pod.isMember(signerAddress))) { + throw new Error('Signer was not part of this pod'); + } + this.status = 'executed'; + return executeSafeTransaction(this.rejectTransaction, signer); + }; } diff --git a/src/lib/services/create-safe-transaction.ts b/src/lib/services/create-safe-transaction.ts index cd4cd41..7e4e5fd 100644 --- a/src/lib/services/create-safe-transaction.ts +++ b/src/lib/services/create-safe-transaction.ts @@ -1,4 +1,6 @@ import { ethers } from 'ethers'; +import type Pod from '../../Pod'; +import type Proposal from '../../Proposal'; import { getSafeInfo, getGasEstimation, @@ -6,8 +8,10 @@ import { submitSafeTransactionToService, getSafeTransactionsBySafe, approveSafeTransaction, + getSafeTransactionByHash, + createRejectTransaction, } from './transaction-service'; - +import { encodeFunctionData } from '../utils'; /** * Gets the nonce of the next transaction for a safe * @param safeAddress - safe address @@ -38,6 +42,7 @@ export async function createSafeTransaction( value?: string; data?: string; sender: string; + nonce?: number; }, signer: ethers.Signer, ) { @@ -55,7 +60,8 @@ export async function createSafeTransaction( sender: ethers.utils.getAddress(input.sender), // Get the checksummed address confirmationsRequired: threshold, safeTxGas, - nonce, + // If provided an override nonce + nonce: input.nonce ? input.nonce : nonce, operation: 0, baseGas: 0, gasPrice: '0', @@ -67,4 +73,130 @@ export async function createSafeTransaction( const createdSafeTransaction = await submitSafeTransactionToService({ safeTxHash, ...data }); await approveSafeTransaction(createdSafeTransaction, signer); + + return createdSafeTransaction; +} + +export async function rejectSuperProposal( + superProposalTxHash: string, + subProposal: Proposal, + signer: ethers.Signer, +) { + const subPod = subProposal.pod; + // Fetch the superProposal + const superProposal = await getSafeTransactionByHash(superProposalTxHash); + const superPodTransactions = await getSafeTransactionsBySafe(superProposal.safe, { + nonce: superProposal.nonce, + }); + + // The super reject, i.e., the transaction that rejects the super proposal + let superReject = superPodTransactions.find( + safeTx => safeTx.data === null && safeTx.to === superProposal.safe, + ); + // Need to create a super reject ourselves. This is a standard Gnosis reject. + if (!superReject) { + // This will not sign the transaction because we're just passing the pod safe. + superReject = await createRejectTransaction(superProposal, subPod.safe); + } + + // The sub reject, i.e., the transaction that approves the super reject + const subPodTransactions = await getSafeTransactionsBySafe(subPod.safe, { + nonce: subProposal.id, + }); + let subReject = subPodTransactions.find( + safeTx => + safeTx.dataDecoded.method === 'approveHash' && + safeTx.dataDecoded.parameters[0].value === superReject.safeTxHash, + ); + + // Need to create the sub reject. This is _not_ a standard Gnosis reject + // Instead, we need to approve the super reject proposal. + if (!subReject) { + // This will create + approve the sub reject + subReject = await createSafeTransaction( + { + safe: subPod.safe, + to: superProposal.safe, + data: encodeFunctionData('GnosisSafe', 'approveHash', [superReject.safeTxHash]), + sender: await signer.getAddress(), + nonce: subProposal.id, + }, + signer, + ); + } else { + // Just need to approve subReject + await approveSafeTransaction(subReject, signer); + } +} + +/** + * Creates a nested proposal (i.e., a proposal on a subpod to perform an action to the superpod) + * @param superProposal + * @param input.safe - Subpod safe address + * @param input.to - Contract address the transaction should be executed against + * @param input.value - Value + * @param input.data - Transaction data that should be performed by the superpod + * @param input.sender - Sender of transaction (i.e., subpod member) + * @param signer - Signer of subpod member + * @returns + */ +export async function createNestedProposal( + superProposal: { + safe: string; + to: string; + value?: string; + data?: string; + sender: string; + }, + subPod: Pod, + signer: ethers.Signer, +) { + const [{ threshold: superThreshold }, [{ nonce: superNonce }], superTxGas] = await Promise.all([ + getSafeInfo(superProposal.safe), + getSafeTransactionsBySafe(superProposal.safe, { limit: 1 }), + getGasEstimation(superProposal), + ]); + + // Data for the proposal that will be created on the superpod. + // This will be sent from the subpod + const superProposalData = { + safe: superProposal.safe, + to: superProposal.to, + value: superProposal.value || '0', + data: superProposal.data || ethers.constants.HashZero, + sender: ethers.utils.getAddress(subPod.safe), // Get the checksummed address + confirmationsRequired: superThreshold, + safeTxGas: superTxGas, + nonce: superNonce + 1, // We got the latest transaction, so add 1 to it. + operation: 0, + baseGas: 0, + gasPrice: '0', + }; + const superProposalHash = await getSafeTxHash(superProposalData); + + // Creating the sub proposal + const signerAddress = await signer.getAddress(); + try { + await createSafeTransaction( + { + safe: subPod.safe, + to: superProposal.safe, + data: encodeFunctionData('GnosisSafe', 'approveHash', [superProposalHash]), + sender: signerAddress, + }, + signer, + ); + } catch (err) { + throw new Error(`Error when creating subproposal: ${err.response.data}`); + } + + // Creating the super proposal + try { + await submitSafeTransactionToService({ + ...superProposalData, + safeTxHash: superProposalHash, + }); + } catch (err) { + throw new Error(`Error when creating superproposal: ${err.response.data}`); + } } diff --git a/src/lib/services/super-proposal.ts b/src/lib/services/super-proposal.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/services/transaction-service.ts b/src/lib/services/transaction-service.ts index febf779..ef35d0c 100644 --- a/src/lib/services/transaction-service.ts +++ b/src/lib/services/transaction-service.ts @@ -1,6 +1,7 @@ import axios from 'axios'; import { ethers, BigNumber } from 'ethers'; import { getSafeSingletonDeployment } from '@gnosis.pm/safe-deployments'; +import type Proposal from '../../Proposal'; import { config } from '../../config'; import { lookupContractAbi } from './etherscan'; import { signMessage } from '../utils'; @@ -221,6 +222,7 @@ export async function submitSafeTransactionToService( ...transaction, }); } catch (err) { + throw new Error(err.response.data); // do nothing? return null; } @@ -297,12 +299,14 @@ export async function approveSafeTransaction( /** * Creates a reject transaction on Gnosis + * If provided a Signer, then this will auto-approve the tx. */ export async function createRejectTransaction( safeTransaction: SafeTransaction, - signer: ethers.Signer, + signerOrAddress: ethers.Signer | string, ) { - const signerAddress = await signer.getAddress(); + const signerAddress = + typeof signerOrAddress === 'string' ? signerOrAddress : await signerOrAddress.getAddress(); const data = { safe: safeTransaction.safe, to: safeTransaction.safe, @@ -322,7 +326,10 @@ export async function createRejectTransaction( const safeTxHash = await getSafeTxHash(data); const createdSafeTransaction = await submitSafeTransactionToService({ safeTxHash, ...data }); - await approveSafeTransaction(createdSafeTransaction, signer); + // Approve only if it's a signer. + if (typeof signerOrAddress !== 'string') { + await approveSafeTransaction(createdSafeTransaction, signerOrAddress); + } return createdSafeTransaction; } @@ -366,3 +373,34 @@ export async function executeSafeTransaction( }, ); } + +export async function executeRejectSuperProposal( + superProposalTxHash: string, + subProposal: Proposal, + signer: ethers.Signer, +) { + const subPod = subProposal.pod; + // Fetch the superProposal + const superProposal = await getSafeTransactionByHash(superProposalTxHash); + const superPodTransactions = await getSafeTransactionsBySafe(superProposal.safe, { + nonce: superProposal.nonce, + }); + + // The super reject, i.e., the transaction that rejects the super proposal + const superReject = superPodTransactions.find( + safeTx => safeTx.data === null && safeTx.to === superProposal.safe, + ); + if (!superReject) throw new Error('Could not find corresponding superReject'); + + // The sub reject, i.e., the transaction that approves the super reject + const subPodTransactions = await getSafeTransactionsBySafe(subPod.safe, { + nonce: subProposal.id, + }); + const subReject = subPodTransactions.find( + safeTx => + safeTx.dataDecoded.method === 'approveHash' && + safeTx.dataDecoded.parameters[0].value === superReject.safeTxHash, + ); + + await executeSafeTransaction(subReject, signer); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c296268..405c655 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -5,13 +5,14 @@ import MemberToken from '@orcaprotocol/contracts/deployments/rinkeby/MemberToken import { getSafeSingletonDeployment } from '@gnosis.pm/safe-deployments'; import { config } from '../config'; +const GnosisSafe = getSafeSingletonDeployment({ version: '1.3.0' }); + // Mapping contractNames to JSONs const contractJsons = { MemberToken, + GnosisSafe, }; -const GnosisSafe = getSafeSingletonDeployment({ version: '1.3.0' }); - /** * Returns ethers contract based on name * @param contractName diff --git a/test/fixtures/index.ts b/test/fixtures/index.ts index 55a2239..928cf70 100644 --- a/test/fixtures/index.ts +++ b/test/fixtures/index.ts @@ -171,6 +171,7 @@ export const erc20TransferTransaction = { export function getSafeTransactionFixture(fetchType?: string) { if (fetchType === 'queued') return safeTransactions; + if (fetchType === 'subProposal') return subProposalFixture; // Skip first unqueued transaction. return safeTransactions.slice(1); } @@ -343,3 +344,206 @@ const safeTransactions = [ signatures: '0x0000000000000000000000001cc62ce7cb56ed99513823064295761f9b7c856e000000000000000000000000000000000000000000000000000000000000000001' } ]; + +const subProposalFixture = [ + { + safe: '0x4d3ba1AdabA15796CC3d11E48e8EC28e3A5F7C41', + to: '0xBe71ECaA104645ab78ed62A52763b2854e6DaD2E', + value: '0', + data: '0xd4d9bdcdbc9840f7f81374dd6b1f8015d1fb7a3a7c2327c23196628348a31abd968cf99c', + operation: 0, + gasToken: null, + safeTxGas: 109160, + baseGas: 0, + gasPrice: '0', + refundReceiver: null, + nonce: 38, + executionDate: '2022-04-29T17:10:59Z', + submissionDate: '2022-04-29T17:10:50.185340Z', + modified: '2022-04-29T17:11:17.390292Z', + blockNumber: 10589857, + transactionHash: '0x3ee16adba254e93315d04ef1d17db51bc8f321c2be819ce6bcf8e341dcd9b5eb', + safeTxHash: '0x3789c6928fbb0c335919c56cde2a3b4ff9a7ee8e343b514a04ddadb7a1b6e9b1', + executor: '0xf0C7d25c942264D6F21871c05d3dB3b98344b499', + isExecuted: true, + isSuccessful: true, + ethGasPrice: '7975823969', + maxFeePerGas: '14145029526', + maxPriorityFeePerGas: '1500000000', + gasUsed: 90603, + fee: '722633579063307', + origin: null, + dataDecoded: { + method: 'approveHash', + parameters: [ + { + name: 'hashToApprove', + type: 'bytes32', + value: '0xbc9840f7f81374dd6b1f8015d1fb7a3a7c2327c23196628348a31abd968cf99c' + } + ] + }, + confirmationsRequired: 1, + confirmations: [ + { + owner: '0xf0C7d25c942264D6F21871c05d3dB3b98344b499', + submissionDate: '2022-04-29T17:10:50.590010Z', + transactionHash: null, + signature: '0xd07dd94098fe82c053d18d6b22c8666817f388bfe58034d023f58b6ff6ab958f1fd5561a7ea733c9076e692a8b87247308a4175477525f0057d5997ac808fe3420', + signatureType: 'ETH_SIGN' + } + ], + trusted: true, + signatures: '0xd07dd94098fe82c053d18d6b22c8666817f388bfe58034d023f58b6ff6ab958f1fd5561a7ea733c9076e692a8b87247308a4175477525f0057d5997ac808fe3420' + }, + { + safe: '0x4d3ba1AdabA15796CC3d11E48e8EC28e3A5F7C41', + to: '0xBe71ECaA104645ab78ed62A52763b2854e6DaD2E', + value: '0', + data: '0xd4d9bdcd2e25c70fbb536eb1f64746dd0bdda89d72576dc05910053dae9afd76e481f98d', + operation: 0, + gasToken: null, + safeTxGas: 109160, + baseGas: 0, + gasPrice: '0', + refundReceiver: null, + nonce: 38, + executionDate: null, + submissionDate: '2022-04-29T17:10:42.848580Z', + modified: '2022-04-29T17:10:43.494865Z', + blockNumber: null, + transactionHash: null, + safeTxHash: '0xd7c095b5bc6a9d34295b4c620be5adef3acacd0a20c9cd45099d0ae38a27d556', + executor: null, + isExecuted: false, + isSuccessful: null, + ethGasPrice: null, + maxFeePerGas: null, + maxPriorityFeePerGas: null, + gasUsed: null, + fee: null, + origin: null, + dataDecoded: { + method: 'approveHash', + parameters: [ + { + name: 'hashToApprove', + type: 'bytes32', + value: '0x2e25c70fbb536eb1f64746dd0bdda89d72576dc05910053dae9afd76e481f98d' + } + ] + }, + confirmationsRequired: 1, + confirmations: [ + { + owner: '0xf0C7d25c942264D6F21871c05d3dB3b98344b499', + submissionDate: '2022-04-29T17:10:43.494865Z', + transactionHash: null, + signature: '0xfbcc82bf8b9fe4c708ec01f3d72f7cc9efbfade483f2fd5f9630b94fe4669c3b5696e03d2ea0b69d2b3f9ebbc2689a8891c8a267331d53eaee6b57809576c7261f', + signatureType: 'ETH_SIGN' + } + ], + trusted: true, + signatures: null + }, + { + safe: '0x4d3ba1AdabA15796CC3d11E48e8EC28e3A5F7C41', + to: '0xBe71ECaA104645ab78ed62A52763b2854e6DaD2E', + value: '0', + data: '0xd4d9bdcd2734e0ad044c85994633c20675336aa75b626e4ec6f466c95c7fa90ae2bfef00', + operation: 0, + gasToken: null, + safeTxGas: 109148, + baseGas: 0, + gasPrice: '0', + refundReceiver: null, + nonce: 37, + executionDate: '2022-04-29T16:04:16Z', + submissionDate: '2022-04-29T16:04:01.703511Z', + modified: '2022-04-29T16:05:05.938327Z', + blockNumber: 10589591, + transactionHash: '0x2c8593c35c448b3921bfc786b99b9eaace22cad064bf6e772762b9f58e6168e4', + safeTxHash: '0x12d3e2f358ae76d87726fc79aa3df4f0a51f31d19f0ef647f6cf1e7e09819884', + executor: '0xf0C7d25c942264D6F21871c05d3dB3b98344b499', + isExecuted: true, + isSuccessful: true, + ethGasPrice: '28492731350', + maxFeePerGas: '54072306826', + maxPriorityFeePerGas: '1500000000', + gasUsed: 90603, + fee: '2581526938504050', + origin: null, + dataDecoded: { + method: 'approveHash', + parameters: [ + { + name: 'hashToApprove', + type: 'bytes32', + value: '0x2734e0ad044c85994633c20675336aa75b626e4ec6f466c95c7fa90ae2bfef00' + } + ] + }, + confirmationsRequired: 1, + confirmations: [ + { + owner: '0xf0C7d25c942264D6F21871c05d3dB3b98344b499', + submissionDate: '2022-04-29T16:04:02.191133Z', + transactionHash: null, + signature: '0x0bcaf8c819b5f8ebff8cf490776e7fdff523b1c118ee9a7799ab39332e7f1e872cafb05aa829faa8012a93a62125ba0afe53f8a701a1c8f8e0de2636d362201e1f', + signatureType: 'ETH_SIGN' + } + ], + trusted: true, + signatures: '0x0bcaf8c819b5f8ebff8cf490776e7fdff523b1c118ee9a7799ab39332e7f1e872cafb05aa829faa8012a93a62125ba0afe53f8a701a1c8f8e0de2636d362201e1f' + }, + { + safe: '0x4d3ba1AdabA15796CC3d11E48e8EC28e3A5F7C41', + to: '0xBe71ECaA104645ab78ed62A52763b2854e6DaD2E', + value: '0', + data: '0xd4d9bdcdd4eb322d9d2c4dfeedc4e55a7e254a17bc46a65412adf4f69b23e251f7cce017', + operation: 0, + gasToken: null, + safeTxGas: 109160, + baseGas: 0, + gasPrice: '0', + refundReceiver: null, + nonce: 37, + executionDate: null, + submissionDate: '2022-04-29T16:02:49.935460Z', + modified: '2022-04-29T16:02:50.685863Z', + blockNumber: null, + transactionHash: null, + safeTxHash: '0x9ac037441a387edd1a25a127e538f2efb526458647297225bd0febbbc5a777e7', + executor: null, + isExecuted: false, + isSuccessful: null, + ethGasPrice: null, + maxFeePerGas: null, + maxPriorityFeePerGas: null, + gasUsed: null, + fee: null, + origin: null, + dataDecoded: { + method: 'approveHash', + parameters: [ + { + name: 'hashToApprove', + type: 'bytes32', + value: '0xd4eb322d9d2c4dfeedc4e55a7e254a17bc46a65412adf4f69b23e251f7cce017' + } + ] + }, + confirmationsRequired: 1, + confirmations: [ + { + owner: '0xf0C7d25c942264D6F21871c05d3dB3b98344b499', + submissionDate: '2022-04-29T16:02:50.685863Z', + transactionHash: null, + signature: '0x670f320e8bf16fd71ae98bd612006c5d980d010ff51e1b226adb344a7f3531050f97600858ac0ed3c4cbfc0689f2d1c989a83a8a8ac0e27675dd7fe2e0ae68d01f', + signatureType: 'ETH_SIGN' + } + ], + trusted: true, + signatures: null + }, +]; diff --git a/test/proposal.test.ts b/test/proposal.test.ts index 2397d3f..3b47dce 100644 --- a/test/proposal.test.ts +++ b/test/proposal.test.ts @@ -75,6 +75,18 @@ describe('Pod.getProposals', () => { expect(proposals.length).toBe(3); expect(mockGetSafeTransactions).toHaveBeenCalledWith('0x4d3ba1AdabA15796CC3d11E48e8EC28e3A5F7C41', { nonce_gte: 1, limit: 5 }) }); + + test('Pod.getProposals pairs sub proposal approve/rejects properly', async () => { + standardMock('subProposal'); + const pod = await getPod('0x4d3ba1AdabA15796CC3d11E48e8EC28e3A5F7C41'); + const proposals = await pod.getProposals(); + + // Expect reverse chronological + expect(proposals[0].id).toBeGreaterThan(proposals[1].id); + expect(proposals[0].timestamp.getTime()).toBeGreaterThan(proposals[1].timestamp.getTime()); + // We're taking in 4 safe transactions that should condense down to 2 Proposals (with approve + rejects) + expect(proposals.length).toBe(2); + }); }); describe('Proposal details', () => { @@ -128,7 +140,7 @@ describe('Proposal approve/reject', () => { const proposal = (await pod.getProposals())[0]; await proposal.approve(mockSigner); - console.log('proposal.safeTransaction', proposal.safeTransaction); + expect(mockApprove).toHaveBeenCalledWith(proposal.safeTransaction, mockSigner); expect(proposal.approvals).toEqual(expect.arrayContaining([userAddress])); }); diff --git a/tsconfig.json b/tsconfig.json index cdbd09f..a2869c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,6 @@ "resolveJsonModule": true, "skipLibCheck": true, }, - "include": ["src"], + "include": ["src", "scripts"], "exclude": ["./node_modules"], } From c2c9520c462b5241a9851b3034ab413f17ca0569 Mon Sep 17 00:00:00 2001 From: Will Kim Date: Wed, 4 May 2022 18:11:06 -0400 Subject: [PATCH 2/3] fix: update scripts to actually work --- README.md | 4 + scripts/approve-proposal.ts | 18 +- scripts/approve-superproposal.ts | 47 +++-- scripts/reject-proposal.ts | 20 +- scripts/reject-superproposal.ts | 50 +++-- src/Pod.ts | 24 ++- src/Proposal.ts | 35 +++- src/lib/services/create-safe-transaction.ts | 193 ++++++++++++++------ src/lib/services/transaction-service.ts | 40 ++-- test/token.test.ts | 26 +++ tsconfig.json | 2 +- 11 files changed, 331 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 533562b..6f8f145 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,7 @@ import { Pod, Proposal } from '@orcaprotocol/orca-sdk'; ### Additional Documentation Additional documentation can be found [here](https://orcaprotocol.github.io/orca-sdk/) + +## Test Scripts + +We have some test scripts to test approve/reject proposals and super proposals. They can be executed by duplicating the `env-examples.json` with private keys in `env.json`, and executing the transactions with `npx ts-node ./scripts/reject-superproposal.ts`. diff --git a/scripts/approve-proposal.ts b/scripts/approve-proposal.ts index 5271036..b7ea851 100644 --- a/scripts/approve-proposal.ts +++ b/scripts/approve-proposal.ts @@ -7,17 +7,23 @@ async function main() { // Get the pod we're working with - const adminTest = await getPod(adminPodAddress); + const pod = await getPod(adminPodAddress); + + if ((await pod.getProposals({ queued: true }))[0].status !== 'executed') { + throw new Error( + 'Super pod had an active/queued transaction. This script expects no enqueued transactions', + ); + } // We mint/burn the dummy account based on whether its a member or not. - const isMember = await adminTest.isMember(dummyAccount); + const isMember = await pod.isMember(dummyAccount); if (isMember) { - await adminTest.proposeBurnMember(dummyAccount, walletOne); + await pod.proposeBurnMember(dummyAccount, walletOne); } else { - await adminTest.proposeMintMember(dummyAccount, walletOne); + await pod.proposeMintMember(dummyAccount, walletOne); } - const proposal = (await adminTest.getProposals())[0]; + const proposal = (await pod.getProposals())[0]; await proposal.approve(walletTwo); await proposal.executeApprove(walletTwo); @@ -26,7 +32,7 @@ async function main() { await sleep(5000); if (proposal.status !== 'executed') throw new Error('Proposal status not right'); - const refetchProposal = (await adminTest.getProposals())[0]; + const refetchProposal = (await pod.getProposals())[0]; if (!refetchProposal.safeTransaction.isExecuted) throw new Error('Proposal not executed according to gnosis'); } diff --git a/scripts/approve-superproposal.ts b/scripts/approve-superproposal.ts index 280c5ea..d47f48f 100644 --- a/scripts/approve-superproposal.ts +++ b/scripts/approve-superproposal.ts @@ -1,16 +1,19 @@ +/* eslint-disable no-console */ import { getPod } from '../src'; -import { adminPodAddress, dummyAccount, subPodAddress } from '../env.json'; +import { adminPodAddress, dummyAccount, subPodAddress, subPodTwoAddress } from '../env.json'; import { setup, sleep } from './utils'; async function main() { const { walletOne, walletTwo } = setup(); - const adminPod = await getPod(adminPodAddress); + const superPod = await getPod(adminPodAddress); const subPod = await getPod(subPodAddress); + const subPodTwo = await getPod(subPodTwoAddress); if ( - (await adminPod.getProposals({ queued: true }))[0].status !== 'executed' || - (await subPod.getProposals({ queued: true }))[0].status !== 'executed' + (await superPod.getProposals({ queued: true }))[0].status !== 'executed' || + (await subPod.getProposals({ queued: true }))[0].status !== 'executed' || + (await subPodTwo.getProposals({ queued: true }))[0].status !== 'executed' ) { throw new Error( 'Admin or sub pod had an active/queued transaction. This script expects no enqueued transactions', @@ -18,35 +21,43 @@ async function main() { } // We mint/burn the dummy account based on whether its a member or not. - const isMember = await adminPod.isMember(dummyAccount); + const isMember = await superPod.isMember(dummyAccount); + console.log('Creating super proposal + sub proposal on subPod'); try { if (isMember) { - console.log('Burning'); - await adminPod.proposeBurnMemberFromPod(subPod, dummyAccount, walletOne); + await superPod.proposeBurnMemberFromPod(subPod, dummyAccount, walletOne); } else { - console.log('Minting'); - await adminPod.proposeMintMemberFromPod(subPod, dummyAccount, walletOne); + await superPod.proposeMintMemberFromPod(subPod, dummyAccount, walletOne); } } catch (err) { console.log(err); throw new Error('Error creating proposal on subpod'); } - console.log('Executing subproposal'); + await sleep(5000); + + let [superProposal] = await superPod.getProposals(); + if (superProposal.status !== 'active') { + await sleep(5000); + [superProposal] = await superPod.getProposals(); + } + + console.log('Approving super proposal from sub pod two'); + await superProposal.approveFromSubPod(subPodTwo, walletOne); + await sleep(5000); + + console.log('Executing both sub proposals'); const subProposal = (await subPod.getProposals())[0]; await subProposal.executeApprove(walletOne); - // Let the tx service catch up - console.log('Letting tx service catch up...'); + const subProposalTwo = (await subPodTwo.getProposals())[0]; + await subProposalTwo.executeApprove(walletOne); + + console.log('Letting the blockchain + transaction service catch up'); await sleep(40000); console.log('Approving + executing superproposal'); - let superProposal = (await adminPod.getProposals())[0]; - - console.log('superProposal', superProposal); - await superProposal.approve(walletOne); - // Refetch - [superProposal] = await adminPod.getProposals(); + [superProposal] = await superPod.getProposals(); await superProposal.executeApprove(walletOne); console.log('We did it! 🎉'); diff --git a/scripts/reject-proposal.ts b/scripts/reject-proposal.ts index c6bbe9d..a8119d6 100644 --- a/scripts/reject-proposal.ts +++ b/scripts/reject-proposal.ts @@ -1,21 +1,27 @@ import { getPod } from '../src'; -import { adminTestPod } from '../env.json'; +import { adminPodAddress, dummyAccount } from '../env.json'; import { setup, sleep } from './utils'; async function main() { const { walletOne, walletTwo } = setup(); - const adminTest = await getPod(adminTestPod); + const superPod = await getPod(adminPodAddress); + + if ((await superPod.getProposals({ queued: true }))[0].status !== 'executed') { + throw new Error( + 'Super pod had an active/queued transaction. This script expects no enqueued transactions', + ); + } // We mint/burn the dummy account based on whether its a member or not. - const isMember = await adminTest.isMember(dummyAccount); + const isMember = await superPod.isMember(dummyAccount); if (isMember) { - await adminTest.proposeBurnMember(dummyAccount, walletOne); + await superPod.proposeBurnMember(dummyAccount, walletOne); } else { - await adminTest.proposeMintMember(dummyAccount, walletOne); + await superPod.proposeMintMember(dummyAccount, walletOne); } - const proposal = (await adminTest.getProposals())[0]; + const proposal = (await superPod.getProposals())[0]; // await proposal.reject(walletOne); // await proposal.reject(walletTwo); @@ -25,7 +31,7 @@ async function main() { await sleep(5000); if (proposal.status !== 'executed') throw new Error('Proposal status not right'); - const refetchProposal = (await adminTest.getProposals())[0]; + const refetchProposal = (await superPod.getProposals())[0]; if (!refetchProposal.safeTransaction.isExecuted) throw new Error('Proposal not executed according to gnosis'); } diff --git a/scripts/reject-superproposal.ts b/scripts/reject-superproposal.ts index 35f4de1..f1a6cec 100644 --- a/scripts/reject-superproposal.ts +++ b/scripts/reject-superproposal.ts @@ -1,16 +1,19 @@ +/* eslint-disable no-console */ import { getPod } from '../src'; -import { adminPodAddress, dummyAccount, subPodAddress } from '../env.json'; +import { adminPodAddress, dummyAccount, subPodAddress, subPodTwoAddress } from '../env.json'; import { setup, sleep } from './utils'; async function main() { const { walletOne, walletTwo } = setup(); - const adminPod = await getPod(adminPodAddress); + const superPod = await getPod(adminPodAddress); const subPod = await getPod(subPodAddress); + const subPodTwo = await getPod(subPodTwoAddress); if ( - (await adminPod.getProposals({ queued: true }))[0].status !== 'executed' || - (await subPod.getProposals({ queued: true }))[0].status !== 'executed' + (await superPod.getProposals({ queued: true }))[0].status !== 'executed' || + (await subPod.getProposals({ queued: true }))[0].status !== 'executed' || + (await subPodTwo.getProposals({ queued: true }))[0].status !== 'executed' ) { throw new Error( 'Admin or sub pod had an active/queued transaction. This script expects no enqueued transactions', @@ -18,43 +21,52 @@ async function main() { } // We mint/burn the dummy account based on whether its a member or not. - const isMember = await adminPod.isMember(dummyAccount); - console.log('Creating '); + const isMember = await superPod.isMember(dummyAccount); + console.log('Creating super proposal'); try { if (isMember) { console.log('Burning'); - await adminPod.proposeBurnMemberFromPod(subPod, dummyAccount, walletOne); + await superPod.proposeBurnMemberFromPod(subPod, dummyAccount, walletOne); } else { console.log('Minting'); - await adminPod.proposeMintMemberFromPod(subPod, dummyAccount, walletOne); + await superPod.proposeMintMemberFromPod(subPod, dummyAccount, walletOne); } } catch (err) { console.log(err); throw new Error('Error creating proposal on subpod'); } - console.log('Rejecting the subproposal'); - const [subProposal] = await subPod.getProposals(); - // await subProposal.executeApprove(walletOne); - await subProposal.reject(walletOne); + let [superProposal] = await superPod.getProposals(); + console.log('Rejecting the first sub proposal'); + await superProposal.rejectFromSubPod(subPod, walletOne); + const [subProposal] = await subPod.getProposals(); + console.log('Executing the sub proposal reject'); await subProposal.executeReject(walletOne); + console.log('Rejecting super proposal from subPodTwo'); + [superProposal] = await superPod.getProposals(); + await superProposal.rejectFromSubPod(subPod, walletTwo); + + console.log('Creating the second sub reject'); + await superProposal.rejectFromSubPod(subPodTwo, walletOne); + await sleep(5000); + + console.log('Executing the second reject'); + const [subProposalTwo] = await subPodTwo.getProposals(); + console.log('subProposalTwo', subProposalTwo); + await subProposalTwo.executeReject(walletOne); + console.log('Letting tx service catch up...'); await sleep(40000); - console.log('Rejecting the super proposal'); - let [superProposal] = await adminPod.getProposals(); - console.log('superProposal', superProposal); - await superProposal.reject(walletOne); - await sleep(40000); - [superProposal] = await adminPod.getProposals(); + [superProposal] = await superPod.getProposals(); await superProposal.executeReject(walletOne); console.log('Rejection seems to have worked, now waiting to refetch from Gnosis'); await sleep(30000); - [superProposal] = await adminPod.getProposals(); + [superProposal] = await superPod.getProposals(); console.log(`Gnosis says the proposal status is ${superProposal.status}`); console.log('If that seems wrong, it (probably) is slow, check the pod page to be sure'); diff --git a/src/Pod.ts b/src/Pod.ts index d8edaf4..e71d9d7 100644 --- a/src/Pod.ts +++ b/src/Pod.ts @@ -12,6 +12,7 @@ import { getPreviousModule, } from './lib/utils'; import { + createRejectTransaction, getSafeInfo, getSafeTransactionsBySafe, populateDataDecoded, @@ -172,8 +173,8 @@ export default class Pod { * which can be overridden by passing { limit: 10 } for example in the options. * * By default, the first Proposal will be the active proposal. Queued proposals can be fetched - * by passing { queued: true } in the options. This will return any queued proposals, as well any proposals - * that follow (such as active or executed proposals) + * by passing { queued: true } in the options. This will return all queued and active proposals (but not + * executed proposals) * * @param options * @returns @@ -189,6 +190,7 @@ export default class Pod { const { limit = 5 } = options; // If looking for queued, then we need to only fetch current nonces. + // TODO: This is not working as intended, or, uh idk. I'm not sure what intended should be here. const params = options.queued ? { nonce_gte: nonce, limit } : { limit }; const safeTransactions = await Promise.all( @@ -702,6 +704,7 @@ export default class Pod { const { address: memberTokenAddress } = getContract('MemberToken', signer); try { // Create a safe transaction on this pod, sent from the admin pod + // TODO: Gotta update to make this work. await createSafeTransaction( { sender: subPod.safe, @@ -880,4 +883,21 @@ export default class Pod { throw new Error(err); } }; + + /** + * Creates a reject proposal at a given nonce, mostly used to un-stuck the transaction queue + * @param nonce - nonce to create the reject transaction at + * @param signer - Signer or address of pod member + */ + createRejectProposal = async (nonce: number, signer: ethers.Signer | string) => { + await createRejectTransaction( + { + safe: this.safe, + to: this.safe, + nonce, + confirmationsRequired: this.threshold, + }, + signer, + ); + }; } diff --git a/src/Proposal.ts b/src/Proposal.ts index 6f2b44d..76acfc1 100644 --- a/src/Proposal.ts +++ b/src/Proposal.ts @@ -4,9 +4,10 @@ import { approveSafeTransaction, createRejectTransaction, executeSafeTransaction, - rejectSuperProposal, + executeRejectSuperProposal, SafeTransaction, } from './lib/services/transaction-service'; +import { approveSuperProposal, rejectSuperProposal } from './lib/services/create-safe-transaction'; import { checkAddress } from './lib/utils'; export type ProposalStatus = 'active' | 'executed' | 'queued'; @@ -143,12 +144,6 @@ export default class Proposal { reject = async (signer: ethers.Signer) => { const signerAddress = checkAddress(await signer.getAddress()); - if (this.isSubProposal) { - // parameters[0].value is the superProposalTxHash - await rejectSuperProposal(this.parameters[0].value, this, signer); - return; - } - if (this.rejections.includes(signerAddress)) { throw new Error('Signer has already rejected this proposal'); } @@ -169,6 +164,32 @@ export default class Proposal { this.rejections.push(signerAddress); }; + /** + * Approves a super proposal from a sub pod. This creates a sub proposal if one does not exist. + * @param subPod - Pod to approve from + * @param signer - Signer of sub pod member + */ + approveFromSubPod = async (subPod: Pod, signer: ethers.Signer) => { + const sender = await signer.getAddress(); + if (!(await this.pod.isMember(subPod.safe))) { + throw new Error(`${subPod.ensName} is not a sub pod of ${this.pod.ensName}`); + } + await approveSuperProposal({ sender, ...this.safeTransaction }, subPod, signer); + }; + + /** + * Rejects a super proposal from a sub pod. This creates a sub proposal if one does not exist. + * @param subPod - Pod to reject from + * @param signer - Signer of sub pod member + */ + rejectFromSubPod = async (subPod: Pod, signer: ethers.Signer) => { + const sender = await signer.getAddress(); + if (!(await this.pod.isMember(subPod.safe))) { + throw new Error(`${subPod.ensName} is not a sub pod of ${this.pod.ensName}`); + } + await rejectSuperProposal({ sender, ...this.safeTransaction }, subPod, signer); + }; + /** * Executes proposal * @param signer - Signer of pod member diff --git a/src/lib/services/create-safe-transaction.ts b/src/lib/services/create-safe-transaction.ts index 7e4e5fd..dc477ca 100644 --- a/src/lib/services/create-safe-transaction.ts +++ b/src/lib/services/create-safe-transaction.ts @@ -1,14 +1,13 @@ import { ethers } from 'ethers'; import type Pod from '../../Pod'; -import type Proposal from '../../Proposal'; import { + SafeTransaction, getSafeInfo, getGasEstimation, getSafeTxHash, submitSafeTransactionToService, getSafeTransactionsBySafe, approveSafeTransaction, - getSafeTransactionByHash, createRejectTransaction, } from './transaction-service'; import { encodeFunctionData } from '../utils'; @@ -77,58 +76,6 @@ export async function createSafeTransaction( return createdSafeTransaction; } -export async function rejectSuperProposal( - superProposalTxHash: string, - subProposal: Proposal, - signer: ethers.Signer, -) { - const subPod = subProposal.pod; - // Fetch the superProposal - const superProposal = await getSafeTransactionByHash(superProposalTxHash); - const superPodTransactions = await getSafeTransactionsBySafe(superProposal.safe, { - nonce: superProposal.nonce, - }); - - // The super reject, i.e., the transaction that rejects the super proposal - let superReject = superPodTransactions.find( - safeTx => safeTx.data === null && safeTx.to === superProposal.safe, - ); - // Need to create a super reject ourselves. This is a standard Gnosis reject. - if (!superReject) { - // This will not sign the transaction because we're just passing the pod safe. - superReject = await createRejectTransaction(superProposal, subPod.safe); - } - - // The sub reject, i.e., the transaction that approves the super reject - const subPodTransactions = await getSafeTransactionsBySafe(subPod.safe, { - nonce: subProposal.id, - }); - let subReject = subPodTransactions.find( - safeTx => - safeTx.dataDecoded.method === 'approveHash' && - safeTx.dataDecoded.parameters[0].value === superReject.safeTxHash, - ); - - // Need to create the sub reject. This is _not_ a standard Gnosis reject - // Instead, we need to approve the super reject proposal. - if (!subReject) { - // This will create + approve the sub reject - subReject = await createSafeTransaction( - { - safe: subPod.safe, - to: superProposal.safe, - data: encodeFunctionData('GnosisSafe', 'approveHash', [superReject.safeTxHash]), - sender: await signer.getAddress(), - nonce: subProposal.id, - }, - signer, - ); - } else { - // Just need to approve subReject - await approveSafeTransaction(subReject, signer); - } -} - /** * Creates a nested proposal (i.e., a proposal on a subpod to perform an action to the superpod) * @param superProposal @@ -200,3 +147,141 @@ export async function createNestedProposal( throw new Error(`Error when creating superproposal: ${err.response.data}`); } } + +/** + * Creates and approves a sub proposal to approve a super proposal + * @param superProposal + * @param subPod + * @param signer + */ +export async function approveSuperProposal( + superProposal: SafeTransaction, + subPod: Pod, + signer: ethers.Signer, +) { + const signerAddress = await signer.getAddress(); + if (!(await subPod.isMember(signerAddress))) { + throw new Error(`Signer was not a member of subpod ${subPod.ensName}`); + } + + // TODO: There's an (unlikely) chance that we might fail to get all queued/active proposals + // Need to handle that down the line. + const subPodProposals = await subPod.getProposals({ queued: true, limit: 10 }); + + // Look for existing sub proposal + const subProposal = subPodProposals.find( + proposal => + proposal.method === 'approveHash' && + proposal.parameters[0].value === superProposal.safeTxHash && + proposal.status !== 'executed', + ); + if (subProposal) { + if (subProposal.approvals.includes(signerAddress)) + throw new Error('Signer already approved sub proposal'); + try { + // Approve the existing sub proposal + await approveSafeTransaction(subProposal.safeTransaction, signer); + } catch (err) { + throw new Error(`Error when approving sub proposal: ${err}`); + } + return; + } + + // Otherwise, we have to create the sub proposal + try { + // createSafeTransaction also approves the transaction. + await createSafeTransaction( + { + safe: subPod.safe, + to: superProposal.safe, + data: encodeFunctionData('GnosisSafe', 'approveHash', [superProposal.safeTxHash]), + sender: signerAddress, + }, + signer, + ); + } catch (err) { + throw new Error(`Error when creating sub proposal: ${err.response.data}`); + } +} + +/** + * Rejects a super proposal + * + * Super proposal rejections, from the sub proposal point of view, are separate approveHash calls that approve + * a rejection transaction on the super pod + * + * @param superProposalTxHash - The transaction hash that identifies the original super proposal (i.e., not the rejection super proposal) + * @param subProposal - The sub proposal related to the superProposalTxHash + * @param signer - Signer of sub pod member + */ +export async function rejectSuperProposal( + superProposal: SafeTransaction, + subPod: Pod, + signer: ethers.Signer, +) { + const superPodTransactions = await getSafeTransactionsBySafe(superProposal.safe, { + nonce: superProposal.nonce, + }); + + // The super reject, i.e., the transaction that rejects the super proposal + let superReject = superPodTransactions.find( + safeTx => safeTx.data === null && safeTx.to === superProposal.safe, + ); + if (!superReject) { + // No such tx, we have to create ourselves. This is a standard Gnosis reject + // This will not sign the transaction because we're just passing the pod safe. + superReject = await createRejectTransaction(superProposal, subPod.safe); + } + + const signerAddress = await signer.getAddress(); + + // TODO: There's an (unlikely) chance that we might fail to get all queued/active proposals + // Need to handle that down the line. + const subPodProposals = await subPod.getProposals({ queued: true, limit: 10 }); + const subReject = subPodProposals.find( + proposal => + proposal.method === 'approveHash' && + // Looking for an approveHash that is NOT for the super proposal we have, that should be the reject + // TODO: This theoretically fails if there are two unrelated super proposals being voted on simultaneously. + proposal.parameters[0].value !== superProposal.safeTxHash && + proposal.status !== 'executed', + ); + // If subReject exists, we can just approve it and end the call. + if (subReject) { + if (subReject.approvals.includes(signerAddress)) + throw new Error('Signer already approved sub proposal'); + try { + // Approve the existing sub proposal + await approveSafeTransaction(subReject.safeTransaction, signer); + return; + } catch (err) { + throw new Error(`Error when approving sub proposal: ${err}`); + } + } + + // If sub reject does not exist, we need to create it. + // Find the matching sub approve so we know what nonce to use. + const subApprove = subPodProposals.find( + proposal => + proposal.method === 'approveHash' && + // Looking for an approveHash that is NOT for the super proposal we have, that should be the reject + // TODO: This theoretically fails if there are two unrelated super proposals being voted on simultaneously. + proposal.parameters[0].value === superProposal.safeTxHash && + proposal.status !== 'executed', + ); + + // Create the sub reject. This is _not_ a standard Gnosis reject + // Instead, we need to create a sub proposal that approves the super reject proposal. + await createSafeTransaction( + { + safe: subPod.safe, + to: superProposal.safe, + data: encodeFunctionData('GnosisSafe', 'approveHash', [superReject.safeTxHash]), + sender: await signer.getAddress(), + // If the sub approve exists, use the same nonce. Otherwise createSafeTransaction will auto-populate + // the appropriate nonce if we pass null. + nonce: subApprove ? subApprove.id : null, + }, + signer, + ); +} diff --git a/src/lib/services/transaction-service.ts b/src/lib/services/transaction-service.ts index ef35d0c..4c4d4c3 100644 --- a/src/lib/services/transaction-service.ts +++ b/src/lib/services/transaction-service.ts @@ -300,6 +300,8 @@ export async function approveSafeTransaction( /** * Creates a reject transaction on Gnosis * If provided a Signer, then this will auto-approve the tx. + * @param safeTransaction + * @param signerOrAddress - If provided a signer, it will approve. Address or signer must be safe owner. */ export async function createRejectTransaction( safeTransaction: SafeTransaction, @@ -374,32 +376,42 @@ export async function executeSafeTransaction( ); } +/** + * Executes a super proposal rejection + * @param superProposalTxHash - Transaction hash of the original super proposal (not the reject super proposal) + * @param subProposal - Proposal related to the superProposalTxHash + * @param signer - Signer of sub proposal member + */ export async function executeRejectSuperProposal( superProposalTxHash: string, subProposal: Proposal, signer: ethers.Signer, ) { - const subPod = subProposal.pod; - // Fetch the superProposal const superProposal = await getSafeTransactionByHash(superProposalTxHash); - const superPodTransactions = await getSafeTransactionsBySafe(superProposal.safe, { - nonce: superProposal.nonce, - }); + const subPod = subProposal.pod; - // The super reject, i.e., the transaction that rejects the super proposal - const superReject = superPodTransactions.find( + const [superPodTransactions, subPodTransactions] = await Promise.all([ + getSafeTransactionsBySafe(superProposal.safe, { + nonce: superProposal.nonce, + }), + getSafeTransactionsBySafe(subPod.safe, { + nonce: subProposal.id, + }), + ]); + + // The super reject, i.e., the standard gnosis reject that rejects the super proposal + let superReject = superPodTransactions.find( safeTx => safeTx.data === null && safeTx.to === superProposal.safe, ); - if (!superReject) throw new Error('Could not find corresponding superReject'); + if (!superReject) { + superReject = await createRejectTransaction(superProposal, subProposal.pod.safe); + } - // The sub reject, i.e., the transaction that approves the super reject - const subPodTransactions = await getSafeTransactionsBySafe(subPod.safe, { - nonce: subProposal.id, - }); + // The sub reject, i.e., the sub pod transaction that approves the super reject const subReject = subPodTransactions.find( safeTx => - safeTx.dataDecoded.method === 'approveHash' && - safeTx.dataDecoded.parameters[0].value === superReject.safeTxHash, + safeTx?.dataDecoded?.method === 'approveHash' && + safeTx?.dataDecoded?.parameters[0].value === superReject.safeTxHash, ); await executeSafeTransaction(subReject, signer); diff --git a/test/token.test.ts b/test/token.test.ts index 8710d14..24f27dc 100644 --- a/test/token.test.ts +++ b/test/token.test.ts @@ -211,7 +211,11 @@ describe('proposeMintMemberFromPod', () => { // This should be a member of admin pod. getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), }; +<<<<<<< HEAD const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); +======= + const createSafeTx = jest.spyOn(txService, 'createNestedProposal').mockReturnValueOnce({}); +>>>>>>> fc47577 (fix: update scripts to actually work) const adminPod = await sdk.getPod(orcanautAddress); // subPod is member of adminPod @@ -226,6 +230,7 @@ describe('proposeMintMemberFromPod', () => { to: memberTokenAddress, data: '0x94d008ef0000000000000000000000001cc62ce7cb56ed99513823064295761f9b7c856e0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', }, +<<<<<<< HEAD mockSigner, ); }); @@ -270,6 +275,9 @@ describe('proposeMintMemberFromPod', () => { to: memberTokenAddress, data: '0x94d008ef0000000000000000000000001cc62ce7cb56ed99513823064295761f9b7c856e0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', }, +======= + adminPod, +>>>>>>> fc47577 (fix: update scripts to actually work) mockSigner, ); }); @@ -280,7 +288,11 @@ describe('proposeMintMemberFromPod', () => { // This should be a member of admin pod. getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), }; +<<<<<<< HEAD const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); +======= + const createSafeTx = jest.spyOn(txService, 'createNestedProposal').mockReturnValueOnce({}); +>>>>>>> fc47577 (fix: update scripts to actually work) const parentPod = await sdk.getPod(orcanautAddress); // subPod is member of adminPod @@ -295,6 +307,7 @@ describe('proposeMintMemberFromPod', () => { to: memberTokenAddress, data: '0x94d008ef0000000000000000000000001cc62ce7cb56ed99513823064295761f9b7c856e0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', }, + subPod, mockSigner, ); }); @@ -390,7 +403,11 @@ describe('proposeBurnMemberFromPod', () => { // This should be a member of admin pod. getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), }; +<<<<<<< HEAD const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); +======= + const createSafeTx = jest.spyOn(txService, 'createNestedProposal').mockReturnValueOnce({}); +>>>>>>> fc47577 (fix: update scripts to actually work) const adminPod = await sdk.getPod(orcanautAddress); // subPod is member of adminPod @@ -405,6 +422,7 @@ describe('proposeBurnMemberFromPod', () => { to: memberTokenAddress, data: '0x9dc29fac000000000000000000000000094a473985464098b59660b37162a284b51327530000000000000000000000000000000000000000000000000000000000000006', }, +<<<<<<< HEAD mockSigner, ); }); @@ -446,6 +464,9 @@ describe('proposeBurnMemberFromPod', () => { to: memberTokenAddress, data: '0x9dc29fac000000000000000000000000094a473985464098b59660b37162a284b51327530000000000000000000000000000000000000000000000000000000000000006', }, +======= + adminPod, +>>>>>>> fc47577 (fix: update scripts to actually work) mockSigner, ); }); @@ -456,7 +477,11 @@ describe('proposeBurnMemberFromPod', () => { // This should be a member of admin pod. getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), }; +<<<<<<< HEAD const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); +======= + const createSafeTx = jest.spyOn(txService, 'createNestedProposal').mockReturnValueOnce({}); +>>>>>>> fc47577 (fix: update scripts to actually work) const parentPod = await sdk.getPod(orcanautAddress); // subPod is member of adminPod @@ -471,6 +496,7 @@ describe('proposeBurnMemberFromPod', () => { to: memberTokenAddress, data: '0x9dc29fac000000000000000000000000094a473985464098b59660b37162a284b51327530000000000000000000000000000000000000000000000000000000000000001', }, + subPod, mockSigner, ); }); diff --git a/tsconfig.json b/tsconfig.json index a2869c9..cdbd09f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,6 @@ "resolveJsonModule": true, "skipLibCheck": true, }, - "include": ["src", "scripts"], + "include": ["src"], "exclude": ["./node_modules"], } From 6d297dbb2a3790d28abca675a2d95b1679032001 Mon Sep 17 00:00:00 2001 From: Will Kim Date: Mon, 9 May 2022 16:55:47 -0400 Subject: [PATCH 3/3] refactor: rearrange some items, rebase --- .eslintignore | 1 + test/token.test.ts | 116 ++------------------------------------------- 2 files changed, 5 insertions(+), 112 deletions(-) diff --git a/.eslintignore b/.eslintignore index d2fc2db..aca4096 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,4 @@ test dist jest.config.ts docs +scripts diff --git a/test/token.test.ts b/test/token.test.ts index 24f27dc..4e98150 100644 --- a/test/token.test.ts +++ b/test/token.test.ts @@ -5,7 +5,6 @@ import contracts from '@orcaprotocol/contracts'; import * as sdk from '../src'; import * as utils from '../src/lib/utils'; import * as fetchers from '../src/fetchers'; -import * as txService from '../src/lib/services/transaction-service'; import * as createSafe from '../src/lib/services/create-safe-transaction'; import { artNautPod, @@ -211,11 +210,7 @@ describe('proposeMintMemberFromPod', () => { // This should be a member of admin pod. getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), }; -<<<<<<< HEAD - const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); -======= - const createSafeTx = jest.spyOn(txService, 'createNestedProposal').mockReturnValueOnce({}); ->>>>>>> fc47577 (fix: update scripts to actually work) + const createSafeTx = jest.spyOn(createSafe, 'createNestedProposal').mockReturnValueOnce({}); const adminPod = await sdk.getPod(orcanautAddress); // subPod is member of adminPod @@ -230,54 +225,7 @@ describe('proposeMintMemberFromPod', () => { to: memberTokenAddress, data: '0x94d008ef0000000000000000000000001cc62ce7cb56ed99513823064295761f9b7c856e0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', }, -<<<<<<< HEAD - mockSigner, - ); - }); - - test('The function should also accept podIds or safe addresses in lieu of a Pod object', async () => { - jest.spyOn(fetchers, 'getPodFetchersByAddressOrEns').mockResolvedValueOnce({ - // Mock artNaut admin to be orcanaut pod. - Controller: { podAdmin: jest.fn().mockResolvedValue(orcanautPod.safe) }, - safe: artNautPod.safe, - podId: artNautPod.id, - Name: { name: artNautPod.ensName }, - }).mockResolvedValueOnce({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Controller: { podAdmin: jest.fn().mockResolvedValue(orcanautPod.admin) }, - safe: orcanautAddress, - podId: orcanautPod.id, - Name: { name: orcanautPod.ensName }, - }); - jest.spyOn(utils, 'getContract').mockReturnValueOnce({ - address: memberTokenAddress, - }); - jest.spyOn(axios, 'post') - .mockResolvedValueOnce(constructGqlGetUsers(orcanautPod.members)) - .mockResolvedValueOnce(constructGqlGetUsers(artNautPod.members)); - - const mockSigner = { - // This should be a member of admin pod. - getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), - }; - const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); - - // subPod is member of adminPod - const subPod = await sdk.getPod('art-naut.pod.xyz'); - - // Creates a proposal on the admin pod to mint a new member to subPod using admin privileges. - await subPod.proposeMintMemberFromPod(orcanautPod.id, userAddress2, mockSigner); - expect(createSafeTx).toHaveBeenCalledWith( - { - sender: orcanautPod.safe, - safe: subPod.safe, - to: memberTokenAddress, - data: '0x94d008ef0000000000000000000000001cc62ce7cb56ed99513823064295761f9b7c856e0000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000', - }, -======= adminPod, ->>>>>>> fc47577 (fix: update scripts to actually work) mockSigner, ); }); @@ -288,11 +236,7 @@ describe('proposeMintMemberFromPod', () => { // This should be a member of admin pod. getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), }; -<<<<<<< HEAD - const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); -======= - const createSafeTx = jest.spyOn(txService, 'createNestedProposal').mockReturnValueOnce({}); ->>>>>>> fc47577 (fix: update scripts to actually work) + const createSafeTx = jest.spyOn(createSafe, 'createNestedProposal').mockReturnValueOnce({}); const parentPod = await sdk.getPod(orcanautAddress); // subPod is member of adminPod @@ -403,11 +347,7 @@ describe('proposeBurnMemberFromPod', () => { // This should be a member of admin pod. getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), }; -<<<<<<< HEAD - const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); -======= - const createSafeTx = jest.spyOn(txService, 'createNestedProposal').mockReturnValueOnce({}); ->>>>>>> fc47577 (fix: update scripts to actually work) + const createSafeTx = jest.spyOn(createSafe, 'createNestedProposal').mockReturnValueOnce({}); const adminPod = await sdk.getPod(orcanautAddress); // subPod is member of adminPod @@ -422,51 +362,7 @@ describe('proposeBurnMemberFromPod', () => { to: memberTokenAddress, data: '0x9dc29fac000000000000000000000000094a473985464098b59660b37162a284b51327530000000000000000000000000000000000000000000000000000000000000006', }, -<<<<<<< HEAD - mockSigner, - ); - }); - - test('The function should also accept podIds or safe addresses in lieu of a Pod object', async () => { - jest.spyOn(fetchers, 'getPodFetchersByAddressOrEns').mockResolvedValueOnce({ - // Mock artNaut admin to be orcanaut pod. - Controller: { podAdmin: jest.fn().mockResolvedValue(orcanautPod.safe) }, - safe: artNautPod.safe, - podId: artNautPod.id, - Name: { name: artNautPod.ensName }, - }).mockResolvedValueOnce({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Controller: { podAdmin: jest.fn().mockResolvedValue(orcanautPod.admin) }, - safe: orcanautAddress, - podId: orcanautPod.id, - Name: { name: orcanautPod.ensName }, - }); - jest.spyOn(utils, 'getContract').mockReturnValueOnce({ - address: memberTokenAddress, - }); - - const mockSigner = { - // This should be a member of admin pod. - getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), - }; - const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); - - // subPod is member of adminPod - const subPod = await sdk.getPod('art-naut.pod.xyz'); - - // Creates a proposal on the admin pod to mint a new member to subPod using admin privileges. - await subPod.proposeBurnMemberFromPod(orcanautPod.id, artNautPod.members[0], mockSigner); - expect(createSafeTx).toHaveBeenCalledWith( - { - sender: orcanautPod.safe, - safe: subPod.safe, - to: memberTokenAddress, - data: '0x9dc29fac000000000000000000000000094a473985464098b59660b37162a284b51327530000000000000000000000000000000000000000000000000000000000000006', - }, -======= adminPod, ->>>>>>> fc47577 (fix: update scripts to actually work) mockSigner, ); }); @@ -477,11 +373,7 @@ describe('proposeBurnMemberFromPod', () => { // This should be a member of admin pod. getAddress: jest.fn().mockResolvedValueOnce(orcanautPod.members[0]), }; -<<<<<<< HEAD - const createSafeTx = jest.spyOn(createSafe, 'createSafeTransaction').mockReturnValueOnce({}); -======= - const createSafeTx = jest.spyOn(txService, 'createNestedProposal').mockReturnValueOnce({}); ->>>>>>> fc47577 (fix: update scripts to actually work) + const createSafeTx = jest.spyOn(createSafe, 'createNestedProposal').mockReturnValueOnce({}); const parentPod = await sdk.getPod(orcanautAddress); // subPod is member of adminPod