Zero-configuration deployment + upgradable Ethereum smart contracts. Some people call this chainops (like devops).
# Deploy a contract TakeMarket.sol.
niacin deploy TakeMarket
# This deploys TakeMarket with a proxy that makes it upgradeable.
# These details are stored in a deployment manifest.
cat manifest.json
# The manifest stores:
# - ABI's
# - deployment tx (eg. deploy block)
# - contract addresses
# - proxy addresses
# - git metadata (so you can revert easily)
# Run it again, and you get a great preview of the current deployment state.
niacin deploy TakeMarket
# ╔══════════════════════════╤═════════╤═══════════╤════════╤════════════════════════════════════════════╗
# ║ Contract │ Version │ Status │ Action │ Proxy Address ║
# ╟──────────────────────────┼─────────┼───────────┼────────┼────────────────────────────────────────────╢
# ║ src/TakeMarket.sol │ 1 │ unchanged │ none │ 0xefc1aB2475ACb7E60499Efb171D173be19928a05 ║
# ╟──────────────────────────┼─────────┼───────────┼────────┼────────────────────────────────────────────╢
# ║ src/TakeMarketShares.sol │ 1 │ unchanged │ none │ 0xD49a0e9A4CD5979aE36840f542D2d7f02C4817Be ║
# ╚══════════════════════════╧═════════╧═══════════╧════════╧════════════════════════════════════════════╝
# Or just deploy all contracts.
niacin deploy -a
# Deploy a new system to the same chain.
niacin deploy -a --manifest staging.json
# Deploy a new system to a different chain.
RPC_URL="https://polygon‑rpc.com" PRIVATE_KEY="0x" niacin deploy -a --manifest polygon.json
# Multichain is really easy with Niacin.
mkdir deployments/
RPC_URL="https://polygon‑rpc.com" niacin deploy -a --manifest deployments/polygon.json
RPC_URL="https://arb1.arbitrum.io/rpc" niacin deploy -a --manifest deployments/arbitrum.json
RPC_URL="https://rpc.ankr.com/gnosis" niacin deploy -a --manifest deployments/gnosis.json
Want to use your deployed contracts from frontends and subgraphs, without copy-pasting JSON files? Easy.
niacin generate-npm-pkg --manifest polygon.json > index.js
const deployments = require('./index')
> deployments
{
TakeMarket: {
version: 1,
abi: [
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object], [Object],
[Object]
],
address: '0xa7AdbF0538C022C3a1805f16b3a6eF74bDD58A37',
deployBlock: 161
},
TakeMarketShares: {
version: 1,
abi: [ [Object], [Object], [Object], [Object] ],
address: '0x6166169180C5426902BE92e879feBEE0Ae280978',
deployBlock: 163
}
}
Most frontend build tools don't allow you to import JSON outside of the project directory. Don't worry! index.js
is completely self-contained. You can put it anywhere.
Niacin also remembers to keep the deploy block, so you don't have to copy-paste that either. And your subgraphs index faster.
Can I get this working with third-party contracts? Yes.
Here's how you fetch the Curve 3pool's ABI's from Etherscan, and generate a Solidity and JS code for using them:
niacin add-vendor --name Curve3Pool --fetch-from-etherscan https://optimistic.etherscan.io/address/0x1337BedC9D22ecbe766dF105c9623922A27963EC
niacin generate-sol-interface --name Curve3Pool > src/vendor/Curve3Pool.sol
niacin generate-npm-pkg > index.js
// SPDX-License-Identifier: UNLICENSED
// This file was autogenerated by Niacin, using abi-to-sol.
pragma solidity ^0.8.20;
interface Curve3Pool {
event AddLiquidity(
address indexed provider,
uint256[3] token_amounts,
uint256[3] fees,
uint256 invariant,
uint256 token_supply
);
event RemoveLiquidity(
address indexed provider,
uint256[3] token_amounts,
uint256[3] fees,
uint256 token_supply
);
// ...
function get_virtual_price() external view returns (uint256);
function calc_token_amount(
uint256[3] memory _amounts,
bool _is_deposit
) external view returns (uint256);
function add_liquidity(
uint256[3] memory _amounts,
uint256 _min_mint_amount
) external returns (uint256);
// ...
}
const deployments = require('./index')
> deployments
{
// ...
TakeMarketShares: {
version: 1,
abi: [ [Object], [Object], [Object], [Object] ],
address: '0x6166169180C5426902BE92e879feBEE0Ae280978',
deployBlock: 163
},
Curve3Pool: {
abi: [
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object], [Object], [Object], [Object],
[Object], [Object]
],
address: '0x1337BedC9D22ecbe766dF105c9623922A27963EC'
},
Niacin even comes with a built-in CLI for interacting with contracts, like seth
/cast
but easier:
# List the contracts you can call.
$ niacin call
Usage:
niacin call <contract> <method> [<arg0>] [<arg1>] ...
Available contracts:
- AddressProvider
- ProxyTakeMarket
- ProxyTakeMarketShares
- TakeMarket
- TakeMarketShares
- Curve3Pool
- WETH
# List the methods on a contract.
$ niacin call TakeMarket
Usage:
niacin call <contract> <method> [<arg0>] [<arg1>] ...
Available methods:
- getMessage()
- setHello(string)
# Call a write method.
$ niacin call TakeMarket setHello "heyheyheyyyyyy"
0x61780d2fd81bc1b72ea3473aa1f3a72ec5106d7dc014bd6a998934bb0d644c14
# Call a read method.
$ niacin call TakeMarket getMessage
heyheyheyyyyyy
# This even works with vendored dependencies:
$ niacin call WETH name
Wrapped Ether
How about contracts? Do I need to copy-paste addresses to lookup contracts in my system? No, you can now easily resolve their addresses on-chain using requireAddress(target)
. It is smart and caches entries, meaning no extra CALL
's like a Beacon.
import {MixinResolver} from "@niacin/mixins/MixinResolver.sol";
contract TakeMarket is
MixinResolver,
{
function getDependencies() public override pure returns (bytes32[] memory addresses) {
bytes32[] memory requiredAddresses = new bytes32[](1);
requiredAddresses[0] = bytes32("TakeMarketShares");
return requiredAddresses;
}
function takeMarketShares() internal view returns (address) {
return requireAddress(bytes32("TakeMarketShares"));
}
// ...
What about initializing contracts? And other sorts of scripting? Niacin supports that too:
module.exports = async function (niacin) {
const { TakeMarket } = niacin.contracts
await niacin.initialize({
contract: TakeMarket,
args: [1111]
})
const markets = ['1', '2', '3']
for (const market of markets) {
// Create a market.
await niacin.runStep({
contract: TakeMarket,
read: 'getTakeSharesContract',
readArgs: [market],
stale: value => value == '0x0000000000000000000000000000000000000000',
write: 'getOrCreateTakeSharesContract',
writeArgs: [market],
})
}
}
These migration scripts are smart. Initializers/getters/setters only run when values are stale / have changed.
Unlike OpenZeppelin initializers, integrating them is super easy:
import {MixinResolver} from "@niacin/mixins/MixinResolver.sol";
import {MixinInitializable} from "@niacin/mixins/MixinInitializable.sol";
contract TakeMarket is
MixinResolver,
MixinInitializable
{
uint public counter;
function initialize(uint _counter) public initializer {
counter = _counter;
}
Just annotate your initializer function with initializer
, and it will only be able to be called by the deployer. No inheritance pains.
What's more? We have support for autogenerated deployment websites, with interactive contract UI's:
No need to connect wallet. Automatically connects an RPC provider based on the ChainList database of chain ID's and public RPC nodes.
Niacin is really quite simple - contracts are backed by upgradeable delegatecall
proxies, each contract can inherit from MixinResolver
, which gives it the ability to resolve other contract's addresses at runtime. Unlike other approaches (beacons), these addresses are loaded from a resolution cache in storage, which is more efficient. All contracts are registered onchain in the AddressProvider
, which the mixinresolver calls out to when rebuilding its cache.
Some cool implementation details:
- Using
MixinResolver
does not require passing it theaddressProvider
address. This is saved when theProxy
is created. The sharing of this storage is achieved very ergonomically using the store pattern. - The store pattern involves typing an area of storage using a
WhateverStore
struct. For example,ImplStore
for the implementation storage. Then defining a mixin contract, theImplStorage
contract which allows us to read and write to this struct. Unlike most storage namespacing, we can modify the struct directly_implStore().x = y
, unlike typical approaches which can only read/write single values ie.storagePut(key, val)
,storageGet(key)
. This is because_implStore()
returns a struct with a modified slot. This is highly ergonomic. - Using this approach, both
Proxy
andMixinResolver
(used by implementations) inherit fromImplStore
and are able to share access to the_implStore().resolver
value. - This seems to make Solidity inheritance a lot easier to deal with too, as it is a proper separation of concerns.
Contracts that require dynamic dependency resolution simply inherit from MixinResolver
. This is how it is done in Synthetix:
contract MyContract is MixinResolver {
constructor(address _resolver) MixinResolver(_resolver) {
// ...
}
}
We make an improvement here in not requiring initialisation of the MixinResolver
:
contract MyContract is MixinResolver {
}
Instead, we can set the resolver in the proxy, and share this storage with the implementation as so:
// Implementation (through inheritance).
contract MixinResolver is ImplStorage {
function requireAddress(bytes32 target) internal {
AddressProvider provider = AddressProvider(_implStore().addressProvider)
return provider.requireAddress(target)
}
}
// Proxy.
contract Proxy is ImplStorage {
constructor(address _addressProvider) {
_implStore().addressProvider = _addressProvider;
}
}
// This store is consumed by both the proxy and by implementations.
struct ImplStore {
// An address provider which the implementation uses to resolve dependencies.
address addressProvider;
// The proxy for this implementation.
address proxy;
// The address cache for the implementation's dependencies.
mapping(bytes32 => address) addressCache;
}
contract ImplStorage {
bytes32 constant private STORE_SLOT = bytes32(uint(keccak256("eth.nakamofo.niacin.v1.impl")) - 1);
function _implStore() internal pure returns (ImplStore storage store) {
bytes32 s = STORE_SLOT;
assembly {
store.slot := s
}
}
}