diff --git a/contracts/CrowdPooling/impl/CP.sol b/contracts/CrowdPooling/impl/CP.sol new file mode 100644 index 0000000..5878180 --- /dev/null +++ b/contracts/CrowdPooling/impl/CP.sol @@ -0,0 +1,82 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {CPVesting} from "./CPVesting.sol"; +import {IERC20} from "../../intf/IERC20.sol"; +import {IPermissionManager} from "../../lib/PermissionManager.sol"; +import {IFeeRateModel} from "../../lib/FeeRateModel.sol"; +import {SafeMath} from "../../lib/SafeMath.sol"; + +contract CP is CPVesting { + using SafeMath for uint256; + + function init( + address[] calldata addressList, + uint256[] calldata timeLine, + uint256[] calldata valueList + ) external { + /* + Address List + 0. owner + 1. maintainer + 2. baseToken + 3. quoteToken + 4. permissionManager + 5. feeRateModel + 6. poolFactory + */ + + initOwner(addressList[0]); + _MAINTAINER_ = addressList[1]; + _BASE_TOKEN_ = IERC20(addressList[2]); + _QUOTE_TOKEN_ = IERC20(addressList[3]); + _BIDDER_PERMISSION_ = IPermissionManager(addressList[4]); + _MT_FEE_RATE_MODEL_ = IFeeRateModel(addressList[5]); + _POOL_FACTORY_ = addressList[6]; + + /* + Time Line + 0. phase bid starttime + 1. phase bid duration + 2. phase calm duration + 3. freeze duration + */ + + require(block.timestamp <= timeLine[0], "TIMELINE_WRONG"); + + _PHASE_BID_STARTTIME_ = timeLine[0]; + _PHASE_BID_ENDTIME_ = _PHASE_BID_STARTTIME_.add(timeLine[1]); + _PHASE_CALM_ENDTIME_ = _PHASE_BID_ENDTIME_.add(timeLine[2]); + + _FREEZE_DURATION_ = timeLine[3]; + + /* + Value List + 0. pool quote cap + 1. pool base reserve + 2. owner quote ratio + 3. k + 4 i + */ + + require(valueList[4] > 0 && valueList[4] <= 10**36, "I_VALUE_WRONG"); + require(valueList[3] <= 10**18, "K_VALUE_WRONG"); + require(valueList[2] <= 10**18, "OWNER_RATIO_WRONG"); + + _POOL_QUOTE_CAP_ = valueList[0]; + _POOL_BASE_RESERVE_ = valueList[1]; + _OWNER_QUOTE_RATIO_ = valueList[2]; + _K_ = valueList[2]; + _I_ = valueList[3]; + + _TOTAL_BASE_ = _BASE_TOKEN_.balanceOf(address(this)); + require(_TOTAL_BASE_ >= _POOL_BASE_RESERVE_, "BASE_TOKEN_NOT_ENOUGH"); + } +} diff --git a/contracts/PoolKickstarter/impl/CAFunding.sol b/contracts/CrowdPooling/impl/CPFunding.sol similarity index 77% rename from contracts/PoolKickstarter/impl/CAFunding.sol rename to contracts/CrowdPooling/impl/CPFunding.sol index 401cbfb..a9b9604 100644 --- a/contracts/PoolKickstarter/impl/CAFunding.sol +++ b/contracts/CrowdPooling/impl/CPFunding.sol @@ -13,11 +13,11 @@ import {SafeERC20} from "../../lib/SafeERC20.sol"; import {DecimalMath} from "../../lib/DecimalMath.sol"; import {IERC20} from "../../intf/IERC20.sol"; import {IDVM} from "../../DODOVendingMachine/intf/IDVM.sol"; -import {IDVMFactory} from "../../Factory/DVMFactory.sol"; -import {CAStorage} from "./CAStorage.sol"; +import {IUnownedDVMFactory} from "../../Factory/UnownedDVMFactory.sol"; +import {CPStorage} from "./CPStorage.sol"; import {PMMPricing} from "../../lib/PMMPricing.sol"; -contract CAFunding is CAStorage { +contract CPFunding is CPStorage { using SafeERC20 for IERC20; // ============ BID & CALM PHASE ============ @@ -61,7 +61,6 @@ contract CAFunding is CAStorage { (uint256 poolBase, uint256 poolQuote, uint256 ownerQuote) = getSettleResult(); _UNUSED_QUOTE_ = _QUOTE_TOKEN_.balanceOf(address(this)).sub(poolQuote).sub(ownerQuote); _UNUSED_BASE_ = _BASE_TOKEN_.balanceOf(address(this)).sub(poolBase); - uint256 avgPrice = DecimalMath.divCeil(poolQuote.add(ownerQuote), _UNUSED_BASE_); // 这里的目的是让开盘价尽量等于avgPrice // 我们统一设定k=1,如果quote和base不平衡,就必然要截断一边 @@ -70,39 +69,36 @@ contract CAFunding is CAStorage { // i = m (1-quote/(m*base)) // if quote = m*base i = 1 // if quote > m*base reverse - uint256 baseDepth = DecimalMath.mulFloor(avgPrice, poolBase); - if (poolQuote == baseDepth) { - _POOL_ = IDVMFactory(_POOL_FACTORY_).createDODOVendingMachine( - address(this), - address(_BASE_TOKEN_), - address(_QUOTE_TOKEN_), - 3e15, - 0, - 1, - DecimalMath.ONE - ); - } else if (poolQuote < baseDepth) { - uint256 ratio = DecimalMath.ONE.sub(DecimalMath.divFloor(poolQuote, baseDepth)); - _POOL_ = IDVMFactory(_POOL_FACTORY_).createDODOVendingMachine( - address(this), - address(_BASE_TOKEN_), - address(_QUOTE_TOKEN_), - 3e15, - 0, - avgPrice.mul(ratio).mul(ratio).divCeil(DecimalMath.ONE2), - DecimalMath.ONE - ); - } else if (poolQuote > baseDepth) { - uint256 ratio = DecimalMath.ONE.sub(DecimalMath.divFloor(baseDepth, poolQuote)); - _POOL_ = IDVMFactory(_POOL_FACTORY_).createDODOVendingMachine( - address(this), - address(_QUOTE_TOKEN_), - address(_BASE_TOKEN_), - 3e15, - 0, - DecimalMath.reciprocalFloor(avgPrice).mul(ratio).mul(ratio).divCeil( + { + uint256 avgPrice = DecimalMath.divCeil(poolQuote.add(ownerQuote), _UNUSED_BASE_); + uint256 baseDepth = DecimalMath.mulFloor(avgPrice, poolBase); + address _poolBaseToken; + address _poolQuoteToken; + uint256 _poolI; + if (poolQuote == baseDepth) { + _poolBaseToken = address(_BASE_TOKEN_); + _poolQuoteToken = address(_QUOTE_TOKEN_); + _poolI = 1; + } else if (poolQuote < baseDepth) { + _poolBaseToken = address(_BASE_TOKEN_); + _poolQuoteToken = address(_QUOTE_TOKEN_); + uint256 ratio = DecimalMath.ONE.sub(DecimalMath.divFloor(poolQuote, baseDepth)); + _poolI = avgPrice.mul(ratio).mul(ratio).divCeil(DecimalMath.ONE2); + } else if (poolQuote > baseDepth) { + _poolBaseToken = address(_QUOTE_TOKEN_); + _poolQuoteToken = address(_BASE_TOKEN_); + uint256 ratio = DecimalMath.ONE.sub(DecimalMath.divFloor(baseDepth, poolQuote)); + _poolI = DecimalMath.reciprocalFloor(avgPrice).mul(ratio).mul(ratio).divCeil( DecimalMath.ONE2 - ), + ); + } + _POOL_ = IUnownedDVMFactory(_POOL_FACTORY_).createDODOVendingMachine( + address(this), + _poolBaseToken, + _poolQuoteToken, + 3e15, + 0, + _poolI, DecimalMath.ONE ); } diff --git a/contracts/PoolKickstarter/impl/CAStorage.sol b/contracts/CrowdPooling/impl/CPStorage.sol similarity index 84% rename from contracts/PoolKickstarter/impl/CAStorage.sol rename to contracts/CrowdPooling/impl/CPStorage.sol index 31c7565..13dc195 100644 --- a/contracts/PoolKickstarter/impl/CAStorage.sol +++ b/contracts/CrowdPooling/impl/CPStorage.sol @@ -15,7 +15,7 @@ import {IFeeRateModel} from "../../lib/FeeRateModel.sol"; import {SafeMath} from "../../lib/SafeMath.sol"; import {IERC20} from "../../intf/IERC20.sol"; -contract CAStorage is InitializableOwnable, ReentrancyGuard { +contract CPStorage is InitializableOwnable, ReentrancyGuard { using SafeMath for uint256; uint256 internal constant _SETTLEMENT_EXPIRED_TIME_ = 86400 * 7; @@ -71,7 +71,7 @@ contract CAStorage is InitializableOwnable, ReentrancyGuard { modifier phaseBid() { require( - block.timestamp > _PHASE_BID_STARTTIME_ && block.timestamp <= _PHASE_BID_ENDTIME_, + block.timestamp >= _PHASE_BID_STARTTIME_ && block.timestamp < _PHASE_BID_ENDTIME_, "NOT_PHASE_BID" ); _; @@ -79,7 +79,7 @@ contract CAStorage is InitializableOwnable, ReentrancyGuard { modifier phaseCalm() { require( - block.timestamp > _PHASE_BID_ENDTIME_ && block.timestamp <= _PHASE_CALM_ENDTIME_, + block.timestamp >= _PHASE_BID_ENDTIME_ && block.timestamp < _PHASE_CALM_ENDTIME_, "NOT_PHASE_CALM" ); _; @@ -87,14 +87,14 @@ contract CAStorage is InitializableOwnable, ReentrancyGuard { modifier phaseBidOrCalm() { require( - block.timestamp > _PHASE_BID_STARTTIME_ && block.timestamp <= _PHASE_CALM_ENDTIME_, + block.timestamp >= _PHASE_BID_STARTTIME_ && block.timestamp < _PHASE_CALM_ENDTIME_, "NOT_PHASE_BID_OR_CALM" ); _; } modifier phaseSettlement() { - require(block.timestamp > _PHASE_CALM_ENDTIME_, "NOT_PHASE_EXE"); + require(block.timestamp >= _PHASE_CALM_ENDTIME_, "NOT_PHASE_EXE"); _; } diff --git a/contracts/PoolKickstarter/impl/CAVesting.sol b/contracts/CrowdPooling/impl/CPVesting.sol similarity index 93% rename from contracts/PoolKickstarter/impl/CAVesting.sol rename to contracts/CrowdPooling/impl/CPVesting.sol index 4c3e8aa..ff8ace0 100644 --- a/contracts/PoolKickstarter/impl/CAVesting.sol +++ b/contracts/CrowdPooling/impl/CPVesting.sol @@ -13,16 +13,16 @@ import {DecimalMath} from "../../lib/DecimalMath.sol"; import {Ownable} from "../../lib/Ownable.sol"; import {SafeERC20} from "../../lib/SafeERC20.sol"; import {IERC20} from "../../intf/IERC20.sol"; -import {CAFunding} from "./CAFunding.sol"; +import {CPFunding} from "./CPFunding.sol"; /** - * @title CAVesting + * @title CPVesting * @author DODO Breeder * * @notice Lock Token and release it linearly */ -contract CAVesting is CAFunding { +contract CPVesting is CPFunding { using SafeMath for uint256; using SafeERC20 for IERC20; diff --git a/contracts/PoolKickstarter/impl/CA.sol b/contracts/PoolKickstarter/impl/CA.sol deleted file mode 100644 index 1df62dc..0000000 --- a/contracts/PoolKickstarter/impl/CA.sol +++ /dev/null @@ -1,98 +0,0 @@ -/* - - Copyright 2020 DODO ZOO. - SPDX-License-Identifier: Apache-2.0 - -*/ - -pragma solidity 0.6.9; -pragma experimental ABIEncoderV2; - -import {CAVesting} from "./CAVesting.sol"; -import {IERC20} from "../../intf/IERC20.sol"; -import {IPermissionManager} from "../../lib/PermissionManager.sol"; -import {IFeeRateModel} from "../../lib/FeeRateModel.sol"; - -contract CA is CAVesting { - function init( - address[] calldata addressList, - uint256[] calldata timeLine, - uint256[] calldata valueList, - bytes calldata basePayBackData, - bytes calldata quotePayBackData - ) external { - /* - Address List - 0. owner - 1. maintainer - 2. baseToken - 3. quoteToken - 4. basePayBack - 5. quotePayBack - 6. permissionManager - 7. feeRateModel - */ - - initOwner(addressList[0]); - _MAINTAINER_ = addressList[1]; - _BASE_TOKEN_ = IERC20(addressList[2]); - _QUOTE_TOKEN_ = IERC20(addressList[3]); - _BASE_PAY_BACK_ = addressList[4]; - _QUOTE_PAY_BACK_ = addressList[5]; - _BIDDER_PERMISSION_ = IPermissionManager(addressList[6]); - _MT_FEE_RATE_MODEL_ = IFeeRateModel(addressList[7]); - - /* - Time Line - 0. phase bid starttime - 1. phase bid endtime - 2. phase calm endtime - 3. start vesting time - 4. vesting duration - */ - - require( - block.timestamp <= timeLine[0] && - timeLine[0] <= timeLine[1] && - timeLine[1] <= timeLine[2] && - timeLine[2] <= timeLine[3], - "TIMELINE_WRONG" - ); - - _PHASE_BID_STARTTIME_ = timeLine[0]; - _PHASE_BID_ENDTIME_ = timeLine[1]; - _PHASE_CALM_ENDTIME_ = timeLine[2]; - _START_VESTING_TIME_ = timeLine[3]; - - _VESTING_DURATION_ = timeLine[4]; - - /* - Value List - 0. quote cap - 1. cliff rate - 2. k - 3. i - 4. owner ratio - */ - - require( - valueList[1] <= 10**18 && - valueList[2] <= 10**18 && - valueList[3] > 0 && - valueList[3] <= 10**36 && - valueList[4] <= 10**18, - "VALUE_RANGE_WRONG" - ); - - _QUOTE_CAP_ = valueList[0]; - _CLIFF_RATE_ = valueList[1]; - _K_ = valueList[2]; - _I_ = valueList[3]; - _OWNER_RATIO_ = valueList[4]; - - // ============ External Call Data ============ - - _BASE_PAY_BACK_CALL_DATA_ = basePayBackData; - _QUOTE_PAY_BACK_CALL_DATA_ = quotePayBackData; - } -} diff --git a/test/CrowdPooling/CPBid.test.ts b/test/CrowdPooling/CPBid.test.ts new file mode 100644 index 0000000..5ef337a --- /dev/null +++ b/test/CrowdPooling/CPBid.test.ts @@ -0,0 +1,65 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +// import * as assert from 'assert'; + +import { decimalStr } from '../utils/Converter'; +// import { logGas } from '../utils/Log'; +import { CPContext, CPContextInitConfig } from '../utils/CrowdPoolingContext'; +// import { assert } from 'chai'; +import BigNumber from 'bignumber.js'; +const truffleAssert = require('truffle-assertions'); + +let bidder1: string; +let bidder2: string; +let config: CPContextInitConfig + +async function init(ctx: CPContext): Promise { + bidder1 = ctx.SpareAccounts[1] + bidder2 = ctx.SpareAccounts[2] + await ctx.QUOTE.methods.mint(bidder1, decimalStr("1000")).send(ctx.sendParam(ctx.Deployer)) + await ctx.QUOTE.methods.mint(bidder2, decimalStr("1000")).send(ctx.sendParam(ctx.Deployer)) +} + +describe("Funding", () => { + let snapshotId: string; + let ctx: CPContext; + + before(async () => { + config = { + totalBase: decimalStr("10000"), + poolBaseReserve: decimalStr("5000"), + poolQuoteCap: decimalStr("50000"), + ownerQuoteRatio: decimalStr("0.1"), + k: decimalStr("0.5"), + i: decimalStr("10"), + lpFeeRate: decimalStr("0.002"), + bidDuration: new BigNumber(86400), + calmDuration: new BigNumber(86400), + freezeDuration: new BigNumber(86400), + } + ctx = new CPContext(); + await ctx.init(config); + await init(ctx); + }); + + beforeEach(async () => { + snapshotId = await ctx.EVM.snapshot(); + }); + + afterEach(async () => { + await ctx.EVM.reset(snapshotId); + }); + + describe("bid & cancel", () => { + + it("bid", async () => { + await ctx.QUOTE.methods.transfer(ctx.CP.options.address, decimalStr("100")).send(ctx.sendParam(bidder1)) + await ctx.CP.methods.bid(bidder1).send(ctx.sendParam(bidder1)) + }) + }) +}) \ No newline at end of file diff --git a/test/utils/Contracts.ts b/test/utils/Contracts.ts index bbec31f..988f931 100644 --- a/test/utils/Contracts.ts +++ b/test/utils/Contracts.ts @@ -31,6 +31,7 @@ export const DODO_MINE_READER_NAME = "DODOMineReader" export const DVM_VAULT_NAME = "DVMVault" export const DVM_NAME = "DVM" export const DVM_FACTORY_NAME = "DVMFactory" +export const UNOWNED_DVM_FACTORY_NAME = "UnownedDVMFactory" export const DVM_PROXY_NAME = "DVMProxy" export const CONST_FEE_RATE_MODEL_NAME = "ConstFeeRateModel" export const PERMISSION_MANAGER_NAME = "PermissionManager" @@ -44,6 +45,7 @@ export const DODO_SELL_HELPER = "DODOSellHelper" export const DVM_ADMIN_NAME = "DVMAdmin" export const DPP_ADMIN_NAME = "DPPAdmin" export const DODO_CALLEE_HELPER_NAME = "DODOCalleeHelper" +export const CROWD_POOLING_NAME = "CP" interface ContractJson { abi: any; diff --git a/test/utils/CrowdPoolingContext.ts b/test/utils/CrowdPoolingContext.ts new file mode 100644 index 0000000..1d9784e --- /dev/null +++ b/test/utils/CrowdPoolingContext.ts @@ -0,0 +1,133 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +import BigNumber from 'bignumber.js'; +import Web3 from 'web3'; +import { Contract } from 'web3-eth-contract'; + +import * as contracts from './Contracts'; +import { decimalStr, mweiStr, MAX_UINT256 } from './Converter'; +import { EVM, getDefaultWeb3 } from './EVM'; +import * as log from './Log'; + +BigNumber.config({ + EXPONENTIAL_AT: 1000, + DECIMAL_PLACES: 80, +}); + +export interface CPContextInitConfig { + totalBase: string; + poolBaseReserve: string; + poolQuoteCap: string; + ownerQuoteRatio: string; + k: string; + i: string; + lpFeeRate: string; + bidDuration: BigNumber; + calmDuration: BigNumber; + freezeDuration: BigNumber; +} + + +export class CPContext { + EVM: EVM; + Web3: Web3; + UnownedDVMFactory: Contract; + CP: Contract; + BASE: Contract; + QUOTE: Contract; + Deployer: string; + Maintainer: string; + SpareAccounts: string[]; + + constructor() { } + + async init(config: CPContextInitConfig) { + this.EVM = new EVM(); + this.Web3 = getDefaultWeb3(); + + const allAccounts = await this.Web3.eth.getAccounts(); + this.Deployer = allAccounts[0]; + this.Maintainer = allAccounts[1]; + this.SpareAccounts = allAccounts.slice(2, 10); + + var cloneFactory = await contracts.newContract( + contracts.CLONE_FACTORY_CONTRACT_NAME + ); + var dvmTemplate = await contracts.newContract(contracts.DVM_NAME) + var feeRateModel = await contracts.newContract(contracts.FEE_RATE_MODEL_NAME) + var permissionManager = await contracts.newContract(contracts.PERMISSION_MANAGER_NAME) + var defaultGasSource = await contracts.newContract(contracts.EXTERNAL_VALUE_NAME) + + this.BASE = await contracts.newContract( + contracts.MINTABLE_ERC20_CONTRACT_NAME, + ["TestBase", "BASE", 18] + ); + this.QUOTE = await contracts.newContract( + contracts.MINTABLE_ERC20_CONTRACT_NAME, + ["TestQuote", "QUOTE", 18] + ); + + this.UnownedDVMFactory = await contracts.newContract(contracts.UNOWNED_DVM_FACTORY_NAME, + [ + cloneFactory.options.address, + dvmTemplate.options.address, + feeRateModel.options.address, + this.Maintainer, + feeRateModel.options.address, + permissionManager.options.address, + defaultGasSource.options.address + ] + ) + + this.CP = await contracts.newContract(contracts.CROWD_POOLING_NAME) + this.BASE.methods.mint(this.CP.options.address, config.totalBase).send(this.sendParam(this.Deployer)) + + this.CP.methods.init( + [ + this.Deployer, + this.Maintainer, + this.BASE.options.address, + this.QUOTE.options.address, + permissionManager.options.address, + feeRateModel.options.address, + this.UnownedDVMFactory.options.address + ], + [ + (await this.Web3.eth.getBlock(await this.Web3.eth.getBlockNumber())).timestamp, + config.bidDuration, + config.calmDuration, + config.freezeDuration + ], + [ + config.poolQuoteCap, + config.poolBaseReserve, + config.ownerQuoteRatio, + config.k, + config.i + ] + ).send(this.sendParam(this.Deployer)) + + await defaultGasSource.methods.init(this.Deployer, MAX_UINT256).send(this.sendParam(this.Deployer)); + await feeRateModel.methods.init(this.Deployer, decimalStr("0.001")).send(this.sendParam(this.Deployer)); + + console.log(log.blueText("[Init CrowdPooling context]")); + } + + sendParam(sender, value = "0") { + return { + from: sender, + gas: process.env["COVERAGE"] ? 10000000000 : 7000000, + gasPrice: mweiStr("1000"), + value: decimalStr(value), + }; + } + + async mintTestToken(to: string, token: Contract, amount: string) { + await token.methods.mint(to, amount).send(this.sendParam(this.Deployer)); + } +} \ No newline at end of file