Skip to content

Added support for indivisible tokens in capped STO #415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 10, 2018
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