Skip to content

Commit

Permalink
Ported STO fixes from dev-2.1.0 (#591)
Browse files Browse the repository at this point in the history
* Ported STO fixes from dev-2.1.0

* Revert when cap reached instead of failing silently

* Restored missing require

* USDTSTO granularity edge case fixed
  • Loading branch information
maxsam4 authored and adamdossa committed Mar 15, 2019
1 parent 91b415f commit 4726245
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 53 deletions.
45 changes: 11 additions & 34 deletions contracts/modules/STO/Capped/CappedSTO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ contract CappedSTO is CappedSTOStorage, STO, ReentrancyGuard {
weiAmount = weiAmount.sub(refund);

_forwardFunds(refund);
_postValidatePurchase(_beneficiary, weiAmount);
}

/**
Expand All @@ -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));
}

/**
Expand Down Expand Up @@ -185,7 +183,6 @@ contract CappedSTO is CappedSTOStorage, STO, ReentrancyGuard {
_processPurchase(_beneficiary, tokens);
emit TokenPurchase(msg.sender, _beneficiary, _investedAmount, tokens);

_updatePurchasingState(_beneficiary, _investedAmount);
}

/**
Expand All @@ -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.
Expand All @@ -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));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion contracts/modules/STO/STO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
39 changes: 22 additions & 17 deletions contracts/modules/STO/USDTiered/USDTieredSTO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 //
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
153 changes: 152 additions & 1 deletion test/p_usd_tiered_sto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 }));
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 4726245

Please sign in to comment.