Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.

## CappedSTO 2.0.1
* `rate` is now accepted as multiplied by 10^18 to allow settting higher price than 1ETH/POLY per token.
* Indivisble tokens are now supported. When trying to buy partial tokens, allowed full units of tokens will be purchased and remaining funds will be returned.

## USDTieredSTO 2.1.0
* Added `buyTokensView` and `getTokensMintedByTier` to USDTSTO.
Expand Down
39 changes: 25 additions & 14 deletions contracts/modules/STO/CappedSTO.sol
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ contract CappedSTO is ISTO, ReentrancyGuard {
public
onlyFactory
{
require(endTime == 0, "Already configured");
require(_rate > 0, "Rate of token should be greater than 0");
require(_fundsReceiver != address(0), "Zero address is not permitted");
/*solium-disable-next-line security/no-block-members*/
Expand Down Expand Up @@ -110,9 +111,10 @@ contract CappedSTO is ISTO, ReentrancyGuard {
require(fundRaiseTypes[uint8(FundRaiseType.ETH)], "Mode of investment is not ETH");

uint256 weiAmount = msg.value;
_processTx(_beneficiary, weiAmount);
uint256 refund = _processTx(_beneficiary, weiAmount);
weiAmount = weiAmount.sub(refund);

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

Expand All @@ -123,9 +125,9 @@ contract CappedSTO is ISTO, ReentrancyGuard {
function buyTokensWithPoly(uint256 _investedPOLY) public nonReentrant{
require(!paused, "Should not be paused");
require(fundRaiseTypes[uint8(FundRaiseType.POLY)], "Mode of investment is not POLY");
_processTx(msg.sender, _investedPOLY);
_forwardPoly(msg.sender, wallet, _investedPOLY);
_postValidatePurchase(msg.sender, _investedPOLY);
uint256 refund = _processTx(msg.sender, _investedPOLY);
_forwardPoly(msg.sender, wallet, _investedPOLY.sub(refund));
_postValidatePurchase(msg.sender, _investedPOLY.sub(refund));
}

/**
Expand Down Expand Up @@ -183,11 +185,13 @@ contract CappedSTO is ISTO, ReentrancyGuard {
* @param _beneficiary Address performing the token purchase
* @param _investedAmount Value in wei involved in the purchase
*/
function _processTx(address _beneficiary, uint256 _investedAmount) internal {
function _processTx(address _beneficiary, uint256 _investedAmount) internal returns(uint256 refund) {

_preValidatePurchase(_beneficiary, _investedAmount);
// calculate token amount to be created
uint256 tokens = _getTokenAmount(_investedAmount);
uint256 tokens;
(tokens, refund) = _getTokenAmount(_investedAmount);
_investedAmount = _investedAmount.sub(refund);

// update state
if (fundRaiseTypes[uint8(FundRaiseType.POLY)]) {
Expand All @@ -212,7 +216,9 @@ contract CappedSTO is ISTO, 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");
require(totalTokensSold.add(_getTokenAmount(_investedAmount)) <= cap, "Investment more than cap is not allowed");
uint256 tokens;
(tokens, ) = _getTokenAmount(_investedAmount);
require(totalTokensSold.add(tokens) <= cap, "Investment more than cap is not allowed");
/*solium-disable-next-line security/no-block-members*/
require(now >= startTime && now <= endTime, "Offering is closed/Not yet started");
}
Expand Down Expand Up @@ -261,18 +267,23 @@ contract CappedSTO is ISTO, ReentrancyGuard {
* @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 tokenAmount) {
tokenAmount = _investedAmount.mul(rate);
tokenAmount = tokenAmount.div(uint256(10) ** 18);
return tokenAmount;
function _getTokenAmount(uint256 _investedAmount) internal view returns (uint256 _tokens, uint256 _refund) {
_tokens = _investedAmount.mul(rate);
_tokens = _tokens.div(uint256(10) ** 18);
uint256 granularity = ISecurityToken(securityToken).granularity();
_tokens = _tokens.div(granularity);
_tokens = _tokens.mul(granularity);
_refund = _investedAmount.sub((_tokens.mul(uint256(10) ** 18)).div(rate));
}

/**
* @notice Determines how ETH is stored/forwarded on purchases.
*/
function _forwardFunds() internal {
wallet.transfer(msg.value);
function _forwardFunds(uint256 _refund) internal {
wallet.transfer(msg.value.sub(_refund));
msg.sender.transfer(_refund);
}

/**
Expand Down
60 changes: 31 additions & 29 deletions test/b_capped_sto.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,16 +338,6 @@ contract("CappedSTO", accounts => {
);
});

it("Should buy the tokens -- Failed due to wrong granularity", async () => {
await catchRevert(
web3.eth.sendTransaction({
from: account_investor1,
to: I_CappedSTO_Array_ETH[0].address,
value: web3.utils.toWei("0.1111", "ether")
})
);
});

it("Should Buy the tokens", async () => {
blockNo = latestBlock();
fromTime = latestTime();
Expand Down Expand Up @@ -427,17 +417,8 @@ contract("CappedSTO", accounts => {
assert.isFalse(await I_CappedSTO_Array_ETH[0].paused.call());
});

it("Should buy the tokens -- Failed due to wrong granularity", async () => {
await catchRevert(
web3.eth.sendTransaction({
from: account_investor1,
to: I_CappedSTO_Array_ETH[0].address,
value: web3.utils.toWei("0.1111", "ether")
})
);
});

it("Should restrict to buy tokens after hiting the cap in second tx first tx pass", async () => {
it("Should buy the granular unit tokens and refund pending amount", async () => {
await I_SecurityToken_ETH.changeGranularity(10 ** 21, {from: token_owner});
let tx = await I_GeneralTransferManager.modifyWhitelist(
account_investor2,
fromTime,
Expand All @@ -448,15 +429,26 @@ contract("CappedSTO", accounts => {
from: account_issuer
}
);

assert.equal(tx.logs[0].args._investor, account_investor2, "Failed in adding the investor in whitelist");
const initBalance = BigNumber(await web3.eth.getBalance(account_investor2));
tx = await I_CappedSTO_Array_ETH[0].buyTokens(account_investor2, {from: account_investor2, value: web3.utils.toWei("1.5", "ether"), gasPrice: 1});
const finalBalance = BigNumber(await web3.eth.getBalance(account_investor2));
assert.equal(finalBalance.add(BigNumber(tx.receipt.gasUsed)).add(web3.utils.toWei("1", "ether")).toNumber(), initBalance.toNumber());
await I_SecurityToken_ETH.changeGranularity(1, {from: token_owner});
assert.equal((await I_CappedSTO_Array_ETH[0].getRaised.call(ETH)).dividedBy(new BigNumber(10).pow(18)).toNumber(), 2);

assert.equal((await I_SecurityToken_ETH.balanceOf(account_investor2)).dividedBy(new BigNumber(10).pow(18)).toNumber(), 1000);
});

it("Should restrict to buy tokens after hiting the cap in second tx first tx pass", async () => {


// Fallback transaction
await web3.eth.sendTransaction({
from: account_investor2,
to: I_CappedSTO_Array_ETH[0].address,
gas: 2100000,
value: web3.utils.toWei("9", "ether")
value: web3.utils.toWei("8", "ether")
});

assert.equal((await I_CappedSTO_Array_ETH[0].getRaised.call(ETH)).dividedBy(new BigNumber(10).pow(18)).toNumber(), 10);
Expand Down Expand Up @@ -793,8 +785,8 @@ contract("CappedSTO", accounts => {
await I_CappedSTO_Array_POLY[0].unpause({ from: account_issuer });
});


it("Should restrict to buy tokens after hiting the cap in second tx first tx pass", async () => {
it("Should buy the granular unit tokens and charge only required POLY", async () => {
await I_SecurityToken_POLY.changeGranularity(10 ** 22, {from: token_owner});
let tx = await I_GeneralTransferManager.modifyWhitelist(
account_investor2,
P_fromTime,
Expand All @@ -806,15 +798,25 @@ contract("CappedSTO", accounts => {
gas: 500000
}
);

console.log((await I_SecurityToken_POLY.balanceOf(account_investor2)).dividedBy(new BigNumber(10).pow(18)).toNumber());
assert.equal(tx.logs[0].args._investor, account_investor2, "Failed in adding the investor in whitelist");

await I_PolyToken.getTokens(10000 * Math.pow(10, 18), account_investor2);

await I_PolyToken.approve(I_CappedSTO_Array_POLY[0].address, 9000 * Math.pow(10, 18), { from: account_investor2 });
const initRaised = (await I_CappedSTO_Array_POLY[0].getRaised.call(POLY)).dividedBy(new BigNumber(10).pow(18)).toNumber();
tx = await I_CappedSTO_Array_POLY[0].buyTokensWithPoly(3000 * Math.pow(10, 18), { from: account_investor2 });
await I_SecurityToken_POLY.changeGranularity(1, {from: token_owner});
assert.equal((await I_CappedSTO_Array_POLY[0].getRaised.call(POLY)).dividedBy(new BigNumber(10).pow(18)).toNumber(), initRaised + 2000); //2000 this call, 1000 earlier
assert.equal((await I_PolyToken.balanceOf(account_investor2)).dividedBy(new BigNumber(10).pow(18)).toNumber(), 8000);
assert.equal(
(await I_SecurityToken_POLY.balanceOf(account_investor2)).dividedBy(new BigNumber(10).pow(18)).toNumber(),
10000
);
});


it("Should restrict to buy tokens after hiting the cap in second tx first tx pass", async () => {
// buyTokensWithPoly transaction
await I_CappedSTO_Array_POLY[0].buyTokensWithPoly(9000 * Math.pow(10, 18), { from: account_investor2 });
await I_CappedSTO_Array_POLY[0].buyTokensWithPoly(7000 * Math.pow(10, 18), { from: account_investor2 });

assert.equal((await I_CappedSTO_Array_POLY[0].getRaised.call(POLY)).dividedBy(new BigNumber(10).pow(18)).toNumber(), 10000);

Expand Down