This repository stores the tokenomics ecosystem related contracts, scripts and test for Rand Network.
These contracts are the following:
- Rand Token contract (RND) ERC20
- Vesting Controller (VC) ERC721
- Safety Module (SM) ERC20
- Governance (rDAO)
To run the repository test follow these steps:
npm install .
npx hardhat node &
npx hardhat test
There are several custom hardhat tasks have been implemented for deployment, upgrading, abi to interface conversion, contract flattening, ipfs uploads.
abi2interface generates an interface file from a contract
name and save it to the contracts folder.
hh abi2interface VestingControllerERC721
abi2ipfs generates an interface file from a contract
name and uploads it to IPFS
and pins using Pinata service. It returns the IPFS hash and clickable link. NOTE: pinata API key is needed in the .env file
hh abi2ipfs VestingControllerERC721
verify Verifying the contract using Etherscan API. NOTE: Etherscan API key is needed in the .env file
hh verify <eth_contract_address>
deploy deploys a contract using the scripts/deploy_taks.js
script. It allows for verifying using Etherscan API and initial settings like granting allowance to the VestingController and minting a few sample investments. Enables live-net deployment with initialization of rights granting to multisig and relayer. Exporting allows to get a CSV file which fits the Notion page of deployed contracts and allows easy importing to this Notion database.
NOTE: Etherscan API key is needed in the .env file
hh deploy --name-prefix <name> --initialize --multisig-address <address> --relayer-address <address> --export-csv --verify
Avaiable flags and arguments:
--name-prefix - Adds a name prefix to the deployed token names and symbols
--test-mint - Mints a few tokens for testing (cannot use with --name-prefix=Rand)
--initialize - Allows role granting to multisig and relayer and renouncing from deployer
- Requires --multisig-address and --relayer-address
--multisig-address - Address to be granted DEFAULT_ADMIN_ROLE and PAUSER_ROLE
--relayer-address - Address to be granted MINTER_ROLE (VC, NFT)
--verify - Verifies contracts on Etherscan or other derivative explorers on other chains
--export-csv - Exports deployed contract address data to a csv file to the project root
flatten-clean Flattens files for Etherscan manual single-file verification process.
hh flatten-clean contracts/VestingControllerERC721.sol
folder2ipfs Uploads a complete folder to IPFS
and pins using Pinata service. It returns the IPFS hash and clickable link. NOTE: pinata API key is needed in the .env file
hh folder2ipfs uri/sample_to_deploy_ipfs_generated
upgradeProxy Upgrades a proxy with a new implementation. First it deploys the new implementation and sets the proxy for the new implementation as of OpenZeppelin Upgrades library.
hh upgradeProxy <eth_contract_address> VestingControllerERC721
upgradeProxyAndVerify Upgrades a proxy with a new implementation. First it deploys the new implementation and sets the proxy for the new implementation as of OpenZeppelin Upgrades library. Additionally it also verifies the new implementation using Etherscan API. NOTE: Etherscan API key is needed in the .env file
hh upgradeProxyAndVerify --verify <eth_contract_address> VestingControllerERC721
This contract is the Rand token, a standard OZ ERC20 implementation with upgradability via UUPS OZ Proxy. Custom functionality include accessible only the Safety Module:
function adminTransfer(
address owner,
address recipient,
uint256 amount
)
Another customized function in order to allow Rand to burn tokens from wallets (due diligence and compliance reasons)
function burnFromAdmin(address account, uint256 amount)
Address registry is a simple contract to keep track of the ecosystem contract addresses. It can register a new entity with setNewAddress(string calldata name, address contractAddress)
and also update an existing entity with the updateAddress(string calldata name, address contractAddress)
. To fetch the current address for a contract use getAddressOf(string calldata name)
and also the getRegistryList()
function is useful to list the strings stored for the contracts.
In the ecosystem contracts there is a function to update the address of the registry used which can be done on the contracts with updateRegistryAddress(IAddressRegistry newAddress)
.
as per the ./contracts/ecosystem/ImportsManager.sol:
MULTISIG = "MS";
RAND_TOKEN = "RND";
VESTING_CONTROLLER = "VC";
SAFETY_MODULE = "SM";
ECOSYSTEM_RESERVE = "RES";
GOVERNANCE = "GOV";
INVESTOR_NFT = "NFT";
BPT_TOKEN = "BPT";
OPENZEPPELIN_DEFENDER = "OZ";
It is an OZ ERC721 implementation to hold information about the vesting investors. As an additional function this contract manages the early sale investor NFTs as the ERC721 functionality implements it.
In order to create a new investment the following function is called:
function mintNewInvestment(
address recipient,
uint256 rndTokenAmount,
uint256 vestingPeriod,
uint256 vestingStartTime,
uint256 cliffPeriod
)
This function will mint a new NFT token, and assign a VestingInvestment
struct to the newly created tokenId
. It would also emit event NewInvestmentTokenMinted
.
For early investors Rand is gifting a special NFT that is based on the level of the initial investment of the investor. These NFT tokens are held in a separate ERC721
contract.
To set the baseURI
the setBaseURI(string memory newURI)
must be called e.g: setBaseURI("ipfs://QmfYu4vZFNRgsnwb8cCnzF4y8ceQfcCMuyj5wm1xT7tre6/")
. This IPFS path must point to a folder containing each of the separate JSON Metadata files for each tokenId
.
IMPORTANT NOTE: You must include the ending /
to the baseURI
to properly return the tokenURI
.
The tokenURI(uint256 tokenId)
is an overriden function as it returns an alternative IPFS address for vesting investment NFT that are deplated (total amount is claimed), so it would show another image for the NFT.
NFT tokens are only transerable when the investment is deplated totally. This is enforced by the override _transfer()
of the ERC721
.
Sample IPFS JSON folder:
uri/sample_to_deploy_ipfs
├── 1
├── 1_ <--- this is underscore signs the alternative json that is shown when the investment is depleated
├── 2
├── 2_
└── contract_uri
Repository contains a hardhat task
to automate IPFS uploading for URIs. To use this prepare the folder containing the JSONs, then follow this example:
➜ tokenomics git:(Safety-Module-ERC20) ✗ hh folder2ipfs uri/sample_to_deploy_ipfs
/Users/adr/Dev/Rand-Network/tokenomics/uri/sample_to_deploy_ipfs
{
IpfsHash: 'QmU4JZXUmee8aakZYSqZQVkDjPQALNzXnNPeJMyctUjUoe',
PinSize: 1551,
Timestamp: '2022-03-03T14:15:53.872Z',
isDuplicate: true
}
Successful upload of uri/sample_to_deploy_ipfs to IPFS:
https://cloudflare-ipfs.com/ipfs/QmU4JZXUmee8aakZYSqZQVkDjPQALNzXnNPeJMyctUjUoe
The Safety Module (SM) acts as a staking contract to enable Rand token holders to stake their RND and therefore supply shortfall liquidity in case of a bug or hack in the ecosystem contracts. Staking is rewarded in RND tokens with an emission rate from the Ecosystem Reserve Contract.
The staking contract allows users to stake and unstake tokens in exchange for staked RND (sRND). This sRND token represents the share of the user in the SM and is non-transferable in order to disallow dumping in case of a shortfall event.
When users would like to stake in their tokens, there is the stake(uint256 amount)
function to do that with a simple and an overloaded version. One is allows with a simple uint256 amount
to stake in RND tokens without a vesting scheme. The overloaded version requests a uint256 tokenId
also which defines the vesting investment tokenId
from the vesting controller.
In case of unstaking the user must go through a cooldown period first. In order to initiate this must call the cooldown()
and after the period ends defined by COOLDOWN_SECONDS
the user need to call the redeem(uint256 amount)
function. This has also an overloaded version for vesting investors specifying the tokenId
. After the cooldown period the user has a period for unstaking. After this period of UNSTAKE_WINDOW
the user must call again the cooldown()
if wants to unstake.
To claim rewards the user can call the claimRewards(uint256 amount)
and to simply check the balance of his accrued rewards he can call calculateTotalRewards(address user)
.
There are two functions to update periods for unstaking:
updateCooldownPeriod(uint256 newPeriod)
and updateUnstakePeriod(uint256 newPeriod)
.
Rewards are calculated using the scheme applied by Aave Safety Module. This basically calculates an asset level index, and a user level index for the staked asset. Based on an emission rate of the rewards it can be calculated the rewards claimable by the user.
RewardDistributionManager is inherited by the SafetyModule contract and uses its internal functionality. In order to configure an asset with the emissionRate
the updateAsset( address _asset, uint256 _emission, uint256 _totalStaked)
is called.
As Aave does it allows also to stake Balancer Pool Tokens in the Safety Module. It allows for creating and freezing liquidity for the protocol. Another separate contract must be deployed for further assets. Also the applicable functions must be exposed in the contract inheriting.
Governance is simple contract to summarize balances from multiple contracts for an account address. It uses the standard balanceOf
function of RND
and SM
, while iterating over all the account holders investment tokens inside VC
and summarize each individual investment by the following formula:
VC balanceOf = rndTokenAmount - rndClaimedAmount - rndStakedAmount
Governance contract does not use an ERC20 standard just simply implements the balanceOf(account)
and totalSupply()
functions so Automata Witness will be able to query these to calculating governance voting on a DAO proposal.
There are two unit test files developed to cover most of the functionality. One is the test/AddressRegistry.js
, which simply tests the AddressRegistry
functions separately. The other file is the most complex, which covers all ecosystem contract tests at test/RND_VC_SM_Gov.js
.
To run the tests locally using the hardhat network
simply run:
hh test
To simplify tests there is a short utility script to help deployment, which can also be used as a hh task
. It is located in scripts/deploy_testnet_task.js
. It returns the deployed ethers
contract objects alongside with the default deployment parameters:
module.exports = {
deploy_testnet,
get_factories,
get_wallets,
_RNDdeployParams,
_VCdeployParams,
_SMdeployParams,
_NFTdeployParams,
_GovDeployParams,
};
These tests are run automatically on the following actions:
on:
push:
branches:
- development
- main
pull_request:
branches:
- development
- main
Required enviroment variables for hardhat to work
MULTISIG_PRIVATE_KEY=
PROXYADMIN_PRIVATE_KEY=
ALICE_PRIVATE_KEY=
BACKEND_PRIVATE_KEY=
MAINNET_URL=
RINKEBY_TESTNET_URL
RINKEBY_ALT
GOERLI_TESTNET_URL=
ROPSTEN_TESTNET_URL=
MOONBEAM_URL=
MOONBASE_URL=
MOONBASE_URL_ALTERNATIVE=
ETHERSCAN_API_KEY=
MOONSCAN_API_KEY=
PINATA_KEY=
PINATA_SECRET=