5afe / safe-contracts-wrapper

MIT License
0 stars 2 forks source link

Polywrap Gnosis Safe Wrapper

Table of contents

Install the package with yarn or npm:

yarn add polywrap
npm install polywrap

Getting Started

The following steps show how to set up the Polywrap Client, deploy a new Safe, create a Safe transaction, generate the required signatures from owners and execute the transaction. However, using the Polywrap Client alone will not allow for the collection of owner signatures off-chain. To do this and be able to see and confirm the pending transactions shown in the Safe Web App, it is recommended that you follow this other guide that covers the use of the Safe Core SDK, combined with the Safe Service Client.

1. Instantiate a Polywrap Client

First of all, we need to create a Polywrap Client, which contains all the required utilities for the SDKs to interact with the blockchain. It acts as a wrapper for web3.js or ethers.js Ethereum libraries.

Usage# Use an import or require statement, depending on which your environment supports.

import { PolywrapClient } from "@polywrap/client-js";

Then, you will be able to use the PolywrapClient like so:

// Simply instantiate the PolywrapClient.
const client = new PolywrapClient();

Polywrap client docs

2. Deploy a new Safe

To deploy a new Safe account invoke deploySafe method of safe-factory-wrapper with the right params to configure the new Safe. This includes defining the list of owners and the threshold of the Safe. A Safe account with three owners and threshold equal three will be used as the starting point for this example but any Safe configuration is valid.

const result = await client.invoke({
  uri: 'ens/safe.factory.eth',
  method: "deploySafe",
  args: {
    safeAccountConfig: {
      owners: [<owner1>, <owner2>, <owner3> ],
      threshold: 1,
    }
  }
});

The deploySafe method executes a transaction from the your current ethereum account, deploys a new Safe and returns a Safe Address. Make sure to save this address as you will need it to interact with safe-wrapper

3. Create a Safe transaction

To create a transaction you can invoke createTransaction method of safe-wrapper. Make sure you provided environment param safeAddress to your call.


const safeTransactionData = {
  to: '0x<address>',
  value: '<eth_value_in_wei>',
  data: '0x<data>'
}

const safeTransaction = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: "createTransaction",
  args: {
      tx: safeTransactionData,
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  }
});

Check the createTransaction method in the Wrapper Reference for additional details on creating MultiSend transactions.

Before executing this transaction, it must be signed by the owners and this can be done off-chain or on-chain. In this example owner1 will sign it off-chain, owner2 will sign it on-chain and owner3 will execute it (the executor also signs the transaction transparently).

3.a. Off-chain signatures

The owner1 account signs the transaction off-chain.

const signedSafeTransaction = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: "addSignature",
  args: {
      tx: safeTransactionData,
    },
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
});

Because the signature is off-chain, there is no interaction with the contract and the signature becomes available at signedSafeTransaction.signatures.

3.b. On-chain signatures

To sign transaction on-chain owner2 should instantiate new PolywrapClient connected to ethereum (Ethereum-plugin-config). After owner2 account is connected to the ethereum-plugin as a signer the transaction hash will be approved on-chain.

// Get transaction hash
const txHash = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: "getTransactionHash",
  args: {
      tx: signedSafeTransaction.data,
    },
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
});

// Approve
await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: "approveTransactionHash",
  args: {
      hash: txHash,
    },
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
});

4. Transaction execution

Lastly, owner3 account is connected to the client as a signer and executor of the Safe transaction to execute it.

const executeTxResponse = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: "executeTransaction",
  args: {
      tx: signedTransaction,
    },
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
});

Safe Factory Wrapper Reference

deploySafe

Deploys a new Safe and returns an instance of the Safe Core SDK connected to the deployed Safe. The address of the Master Copy, Safe contract version and the contract (GnosisSafe.sol or GnosisSafeL2.sol) of the deployed Safe will depend on the initialization of the safeFactory instance.

const safeAccountConfig = {
  owners,
  threshold,
  to, // Optional
  data, // Optional
  fallbackHandler, // Optional
  paymentToken, // Optional
  payment, // Optional
  paymentReceiver // Optional
}

const safeSdk = await safeFactory.deploySafe({ safeAccountConfig })

This method can optionally receive the safeDeploymentConfig parameter to define the saltNonce.

const safeAccountConfig = {
  owners,
  threshold,
  to, // Optional
  data, // Optional
  fallbackHandler, // Optional
  paymentToken, // Optional
  payment, // Optional
  paymentReceiver // Optional
}

const safeDeploymentConfig = {
  saltNonce,
  isL1Safe, // Optional
  version, // Optional
}

const safeDeploymentResponse = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'deploySafe',
  args: { 
    safeAccountConfig, 
    safeDeploymentConfig
    },
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

Safe Wrapper Reference

getAddress

Returns the address of the current SafeProxy contract.

const address = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'getAddress',,
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

getContractVersion

Returns the Safe Master Copy contract version.

const version = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'getContractVersion',,
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

getOwners

Returns the list of Safe owner accounts.

const owners = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'getOwners',
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

getNonce

Returns the Safe nonce.

const nonce = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'getNonce',
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

getThreshold

Returns the Safe threshold.

const threshold = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'getThreshold',
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

getChainId

Returns the chainId of the connected network.

const chainId = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'getChainId',
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

getBalance

Returns the ETH balance of the Safe.

const balance = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'getBalance',
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

getModules

Returns the list of addresses of all the enabled Safe modules.

const moduleAddresses = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'getModules',
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

isModuleEnabled

Checks if a specific Safe module is enabled for the current Safe.

const isEnabled = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'isModuleEnabled',
  args: {
    moduleAddress: <address>
  },
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

isOwner

Checks if a specific address is an owner of the current Safe.

const isOwner = await client.invoke({
  uri: 'ens/safe.wrapper.eth',
  method: 'isOwner',
  args: {
    ownerAddress: <address>
  },
  env: {
    safeAddress: <SAFE_ADDRESS>
  }
})

createTransaction

Returns a Safe transaction ready to be signed by the owners and executed. The Safe Wrapper supports the creation of single Safe transactions but also MultiSend transactions.

If the optional properties are not manually set, the Safe transaction returned will have the default value for each one:

Read more about the Safe transaction properties.

getTransactionHash

Returns the transaction hash of a Safe transaction.

const safeTransactionData: SafeTransactionDataPartial = {
  // ...
}
const safeTransaction =  await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'createTransaction',
    args: {
      tx: safeTransactionData
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

const txHash = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'getTransactionHash',
    args: {
      tx: safeTransaction
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

signTransactionHash

Signs a hash using the current owner account.

const signature = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'signTransactionHash',
    args: {
      hash: txHash
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

signTypedData

Signs a transaction according to the EIP-712 using the current signer account.

const signature = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'signTypedData',
    args: {
      tx: safeTransaction
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

signTransaction

Returns a new SafeTransaction object that includes the signature of the current owner. eth_sign will be used by default to generate the signature.

const signedSafeTransaction = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'addSignature',
    args: {
      tx: safeTransaction
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

Optionally, an additional parameter can be passed to specify a different way of signing:

const signedSafeTransaction = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'addSignature',
    args: {
      tx: safeTransaction
      signingMethod: 'eth_signTypedData'
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })
const signedSafeTransaction = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'addSignature',
    args: {
      tx: safeTransaction
      signingMethod: 'eth_sign' // default option.
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

approveTransactionHash

Approves a hash on-chain using the current owner account.

const txResponse = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'approveTransactionHash',
    args: {
      hash: txHash
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

Optionally, some properties can be passed as execution options:

const options = {
  from, // Optional
  gasLimit, // Optional
  gasPrice, // Optional
  maxFeePerGas, // Optional
  maxPriorityFeePerGas // Optional
  nonce // Optional
}
const txResponse = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'approveTransactionHash',
    args: {
      hash: txHash,
      options: options
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

getOwnersWhoApprovedTx

Returns a list of owners who have approved a specific Safe transaction.

const ownerAddresses = await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'getOwnersWhoApprovedTx',
    args: {
      hash: txHash
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

executeTransaction

Executes a Safe transaction.

const txResponse  await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'executeTransaction',
    args: {
      tx: signedSafeTransaction
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })

Optionally, some properties can be passed as execution options:

const options = {
  from, // Optional
  gasLimit, // Optional
  gasPrice, // Optional
  maxFeePerGas, // Optional
  maxPriorityFeePerGas // Optional
  nonce // Optional
}
const txResponse  await client.invoke({
    uri: 'ens/safe.wrapper.eth',
    method: 'executeTransaction',
    args: {
      tx: signedSafeTransaction,
      options: options,
    },
    env: {
      safeAddress: <SAFE_ADDRESS>
    }
  })