Smart contract for staking incentives with NFTs
nvm use 16
yarn install
yarn build
This script allows the HoprBoost minter to mint Boost NFTs of one "type" and one/multiple "rank" with their respective APYs. If the minter wants to mint Boost NFTs of multiple "types", steps 1-5 need to be repeated for each "type".
-
HOPR Association MS grant minter's account
MINTER_ROLE
onHoprBoost
smart contract -
Download the result of NFT recipients from DuneAnalytics to
inputs
folder and name it after the NFT's type name, e.g.DAO_v2.csv
. An sample query is at https://dune.xyz/queries/140878. Note that- You need to be logged in Dune with our company account to be able to download the entries. Please request access in case you don't have it.
- Name of the csv is case-sensitive. Only one boost per type can be taken into account in the staking contract.
- Column
eoa
andgrade
are mandatory - Addresses in the column
eoa
should start with0x
and wrapped by>
and<
. The followings are valid examples of aneoa
entry:"<a href=""https://blockscout.com/xdai/mainnet/address/0xf69c45b4246fd91f17ab9851987c7f100e0273cf"" target=""_blank"">0xf69c45b4246fd91f17ab9851987c7f100e0273cf</a>"
>0xea674fdde714fd979de3edf0f56aa9716b898ec8<
- If the type has been previously minted (e.g. there's already a
DAO_v2.csv
in theinputs
folder andDAO_v2.log
in theoutputs
folder), please rename the old file to avoid being overridden. (e.g. Rename the old file intoDAO_v2_batch1.csv
andDAO_v2_batch1.log
respectively).
-
Change parameters in
tasks/batchMint.ts
based on the "Request to mint NFT":
const deadline = 1642424400; // Jan 17th 2022, 14:
// Diamond: 5% Gold: 3% Silver: 2% Bronze: 1%
const boost = {
"diamond": rate(5),
"gold": rate(3),
"silver": rate(2),
"bronze": rate(1)
};
Each NFT has a deadline
, before which the boost can be redeemed in the staking contract.
boost
object contains key-value pairs, where the key is the "rank" of the Boost NFT and the value is the APY. E.g. rate(5)
gives the boost factor for a 5% APY. Note that the key is also case-sensitive. It should be the same as entries of the grade
column of the input csv.
Important note: In most cases you only need to change the value inside rate($value)
. Unless needed, do not change neither the deadline
nor the bost key attributes (i.e. diamond
, gold
, silver
, bronze
). If during log the apy
shows NaN
you likely have an error and need to ensure the *.csv
grade
column matches tasks/batchMint.ts
boost
key-value map.
- Save the minter's private key in the
.env
file
MINTER_KEY=0x123...xyz
- Test locally with
If you want to save the output log under
outputs
folder, run
NAME="<replace this with type name>" yarn batchmint:local:save-log
e.g.
NAME="DAO_v2" yarn batchmint:local:save-log
If you don't want or have trouble saving the output file, run
NAME="<replace this with type name>" yarn batchmint:local
e.g.
NAME="DAO_v2" yarn batchmint:local
- Mint in production, run
If you want to save the output log under
outputs
folder, run
NAME="<replace this with type name>" yarn batchmint:xdai:save-log
e.g.
NAME="DAO_v2" yarn batchmint:xdai:save-log
If you don't want or have trouble saving the output file, run
NAME="<replace this with type name>" yarn batchmint:xdai
e.g.
NAME="DAO_v2" yarn batchmint:xdai
- Minter renounces its
MINTER_ROLE
or let the HOPR Association MS revoke minter's accountMINTER_ROLE
onHoprBoost
smart contract. To renounce itsMINTER_ROLE
,- Go to Boost contract on blockscout explorer and connect to MetaMask.
- Insert "
0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6
" and<your account address>
into fields "7. renounceRole" → "role(bytes32)" and "account(address)" respectively and click "Write".
- Create a new branch
nft/<type>
and commit input csv and output logs. E.g.
git checkout -b nft/Wildhorn_v2
git add .
git commit -am "Mint NFT Wildhorn_v2"
git push --set-upstream origin nft/Wildhorn_v2
and create a pull request to main
base in the hopr-stake repo and merge it.
This incentive program will take place on the xDAI chain - Locking xHOPR to receive wxHOPR rewards.
Two smart contracts are introduced for this incentive program:
HoprBoost
- NFT for additional (all the boosts except for “basic locking rewards” and “seed investor rewards”) testing rewards. Boost NFTs can be freely traded on the market.HoprIncentiveLock
- The actual contract for locking tokens and claiming rewards.
inherit ownable
, ERC777Recipient
, ERC721Receiver
, (IERC677Recipient
), ReentrancyGuard
contract
- BASIC_START: [uint256] Block timestamp at which incentive program starts for accounts that stake real
LOCK_TOKEN
. Default value is1627387200
(July 27th 2021 14:00 CET). - SEED_START: [uint256] Block timestamp at which incentive program starts for seed investors that promise to stake their unreleased tokens. Default value is
1630065600
(August 27th 2021 14:00 CET). - PROGRAM_END: [uint256] Block timestamp at which incentive program ends. From this timestamp on, tokens can be unlocked. Default value is
1642424400
(Jan 17th 2022 14:00 CET). - FACTOR_DENOMINATOR: [uint256] Denominator of the “Basic reward factor”. Default value is
1e12
. - BASIC_FACTOR_NUMERATOR: [uint256] Numerator of the “Basic reward factor”, for all accounts (except for seed investors) that participate in the program. Default value is
5787
, which corresponds to 5.787/1e9 per second. Its associated denominator isFACTOR_DENOMINATOR
. - SEED_FACTOR_NUMERATOR: [uint256] Numerator of the “Seed investor reward factor”, for all accounts (except for seed investors) that participate in the program. Default value is
7032
, which corresponds to 7.032/1e9 per second. Its associated denominator isFACTOR_DENOMINATOR
. - BOOST_CAP: [uint256] Cap on actual locked tokens for receiving additional boosts. Default value is 1 million (
1e24
). - LOCK_TOKEN: [address] Token that HOPR holders need to lock to the contract.
xHOPR
address. - REWARD_TOKEN: [address] Token that HOPR holders can claim as rewards.
wxHOPR
address. - nftContract: [address] Address of the NFT smart contract.
- redeemedNft: [mapping(address=>mapping(uint256=>uint256))] Redeemed NFT per account, structured as “account -> index -> NFT tokenId”. The detailed “boost factor, cap, and boost start timestamp” is saved in the “HoprBoost NFT” contract (see section below).
- redeemedNftIndex: [mapping(address=>uint256)] The last index of redeemed NFT of an account. It defines the length of the “redeemed factor” mapping.
- redeemedFactor: [mapping(address=>mapping(uint256=>uint256))] Redeemed boost factors per account, structured as “account -> index -> NFT tokenId”. The detailed “boost factor, cap, and boost start timestamp” is saved in the “HoprBoost NFT” contract (see section below).
- redeemedFactorIndex: [mapping(address=>uint256)] The last index of the redeemed boost factor of an account. It defines the length of the “redeemed factor” mapping.
- accounts: [mapping(address=>Account)] It stores the locked token amount, earned and claimed rewards per account. “Account” structure is defined as
- actualLockedTokenAmount: [uint256] The amount of LOCK_TOKEN being actually locked to the contract. Those tokens can be withdrawn after “UNLOCK_START”
- virtualLockedTokenAmount: [uint256] The amount of LOCK_TOKEN token being virtually locked to the contract. This field is only relevant to seed investors. Those tokens cannot be withdrawn after “UNLOCK_START”.
- lastSyncTimestamp: [uint256] Timestamp at which any “Account” attribute gets synced for the last time. “Sync” happens when tokens are locked, a new boost factor is redeemed, rewards get claimed. When syncing, “cumulatedRewards” is updated.
- cumulatedRewards: [uint256] Rewards accredited to the account at “lastSyncTimestamp”.
- claimedRewards: [uint256] Rewards claimed by the account.
- totalLocked: [uint256] Total amount of tokens being locked in the incentive program. Virtual token locks are not taken into account.
- availableReward: [uint256] Total amount of reward tokens currently available in the lock.
- constructor(_nftAddress: address, _newOwner: address): Provide NFT contract address. Transfer owner role to the new owner address. This new owner can reclaim any ERC20 and ERC721 token being accidentally sent to the lock contract. At deployment, it also registers the lock contract as an
ERC777recipient
. onTokenTransfer
(from:address, tokenAmount: uint256): ERC677 hook. Holders can lock tokens by sending theirLOCK_TOKEN
tokens viatransferAndCall()
function to the lock contract. BeforePROGRAM_END
, it accepts tokens, update “Account” {+tokenAmount, +0, block.timestamp, 0, 0} in accounts - mapping, sync Account state, and updatetotalLocked
; AfterPROGRAM_END
, it refuses tokens.- tokensReceived(tokenAmount:uint256) ERC777 hook. HOPR association sends their tokens to fuel the reward pool. It updates the
availableReward
bytokenAmount
. - lock(accounts: address[], cap: uint256[]) Only owner can call this function to store virtual lock for seed investors. If the investor hasn't locked any token in this account, create an "Account" with {0, cap[i], block.timestamp, 0, 0}. If the investor has locked some tokens in this account, update its
virtualLockedTokenAmount
. - _getCumulatedRewardsIncrement(account:address) Calculates the increment of cumulated rewards during the
lastSyncTimestamp
and block.timestamp. - _sync(account: address) Update “lastSyncTimestamp” with the current block timestamp and update
cumulatedRewards
with_getCumulatedRewardsIncrement(account)
- sync(account: address) Public function for
_sync(account)
. - onERC721Received(tokenId: uint256): Able to receive ERC721 tokens. If the ERC721 is
nftContract
(a.k.a HoprBoost NFT), redeem the HoprBoost NFT by adding the token Id to account’sredeemedFactor
, updatingredeemedFactorIndex
, syncing the account and burning the HoprBoost NFT. This relieves accounts from “approving” + “transferring” NFT. HOPR Boost can thus be redeemed with ERC721 token safeTransferFrom. If the ERC721 is NOTnftContract
, refuse reception. - _claim(account: address): send
REWARD_TOKEN
(Account.cumulatedRewards - Account.claimedRewards
) to the account. UpdateAccount.claimedRewards
and reduceavailableReward
. - claimRewards(account: address):
_sync(account)
and_claim(account)
. unlock
(account: address): If block.timestamp >= UNLOCK_START, it executes_sync(account)
,_claim(account)
and also sends theAccount.actualLockedTokenAmount
back to the account. UpdatetotalLocked
.- getCumulatedRewardsIncrement(account:address) publicly callable and returns
_getCumulatedRewardsIncrement(account)
. - reclaimErc20Tokens(tokenAddress: address) Can only be called by the owner. Reclaim all the ERC20 tokens accidentally sent to the lock contract. For
LOCK_TOKEN
, it removes the difference between the current lock'sLOCK_TOKEN
balance and totalLocked. ForREWARD_TOKEN
, it removes the difference between the current lock'sREWARD_TOKEN
balance andavailableReward
. - reclaimErc721Tokens(tokenAddress: address, tokenId: uint256) Can only be called by the owner. Reclaim all the ERC721 tokens accidentally sent to the lock contract.
Inherit from IHoprBoost
, AccessControlEnumerable
, ERC721URIStorage
, ERC721Enumerable
, ReentrancyGuard
.
- MINTER_ROLE:[bytes32] Identifier of the minter role.
- _boostType: [EnumerableStringSet.StringSet] Backward researchable mapping of boost types (“boost type index” ⇔ “boost type”)
- _boostNumerator: [mapping(uint256=>uint256)] “tokenId -> boost factor”
- _redeemDeadline: [mapping(uint256=>uint256)] “tokenId -> deadline for redeeming a boost”
- _boostTypeIndexOfId: [mapping(uint256=>uint256)] “tokenId -> boost type index”
- constructor("HOPR Boost NFT", "HOPR Boost", _newAdmin: address) Provide name and symbol for ERC721. Set a new admin role. Set the new admin as a minter. Provide the base token URI (for frontend).
- updateBaseURI(baseURI: string) Called by the owner to reset the website where metadata is hosted. Most likely we'll use Pinata. ERC721 token metadata is defined as:
{ "type": "DAO participant", "rank": "gold", "image": "https://badge.example/item-id-8u5h2m.png", "deadline": 1626080323, "boost": 0.05 // daily reward in percentage }
- boostOf (uint256): [uint256, uint256] “tokenId -> (boost factor, boost deadline)” Boost factor per second. It is converted with the function:
APY = boostFactor * 3600 * 24 / 1e12 * 365.
- typeIndexOf (uint256): [uint256] “tokenId -> boost type index”. Type of boost.
- typeOf(uint256): [string] “tokenId -> boost type name”. E.g. “DAO participant”. Type names are case sensitive.
- typeAt(uint256): [string] “typeIndex -> boost type name”. Return type name of a given type index.
- mint(to: address, boostType: string, boostRank: string, boostNumerator: uint256, redeemDeadline: uint256) Called by minter to airdrop NFT tokens to accounts. If the boost type does not exist, create a new type.
TokenURIs
are created as${boostType}/${boostRank}
. - batchMint(to: address[], boostType: string, boostRank: string, boostNumerator: uint256, redeemDeadline: uint256) Called by a minter to create multiple boost factor NFT of the same type and rank.
- safeTransferFrom(from:address, to:address, tokenId: uint256) Function that can be used for redeeming boost. It calls
onERC721Received
under the hood, which triggers the NFT-redeem process. - reclaimERC20Tokens(tokenAddress: address) Only called by the owner. Reclaim all the ERC20 tokens.
- reclaimERC721Tokens(tokenAddress: address, tokenId: uint256) Owner only. It returns all the ERC721 being accidentally sent to the contract.
- supportsInterface(bytes4): Specify interfaces according to ERC165.
- tokenURI(): Returns the URI string
- _baseURI(): get the baseURI of the ERC721.
- _beforeTokenTransfer(): function being called when transferring the ERC721 token
- _burn(): override to prevent burning
- _mintBoost(): register boost information when mint a boost ERC721 token.