Admin API Reference

The Admin API allows you to programmatically create new Admin proposals.

Requests need to be authenticated with a bearer token, which is negotiated from the Team API Key with the corresponding capability. Refer to the authentication section for info on how to negotiate it.

We recommend you use the defender-admin-client npm package for simplifying interactions with the Admin API.
It is not recommended to use the defender-admin-client npm package in a browser environment as sensitive keys would be exposed publicly.

Proposals Endpoint

The /proposals endpoint is used for creating new Admin action proposals via a POST request. Any actions created this way will have no approvals initially. If the recipient contract of the proposal does not exist, it will be created with the parameters provided.

type Network =
  | 'mainnet'
  | 'rinkeby'
  | 'ropsten'
  | 'kovan'
  | 'goerli'
  | 'xdai'
  | 'sokol'
  | 'fuse'
  | 'bsc'
  | 'bsctest'
  | 'fantom'
  | 'fantomtest'
  | 'moonbase'
  | 'moonriver'
  | 'moonbeam'
  | 'matic'
  | 'mumbai'
  | 'avalanche'
  | 'fuji'
  | 'optimism'
  | 'optimism-kovan'
  | 'optimism-goerli'
  | 'arbitrum'
  | 'arbitrum-rinkeby'
  | 'arbitrum-goerli'
  | 'celo'
  | 'alfajores'
  | 'harmony-s0'
  | 'harmony-test-s0'
  | 'aurora'
  | 'auroratest';

type Address = string;
type ProposalType = ProposalStepType | ProposalBatchType;
type ProposalStepType = 'upgrade' | 'custom' | 'pause' | 'send-funds' | 'access-control';
type ProposalBatchType = 'batch';
type Hex = string;
type BigUInt = string | number;
type ProposalFunctionInputsArray = (string | boolean)[];
type ProposalFunctionInputs = (
  | string
  | boolean
  | (string | boolean | (string | boolean | (string | boolean | ProposalFunctionInputsArray)[])[])[]
)[];

interface CreateProposalRequest {
  contract: PartialContract | PartialContract[];
  title: string;
  description: string;
  type: ProposalType;
  via?: Address;
  viaType?: 'EOA' | 'Gnosis Safe' | 'Gnosis Multisig';
  functionInterface?: ProposalTargetFunction;
  functionInputs?: ProposalFunctionInputs;
  metadata?: ProposalMetadata;
  steps?: ProposalStep[];
}

interface PartialContract {
  network: Network;
  address: Address;
  name?: string;
  abi?: string;
}

interface ProposalMetadata {
  newImplementationAddress?: Address;
  newImplementationAbi?: string;
  proxyAdminAddress?: Address;
  action?: 'pause' | 'unpause' | 'grantRole' | 'revokeRole';
  operationType?: 'call' | 'delegateCall';
  account?: Address;
  role?: Hex;
  sendTo?: Address;
  sendValue?: BigUInt;
  sendCurrency?: Token | NativeCurrency;
}

interface Token {
  name: string;
  symbol: string;
  address: Address;
  network: Network;
  decimals: number;
  type: 'ERC20';
}
interface NativeCurrency {
  name: string;
  symbol: string;
  decimals: number;
  type: 'native';
}

interface ProposalStep {
  contractId: string;
  targetFunction?: ProposalTargetFunction;
  functionInputs?: ProposalFunctionInputs;
  metadata?: ProposalMetadata;
  type: ProposalStepType;
}

interface ProposalTargetFunction {
  name?: string;
  inputs?: ProposalFunctionInputType[];
}

interface ProposalFunctionInputType {
  name?: string;
  type: string;
  internalType?: string;
  components?: ProposalFunctionInputType[];
}

Note that the fields via (address of the multisig via which the request is sent), viaType (type of the multisig), functionInterface (ABI definition of the function to call), and functionInputs are required for custom proposals. On the other hand, only metadata.newImplementationAddress is required for upgrade type proposals, since Defender will automatically calculate the remaining fields for you.

An example of an upgrade request:

DATA='{
	"contract": {
		"address": "0x179810822f56b0e79469189741a3fa5f2f9a7631",
		"network": "rinkeby"
	},
	"title": "Upgrade to v2",
	"description": "Upgrading contract to version 2.0",
	"type": "upgrade",
	"metadata": {
		"newImplementation": "0x3E5e9111Ae8eB78Fe1CC3bb8915d5D461F3Ef9A9",
		"newImplementationAbi": "[{\"inputs \": [],\"name \": \"greet \",\"outputs \": [{\"internalType \": \"string \",\"name \": \"\",\"type \": \"string \"}],\"stateMutability \": \"pure \",\"type \": \"function \"}]"
	}
}'

curl \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d "$DATA" \
    "https://defender-api.openzeppelin.com/admin/proposals"
The Defender API will only validate that the function inputs are valid with regards to the signature, but it will not validate that the proposal can actually be executed. This means you can create proposals for calling a non-existing function on a contract, or trying to upgrade a non-upgradeable contract. However, you will not be able to approve them afterwards.

Archiving Proposals

Proposals can be archived (and unarchived) via API calls by passing a boolean archived value in the PUT call payload:

DATA='{
	"archived": true
}'

curl \
  -X PUT \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d "$DATA" \
    "https://defender-api.openzeppelin.com/admin/contracts/${CONTRACT_ID}/proposals/${PROPOSAL_ID}/archived"

The same call can be made using defender-admin-client:

await client.archiveProposal(contractId, proposalId);
await client.unarchiveProposal(contractId, proposalId);

Retrieve a Proposal

Proposals can be retrieved via the API:

curl \
  -X GET \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d "$DATA" \
    "https://defender-api.openzeppelin.com/admin/contracts/${CONTRACT_ID}/proposals/${PROPOSAL_ID}"

The same call can be made using defender-admin-client passing the contract and proposal IDs:

await client.getProposal(contractId, proposalId);

Simulate a Proposal

Proposals can be simulated via the API. The results of a simulation (SimulationResponse) will be stored and can be retrieved with the getProposalSimulation endpoint.

const proposal = await client.getProposal(contractId, proposalId);

// import the ABI and create an ethers interface
const contractInterface = new ethers.utils.Interface(contractABI);

// encode function data
const data = contractInterface.encodeFunctionData(proposal.functionInterface.name, proposal.functionInputs);

const simulation = await client.simulateProposal(
  proposal.contractId, // contractId
  proposal.proposalId, // proposalId
  {
    transactionData: {
      // this is the default hardhat address
      from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', // change this to impersonate the `from` address
      data,
      to: proposal.contract.address,
      value: proposal.metadata.sendValue,
    },
    // default to latest finalized block,
    // can be up to 100 blocks ahead of current block,
    // does not support previous blocks
    blockNumber: undefined,
  },
);

You can also optionally set the simulate flag as part of the createProposal request (as long as this is not a batch proposal) to simulate the proposal within the same request. You can override simulation parameters by setting the overrideSimulationOpts property, which is a SimulationRequest object.

const proposalWithSimulation = await client.createProposal({
  contract: {
    address: '0xA91382E82fB676d4c935E601305E5253b3829dCD',
    network: 'mainnet',
    // provide abi OR overrideSimulationOpts.transactionData.data
    abi: JSON.stringify(contractABI),
  },
  title: 'Flash',
  description: 'Call the Flash() function',
  type: 'custom',
  metadata: {
    sendTo: '0xA91382E82fB676d4c935E601305E5253b3829dCD',
    sendValue: '10000000000000000',
    sendCurrency: {
      name: 'Ethereum',
      symbol: 'ETH',
      decimals: 18,
      type: 'native',
    },
  },
  functionInterface: { name: 'flash', inputs: [] },
  functionInputs: [],
  via: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266',
  viaType: 'EOA',
  // set simulate to true
  simulate: true,
  // optional
  overrideSimulationOpts: {
    transactionData: {
      // or instead of ABI, you can provide data
      data: '0xd336c82d',
    },
  },
});
Enabling the simulate flag as part of the createProposal request is not currently supported for batch proposals.
A simulation may fail due to a number of reasons, such as network congestion, unstable providers or hitting a quota limitation. We would advise you to track the response code to assure a successful response was returned. If a transaction was reverted with a reason string, this can be obtained from the response object under response.meta.returnString. A transaction revert can be tracked from response.meta.reverted.

Retrieve a simulation

You can also retrieve existing simulations for a proposal

const proposal = await client.getProposal(contractId, proposalId);

const simulation = await client.getProposalSimulation(
  proposal.contractId, // contractId
  proposal.proposalId, // proposalId
);

Contracts Endpoint

The /contracts endpoint can be used to manage the contracts imported into the Defender Admin dashboard. By issuing a PUT to the endpoint you can create or update a contract given its network and address:

DATA='{
  "address": "0x179810822f56b0e79469189741a3fa5f2f9a7631",
  "network": "rinkeby",
  "name": "My Contract",
  "abi": "..."
}'

curl \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d "$DATA" \
    "https://defender-api.openzeppelin.com/admin/contracts"

You can also issue a GET request to the same endpoint to retrieve a list of all contracts imported.

Verifications Endpoint

The /verifications endpoint can be used to verify the deployed bytecode of any contract in any of the networks supported by Defender. By issuing a POST to the endpoint you issue a verification request whose results will be stored in Defender’s address book. That in turn will make those results available to anyone with access to your Defender workspace.

DATA='
{
  artifactUri: 'https://gist.githubusercontent.com/johndoe/506bff068172583d4d82ef53ba01e26c/raw/5f142de4c33aefddb2625b2cce1e6aba7791ebdc/compile-artifact.json',
  solidityFilePath: 'contracts/Vault.sol',
  contractName: 'VaultV2',
  contractAddress: '0x38e373CC414e90dDec45cf7166d497409902e998',
  contractNetwork: 'rinkeby',
  referenceUri: 'https://ci-run-or-git-commit.example/',
}'

curl \
  -X POST \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
  -d "$DATA" \
    "https://defender-api.openzeppelin.com/admin/verifications"

You can also issue a GET request to `/verifications/${contractNetwork}/${contractAddress} to get the latest verification information associated to contractAddress in Defender. For the example above, this would be:

curl \
  -X GET \
  -H "X-Api-Key: $KEY" \
  -H "Authorization: Bearer $TOKEN" \
    "https://defender-api.openzeppelin.com/admin/verifications/rinkeby/0x38e373CC414e90dDec45cf7166d497409902e998"

For a more in-depth discussion of bytecode verification in Defender, refer to the Defender Admin Verification section of this documentation.