Skip to content

Commit

Permalink
Merge pull request #5 from gnosis/safe-integration
Browse files Browse the repository at this point in the history
Safe integration
  • Loading branch information
cag authored Jan 7, 2020
2 parents d4af108 + 1dcbfac commit 79e7622
Show file tree
Hide file tree
Showing 4 changed files with 446 additions and 114 deletions.
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ const cpk = await CPK.create({

Please refer to the `migrations/` folder of this package for information on how to deploy the required contracts on a network, and note that these addresses must be available for the connected network in order for *CPK* creation to be successful.

### CPK#getOwnerAccount

This may be used to figure out which account the proxy considers the owner account. It returns a Promise which resolves to the owner account:

```js
const ownerAccount = await cpk.getOwnerAccount()
```

### CPK#address

Once created, the `address` property on a *CPK* instance will provide the proxy's checksummed Ethereum address:
Expand All @@ -91,6 +99,17 @@ Once created, the `address` property on a *CPK* instance will provide the proxy'

This address is calculated even if the proxy has not been deployed yet, and it is deterministically generated from the proxy owner address.

#### Support for WalletConnected Gnosis Safe

If the provider underlying the *CPK* instance is connected to a Gnosis Safe via WalletConnect, the address will match the owner account:

```js
const ownerAccount = await cpk.getOwnerAccount()
cpk.address === ownerAccount // this will be true in that case
```

*CPK* will use the Safe's native support for batching transactions, and will not create an additional proxy contract account.

### CPK#execTransactions

To execute transactions using a *CPK* instance, call `execTransactions` with an *Array* of transactions to execute. If the proxy has not been deployed, this will also batch the proxy's deployment into the transaction. Multiple transactions will be batched and executed together if the proxy has been deployed.
Expand Down Expand Up @@ -155,7 +174,7 @@ const { promiEvent, receipt } = await cpk.execTransactions([
Suppose instead `erc20` and `exchange` are Truffle contract abstraction instances instead. Since Truffle contract abstraction instances contain a reference to an underlying *web3.eth.Contract* instance, they may be used in a similar manner:

```js
const { promiEvent, receipt } = await cpk.execTransactions([
const { promiEvent, hash } = await cpk.execTransactions([
{
operation: CPK.CALL,
to: erc20.address,
Expand Down Expand Up @@ -183,7 +202,7 @@ const { promiEvent, receipt } = await cpk.execTransactions([
Similarly to the example in the previous section, suppose that `erc20` is a *ethers.Contract* instance for an ERC20 token for which the proxy account holds a balance, and `exchange` is a *ethers.Contract* instance of an exchange contract with an deposit requirement, where calling the deposit function on the exchange requires an allowance for the exchange by the depositor. Batching these transactions may be done like so:

```js
const { transactionResponse, transactionReceipt } = await cpk.execTransactions([
const { transactionResponse, hash } = await cpk.execTransactions([
{
operation: CPK.CALL,
to: erc20.address,
Expand Down Expand Up @@ -228,3 +247,24 @@ const txObject = await cpk.execTransactions(
{ gasPrice: `${3e9}` },
);
```

#### Support for WalletConnected Gnosis Safe

When WalletConnected to Gnosis Safe, `execTransactions` will use the Safe's native support for sending batch transactions (via `gs_multi_send`). In this case, the gas price option is not available, and `execTransactions` will only return a transaction hash.

```js
const { hash } = await cpk.execTransactions([
{
operation: CPK.CALL,
to: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1',
value: `${1e18}`,
data: '0x',
},
{
operation: CPK.CALL,
to: '0xffcf8fdee72ac11b5c542428b35eef5769c409f0',
value: `${1e18}`,
data: '0x',
},
]);
```
2 changes: 1 addition & 1 deletion migrations/1-deploy-contracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ module.exports = (deployer, network) => {
['Migrations', 'CPKFactory'].forEach(deploy);

if (network === 'test') {
['MultiSend', 'DefaultCallbackHandler', 'GnosisSafe'].forEach(deploy);
['MultiSend', 'DefaultCallbackHandler', 'GnosisSafe', 'ProxyFactory'].forEach(deploy);
}
};
215 changes: 147 additions & 68 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,26 +180,42 @@ const CPK = class CPK {

const ownerAccount = await this.getOwnerAccount();

const provider = this.apiType === 'web3'
? this.web3.currentProvider
: this.signer.provider.provider
|| this.signer.provider._web3Provider; // eslint-disable-line no-underscore-dangle
const wc = provider && (provider.wc || (provider.connection && provider.connection.wc));
if (
wc && wc.peerMeta && wc.peerMeta.name
&& wc.peerMeta.name.startsWith('Gnosis Safe')
) {
this.isConnectedToSafe = true;
}

if (this.apiType === 'web3') {
this.proxyFactory = new this.web3.eth.Contract(cpkFactoryAbi, proxyFactoryAddress);
this.multiSend = new this.web3.eth.Contract(multiSendAbi, multiSendAddress);

const create2Salt = this.web3.utils.keccak256(this.web3.eth.abi.encodeParameters(
['address', 'uint256'],
[ownerAccount, predeterminedSaltNonce],
));
if (this.isConnectedToSafe) {
this.contract = new this.web3.eth.Contract(safeAbi, ownerAccount);
} else {
this.proxyFactory = new this.web3.eth.Contract(cpkFactoryAbi, proxyFactoryAddress);
const create2Salt = this.web3.utils.keccak256(this.web3.eth.abi.encodeParameters(
['address', 'uint256'],
[ownerAccount, predeterminedSaltNonce],
));

this.contract = new this.web3.eth.Contract(safeAbi, this.web3.utils.toChecksumAddress(
this.web3.utils.soliditySha3(
'0xff',
{ t: 'address', v: this.proxyFactory.options.address },
{ t: 'bytes32', v: create2Salt },
this.contract = new this.web3.eth.Contract(safeAbi, this.web3.utils.toChecksumAddress(
this.web3.utils.soliditySha3(
await this.proxyFactory.methods.proxyCreationCode().call(),
this.web3.eth.abi.encodeParameters(['address'], [this.masterCopyAddress]),
),
).slice(-40),
));
'0xff',
{ t: 'address', v: this.proxyFactory.options.address },
{ t: 'bytes32', v: create2Salt },
this.web3.utils.soliditySha3(
await this.proxyFactory.methods.proxyCreationCode().call(),
this.web3.eth.abi.encodeParameters(['address'], [this.masterCopyAddress]),
),
).slice(-40),
));
}
} else if (this.apiType === 'ethers') {
const abiToViewAbi = (abi) => abi.map(({
constant,
Expand All @@ -210,38 +226,47 @@ const CPK = class CPK {
stateMutability: 'view',
}));

this.proxyFactory = new this.ethers.Contract(
proxyFactoryAddress,
cpkFactoryAbi,
this.signer,
);
this.viewProxyFactory = new this.ethers.Contract(
proxyFactoryAddress,
abiToViewAbi(cpkFactoryAbi),
this.signer,
);

this.multiSend = new this.ethers.Contract(multiSendAddress, multiSendAbi, this.signer);

const create2Salt = this.ethers.utils.keccak256(this.ethers.utils.defaultAbiCoder.encode(
['address', 'uint256'],
[ownerAccount, predeterminedSaltNonce],
));

const address = this.ethers.utils.getAddress(
this.ethers.utils.solidityKeccak256(['bytes', 'address', 'bytes32', 'bytes32'], [
'0xff',
this.proxyFactory.address,
create2Salt,
this.ethers.utils.solidityKeccak256(['bytes', 'bytes'], [
await this.proxyFactory.proxyCreationCode(),
this.ethers.utils.defaultAbiCoder.encode(['address'], [this.masterCopyAddress]),
]),
]).slice(-40),
);
if (this.isConnectedToSafe) {
this.contract = new this.ethers.Contract(ownerAccount, safeAbi, this.signer);
this.viewContract = new this.ethers.Contract(
ownerAccount,
abiToViewAbi(safeAbi),
this.signer,
);
} else {
this.proxyFactory = new this.ethers.Contract(
proxyFactoryAddress,
cpkFactoryAbi,
this.signer,
);
this.viewProxyFactory = new this.ethers.Contract(
proxyFactoryAddress,
abiToViewAbi(cpkFactoryAbi),
this.signer,
);

this.contract = new this.ethers.Contract(address, safeAbi, this.signer);
this.viewContract = new this.ethers.Contract(address, abiToViewAbi(safeAbi), this.signer);
const create2Salt = this.ethers.utils.keccak256(this.ethers.utils.defaultAbiCoder.encode(
['address', 'uint256'],
[ownerAccount, predeterminedSaltNonce],
));

const address = this.ethers.utils.getAddress(
this.ethers.utils.solidityKeccak256(['bytes', 'address', 'bytes32', 'bytes32'], [
'0xff',
this.proxyFactory.address,
create2Salt,
this.ethers.utils.solidityKeccak256(['bytes', 'bytes'], [
await this.proxyFactory.proxyCreationCode(),
this.ethers.utils.defaultAbiCoder.encode(['address'], [this.masterCopyAddress]),
]),
]).slice(-40),
);

this.contract = new this.ethers.Contract(address, safeAbi, this.signer);
this.viewContract = new this.ethers.Contract(address, abiToViewAbi(safeAbi), this.signer);
}
}
}

Expand Down Expand Up @@ -271,6 +296,8 @@ const CPK = class CPK {

let checkSingleCall;
let attemptTransaction;
let attemptSafeProviderSendTx;
let attemptSafeProviderMultiSendTxs;
let codeAtAddress;
let getContractAddress;
let encodeMultiSendCalldata;
Expand All @@ -282,6 +309,12 @@ const CPK = class CPK {
gas: blockGasLimit,
...(options || {}),
};
const promiEventToPromise = (promiEvent) => new Promise(
(resolve, reject) => promiEvent.once(
'transactionHash',
(hash) => resolve({ sendOptions, promiEvent, hash }),
).catch(reject),
);

checkSingleCall = (to, value, data) => this.web3.eth.call({
from: this.address,
Expand All @@ -295,12 +328,36 @@ const CPK = class CPK {

const promiEvent = contract.methods[methodName](...params).send(sendOptions);

return new Promise(
(resolve, reject) => promiEvent.on(
'confirmation',
(confirmationNumber, receipt) => resolve({ sendOptions, promiEvent, receipt }),
).catch(reject),
return promiEventToPromise(promiEvent);
};

attemptSafeProviderSendTx = (txObj) => {
const promiEvent = this.web3.eth.sendTransaction({
...txObj,
...sendOptions,
});

return promiEventToPromise(promiEvent);
};

attemptSafeProviderMultiSendTxs = async (txs) => {
const hash = await (
this.web3.currentProvider.host === 'CustomProvider'
? this.web3.currentProvider.send('gs_multi_send', txs)
: new Promise(
(resolve, reject) => this.web3.currentProvider.send({
jsonrpc: '2.0',
id: new Date().getTime(),
method: 'gs_multi_send',
params: txs,
}, (err, result) => {
if (err) return reject(err);
if (result.error) return reject(result.error);
return resolve(result.result);
}),
)
);
return { hash };
};

codeAtAddress = await this.web3.eth.getCode(this.address);
Expand Down Expand Up @@ -330,8 +387,20 @@ const CPK = class CPK {
...params,
...(options == null ? [] : [options]),
);
const transactionReceipt = await transactionResponse.wait();
return { transactionResponse, transactionReceipt };
return { transactionResponse, hash: transactionResponse.hash };
};

attemptSafeProviderSendTx = async (txObj) => {
const transactionResponse = await this.signer.sendTransaction({
...txObj,
...(options || {}),
});
return { transactionResponse, hash: transactionResponse.hash };
};

attemptSafeProviderMultiSendTxs = async (txs) => {
const hash = await this.signer.provider.send('gs_multi_send', txs);
return { hash };
};

codeAtAddress = (await this.signer.provider.getCode(this.address));
Expand Down Expand Up @@ -365,32 +434,42 @@ const CPK = class CPK {

if (operation === CPK.CALL) {
await checkSingleCall(to, value, data);

if (this.isConnectedToSafe) {
return attemptSafeProviderSendTx({ to, value, data });
}
}

if (codeAtAddress !== '0x') {
if (!this.isConnectedToSafe) {
if (codeAtAddress !== '0x') {
return attemptTransaction(
this.contract, this.viewContract,
'execTransaction',
[
to, value, data, operation,
0, 0, 0, zeroAddress, zeroAddress,
signatureForAddress(ownerAccount),
],
new Error('transaction execution expected to fail'),
);
}

return attemptTransaction(
this.contract, this.viewContract,
'execTransaction',
this.proxyFactory, this.viewProxyFactory,
'createProxyAndExecTransaction',
[
this.masterCopyAddress,
predeterminedSaltNonce,
this.fallbackHandlerAddress,
to, value, data, operation,
0, 0, 0, zeroAddress, zeroAddress,
signatureForAddress(ownerAccount),
],
new Error('transaction execution expected to fail'),
new Error('proxy creation and transaction execution expected to fail'),
);
}
}

return attemptTransaction(
this.proxyFactory, this.viewProxyFactory,
'createProxyAndExecTransaction',
[
this.masterCopyAddress,
predeterminedSaltNonce,
this.fallbackHandlerAddress,
to, value, data, operation,
],
new Error('proxy creation and transaction execution expected to fail'),
);
if (this.isConnectedToSafe) {
return attemptSafeProviderMultiSendTxs(transactions);
}

if (codeAtAddress !== '0x') {
Expand Down
Loading

0 comments on commit 79e7622

Please sign in to comment.