Automatic monitoring for factory clones

The factory-clone pattern can be advantageous for minimizing gas costs. However, since each clone gets deployed to a new address, it may be a challenge to efficiently track and monitor each of these contracts.

This guide shows how to use Defender to monitor a factory contract as well as the clone contracts created by it. Monitor automation is achieved through the following structure of Defender modules:

  • A Monitor watches for successful event emitted by the factory contract that creates a clone. If detected, it triggers an Action and passes along information about the transaction.

  • The Action makes use of the defender-sdk to add the address of the newly created contract to the address book for easier monitoring.

  • Aditionally, the Action uses the defender-sdk to add the clone address to the list of addresses watched by a Monitor.

In this case, the contract ABI can be pre-supplied since clone contracts will have identical ABIs. Alternatively, you may be able to dynamically retrieve the ABI from a verified contract at a given address using Etherscan’s API.

Generate API Key

To programmatically add a contract to the address book, the sdk requires credentials in the form of an API key and secret. Create and copy the credentials in the API keys page.

Create API credentials

Now, navigate to the Secrets page in Defender and create a new secret with the name API_KEY and paste in the API key. Create another secret with the name API_SECRET and paste in the API secret. These secrets will be used by the Action securely.

Save API credentials

Create the Action

Navigate to the Action creation page, enter a name, and select Webhook as trigger. Then, paste the following Action code and save it:

const { Defender } = require('@openzeppelin/defender-sdk');

exports.handler = async function (event) {
  const creds = {
    apiKey: event.secrets.API_KEY,
    apiSecret: event.secrets.API_SECRET,
  }
  const client = new Defender(creds);

  const payload = event.request.body
  const matchReasons = payload.matchReasons
  const newCloneAddress = matchReasons[0].params._clone
  const newCloneAbi = `[
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "ValueChanged",
      "type": "event"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "initialize",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "retrieve",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "store",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ]`
  // Add new clone contract
  await client.proposal.addContract({
    network: 'sepolia',
    address: newCloneAddress,
    name: `Clone ${newCloneAddress}`,
    abi: newCloneAbi,
  })
}
Create Action

The Action is now ready to be triggered by a Monitor.

Manually triggering this Action will be raise an error, since the Action relies on data supplied by a Monitor (such as the address of the newly deployed clone contract address).

Create the Monitor

This Monitor will watch for an event emitted by the factory contract signaling that a new clone has been created. Navigate to the Monitor creation page, choose a name, risk category, and select the Factory contract (add the factory if it’s not already added).

Monitor General Information

Leave Transaction Filters as it is, and continue to the Events tab. Here, select the event name for clone creation and leave the event parameters blank to catch all emitted events.

Monitor Events

Lastly, open the Alerts section and select the Action created in the previous step within the Execute an Action dropdown. Feel free to add any other setting, like notifications, and save the Monitor.

Monitor Alerts

As with any action, the triggering of this Monitor will be recorded in the Logs.

Test run

To test the set up, navigate to Transaction Proposals to manually create a clone through the factory. Select the factory contract, and call the function that creates a clone with any parameters needed.

Transaction Proposal to create clone

Then, execute this this transaction with your preferred approval process, like a Relayer or EOA wallet. Head over to run history of the Action to verify it was triggered by the Monitor, adding the clone contract address to Defender.

Action Run History

Create Monitor for clones

Now that you have a clone contract to serve as a template for all future clone contracts, it’s time to create a Monitor for them. Navigate to the Monitor creation page, choose a name, risk category, and select the clone contract.

Aditionally, feel free to add any other filters for transactions, events, and functions, or notifications. Save the Monitor and observe the logs/notifications to verify that the Monitor is working as expected.

Monitor Clones

Automatically add clones to Monitor

With the last Monitor, you can update the Action to add any newly created contract to the list of addresses being monitored by the Monitor. Update the Action code with the following code, replacing monitorId with the ID of the Monitor created in the previous step:

const { Defender } = require('@openzeppelin/defender-sdk');

exports.handler = async function (event) {
  const creds = {
    apiKey: event.secrets.API_KEY,
    apiSecret: event.secrets.API_SECRET,
  }
  const client = new Defender(creds);

  const payload = event.request.body
  const matchReasons = payload.matchReasons
  const newCloneAddress = matchReasons[0].params._clone
  const newCloneAbi = `[
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "ValueChanged",
      "type": "event"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "initialize",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "retrieve",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "value",
          "type": "uint256"
        }
      ],
      "name": "store",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ]`
  // Add new clone contract
  await client.proposal.addContract({
    network: 'sepolia',
    address: newCloneAddress,
    name: `Clone ${newCloneAddress}`,
    abi: newCloneAbi,
  })

  // Add clone contract to Monitor
  const monitorId = 'REPLACE'
  const monitor = await client.monitor.get(monitorId)
  const subscribedAddresses = monitor.addressRules[0].addresses
  subscribedAddresses.push(newCloneAddress)
  await client.action.update(monitorId, { addresses: subscribedAddresses })
}

Now when the Action runs, not only will it add the contract to Defender, it will also add it to the Monitor.

To verify, execute another test run!