Skip to content

Commit 333b07b

Browse files
author
jj
authored
Adding gnosis safe multisend functionality to owner (Synthetixio#1513)
* Adding safe functionality from the gnosis SDK to the owner command * Updating nominate command to use owner actions when not the owner
1 parent c53070d commit 333b07b

File tree

7 files changed

+488
-470
lines changed

7 files changed

+488
-470
lines changed

package-lock.json

+117
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
"@eth-optimism/hardhat-ovm": "0.2.2",
7979
"@eth-optimism/smock": "1.1.9",
8080
"@eth-optimism/solc": "0.5.16-alpha.7",
81+
"@gnosis.pm/safe-core-sdk": "~0.3.1",
82+
"@gnosis.pm/safe-service-client": "~0.1.1",
8183
"@nomiclabs/hardhat-ethers": "^2.0.2",
8284
"@nomiclabs/hardhat-truffle5": "2.0.0",
8385
"@nomiclabs/hardhat-web3": "2.0.0",

publish/src/SafeBatchSubmitter.js

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use strict';
2+
3+
const ethers = require('ethers');
4+
const { EthersAdapter } = require('@gnosis.pm/safe-core-sdk');
5+
const GnosisSafe = require('@gnosis.pm/safe-core-sdk').default;
6+
const SafeServiceClient = require('@gnosis.pm/safe-service-client').default;
7+
8+
class SafeBatchSubmitter {
9+
constructor({ network, signer, safeAddress }) {
10+
this.network = network;
11+
this.signer = signer;
12+
this.safeAddress = safeAddress;
13+
14+
this.ethAdapter = new EthersAdapter({
15+
ethers,
16+
signer,
17+
});
18+
19+
this.service = new SafeServiceClient(
20+
`https://safe-transaction${network === 'rinkeby' ? '.rinkeby' : ''}.gnosis.io`
21+
);
22+
}
23+
24+
async init() {
25+
const { ethAdapter, service, safeAddress, signer } = this;
26+
this.transactions = [];
27+
this.safe = await GnosisSafe.create({
28+
ethAdapter,
29+
safeAddress,
30+
});
31+
// check if signer is on the list of owners
32+
if (!(await this.safe.isOwner(signer.address))) {
33+
throw Error(`Account ${signer.address} is not a signer on this safe`);
34+
}
35+
const currentNonce = await this.safe.getNonce();
36+
const pendingTxns = await service.getPendingTransactions(safeAddress, currentNonce);
37+
return { currentNonce, pendingTxns };
38+
}
39+
40+
async appendTransaction({ to, value = '0', data, force }) {
41+
const { safe, service, safeAddress, transactions } = this;
42+
if (!force) {
43+
// check it does not exist in the pending list
44+
// Note: this means that a duplicate transaction - like an acceptOwnership on
45+
// the same contract cannot be added in one batch. This could be useful in situations
46+
// where you want to accept, nominate another owner, migrate, then accept again.
47+
// In these cases, use "force: true"
48+
const currentNonce = await safe.getNonce();
49+
const pendingTxns = await service.getPendingTransactions(safeAddress, currentNonce);
50+
51+
this.currentNonce = currentNonce;
52+
this.pendingTxns = pendingTxns;
53+
54+
this.unusedNoncePosition = currentNonce;
55+
56+
let matchedTxnIsPending = false;
57+
58+
for (const {
59+
nonce,
60+
dataDecoded: {
61+
parameters: [{ valueDecoded }],
62+
},
63+
} of pendingTxns.results) {
64+
// figure out what the next unused nonce position is (including everything else in the queue)
65+
this.unusedNoncePosition = Math.max(this.unusedNoncePosition, nonce + 1);
66+
matchedTxnIsPending =
67+
matchedTxnIsPending ||
68+
valueDecoded.find(
69+
entry => entry.to === to && entry.data === data && entry.value === value
70+
);
71+
}
72+
73+
if (matchedTxnIsPending) {
74+
return {};
75+
}
76+
}
77+
78+
transactions.push({ to, value, data, nonce: this.unusedNoncePosition });
79+
return { appended: true };
80+
}
81+
82+
async submit() {
83+
const { safe, transactions, safeAddress, service, unusedNoncePosition: nonce } = this;
84+
if (!safe) {
85+
throw Error('Safe must first be initialized');
86+
}
87+
if (!transactions.length) {
88+
return { transactions };
89+
}
90+
const batchTxn = await safe.createTransaction(...transactions);
91+
const txHash = await safe.getTransactionHash(batchTxn);
92+
const signature = await safe.signTransactionHash(txHash);
93+
94+
try {
95+
await service.proposeTransaction(safeAddress, batchTxn.data, txHash, signature);
96+
97+
return { transactions, nonce };
98+
} catch (err) {
99+
throw Error(`Error trying to submit batch to safe.\n${err}`);
100+
}
101+
}
102+
}
103+
104+
module.exports = SafeBatchSubmitter;

publish/src/commands/nominate.js

+27-18
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const ethers = require('ethers');
4-
const { gray, yellow, red, cyan } = require('chalk');
4+
const { gray, yellow, cyan } = require('chalk');
55

66
const {
77
getUsers,
@@ -17,6 +17,8 @@ const {
1717
confirmAction,
1818
} = require('../util');
1919

20+
const { performTransactionalStep } = require('../command-utils/transact');
21+
2022
const DEFAULTS = {
2123
gasPrice: '15',
2224
gasLimit: 2e5, // 200,000
@@ -44,29 +46,31 @@ const nominate = async ({
4446
}
4547

4648
if (!newOwner || !ethers.utils.isAddress(newOwner)) {
47-
console.error(red('Invalid new owner to nominate. Please check the option and try again.'));
48-
process.exit(1);
49+
throw Error('Invalid new owner to nominate. Please check the option and try again.');
4950
} else {
5051
newOwner = newOwner.toLowerCase();
5152
}
5253

53-
const { config, deployment } = loadAndCheckRequiredSources({
54+
const { config, deployment, ownerActions, ownerActionsFile } = loadAndCheckRequiredSources({
5455
deploymentPath,
5556
network,
5657
});
5758

5859
contracts.forEach(contract => {
5960
if (!(contract in config)) {
60-
console.error(red(`Contract ${contract} isn't in the config for this deployment!`));
61-
process.exit(1);
61+
throw Error(`Contract ${contract} isn't in the config for this deployment!`);
6262
}
6363
});
6464
if (!contracts.length) {
6565
// if contracts not supplied, use all contracts except the DappMaintenance (UI control)
6666
contracts = Object.keys(config).filter(contract => contract !== 'DappMaintenance');
6767
}
6868

69-
const { providerUrl: envProviderUrl, privateKey: envPrivateKey } = loadConnections({
69+
const {
70+
providerUrl: envProviderUrl,
71+
privateKey: envPrivateKey,
72+
explorerLinkPrefix,
73+
} = loadConnections({
7074
network,
7175
useFork,
7276
});
@@ -137,24 +141,29 @@ const nominate = async ({
137141

138142
console.log(
139143
gray(
140-
`${contract} current owner is ${currentOwner}.\nCurrent nominated owner is ${nominatedOwner}.`
144+
`${yellow(contract)} current owner is ${yellow(
145+
currentOwner
146+
)}.\nCurrent nominated owner is ${yellow(nominatedOwner)}.`
141147
)
142148
);
143-
if (signerAddress.toLowerCase() !== currentOwner) {
144-
console.log(cyan(`Cannot nominateNewOwner for ${contract} as you aren't the owner!`));
145-
} else if (currentOwner !== newOwner && nominatedOwner !== newOwner) {
149+
if (currentOwner !== newOwner && nominatedOwner !== newOwner) {
146150
// check for legacy function
147151
const nominationFnc =
148152
'nominateOwner' in deployedContract ? 'nominateOwner' : 'nominateNewOwner';
149153

150-
console.log(yellow(`Invoking ${contract}.${nominationFnc}(${newOwner})`));
151-
const overrides = {
154+
await performTransactionalStep({
155+
contract,
156+
encodeABI: network === 'mainnet',
157+
explorerLinkPrefix,
152158
gasLimit,
153-
gasPrice: ethers.utils.parseUnits(gasPrice, 'gwei'),
154-
};
155-
156-
const tx = await deployedContract[nominationFnc](newOwner, overrides);
157-
await tx.wait();
159+
gasPrice,
160+
ownerActions,
161+
ownerActionsFile,
162+
signer: wallet,
163+
target: address,
164+
write: nominationFnc,
165+
writeArg: newOwner, // explicitly pass array of args so array not splat as params
166+
});
158167
} else {
159168
console.log(gray('No change required.'));
160169
}

0 commit comments

Comments
 (0)