diff --git a/contracts/modules/STO/Capped/CappedSTO.sol b/contracts/modules/STO/Capped/CappedSTO.sol index b2a7fea93..dbb40a76f 100644 --- a/contracts/modules/STO/Capped/CappedSTO.sol +++ b/contracts/modules/STO/Capped/CappedSTO.sol @@ -104,7 +104,6 @@ contract CappedSTO is CappedSTOStorage, STO, ReentrancyGuard { weiAmount = weiAmount.sub(refund); _forwardFunds(refund); - _postValidatePurchase(_beneficiary, weiAmount); } /** @@ -116,7 +115,6 @@ contract CappedSTO is CappedSTOStorage, STO, ReentrancyGuard { require(fundRaiseTypes[uint8(FundRaiseType.POLY)], "Mode of investment is not POLY"); uint256 refund = _processTx(msg.sender, _investedPOLY); _forwardPoly(msg.sender, wallet, _investedPOLY.sub(refund)); - _postValidatePurchase(msg.sender, _investedPOLY.sub(refund)); } /** @@ -185,7 +183,6 @@ contract CappedSTO is CappedSTOStorage, STO, ReentrancyGuard { _processPurchase(_beneficiary, tokens); emit TokenPurchase(msg.sender, _beneficiary, _investedAmount, tokens); - _updatePurchasingState(_beneficiary, _investedAmount); } /** @@ -197,24 +194,11 @@ contract CappedSTO is CappedSTOStorage, STO, ReentrancyGuard { function _preValidatePurchase(address _beneficiary, uint256 _investedAmount) internal view { require(_beneficiary != address(0), "Beneficiary address should not be 0x"); require(_investedAmount != 0, "Amount invested should not be equal to 0"); - uint256 tokens; - (tokens, ) = _getTokenAmount(_investedAmount); - require(totalTokensSold.add(tokens) <= cap, "Investment more than cap is not allowed"); + require(_canBuy(_beneficiary), "Unauthorized"); /*solium-disable-next-line security/no-block-members*/ require(now >= startTime && now <= endTime, "Offering is closed/Not yet started"); } - /** - * @notice Validation of an executed purchase. - Observe state and use revert statements to undo rollback when valid conditions are not met. - */ - function _postValidatePurchase( - address _beneficiary, - uint256 /*_investedAmount*/ - ) internal view { - require(_canBuy(_beneficiary), "Unauthorized"); - } - /** * @notice Source of tokens. Override this method to modify the way in which the crowdsale ultimately gets and sends its tokens. @@ -239,30 +223,23 @@ contract CappedSTO is CappedSTOStorage, STO, ReentrancyGuard { _deliverTokens(_beneficiary, _tokenAmount); } - /** - * @notice Overrides for extensions that require an internal state to check for validity - (current user contributions, etc.) - */ - function _updatePurchasingState( - address, /*_beneficiary*/ - uint256 _investedAmount - ) internal pure { - _investedAmount = 0; //yolo - } - /** * @notice Overrides to extend the way in which ether is converted to tokens. * @param _investedAmount Value in wei to be converted into tokens * @return Number of tokens that can be purchased with the specified _investedAmount * @return Remaining amount that should be refunded to the investor */ - function _getTokenAmount(uint256 _investedAmount) internal view returns(uint256 _tokens, uint256 _refund) { - _tokens = _investedAmount.mul(rate); - _tokens = _tokens.div(uint256(10) ** 18); + function _getTokenAmount(uint256 _investedAmount) internal view returns(uint256 tokens, uint256 refund) { + tokens = _investedAmount.mul(rate); + tokens = tokens.div(uint256(10) ** 18); + if (totalTokensSold.add(tokens) > cap) { + tokens = cap.sub(totalTokensSold); + } uint256 granularity = ISecurityToken(securityToken).granularity(); - _tokens = _tokens.div(granularity); - _tokens = _tokens.mul(granularity); - _refund = _investedAmount.sub((_tokens.mul(uint256(10) ** 18)).div(rate)); + tokens = tokens.div(granularity); + tokens = tokens.mul(granularity); + require(tokens > 0, "Cap reached"); + refund = _investedAmount.sub((tokens.mul(uint256(10) ** 18)).div(rate)); } /** diff --git a/contracts/modules/STO/STO.sol b/contracts/modules/STO/STO.sol index 85faabec0..b3f37680f 100644 --- a/contracts/modules/STO/STO.sol +++ b/contracts/modules/STO/STO.sol @@ -63,7 +63,7 @@ contract STO is ISTO, STOStorage, Module, Pausable { function _setFundRaiseType(FundRaiseType[] memory _fundRaiseTypes) internal { // FundRaiseType[] parameter type ensures only valid values for _fundRaiseTypes - require(_fundRaiseTypes.length > 0, "Raise type is not specified"); + require(_fundRaiseTypes.length > 0 && _fundRaiseTypes.length <= 3, "Raise type is not specified"); fundRaiseTypes[uint8(FundRaiseType.ETH)] = false; fundRaiseTypes[uint8(FundRaiseType.POLY)] = false; fundRaiseTypes[uint8(FundRaiseType.SC)] = false; diff --git a/contracts/modules/STO/USDTiered/USDTieredSTO.sol b/contracts/modules/STO/USDTiered/USDTieredSTO.sol index e2a307deb..e2f49f6ae 100644 --- a/contracts/modules/STO/USDTiered/USDTieredSTO.sol +++ b/contracts/modules/STO/USDTiered/USDTieredSTO.sol @@ -244,11 +244,11 @@ contract USDTieredSTO is USDTieredSTOStorage, STO { } usdTokens = _usdTokens; for(i = 0; i < _usdTokens.length; i++) { - require(_usdTokens[i] != address(0), "Invalid USD token"); + require(_usdTokens[i] != address(0) && _usdTokens[i] != address(polyToken), "Invalid USD token"); usdTokenEnabled[_usdTokens[i]] = true; } emit SetAddresses(wallet, _usdTokens); - } + } //////////////////// // STO Management // @@ -258,7 +258,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO { * @notice Finalizes the STO and mint remaining tokens to treasury address * @notice Treasury wallet address must be whitelisted to successfully finalize */ - function finalize() public { + function finalize() external { _onlySecurityTokenOwner(); require(!isFinalized, "STO already finalized"); isFinalized = true; @@ -275,6 +275,9 @@ contract USDTieredSTO is USDTieredSTOStorage, STO { } address walletAddress = (treasuryWallet == address(0) ? IDataStore(getDataStore()).getAddress(TREASURY) : treasuryWallet); require(walletAddress != address(0), "Invalid address"); + uint256 granularity = ISecurityToken(securityToken).granularity(); + tempReturned = tempReturned.div(granularity); + tempReturned = tempReturned.mul(granularity); ISecurityToken(securityToken).issue(walletAddress, tempReturned, ""); emit ReserveTokenMint(msg.sender, walletAddress, tempReturned, currentTier); finalAmountReturned = tempReturned; @@ -286,7 +289,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO { * @param _investors Array of investor addresses to modify * @param _nonAccreditedLimit Array of uints specifying non-accredited limits */ - function changeNonAccreditedLimit(address[] memory _investors, uint256[] memory _nonAccreditedLimit) public { + function changeNonAccreditedLimit(address[] calldata _investors, uint256[] calldata _nonAccreditedLimit) external { _onlySecurityTokenOwner(); //nonAccreditedLimitUSDOverride require(_investors.length == _nonAccreditedLimit.length, "Length mismatch"); @@ -317,7 +320,7 @@ contract USDTieredSTO is USDTieredSTOStorage, STO { * @notice Function to set allowBeneficialInvestments (allow beneficiary to be different to funder) * @param _allowBeneficialInvestments Boolean to allow or disallow beneficial investments */ - function changeAllowBeneficialInvestments(bool _allowBeneficialInvestments) public { + function changeAllowBeneficialInvestments(bool _allowBeneficialInvestments) external { _onlySecurityTokenOwner(); require(_allowBeneficialInvestments != allowBeneficialInvestments, "Value unchanged"); allowBeneficialInvestments = _allowBeneficialInvestments; @@ -535,22 +538,24 @@ contract USDTieredSTO is USDTieredSTOStorage, STO { internal returns(uint256 spentUSD, uint256 purchasedTokens, bool gotoNextTier) { - uint256 maximumTokens = DecimalMath.div(_investedUSD, _tierPrice); + purchasedTokens = DecimalMath.div(_investedUSD, _tierPrice); uint256 granularity = ISecurityToken(securityToken).granularity(); - maximumTokens = maximumTokens.div(granularity); - maximumTokens = maximumTokens.mul(granularity); - if (maximumTokens > _tierRemaining) { - spentUSD = DecimalMath.mul(_tierRemaining, _tierPrice); - // In case of rounding issues, ensure that spentUSD is never more than investedUSD - if (spentUSD > _investedUSD) { - spentUSD = _investedUSD; - } - purchasedTokens = _tierRemaining; + + if (purchasedTokens > _tierRemaining) { + purchasedTokens = _tierRemaining.div(granularity); gotoNextTier = true; } else { - spentUSD = DecimalMath.mul(maximumTokens, _tierPrice); - purchasedTokens = maximumTokens; + purchasedTokens = purchasedTokens.div(granularity); } + + purchasedTokens = purchasedTokens.mul(granularity); + spentUSD = DecimalMath.mul(purchasedTokens, _tierPrice); + + // In case of rounding issues, ensure that spentUSD is never more than investedUSD + if (spentUSD > _investedUSD) { + spentUSD = _investedUSD; + } + if (purchasedTokens > 0) { ISecurityToken(securityToken).issue(_beneficiary, purchasedTokens, ""); emit TokenPurchase(msg.sender, _beneficiary, purchasedTokens, spentUSD, _tierPrice, _tier); diff --git a/test/p_usd_tiered_sto.js b/test/p_usd_tiered_sto.js index e79c3fe0e..4eec0a9f0 100644 --- a/test/p_usd_tiered_sto.js +++ b/test/p_usd_tiered_sto.js @@ -699,6 +699,48 @@ contract("USDTieredSTO", async (accounts) => { assert.equal(tokens[0], I_DaiToken.address, "USD Tokens should match"); }); + it("Should successfully attach the sixth STO module to the security token", async () => { + let stoId = 5; // Non-divisible token with invalid tier + + _startTime.push(new BN(currentTime).add(new BN(duration.days(2)))); + _endTime.push(new BN(_startTime[stoId]).add(new BN(currentTime).add(new BN(duration.days(100))))); + _ratePerTier.push([new BN(1).mul(e18), new BN(1).mul(e18)]); // [ 1 USD/Token, 1 USD/Token ] + _ratePerTierDiscountPoly.push([new BN(1).mul(e18), new BN(1).mul(e18)]); // [ 1 USD/Token, 1 USD/Token ] + _tokensPerTierTotal.push([new BN(10010).mul(e16), new BN(50).mul(e18)]); // [ 100.1 Token, 50 Token ] + _tokensPerTierDiscountPoly.push([new BN(0), new BN(0)]); // [ 0 Token, 0 Token ] + _nonAccreditedLimitUSD.push(new BN(25).mul(e18)); // [ 25 USD ] + _minimumInvestmentUSD.push(new BN(5)); + _fundRaiseTypes.push([0, 1, 2]); + _wallet.push(WALLET); + _treasuryWallet.push(TREASURYWALLET); + _usdToken.push([I_DaiToken.address]); + + let config = [ + _startTime[stoId], + _endTime[stoId], + _ratePerTier[stoId], + _ratePerTierDiscountPoly[stoId], + _tokensPerTierTotal[stoId], + _tokensPerTierDiscountPoly[stoId], + _nonAccreditedLimitUSD[stoId], + _minimumInvestmentUSD[stoId], + _fundRaiseTypes[stoId], + _wallet[stoId], + _treasuryWallet[stoId], + _usdToken[stoId] + ]; + + let bytesSTO = web3.eth.abi.encodeFunctionCall(functionSignature, config); + let tx = await I_SecurityToken.addModule(I_USDTieredSTOFactory.address, bytesSTO, 0, 0, { from: ISSUER, gasPrice: GAS_PRICE }); + console.log(" Gas addModule: ".grey + tx.receipt.gasUsed.toString().grey); + assert.equal(tx.logs[2].args._types[0], STOKEY, "USDTieredSTO doesn't get deployed"); + assert.equal(web3.utils.hexToString(tx.logs[2].args._name), "USDTieredSTO", "USDTieredSTOFactory module was not added"); + I_USDTieredSTO_Array.push(await USDTieredSTO.at(tx.logs[2].args._module)); + // console.log(I_USDTieredSTO_Array[I_USDTieredSTO_Array.length - 1]); + let tokens = await I_USDTieredSTO_Array[I_USDTieredSTO_Array.length - 1].getUsdTokens.call(); + assert.equal(tokens[0], I_DaiToken.address, "USD Tokens should match"); + }); + it("Should fail because rates and tier array of different length", async () => { let stoId = 0; @@ -1236,7 +1278,8 @@ contract("USDTieredSTO", async (accounts) => { await I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 }); // Change Stable coin address - await I_USDTieredSTO_Array[stoId].modifyAddresses(WALLET, TREASURYWALLET, [I_PolyToken.address], { from: ISSUER }); + let I_DaiToken2 = await PolyTokenFaucet.new(); + await I_USDTieredSTO_Array[stoId].modifyAddresses(WALLET, TREASURYWALLET, [I_DaiToken2.address], { from: ISSUER }); // NONACCREDITED DAI await catchRevert(I_USDTieredSTO_Array[stoId].buyWithUSD(NONACCREDITED1, investment_DAI, I_DaiToken.address, { from: NONACCREDITED1 })); @@ -3894,6 +3937,114 @@ contract("USDTieredSTO", async (accounts) => { await I_SecurityToken.changeGranularity(1, { from: ISSUER }); }); + it("should successfully buy a granular amount when buying indivisible token with illegal tier limits", async () => { + await I_SecurityToken.changeGranularity(e18, { from: ISSUER }); + let stoId = 5; + let tierId = 0; + let investment_Tokens = new BN(110).mul(e18); + let investment_POLY = await convert(stoId, tierId, false, "TOKEN", "POLY", investment_Tokens); + + let refund_Tokens = new BN(0); + let refund_POLY = await convert(stoId, tierId, false, "TOKEN", "POLY", refund_Tokens); + + await I_PolyToken.getTokens(investment_POLY, ACCREDITED1); + await I_PolyToken.approve(I_USDTieredSTO_Array[stoId].address, investment_POLY, { from: ACCREDITED1 }); + + let init_TokenSupply = await I_SecurityToken.totalSupply(); + let init_InvestorTokenBal = await I_SecurityToken.balanceOf(ACCREDITED1); + let init_InvestorETHBal = new BN(await web3.eth.getBalance(ACCREDITED1)); + let init_InvestorPOLYBal = await I_PolyToken.balanceOf(ACCREDITED1); + let init_STOTokenSold = await I_USDTieredSTO_Array[stoId].getTokensSold(); + let init_STOETHBal = new BN(await web3.eth.getBalance(I_USDTieredSTO_Array[stoId].address)); + let init_STOPOLYBal = await I_PolyToken.balanceOf(I_USDTieredSTO_Array[stoId].address); + let init_RaisedETH = await I_USDTieredSTO_Array[stoId].fundsRaised.call(ETH); + let init_RaisedPOLY = await I_USDTieredSTO_Array[stoId].fundsRaised.call(POLY); + let init_WalletETHBal = new BN(await web3.eth.getBalance(WALLET)); + let init_WalletPOLYBal = await I_PolyToken.balanceOf(WALLET); + + let tokensToMint = (await I_USDTieredSTO_Array[stoId].buyWithPOLY.call(ACCREDITED1, investment_POLY, {from: ACCREDITED1}))[2]; + + // Buy With POLY + let tx2 = await I_USDTieredSTO_Array[stoId].buyWithPOLY(ACCREDITED1, investment_POLY, { + from: ACCREDITED1, + gasPrice: GAS_PRICE + }); + let gasCost2 = new BN(GAS_PRICE).mul(new BN(tx2.receipt.gasUsed)); + console.log(" Gas buyWithPOLY: ".grey + new BN(tx2.receipt.gasUsed).toString().grey); + + let final_TokenSupply = await I_SecurityToken.totalSupply(); + let final_InvestorTokenBal = await I_SecurityToken.balanceOf(ACCREDITED1); + let final_InvestorETHBal = new BN(await web3.eth.getBalance(ACCREDITED1)); + let final_InvestorPOLYBal = await I_PolyToken.balanceOf(ACCREDITED1); + let final_STOTokenSold = await I_USDTieredSTO_Array[stoId].getTokensSold(); + let final_STOETHBal = new BN(await web3.eth.getBalance(I_USDTieredSTO_Array[stoId].address)); + let final_STOPOLYBal = await I_PolyToken.balanceOf(I_USDTieredSTO_Array[stoId].address); + let final_RaisedETH = await I_USDTieredSTO_Array[stoId].fundsRaised.call(ETH); + let final_RaisedPOLY = await I_USDTieredSTO_Array[stoId].fundsRaised.call(POLY); + let final_WalletETHBal = new BN(await web3.eth.getBalance(WALLET)); + let final_WalletPOLYBal = await I_PolyToken.balanceOf(WALLET); + + assert.equal( + final_TokenSupply.toString(), + init_TokenSupply + .add(investment_Tokens) + .sub(refund_Tokens) + .toString(), + "Token Supply not changed as expected" + ); + assert.equal(tokensToMint.toString(), investment_Tokens.sub(refund_Tokens).toString(), "View function returned incorrect data"); + assert.equal( + final_InvestorTokenBal.toString(), + init_InvestorTokenBal + .add(investment_Tokens) + .sub(refund_Tokens) + .toString(), + "Investor Token Balance not changed as expected" + ); + assert.equal( + final_InvestorETHBal.toString(), + init_InvestorETHBal.sub(gasCost2).toString(), + "Investor ETH Balance not changed as expected" + ); + assert.equal( + final_InvestorPOLYBal.toString(), + init_InvestorPOLYBal + .sub(investment_POLY) + .add(refund_POLY) + .toString(), + "Investor POLY Balance not changed as expected" + ); + assert.equal( + final_STOTokenSold.toString(), + init_STOTokenSold + .add(investment_Tokens) + .sub(refund_Tokens) + .toString(), + "STO Token Sold not changed as expected" + ); + assert.equal(final_STOETHBal.toString(), init_STOETHBal.toString(), "STO ETH Balance not changed as expected"); + assert.equal(final_STOPOLYBal.toString(), init_STOPOLYBal.toString(), "STO POLY Balance not changed as expected"); + assert.equal(final_RaisedETH.toString(), init_RaisedETH.toString(), "Raised ETH not changed as expected"); + assert.equal( + final_RaisedPOLY.toString(), + init_RaisedPOLY + .add(investment_POLY) + .sub(refund_POLY) + .toString(), + "Raised POLY not changed as expected" + ); + assert.equal(final_WalletETHBal.toString(), init_WalletETHBal.toString(), "Wallet ETH Balance not changed as expected"); + assert.equal( + final_WalletPOLYBal.toString(), + init_WalletPOLYBal + .add(investment_POLY) + .sub(refund_POLY) + .toString(), + "Wallet POLY Balance not changed as expected" + ); + await I_SecurityToken.changeGranularity(1, { from: ISSUER }); + }); + it("should successfully buy a granular amount and refund balance when buying indivisible token with ETH", async () => { await I_SecurityToken.changeGranularity(e18, { from: ISSUER }); let stoId = 4;