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.jsonWant to use your deployed contracts from frontends and subgraphs, without copy-pasting JSON files? Easy.
niacin generate-npm-pkg --manifest polygon.json > index.jsconst 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 EtherHow 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
MixinResolverdoes not require passing it theaddressProvideraddress. This is saved when theProxyis 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
WhateverStorestruct. For example,ImplStorefor the implementation storage. Then defining a mixin contract, theImplStoragecontract 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
ProxyandMixinResolver(used by implementations) inherit fromImplStoreand are able to share access to the_implStore().resolvervalue. - 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
}
}
}
