Contract Proxy Kit
Enable batched transactions and contract account interactions using a unique deterministic Gnosis Safe.
npm install contract-proxy-kit
Usage
The Contract Proxy Kit package exposes a CPK class:
const CPK = require('contract-proxy-kit')
CPK requires either web3.js or ethers.js to function. Currently the following versions are supported:
- web3.js 1.2
- web3.js 2.0 alpha
- ethers.js 4.0
CPK.create
To create a CPK instance, use the static method CPK.create
. This method accepts an options object as a parameter, and will result in a promise which resolves to a CPK instance if successful and rejects with an error otherwise.
Using with web3.js
To use CPK with web3.js, supply CPK.create
with a Web3 instance as the value of the web3
key. For example:
const Web3 = require('web3');
const web3 = new Web3(/*...*/);
const cpk = await CPK.create({ web3 });
The proxy owner will be inferred by first trying web3.eth.defaultAccount
, and then trying to select the 0th account from web3.eth.getAccounts
. However, an owner account may also be explicitly set with the ownerAccount
key:
const cpk = await CPK.create({ web3, ownerAccount: '0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1' });
Using with ethers.js
To use CPK with ethers.js, supply CPK.create
with the ethers
object and an ethers.js Signer which has an active Provider connection. For example:
const { ethers } = require('ethers');
const provider = ethers.getDefaultProvider('homestead');
const wallet = ethers.Wallet.createRandom().connect(provider);
const cpk = await CPK.create({ ethers, signer: wallet });
The proxy owner will be the account associated with the signer.
Networks configuration
Regardless of which type of underlying API is being used, the CPK instance will check the ID of the network given by the provider in order to prepare for contract interactions. By default, the Rinkeby (ID 4), Goerli (ID 5), and Kovan (ID 42) test networks (but currently not the main network) will have preconfigured addresses for the required contracts:
-
masterCopyAddress
: Gnosis Safe master copy -
proxyFactoryAddress
: CPK factory -
multiSendAddress
: MultiSend contract for batching transactions -
fallbackHandlerAddress
: A fallback handler (DefaultCallbackHandler)
However, address configurations for networks may be added or overridden by supplying a configuration object as the value of the networks
key in the options. For example, adding a configuration for a network with ID (4447) may be done in the following manner:
const cpk = await CPK.create({
// ...otherOptions,
networks: {
4447: {
masterCopyAddress: '0x2C2B9C9a4a25e24B174f26114e8926a9f2128FE4',
proxyFactoryAddress: '0x345cA3e014Aaf5dcA488057592ee47305D9B3e10',
multiSendAddress: '0x8f0483125FCb9aaAEFA9209D8E9d7b9C8B9Fb90F',
fallbackHandlerAddress: '0xAa588d3737B611baFD7bD713445b314BD453a5C8',
},
},
});
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:
const ownerAccount = await cpk.getOwnerAccount()
CPK#address
Once created, the address
property on a CPK instance will provide the proxy's checksummed Ethereum address:
> cpk.address
'0xdb6F36fC4e07eAfCAba1D0056609A76C91c5A1bC'
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:
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.
Each of the transactions
provided as input to this function must be an Object with the following properties:
-
operation
: EitherCPK.CALL
(0) orCPK.DELEGATECALL
(1) to execute the transaction as either a normal call or a delegatecall. -
to
: The target address of the transaction. -
value
: The amount of ether to send along with this transaction. -
data
: The calldata to send along with the transaction.
If any of the transactions would revert, this function will reject instead, and nothing will be executed.
For example, if the proxy account holds some ether, it may batch send ether to multiple accounts like so:
const cpk = await CPK.create(/* ... */);
const txObject = await cpk.execTransactions([
{
operation: CPK.CALL,
to: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1',
value: `${1e18}`,
data: '0x',
},
{
operation: CPK.CALL,
to: '0xffcf8fdee72ac11b5c542428b35eef5769c409f0',
value: `${1e18}`,
data: '0x',
},
]);
Example calls to web3.js/Truffle contracts
The data
field may be used to make calls to contracts from the proxy account. Suppose that erc20
is a web3.eth.Contract instance for an ERC20 token for which the proxy account holds a balance, and exchange
is a web3.eth.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:
const { promiEvent, receipt } = await cpk.execTransactions([
{
operation: CPK.CALL,
to: erc20.options.address,
value: 0,
data: erc20.methods.approve(
exchange.options.address,
`${1e18}`,
).encodeABI(),
},
{
operation: CPK.CALL,
to: exchange.options.address,
value: 0,
data: exchange.methods.deposit(
erc20.options.address,
`${1e18}`,
).encodeABI(),
},
]);
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:
const { promiEvent, hash } = await cpk.execTransactions([
{
operation: CPK.CALL,
to: erc20.address,
value: 0,
data: erc20.contract.methods.approve(
exchange.address,
`${1e18}`,
).encodeABI(),
},
{
operation: CPK.CALL,
to: exchange.address,
value: 0,
data: exchange.contract.methods.deposit(
erc20.address,
`${1e18}`,
).encodeABI(),
},
]);
Example calls to ethers.js contracts
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:
const { transactionResponse, hash } = await cpk.execTransactions([
{
operation: CPK.CALL,
to: erc20.address,
value: 0,
data: erc20.interface.functions.approve.encode(
exchange.address,
`${1e18}`,
),
},
{
operation: CPK.CALL,
to: exchange.address,
value: 0,
data: exchange.interface.functions.deposit.encode(
erc20.address,
`${1e18}`,
),
},
]);
Setting the gas price
An additional optional parameter may be passed to execTransactions
to override default options for the transaction. For example, to batch send ether while paying a gas price of 3 Gwei, do this:
const txObject = await cpk.execTransactions(
[
{
operation: CPK.CALL,
to: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1',
value: `${1e18}`,
data: '0x',
},
{
operation: CPK.CALL,
to: '0xffcf8fdee72ac11b5c542428b35eef5769c409f0',
value: `${1e18}`,
data: '0x',
},
],
{ 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.
const { hash } = await cpk.execTransactions([
{
operation: CPK.CALL,
to: '0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1',
value: `${1e18}`,
data: '0x',
},
{
operation: CPK.CALL,
to: '0xffcf8fdee72ac11b5c542428b35eef5769c409f0',
value: `${1e18}`,
data: '0x',
},
]);