Start by installing/upgrading Cannon:
npm i -g @usecannon/cli
Run the setup command to prepare your development environment:
cannon setup
Cannon relies on IPFS for file storage. You can run an IPFS node locally or rely on a remote pinning service (like Pinata or run your own IPFS cluster). We recommend the former for local development and the latter when publishing packages. The setup command will walk you through this step-by-step.
Select your framework
Create a new Foundry project with forge init sample-project
. This will generate the following contract:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
Create a cannonfile.toml in the root directory of the project with the following contents. If you plan to publish this package, you should customize the name. This will deploy the contract and set the number to 420:
name = "sample-foundry-project"
version = "0.1"
description = "Sample Foundry Project"
[setting.number]
defaultValue = "420"
description="Initialization value for the number"
[contract.counter]
artifact = "Counter"
[invoke.set_number]
target = ["counter"]
func = "setNumber"
args = ["<%= settings.number %>"]
Now build the cannonfile for local development and testing:
cannon build
This created a local deployment of your nascent protocol. You can now run this package locally using the command-line tool. (Here, we add the --registry-priority local
option to ensure we’re using the version of this package that you just built, regardless of what others have published.)
cannon sample-foundry-project --registry-priority local
Deploying is just building on a remote network!
cannon build --network REPLACE_WITH_RPC_ENDPOINT --private-key REPLACE_WITH_KEY_THAT_HAS_GAS_TOKENS
Verify your project's contracts on Etherscan:
cannon verify sample-foundry-project --api-key REPLACE_WITH_ETHERSCAN_API_KEY --chain-id REPLACE_WITH_CHAIN_ID
Finally, publish your package on the Cannon registry:
cannon publish sample-foundry-project --private-key REPLACE_WITH_KEY_THAT_HAS_ETH_ON_MAINNET
You can use packages in your Cannonfiles with the import and provision actions.
import
packages to reference the addresses in their deployment data. Find which networks each package has deployment data for on the registry explorer.
For example, the Synthetix Sandbox contains a Cannonfile that deploys the sample integration contract connected to the official deployment addresses. The relevant code looks like this:
[import.synthetix_omnibus]
source ="synthetix-omnibus:latest"
[contract.sample_integration]
artifact = "SampleIntegration"
args = [
"<%= imports.synthetix_omnibus.contracts.system.CoreProxy %>",
"<%= imports.synthetix_omnibus.contracts.system.USDProxy %>"
]
provision
packages to deploy new instances of their protocol's contracts.
For example, the Synthetix Sandbox contains a Cannonfile that provisions a new instance of Synthetix and sets up a custom development environment. This is a simplified version of the relevant code:
[provision.synthetix]
source = "synthetix:latest"
owner = "<%= settings.owner %>"
[invoke.createPool]
target = ["synthetix.CoreProxy"]
from = "<%= settings.user %>"
func = "createPool"
args = [
"1",
"<%= settings.owner %>"
]
Install Cannon for Foundry:
forge install usecannon/cannon-std
Grant your Foundry project permission to read from the filesystem. Add the following line to your foundry.toml
file:
fs_permissions = [{ access = "read", path = "./"}]
Include the Cannon.sol library in your tests. Here's an example:
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import "cannon-std/Cannon.sol";
import "../src/SampleIntegration.sol";
contract SampleIntegrationTest is Test {
using Cannon for Vm;
SampleIntegration sampleIntegration;
function setUp() public {
sampleIntegration = SampleIntegration(vm.getAddress("SampleIntegration"));
}
function testFailSetThresholdRequiresOwner() public {
vm.expectRevert();
sampleIntegration.setThreshold(3);
}
}
Use the test command to run them. (Note that the --chain-id
option can be used to run tests against a forked network.)
npx cannon test
Create a new Hardhat project by following the instructions here. Then install the Hardhat Cannon plug-in:
npm install hardhat-cannon
Load the plug-in at the top of your hardhat.config.js file with require('hardhat-cannon');
or import 'hardhat-cannon';
if your're using Typescript.
In the configuration file, set the default network like so: defaultNetwork: "cannon"
Your project should have the following contract:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;
// Uncomment this line to use console.log
// import "hardhat/console.sol";
contract Lock {
uint public unlockTime;
address payable public owner;
event Withdrawal(uint amount, uint when);
constructor(uint _unlockTime) payable {
require(
block.timestamp < _unlockTime,
"Unlock time should be in the future"
);
unlockTime = _unlockTime;
owner = payable(msg.sender);
}
function withdraw() public {
// Uncomment this line, and the import of "hardhat/console.sol", to print a log in your terminal
// console.log("Unlock time is %o and block timestamp is %o", unlockTime, block.timestamp);
require(block.timestamp >= unlockTime, "You can't withdraw yet");
require(msg.sender == owner, "You aren't the owner");
emit Withdrawal(address(this).balance, block.timestamp);
owner.transfer(address(this).balance);
}
}
Create a cannonfile.toml in the root directory of the project with the following contents. If you plan to publish this package, you should customize the name. This will deploy the contract and set the unlock time to 1700000000:
name = "sample-hardhat-project"
version = "0.1"
description = "Sample Hardhat Project"
[setting.unlock_time]
defaultValue = "1700000000"
description="Initialization value for the unlock time"
[contract.lock]
artifact = "Lock"
args = ["<%= settings.unlock_time %>"]
Now build the cannonfile for local development and testing:
npx hardhat cannon:build
This created a local deployment of your nascent protocol. You can now run this package locally using the command-line tool. (Here, we add the --registry-priority local
option to ensure we’re using the version of this package that you just built, regardless of what others have published.)
cannon sample-hardhat-project --registry-priority local
Deploying is just building on a remote network! Be sure to use a network name that you've specified in your Hardhat Configuration file.
npx hardhat cannon:build --network REPLACE_WITH_NETWORK_NAME
Set up the and verify your project's contracts::
npx hardhat cannon:verify
Finally, publish your package on the Cannon registry:
npx hardhat cannon:publish --private-key REPLACE_WITH_KEY_THAT_HAS_ETH_ON_MAINNET
You can use packages in your Cannonfiles with the import and provision actions.
import
packages to reference the addresses in their deployment data. Find which networks each package has deployment data for on the registry explorer.
For example, the Synthetix Sandbox contains a Cannonfile that deploys the sample integration contract connected to the official deployment addresses. The relevant code looks like this:
[import.synthetix_omnibus]
source ="synthetix-omnibus:latest"
[contract.sample_integration]
artifact = "SampleIntegration"
args = [
"<%= imports.synthetix_omnibus.contracts.system.CoreProxy %>",
"<%= imports.synthetix_omnibus.contracts.system.USDProxy %>"
]
provision
packages to deploy new instances of their protocol's contracts.
For example, the Synthetix Sandbox contains a Cannonfile that provisions a new instance of Synthetix and sets up a custom development environment. This is a simplified version of the relevant code:
[provision.synthetix]
source = "synthetix:latest"
owner = "<%= settings.owner %>"
[invoke.createPool]
target = ["synthetix.CoreProxy"]
from = "<%= settings.user %>"
func = "createPool"
args = [
"1",
"<%= settings.owner %>"
]
You can use the build task in your tests and optionally use the built-in TypeChain support. Here's an example from the Hardhat sample project:
import { expect } from 'chai';
import { Contract } from 'ethers';
import hre from 'hardhat';
import { Greeter } from '../typechain';
describe('Greeter', function () {
let Greeter: Greeter;
before('load', async function () {
const { outputs, signers } = await hre.run('cannon:build');
const { address, abi } = outputs.contracts.Greeter;
Greeter = new Contract(address, abi, signers[0]) as Greeter;
});
it('Should return the new greeting once it is changed', async function () {
expect(await Greeter.greet()).to.equal('Hello world!');
const setGreetingTx = await Greeter.setGreeting('Hola mundo!');
// wait until the transaction is mined
await setGreetingTx.wait();
expect(await Greeter.greet()).to.equal('Hola mundo!');
});
});