diff --git a/contracts/mocks/mDAIMock.sol b/contracts/mocks/mDAIMock.sol new file mode 100644 index 00000000..6e601551 --- /dev/null +++ b/contracts/mocks/mDAIMock.sol @@ -0,0 +1,61 @@ +pragma solidity 0.5.4; + +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; + +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +import "../DSMath.sol"; + +contract mDAIMock is DSMath, ERC20, ERC20Detailed, Ownable { + ERC20 dai; + + uint256 exchangeRate = uint256(100e20).div(99); + + constructor(ERC20 _dai) + public + ERC20() + ERC20Detailed("DMM DAI", "mDAI", 18) + { + dai = _dai; + } + function mint(uint256 daiAmount) public returns (uint256) { + dai.transferFrom(msg.sender, address(this), daiAmount); + uint mintAmount = rdiv(daiAmount, getCurrentExchangeRate()).div(1e9); //div to reduce precision from RAY 1e27 to 1e18 precision of mDAI + _mint( + msg.sender, + mintAmount + ); + return mintAmount; + } + + function redeem(uint256 mdaiAmount) public returns (uint256) { + uint256 daiAmount = rmul( + mdaiAmount, + getCurrentExchangeRate().div(10) + ); + _burn(msg.sender, mdaiAmount); + dai.transfer(msg.sender, daiAmount); + return daiAmount; + } + + function redeemUnderlying(uint256 daiAmount) public returns (uint256) { + uint256 mdaiAmount = rdiv(daiAmount, getCurrentExchangeRate()).div( + 1e9 + ); + _burn(msg.sender, mdaiAmount); + dai.transfer(msg.sender, daiAmount); + return daiAmount; + } + + function exchangeRateCurrent() public returns (uint256) { + exchangeRate += uint256(1e28).div(100); + return exchangeRate; + } + + function getCurrentExchangeRate() public view returns (uint256) { + return exchangeRate; + } + +} \ No newline at end of file diff --git a/stakingModel/contracts/mocks/mDAILowWorthMock.sol b/stakingModel/contracts/mocks/mDAILowWorthMock.sol new file mode 100644 index 00000000..2e2379cf --- /dev/null +++ b/stakingModel/contracts/mocks/mDAILowWorthMock.sol @@ -0,0 +1,62 @@ +pragma solidity 0.5.4; + +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; + +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +import "../../../contracts/DSMath.sol"; + +contract mDAILowWorthMock is DSMath, ERC20, ERC20Detailed, Ownable { + ERC20 dai; + + uint256 exchangeRate = uint256(100e28).div(99); + + constructor(ERC20 _dai) + public + ERC20() + ERC20Detailed("DMM DAI", "mDAI", 18) + { + dai = _dai; + } + function mint(uint256 daiAmount) public returns (uint256) { + dai.transferFrom(msg.sender, address(this), daiAmount); + uint mintAmount = rdiv(daiAmount, exchangeRateStored()).div(1e9); + _mint( + msg.sender, + mintAmount + ); + return mintAmount; + } + + function redeem(uint256 mdaiAmount) public returns (uint256) { + uint256 daiAmount = rmul( + mdaiAmount, + exchangeRateStored().div(10) + ); + + _burn(msg.sender, mdaiAmount); + dai.transfer(msg.sender, daiAmount); + return daiAmount; + } + + function redeemUnderlying(uint256 daiAmount) public returns (uint256) { + uint256 mdaiAmount = rdiv(daiAmount, exchangeRateStored().mul(2)).div( + 1e9 + ); + _burn(msg.sender, mdaiAmount); + dai.transfer(msg.sender, daiAmount.div(2)); + return daiAmount; + } + + function exchangeRateCurrent() public returns (uint256) { + exchangeRate += uint256(1e28).div(100); + return exchangeRate; + } + + function exchangeRateStored() public view returns (uint256) { + return exchangeRate.div(2); + } + +} \ No newline at end of file diff --git a/stakingModel/contracts/mocks/mDAINonMintableMock.sol b/stakingModel/contracts/mocks/mDAINonMintableMock.sol new file mode 100644 index 00000000..ab7f04f6 --- /dev/null +++ b/stakingModel/contracts/mocks/mDAINonMintableMock.sol @@ -0,0 +1,54 @@ +pragma solidity 0.5.4; + +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol"; +import "openzeppelin-solidity/contracts/token/ERC20/ERC20.sol"; + +import "openzeppelin-solidity/contracts/ownership/Ownable.sol"; + +import "../../../contracts/DSMath.sol"; + + +contract mDAINonMintableMock is DSMath, ERC20, ERC20Detailed, Ownable { + ERC20 dai; + + uint256 exchangeRate = uint256(100e28).div(99); + + constructor(ERC20 _dai) public ERC20() ERC20Detailed("DMM DAI", "mDAI", 8) { + dai = _dai; + } + + function mint(uint256 daiAmount) public returns (uint256) { + dai.transferFrom(msg.sender, address(this), daiAmount); + uint mintAmount = rdiv(daiAmount, exchangeRateStored()).div(1e9); + _mint(msg.sender, mintAmount); + return mintAmount; + } + + function redeem(uint256 mdaiAmount) public returns (uint256) { + uint256 daiAmount = rmul( + mdaiAmount, + exchangeRateStored().div(10) + ); + + _burn(msg.sender, mdaiAmount); + dai.transfer(msg.sender, daiAmount); + return daiAmount; + } + + function redeemUnderlying(uint256 daiAmount) public returns (uint256) { + uint256 mdaiAmount = rdiv(daiAmount, exchangeRateStored()).div(1e9); + _burn(msg.sender, mdaiAmount); + dai.transfer(msg.sender, daiAmount); + return daiAmount; + } + + function exchangeRateCurrent() public returns (uint256) { + exchangeRate += uint256(1e28).div(100); + return exchangeRate; + } + + function exchangeRateStored() public view returns (uint256) { + return exchangeRate; + } +} diff --git a/stakingModel/migrations/3_mainnet.js b/stakingModel/migrations/3_mainnet.js index 83a499a0..e0c7ed9b 100644 --- a/stakingModel/migrations/3_mainnet.js +++ b/stakingModel/migrations/3_mainnet.js @@ -2,6 +2,7 @@ const fse = require("fs-extra"); const settings = require("./deploy-settings.json"); const daoAddresses = require("../../releases/deployment.json"); const GoodCompoundStaking = artifacts.require("./GoodCompoundStaking.sol"); +const GoodDMMStaking = artifacts.require("./GoodDMMStaking.sol"); const FundManager = artifacts.require("./GoodFundManager.sol"); const MarketMaker = artifacts.require("./GoodMarketMaker.sol"); const Reserve = artifacts.require("./GoodReserveCDai.sol"); @@ -9,6 +10,7 @@ const Contribution = artifacts.require("./ContributionCalculation.sol"); const BridgeMock = artifacts.require("./BridgeMock.sol"); const DAIMock = artifacts.require("./DAIMock.sol"); const cDAIMock = artifacts.require("./cDAIMock.sol"); +const mDAIMock = artifacts.require("./mDAIMock.sol"); const AbsoluteVote = artifacts.require("./AbsoluteVote.sol"); const SchemeRegistrar = artifacts.require("./SchemeRegistrar.sol"); @@ -37,20 +39,23 @@ module.exports = async function(deployer, network) { const maindao = daoAddresses[network]; const homedao = daoAddresses[homeNetwork]; - let foreignBridgeAddr, daiAddress, cdaiAddress; + let foreignBridgeAddr, daiAddress, cdaiAddress, mdaiAddress; if (network == "test" || network == "develop") { const [foreignBridge, dai] = await Promise.all([ deployer.deploy(BridgeMock, maindao.GoodDollar), deployer.deploy(DAIMock) ]); const cdai = await deployer.deploy(cDAIMock, dai.address); + const mdai = await deployer.deploy(mDAIMock, dai.address); foreignBridgeAddr = foreignBridge.address; daiAddress = dai.address; cdaiAddress = cdai.address; + mdaiAddress = mdai.address; } else { foreignBridgeAddr = maindao.ForeignBridge._foreignBridge; daiAddress = networkSettings.daiAddress; cdaiAddress = networkSettings.cdaiAddress; + mdaiAddress = networkSettings.mdaiAddress; // need to add correct address in deployment script } const ubiBridgeRecipient = networkAddresses.UBIScheme; const homeAvatar = homedao.Avatar; @@ -101,6 +106,16 @@ module.exports = async function(deployer, network) { maindao.Identity ); + const goodDMMStakingP = deployer.deploy( + GoodDMMStaking, + daiAddress, + mdaiAddress, + fundManager.address, + networkSettings.blockInterval, + maindao.Avatar, + maindao.Identity + ); + const reserveP = deployer.deploy( Reserve, daiAddress, @@ -113,20 +128,28 @@ module.exports = async function(deployer, network) { contribcalc.address, networkSettings.blockInterval ); - const [goodCompoundStaking, reserve] = await Promise.all([goodCompoundStakingP, reserveP]); + const [goodCompoundStaking, goodDMMStaking, reserve] = await Promise.all([goodCompoundStakingP, goodDMMStakingP, reserveP]); await marketmaker.initializeToken( cdaiAddress, "100", //1gd "10000", //0.0001 cDai "1000000" //100% rr ); + + await marketmaker.initializeToken( + mdaiAddress, + "100", //1gd + "10000", //0.0001 cDai + "1000000" //100% rr + ); + await marketmaker.transferOwnership(reserve.address); console.log("proposing reserve and fundmanager to DAO"); const absoluteVote = await AbsoluteVote.at(maindao.AbsoluteVote); const schemeRegistrar = await SchemeRegistrar.at(maindao.SchemeRegistrar); - const [p1, p2, p3] = await Promise.all([ + const [p1, p2, p3, p4] = await Promise.all([ schemeRegistrar.proposeScheme( maindao.Avatar, reserve.address, @@ -147,22 +170,31 @@ module.exports = async function(deployer, network) { NULL_HASH, "0x00000010", NULL_HASH + ), + schemeRegistrar.proposeScheme( + maindao.Avatar, + goodDMMStaking.address, + NULL_HASH, + "0x00000010", + NULL_HASH ) ]); let proposalId1 = p1.logs[0].args._proposalId; let proposalId2 = p2.logs[0].args._proposalId; let proposalId3 = p3.logs[0].args._proposalId; + let proposalId4 = p4.logs[0].args._proposalId; console.log("voting..."); await Promise.all([ ...founders.map(f => absoluteVote.vote(proposalId1, 1, 0, f)), ...founders.map(f => absoluteVote.vote(proposalId2, 1, 0, f)), - ...founders.map(f => absoluteVote.vote(proposalId3, 1, 0, f)) + ...founders.map(f => absoluteVote.vote(proposalId3, 1, 0, f)), + ...founders.map(f => absoluteVote.vote(proposalId4, 1, 0, f)) ]); console.log("starting..."); - await Promise.all([reserve.start(), fundManager.start(), goodCompoundStaking.start()]); + await Promise.all([reserve.start(), fundManager.start(), goodCompoundStaking.start(), goodDMMStaking.start()]); console.log("deploying fund manager setReserve scheme..."); const setReserve = await deployer.deploy( @@ -194,12 +226,14 @@ module.exports = async function(deployer, network) { let releasedContracts = { ...networkAddresses, FundManager: fundManager.address, - DAIStaking: goodCompoundStaking.address, + DAICompoundStaking: goodCompoundStaking.address, + DAIDMMStaking: goodDMMStaking.address, Reserve: reserve.address, MarketMaker: marketmaker.address, Contribution: contribcalc.address, DAI: daiAddress, cDAI: cdaiAddress, + mDAI: mdaiAddress, ForeignBridge: foreignBridgeAddr, network, networkId: parseInt(deployer.network_id) diff --git a/stakingModel/migrations/4_deploy_dev.js b/stakingModel/migrations/4_deploy_dev.js index 2e21564e..ed1088d9 100644 --- a/stakingModel/migrations/4_deploy_dev.js +++ b/stakingModel/migrations/4_deploy_dev.js @@ -1,9 +1,11 @@ const fse = require("fs-extra"); const settings = require("./deploy-settings.json"); const GoodCompoundStaking = artifacts.require("./GoodCompoundStaking.sol"); +const GoodDMMStaking = artifacts.require("./GoodDMMStaking.sol"); const Reserve = artifacts.require("./GoodReserveCDai.sol"); const DAIMock = artifacts.require("./DAIMock.sol"); const cDAIMock = artifacts.require("./cDAIMock.sol"); +const mDAIMock = artifacts.require("./mDAIMock.sol"); const GoodDollar = artifacts.require("./GoodDollar.sol"); const UBIScheme = artifacts.require("./UBIScheme.sol"); @@ -38,11 +40,13 @@ module.exports = async function(deployer, network) { let staking_mainnet_addresses = staking_deployment[network]; const dai = await DAIMock.at(staking_mainnet_addresses.DAI); const cDAI = await cDAIMock.at(staking_mainnet_addresses.cDAI); - const simpleStaking = await GoodCompoundStaking.at(staking_mainnet_addresses.DAIStaking); + const mDAI = await mDAIMock.at(staking_mainnet_addresses.mDAI); + const simpleStaking = await GoodCompoundStaking.at(staking_mainnet_addresses.DAICompoundStaking); + const simpleStakingDMM = await GoodDMMStaking.at(staking_mainnet_addresses.DAIDMMStaking); const goodReserve = await Reserve.at(staking_mainnet_addresses.Reserve); console.log("minting dai"); - await dai.allocateTo(accounts[0], web3.utils.toWei("100", "ether")); + await dai.allocateTo(accounts[0], web3.utils.toWei("200", "ether")); const approveStaking = dai.approve( simpleStaking.address, @@ -50,35 +54,60 @@ module.exports = async function(deployer, network) { ); const approveMinting = dai.approve(cDAI.address, web3.utils.toWei("20", "ether")); + const approveStakingDMM = dai.approve( + simpleStakingDMM.address, + web3.utils.toWei("80", "ether") + ); + const approveMintingDMM = dai.approve(mDAI.address, web3.utils.toWei("20", "ether")); + console.log("approving..."); await Promise.all([approveStaking, approveMinting]); + await Promise.all([approveStakingDMM, approveMintingDMM]); let ownercDaiBalanceBefore = await cDAI.balanceOf(accounts[0]); + let ownermDaiBalanceBefore = await mDAI.balanceOf(accounts[0]); const staking = simpleStaking.stake(web3.utils.toWei("80", "ether")); const minting = cDAI.mint(web3.utils.toWei("20", "ether")); + const stakingDMM = simpleStakingDMM.stake(web3.utils.toWei("80", "ether")); + const mintingDMM = mDAI.mint(web3.utils.toWei("20", "ether")); + console.log("staking and minting..."); await Promise.all([staking, minting]); + await Promise.all([stakingDMM, mintingDMM]); let ownercDaiBalanceAfter = await cDAI.balanceOf(accounts[0]); + let ownermDaiBalanceAfter = await mDAI.balanceOf(accounts[0]); let totalMinted = ownercDaiBalanceAfter.sub(ownercDaiBalanceBefore); + let totalMintedDMM = ownermDaiBalanceAfter.sub(ownermDaiBalanceBefore); const preloadStaking = cDAI.transfer( simpleStaking.address, Math.floor(totalMinted.toNumber() / 2).toString() ); + const preloadStakingDMM = mDAI.transfer( + simpleStakingDMM.address, + Math.floor(totalMintedDMM.toNumber() / 2).toString() + ); + const approveCdai = cDAI.approve( goodReserve.address, Math.floor(totalMinted.toNumber() / 2).toString() ); + const approveMdai = mDAI.approve( + goodReserve.address, + Math.floor(totalMintedDMM.toNumber() / 2).toString() + ); + console.log( "preload staking contract and increase the cdai allowance to preload the reserve contract..." ); await Promise.all([preloadStaking, approveCdai]); + await Promise.all([preloadStakingDMM, approveMdai]); console.log("preload reserve with CDAI"); await goodReserve.buy( @@ -86,5 +115,11 @@ module.exports = async function(deployer, network) { Math.floor(totalMinted.toNumber() / 2).toString(), 0 ); + + // await goodReserve.buy( + // mDAI.address, + // Math.floor(totalMintedDMM.toNumber() / 2).toString(), + // 0 + // ); } }; diff --git a/stakingModel/test/GoodFundManager.e2e.test.js b/stakingModel/test/GoodFundManager.e2e.test.js index 5609f8e6..b4cf96de 100644 --- a/stakingModel/test/GoodFundManager.e2e.test.js +++ b/stakingModel/test/GoodFundManager.e2e.test.js @@ -1,6 +1,8 @@ const GoodCompoundStaking = artifacts.require("GoodCompoundStaking"); +const GoodDMMStaking = artifacts.require("GoodDMMStaking"); const DAIMock = artifacts.require("DAIMock"); const cDAIMock = artifacts.require("cDAIMock"); +const mDAIMock = artifacts.require("mDAIMock"); const GoodReserve = artifacts.require("GoodReserveCDai"); const MarketMaker = artifacts.require("GoodMarketMaker"); const GoodDollar = artifacts.require("GoodDollar"); @@ -40,7 +42,9 @@ async function proposeAndRegister( contract("GoodFundManager - network e2e tests", ([founder, staker]) => { let dai, cDAI, + mDAI, goodCompoundStaking, + goodDMMStaking, goodReserve, goodFundManager, goodDollar, @@ -63,7 +67,9 @@ contract("GoodFundManager - network e2e tests", ([founder, staker]) => { ubiBridgeRecipient = staking_addresses.UBIScheme; dai = await DAIMock.at(staking_addresses.DAI); cDAI = await cDAIMock.at(staking_addresses.cDAI); - goodCompoundStaking = await GoodCompoundStaking.at(staking_addresses.DAIStaking); + mDAI = await mDAIMock.at(staking_addresses.mDAI); + goodCompoundStaking = await GoodCompoundStaking.at(staking_addresses.DAICompoundStaking); + goodDMMStaking = await GoodDMMStaking.at(staking_addresses.DAIDMMStaking); goodReserve = await GoodReserve.at(staking_addresses.Reserve); goodFundManager = await GoodFundsManager.at(staking_addresses.FundManager); marketMaker = await MarketMaker.at(staking_addresses.MarketMaker); @@ -77,17 +83,26 @@ contract("GoodFundManager - network e2e tests", ([founder, staker]) => { goodReserve.address ); - await dai.mint(staker, web3.utils.toWei("100", "ether")); + await dai.mint(staker, web3.utils.toWei("200", "ether")); await dai.approve(goodCompoundStaking.address, web3.utils.toWei("100", "ether"), { from: staker }); + await dai.approve(goodDMMStaking.address, web3.utils.toWei("100", "ether"), { + from: staker + }); await goodCompoundStaking .stake(web3.utils.toWei("100", "ether"), { from: staker }) .catch(console.log); + await goodDMMStaking + .stake(web3.utils.toWei("100", "ether"), { + from: staker + }) + .catch(console.log); await cDAI.exchangeRateCurrent(); await cDAI.exchangeRateStored(); + await mDAI.exchangeRateCurrent(); }); it("should be able to set the reserve", async () => { @@ -106,14 +121,21 @@ contract("GoodFundManager - network e2e tests", ([founder, staker]) => { expect(reserve1).to.be.equal(goodReserve.address); }); - it("should not mint UBI if not in the interval", async () => { + it("should not mint UBI if not in the interval(Compound)", async () => { const error = await goodFundManager .transferInterest(goodCompoundStaking.address) .catch(e => e); expect(error.message).to.have.string("wait for the next interval"); }); - it("should collect the interest and transfer it to the reserve and the bridge recipient should recieves minted gd", async () => { + it("should not mint UBI if not in the interval(DMM)", async () => { + const error = await goodFundManager + .transferInterest(goodDMMStaking.address) + .catch(e => e); + expect(error.message).to.have.string("wait for the next interval"); + }); + + it("should collect the interest and transfer it to the reserve and the bridge recipient should recieves minted gd (Compound)", async () => { await next_interval(await goodFundManager.blockInterval()); await cDAI.exchangeRateCurrent(); let recipientBefore = await goodDollar.balanceOf(ubiBridgeRecipient); @@ -128,4 +150,20 @@ contract("GoodFundManager - network e2e tests", ([founder, staker]) => { Math.floor(gdPriceBefore.toNumber() / 100).toString() ); }); + + it("should collect the interest and transfer it to the reserve and the bridge recipient should recieves minted gd (DMM)", async () => { + await next_interval(await goodFundManager.blockInterval()); + await mDAI.exchangeRateCurrent(); + let recipientBefore = await goodDollar.balanceOf(ubiBridgeRecipient); + const gdPriceBefore = await marketMaker.currentPrice(mDAI.address); + await goodFundManager.transferInterest(goodDMMStaking.address); + const gdPriceAfter = await marketMaker.currentPrice(mDAI.address); + let recipientAfter = await goodDollar.balanceOf(ubiBridgeRecipient); + let stakingGDBalance = await goodDollar.balanceOf(goodDMMStaking.address); + expect(stakingGDBalance.toString()).to.be.equal("0"); //100% of interest is donated, so nothing is returned to staking + expect(recipientAfter.sub(recipientBefore).toString()).to.be.equal("1904085"); // total of interest + minted from expansion (received 100%) + expect(Math.floor(gdPriceAfter.toNumber() / 100).toString()).to.be.equal( + Math.floor(gdPriceBefore.toNumber() / 100).toString() + ); + }); });