Contract keeping tracking of voting/reward weights and handling payout of rewards. ERC20 compatible but transfers are disabled.
Owner of ERC20NonTransferableRewardsOwned
and handles the locking and unlocking of depositToken
and mints/burns ERC20NonTransferableRewardsOwned
on deposit and withdraw.
The SharesTimeLock
contract allows users to deposit the depositToken
and lock it to receive stakedDepositToken
, which represents a share in the total voting and reward weight.
The duration of the lock is limited to 36 months and is at minimum 1 month. The voting and reward weight for each lock time is determined by the maxRatioArray
in SharesTimeLock.sol
.
Once locked the depositToken
cannot be withdrawn early but can be locked again for the max duration by calling: boostToMax
this extends your lock to the max duration and if the lock is longer than the previous one mints you more stakedDepositToken
.
If a user's lock expires he should not be entitled anymore to a share of the voting and reward weight. Due to the nature of how smart contracts work this ejection needs to be done actively. Any user can remove an expired lock
from staking by calling the eject
function. Other stakers are incentivised to do so to because it gives them a bigger share of the voting and reward weight.
For users to be able to claim their rewards they need to participate in offchain voting. Participation is tracked ofchain and tracked using a merkle tree, the root of this tree is tracked as participationMerkleRoot
.
A user can be in the 3 following states:
When an address is not included in the merkle tree it cannot claim rewards
When an address is included into the tree and its value is set to 0
it has been inactive and the rewards accrued can be redistributed to other stakers by calling redistribute
.
When an address is included into the tree and its value is set to 1
it has been active and the rewards can be claimed by calling claim
. Rewards can also be claimed for another address using claimFor
yarn test
Runs all tests in test/
yarn coverage
Runs all tests with solidity-coverage and generates a coverage report.
yarn compile
Compiles artifacts into artifacts/
and generates typechain interfaces in typechain/
yarn lint
Runs solhint against the contracts.
A crude implementation of a script which looks at DOUGH holder participation in snapshot and generates a JSON file of participation can be run like so (takes some time):
npx hardhat generate-participation --output participation.json --inactive-time 1611824460 --network mainnet
npx hardhat generate-leafs --input participation --output merkleLeafs.json
These leafs can be used in applications to generate merkle proofs or compute the root
Will log the merkle root which can be set in the dToken contract to update participation
npx hardhat generate-merkle-root --input merkleLeafs.json
If for whatever reason you need to generate a proof outside the UI you can do it through the following command
npx hardhat generate-proof --input merkleLeafs.json --output proof.json --address 0x8EDAB1576B34b0BFdcdF4F368eFDE5200ff6F4e8
npx hardhat deploy-staking --deposit-token [DOUGH_ADDRESS] --reward-token [REWARD_ADDRESS] --name veDOUGH --symbol veDOUGH --min-lock-duration 15552000 --max-lock-duration 93312000 --min-lock-amount [min amount to lock in wei] --network mainnet
Users can lock their DOUGH for 6-36 months. They can do so by calling the following function. The amount of tokens deposited needs to be approved
by the caller first.
function depositByMonths(uint256 amount, uint256 months, address receiver) external;
After a lock has expired it can be withdrawn.
function withdraw(uint256 lockId) external;
When a user has staked for a shorter duration than 36 months or they want to extend their lock they can do so by boosting it. This deletes the old lock and generates a new one with a duration of 36 months
function boostToMax(uint256 lockId) external;
When a lock expired it can be ejected by anyone. Stakers are incentivised to do this to increase their proportional share of the rewards. Ejection can be done in batches:
function eject(address[] memory lockAccounts, uint256[] memory lockIds) external;
To prevent small locks interfering with the ejections a reasonable min lock should be set. This can be upgraded by the owner
function setMinLockAmount(uint256 minLockAmount_) external;
Only whitelisted contracts can lock tokens or deposit to an address other than themselves. To whitelist an address from the owner
function setWhitelisted(address user, bool isWhitelisted) external;
Get all staking data for an address
function getStakingData(address account) external view returns (StakingData memory data);
// StakingData data structure
struct StakingData {
uint256 totalStaked; // total amount of DOUGH staked
uint256 veTokenTotalSupply; // total amount of veDOUGH
uint256 accountVeTokenBalance; // account veDOUGH balance
uint256 accountWithdrawableRewards; // amount of RWRD an account can withdraw (not taking into consideration participation)
uint256 accountWithdrawnRewards; // amount of RWRD withdrawn by this address
uint256 accountDepositTokenBalance; // DOUGH balance of account
uint256 accountDepositTokenAllowance; // DOUGH approved to SharesTimeLock contract
Lock[] accountLocks; // Locks of an account
}
// Lock data structure
struct Lock {
uint256 amount; // amount locked
uint32 lockedAt; // timestamp when tokens were locked
uint32 lockDuration; // duration of lock
}
If a lock can be ejected or not
function canEject(address account, uint256 lockId) public view returns(bool);
This contract supports all ERC20 read functions to fetch balances and the totalSupply
Claim rewards for another address submitting a proof of participation
function claimFor(address account, bytes32[] memory proof) external;
Claim rewards for the calling address submitting a proof of participation
function claim(bytes32[] calldata proof) external;
When a user has been flagged in the merkle tree as inactive their rewards can be redistributed to other stakers by anyone. Anyone can do so in batches by submitting a proof of inactivity for a specific account.
function redistribute(address[] calldata accounts, bytes32[][] calldata proofs) external;
Any address is able to distribute rewards to stakers by approving and having a sufficient amount of RWRD tokens.
function distributeRewards(uint256 amount) external;
The maintainer can set the merkle root.
function setParticipationMerkleRoot(bytes32 newParticipationMerkleRoot) external;