Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 06287e3

Browse files
authored
Merge pull request stabilitydao#18 from stabilitydao/draft/profit-maker
Profit Maker NFT
2 parents 3d635a0 + 27f26d9 commit 06287e3

File tree

10 files changed

+1354
-32
lines changed

10 files changed

+1354
-32
lines changed

contracts/governance/Gov.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ contract Gov is Initializable, GovernorUpgradeable, GovernorSettingsUpgradeable,
121121
super._setFTMultiplier(id, multiplier);
122122
}
123123

124-
function addNFT(ERC721VotesUpgradeable token, uint256 multiplier) public onlyRole(POWER_CHANGER_ROLE) {
125-
super._addNFT(token, multiplier);
124+
function addNFT(ERC721VotesUpgradeable token, uint256 multiplier, bool noQuorum) public onlyRole(POWER_CHANGER_ROLE) {
125+
super._addNFT(token, multiplier, noQuorum);
126126
}
127127

128128
function setNFTMultiplier(uint256 id, uint256 multiplier) public onlyRole(POWER_CHANGER_ROLE) {

contracts/governance/GovVotes.sol

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ abstract contract GovVotes is Initializable, GovernorUpgradeable {
2424
struct GovNFT {
2525
ERC721VotesUpgradeable token;
2626
uint256 multiplier;
27+
bool noQuorum;
2728
}
2829

30+
uint64 public VOTES_BASE_MULTIPLIER;
31+
2932
GovFT[] public govFT;
3033
GovNFT[] public govNFT;
3134

@@ -44,10 +47,11 @@ abstract contract GovVotes is Initializable, GovernorUpgradeable {
4447
votes += govNFT[i].token.getPastVotes(account, blockNumber).mul(govNFT[i].multiplier);
4548
}
4649

47-
return votes.div(1000);
50+
return votes.div(VOTES_BASE_MULTIPLIER);
4851
}
4952

5053
function __GovVotes_init(ERC20VotesUpgradeable tokenAddress) internal initializer {
54+
VOTES_BASE_MULTIPLIER = 1000;
5155
__Context_init_unchained();
5256
__ERC165_init_unchained();
5357
__IGovernor_init_unchained();
@@ -57,7 +61,7 @@ abstract contract GovVotes is Initializable, GovernorUpgradeable {
5761
function __GovernorVotes_init_unchained(ERC20VotesUpgradeable tokenAddress) internal initializer {
5862
govFT.push(GovFT({
5963
token: tokenAddress,
60-
multiplier: 1 * 1000 // 1x == 1000
64+
multiplier: VOTES_BASE_MULTIPLIER // 1x == 1000
6165
}));
6266
}
6367

@@ -72,10 +76,11 @@ abstract contract GovVotes is Initializable, GovernorUpgradeable {
7276
govFT[id].multiplier = multiplier;
7377
}
7478

75-
function _addNFT(ERC721VotesUpgradeable token, uint256 multiplier) internal virtual {
79+
function _addNFT(ERC721VotesUpgradeable token, uint256 multiplier, bool noQuorum_) internal virtual {
7680
govNFT.push(GovNFT({
7781
token: token,
78-
multiplier: multiplier // 1x == 1000
82+
multiplier: multiplier, // 1x == 1000
83+
noQuorum: noQuorum_
7984
}));
8085
}
8186

contracts/governance/GovVotesQuorumFraction.sol

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ abstract contract GovVotesQuorumFraction is Initializable, GovVotes {
4242
uint256 totalSupply;
4343
uint256 length = govFT.length;
4444
for (uint256 i; i < length; i++) {
45-
totalSupply += govFT[i].token.getPastTotalSupply(blockNumber).mul(govFT[i].multiplier).div(1000);
45+
totalSupply += govFT[i].token.getPastTotalSupply(blockNumber).mul(govFT[i].multiplier).div(VOTES_BASE_MULTIPLIER);
4646
}
4747

4848
length = govNFT.length;
4949
for (uint256 i; i < length; i++) {
50-
totalSupply += govNFT[i].token.getPastTotalSupply(blockNumber).mul(govNFT[i].multiplier).div(1000);
50+
if (!govNFT[i].noQuorum) {
51+
totalSupply += govNFT[i].token.getPastTotalSupply(blockNumber).mul(govNFT[i].multiplier).div(VOTES_BASE_MULTIPLIER);
52+
}
5153
}
5254

5355
return (totalSupply * quorumNumerator()) / quorumDenominator();

contracts/token/ProfitMaker.sol

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.9;
3+
4+
import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol";
5+
import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
6+
import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
7+
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol";
8+
import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/draft-ERC721VotesUpgradeable.sol";
9+
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
10+
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
11+
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
12+
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";
13+
14+
contract ProfitMaker is Initializable, ERC721Upgradeable, ERC721VotesUpgradeable, ERC721EnumerableUpgradeable, OwnableUpgradeable, UUPSUpgradeable {
15+
using CountersUpgradeable for CountersUpgradeable.Counter;
16+
17+
event Harvest(address indexed token, address recipient, uint256 amount);
18+
event Released(address indexed token, uint256 amount);
19+
event SetMintTime(uint64 from, uint64 to);
20+
21+
IERC20Upgradeable public profitToken;
22+
uint64 public mintingStart;
23+
uint64 public mintingEnd;
24+
25+
struct Unlock {
26+
uint64 start;
27+
uint64 duration;
28+
uint256 released;
29+
uint256[] balances;
30+
}
31+
32+
mapping(address => Unlock) public unlocks;
33+
34+
CountersUpgradeable.Counter private _tokenIdCounter;
35+
36+
/// @custom:oz-upgrades-unsafe-allow constructor
37+
constructor() initializer {}
38+
39+
function initialize(IERC20Upgradeable profitToken_) public initializer {
40+
profitToken = profitToken_;
41+
__ERC721_init("Profit Maker", "PM");
42+
__ERC721Enumerable_init();
43+
__Ownable_init();
44+
__UUPSUpgradeable_init();
45+
}
46+
47+
function _baseURI() internal pure override returns (string memory) {
48+
return "https://api.stabilitydao.org/maker/";
49+
}
50+
51+
function safeMint(address to) public {
52+
require(profitToken.balanceOf(msg.sender) >= 10000 ether, "Not enough PROFIT tokens");
53+
require(mintingStart <= uint64(block.timestamp), "Mint is not available right now");
54+
require(mintingEnd >= uint64(block.timestamp), "Mint is not available right now");
55+
uint256 tokenId = _tokenIdCounter.current();
56+
require(tokenId < 80, "All tokens have already been minted");
57+
profitToken.transferFrom(msg.sender, address(this), 10000 ether);
58+
_tokenIdCounter.increment();
59+
_safeMint(to, tokenId);
60+
}
61+
62+
function setUnlock(address token_, uint64 start_, uint64 duration_) public onlyOwner {
63+
unlocks[token_].start = start_;
64+
unlocks[token_].duration = duration_;
65+
}
66+
67+
/**
68+
* @dev Amount of token already released
69+
*/
70+
function released(address token_) public view virtual returns (uint256) {
71+
return unlocks[token_].released;
72+
}
73+
74+
function balanceToHarvest(address token_, uint256 tokenId_) public view returns (uint256) {
75+
require(ownerOf(tokenId_) == msg.sender, "You are not owner of token.");
76+
require(unlocks[token_].start > 0, "Token dont have unlock.");
77+
return unlocks[token_].balances[tokenId_];
78+
}
79+
80+
/**
81+
* @dev Withdraw user balance
82+
*/
83+
function harvest(address token_, uint256 tokenId_) public {
84+
require(ownerOf(tokenId_) == msg.sender, "You are not owner of token.");
85+
require(unlocks[token_].start > 0, "Token dont have unlock.");
86+
uint256 releasable = unlocks[token_].balances[tokenId_];
87+
require(releasable > 0, "No tokens to harvest");
88+
unlocks[token_].balances[tokenId_] = 0;
89+
SafeERC20Upgradeable.safeTransfer(IERC20Upgradeable(token_), msg.sender, releasable);
90+
emit Harvest(token_, msg.sender, releasable);
91+
}
92+
93+
/**
94+
* @dev Release the tokens that have already vested.
95+
*
96+
* Emits a {Released} event.
97+
*/
98+
function releaseToBalance(address token) public {
99+
uint256 releasable = vestedAmount(token, uint64(block.timestamp)) - released(token);
100+
require(releasable > 0, "Zero to release");
101+
102+
unlocks[token].released += releasable;
103+
104+
uint256 totalUsers = _tokenIdCounter.current();
105+
uint256 toRelease = releasable / totalUsers;
106+
for (uint256 i; i <= totalUsers; i++) {
107+
if (unlocks[token].balances.length > i) {
108+
unlocks[token].balances[i] += toRelease;
109+
} else {
110+
unlocks[token].balances.push(toRelease);
111+
}
112+
}
113+
114+
emit Released(token, releasable);
115+
}
116+
117+
/**
118+
* @dev Calculates the amount of tokens that has already vested. Default implementation is a linear vesting curve.
119+
*/
120+
function vestedAmount(address token, uint64 timestamp) public view virtual returns (uint256) {
121+
return _vestingSchedule(token, IERC20Upgradeable(token).balanceOf(address(this)) + released(token), timestamp);
122+
}
123+
124+
/**
125+
* @dev Virtual implementation of the vesting formula. This returns the amout vested, as a function of time, for
126+
* an asset given its total historical allocation.
127+
*/
128+
function _vestingSchedule(address token, uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) {
129+
if (unlocks[token].start == 0 || timestamp < unlocks[token].start) {
130+
return 0;
131+
} else if (timestamp > unlocks[token].start + unlocks[token].duration) {
132+
return totalAllocation;
133+
} else {
134+
return (totalAllocation * (timestamp - unlocks[token].start)) / unlocks[token].duration;
135+
}
136+
}
137+
138+
function setMintState(uint64 start_, uint64 end_) public onlyOwner {
139+
mintingStart = start_;
140+
mintingEnd = end_;
141+
emit SetMintTime(start_, end_);
142+
}
143+
144+
function _authorizeUpgrade(address newImplementation)
145+
internal
146+
onlyOwner
147+
override
148+
{}
149+
150+
// The following functions are overrides required by Solidity.
151+
152+
function _beforeTokenTransfer(address from, address to, uint256 tokenId)
153+
internal
154+
override(ERC721Upgradeable, ERC721EnumerableUpgradeable)
155+
{
156+
super._beforeTokenTransfer(from, to, tokenId);
157+
}
158+
159+
function _afterTokenTransfer(
160+
address from,
161+
address to,
162+
uint256 tokenId
163+
) internal virtual override(ERC721Upgradeable, ERC721VotesUpgradeable) {
164+
super._afterTokenTransfer(from, to, tokenId);
165+
}
166+
167+
function supportsInterface(bytes4 interfaceId)
168+
public
169+
view
170+
override(ERC721Upgradeable, ERC721EnumerableUpgradeable)
171+
returns (bool)
172+
{
173+
return super.supportsInterface(interfaceId);
174+
}
175+
}

deploy/ProfitMaker.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const { ethers, upgrades } = require('hardhat')
2+
3+
module.exports = async ({ getNamedAccounts, deployments, getChainId }) => {
4+
// type proxy address for upgrade contract
5+
// deployer must have upgrade access
6+
const upgradeProxy = null // poly: '0x..'
7+
8+
const { save, get } = deployments
9+
const { deployer } = await getNamedAccounts()
10+
const chainId = await getChainId()
11+
12+
console.log('')
13+
14+
// noinspection PointlessBooleanExpressionJS
15+
if (!upgradeProxy) {
16+
console.log(`== ProfitMaker deployment to ${hre.network.name} ==`)
17+
try {
18+
const deplpoyment = await get('ProfitMaker')
19+
console.log(
20+
`ProfitMaker already deployed to ${hre.network.name} at ${deplpoyment.address}`
21+
)
22+
return
23+
} catch (e) {
24+
// not deployed yet
25+
}
26+
} else {
27+
console.log(`==== ProfitMaker upgrade at ${hre.network.name} ====`)
28+
console.log(`Proxy address: ${upgradeProxy}`)
29+
}
30+
31+
const token = await deployments.get('ProfitToken')
32+
33+
console.log('ChainId:', chainId)
34+
console.log('Deployer address:', deployer)
35+
console.log('ProfitToken address:', token.address)
36+
37+
// noinspection PointlessBooleanExpressionJS
38+
if (!upgradeProxy) {
39+
// return
40+
const ProfitMakerFactory = await ethers.getContractFactory('ProfitMaker')
41+
42+
const profitmaker = await upgrades.deployProxy(
43+
ProfitMakerFactory,
44+
[token.address],
45+
{
46+
kind: 'uups',
47+
}
48+
)
49+
50+
await profitmaker.deployed()
51+
52+
const artifact = await hre.artifacts.readArtifact('ProfitMaker')
53+
54+
await save('ProfitMaker', {
55+
address: profitmaker.address,
56+
abi: artifact.abi,
57+
})
58+
59+
let receipt = await profitmaker.deployTransaction.wait()
60+
console.log(
61+
`ProfitMaker proxy deployed at: ${profitmaker.address} (block: ${
62+
receipt.blockNumber
63+
}) with ${receipt.gasUsed.toNumber()} gas`
64+
)
65+
66+
// hardhat verify --network r.. 0x
67+
}
68+
}
69+
70+
module.exports.tags = ['ProfitMaker']
71+
module.exports.dependencies = ['ProfitToken']

hardhat.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'solidity-coverage'
55
import 'hardhat-gas-reporter'
66
import '@openzeppelin/hardhat-upgrades'
77
import '@typechain/hardhat'
8+
import '@nomiclabs/hardhat-web3'
89
require('dotenv').config()
910
const addressses = require('@stabilitydao/addresses/index.cjs')
1011
const { POLYGON, ROPSTEN, RINKEBY, GOERLI, KOVAN, MUMBAI } = addressses

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
"@nomiclabs/hardhat-ethers": "^2.0.2",
3434
"@nomiclabs/hardhat-etherscan": "^2.1.7",
3535
"@nomiclabs/hardhat-waffle": "^2.0.1",
36+
"@nomiclabs/hardhat-web3": "^2.0.0",
3637
"@openzeppelin/contracts": "^4.5.0",
3738
"@openzeppelin/contracts-upgradeable": "^4.5.0",
3839
"@openzeppelin/hardhat-upgrades": "^1.12.0",
40+
"@openzeppelin/test-helpers": "^0.5.15",
3941
"@stabilitydao/addresses": "stabilitydao/addresses",
4042
"@typechain/ethers-v5": "^8.0.5",
4143
"@typechain/hardhat": "^3.0.0",
@@ -54,6 +56,7 @@
5456
"solidity-coverage": "^0.7.17",
5557
"ts-node": "^10.4.0",
5658
"typechain": "^6.0.5",
57-
"typescript": "^4.5.4"
59+
"typescript": "^4.5.4",
60+
"web3": "^1.7.0"
5861
}
5962
}

test/Gov.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ describe('Gov', function () {
422422
})
423423

424424
it('NFT voting', async function () {
425-
await expect(gov.addNFT(govNft.address, 1)).to.be.revertedWith(
425+
await expect(gov.addNFT(govNft.address, 1, true)).to.be.revertedWith(
426426
'is missing role'
427427
)
428428
await gov.grantRole(
@@ -432,7 +432,7 @@ describe('Gov', function () {
432432

433433
// PROFIT token multiplier: 1000
434434
// NFT with multiplier 10 * 1000 * 10**18: 10 PROFIT has same voting power as 1 NFT
435-
await gov.addNFT(govNft.address, ethers.utils.parseEther('10000'))
435+
await gov.addNFT(govNft.address, ethers.utils.parseEther('10000'), false)
436436

437437
await govNft.mint(_deployer.address, 1)
438438
expect(await govNft.balanceOf(_deployer.address)).to.eq(1)
@@ -541,7 +541,7 @@ describe('Gov', function () {
541541
)
542542
).to.eq(ethers.utils.parseEther('8'))
543543

544-
await gov.addNFT(govNft.address, ethers.utils.parseEther('500'))
544+
await gov.addNFT(govNft.address, ethers.utils.parseEther('500'), false)
545545
await govNft.mint(_tester.address, 1)
546546
await govNft.mint(_tester.address, 2)
547547

@@ -580,7 +580,7 @@ describe('Gov', function () {
580580
)
581581
).to.be.not.reverted
582582

583-
// (1000000+1000000+3)×0,01 * 10**18
583+
// without noQuorum '20000030000000000000000' == (1000000+1000000+3)×0,01 * 10**18
584584
expect(
585585
await gov.quorum((await ethers.provider.getBlockNumber()) - 1)
586586
).to.eq('20000030000000000000000')

0 commit comments

Comments
 (0)