Skip to content

Commit

Permalink
Merge branch 'feat/kyc-with-pricing-info'
Browse files Browse the repository at this point in the history
  • Loading branch information
miohtama committed May 17, 2018
2 parents 579ff96 + 716b897 commit e0fea1a
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 32 deletions.
37 changes: 31 additions & 6 deletions contracts/CrowdsaleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,18 @@ contract CrowdsaleBase is Haltable {
}

/**
* Make an investment.
* @dev Make an investment.
*
* Crowdsale must be running for one to invest.
* We must have not pressed the emergency brake.
*
* @param receiver The Ethereum address who receives the tokens
* @param customerId (optional) UUID v4 to track the successful payments on the server side'
* @param tokenAmount Amount of tokens which be credited to receiver
*
* @return tokenAmount How mony tokens were bought
* @return tokensBought How mony tokens were bought
*/
function investInternal(address receiver, uint128 customerId) stopInEmergency internal returns(uint tokensBought) {
function buyTokens(address receiver, uint128 customerId, uint256 tokenAmount) stopInEmergency internal returns(uint tokensBought) {

// Determine if it's a good time to accept investment from this participant
if(getState() == State.PreFunding) {
Expand All @@ -178,9 +179,6 @@ contract CrowdsaleBase is Haltable {

uint weiAmount = msg.value;

// Account presale sales separately, so that they do not count against pricing tranches
uint tokenAmount = pricingStrategy.calculatePrice(weiAmount, weiRaised - presaleWeiRaised, tokensSold, msg.sender, token.decimals());

// Dust transaction
require(tokenAmount != 0);

Expand Down Expand Up @@ -215,6 +213,33 @@ contract CrowdsaleBase is Haltable {
return tokenAmount;
}

/**
* @dev Make an investment based on pricing strategy
*
* This is a wrapper for buyTokens(), but the amount of tokens receiver will
* have depends on the pricing strategy used.
*
* @param receiver The Ethereum address who receives the tokens
* @param customerId (optional) UUID v4 to track the successful payments on the server side'
*
* @return tokensBought How mony tokens were bought
*/
function investInternal(address receiver, uint128 customerId) stopInEmergency internal returns(uint tokensBought) {
return buyTokens(receiver, customerId, pricingStrategy.calculatePrice(msg.value, weiRaised - presaleWeiRaised, tokensSold, msg.sender, token.decimals()));
}

/**
* @dev Calculate tokens user will have for theirr purchase
*
* @param weisTotal How much ethers (in wei) the user putssssss in
* @param pricePerToken What is the price for one token
*
* @return tokensTotal which is weisTotal divided by pricePerToken
*/
function calculateTokens(uint256 weisTotal, uint256 pricePerToken) public constant returns(uint tokensTotal) {
return weisTotal/pricePerToken;
}

/**
* Finalize a succcesful crowdsale.
*
Expand Down
16 changes: 10 additions & 6 deletions contracts/KYCCrowdsale.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ import "./AllocatedCrowdsaleMixin.sol";
import "./KYCPayloadDeserializer.sol";

/**
* A crowdsale that allows only signed payload with server-side specified buy in limits.
*
* A crowdsale that allows buys only from signed payload with server-side specified limits and price.
*
* The token distribution happens as in the allocated crowdsale.
*
*/
contract KYCCrowdsale is AllocatedCrowdsaleMixin, KYCPayloadDeserializer {

/* Server holds the private key to this address to decide if the AML payload is valid or not. */
/* Server holds the private key to this address to sign incoming buy payloads to signal we have KYC records in the books for these users. */
address public signerAddress;

/* A new server-side signer key was set to be effective */
Expand All @@ -37,23 +36,28 @@ contract KYCCrowdsale is AllocatedCrowdsaleMixin, KYCPayloadDeserializer {
// Perform signature check for normal addresses
// (not deployment accounts, etc.)
if(earlyParticipantWhitelist[msg.sender]) {
// For test purchases use this faux customer id
// Deployment provided early participant list is for deployment and diagnostics
// For test purchases use this faux customer id 0x1000
_tokenAmount = investInternal(msg.sender, 0x1000);

} else {
// User comes through the server, check that the signature to ensure ther server
// side KYC has passed for this customer id and whitelisted Ethereum address

bytes32 hash = sha256(dataframe);

var (whitelistedAddress, customerId, minETH, maxETH) = getKYCPayload(dataframe);
var (whitelistedAddress, customerId, minETH, maxETH, pricingInfo) = getKYCPayload(dataframe);

// Check that the KYC data is signed by our server
require(ecrecover(hash, v, r, s) == signerAddress);

// Only whitelisted address can participate the transaction
require(whitelistedAddress == msg.sender);

_tokenAmount = investInternal(msg.sender, customerId);
// Server gives us information what is the buy price for this user
uint256 tokensTotal = calculateTokens(msg.value, pricingInfo);

_tokenAmount = buyTokens(msg.sender, customerId, tokensTotal);
}

if(!earlyParticipantWhitelist[msg.sender]) {
Expand Down
12 changes: 1 addition & 11 deletions contracts/KYCPayloadDeserializer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,13 @@ contract KYCPayloadDeserializer {
uint256 pricingInfo;
}

/**
* Same as above, does not seem to cause any issue.
*/
function getKYCPayload(bytes dataframe) public constant returns(address whitelistedAddress, uint128 customerId, uint32 minEth, uint32 maxEth) {
address _whitelistedAddress = dataframe.sliceAddress(0);
uint128 _customerId = uint128(dataframe.slice16(20));
uint32 _minETH = uint32(dataframe.slice4(36));
uint32 _maxETH = uint32(dataframe.slice4(40));
return (_whitelistedAddress, _customerId, _minETH, _maxETH);
}

/**
* Same as above, but with pricing information included in the payload as the last integer.
*
* @notice In a long run, deprecate the legacy methods above and only use this payload.
*/
function getKYCPresalePayload(bytes dataframe) public constant returns(address whitelistedAddress, uint128 customerId, uint32 minEth, uint32 maxEth, uint256 pricingInfo) {
function getKYCPayload(bytes dataframe) public constant returns(address whitelistedAddress, uint128 customerId, uint32 minEth, uint32 maxEth, uint256 pricingInfo) {
address _whitelistedAddress = dataframe.sliceAddress(0);
uint128 _customerId = uint128(dataframe.slice16(20));
uint32 _minETH = uint32(dataframe.slice4(36));
Expand Down
2 changes: 1 addition & 1 deletion contracts/KYCPresale.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ contract KYCPresale is CrowdsaleBase, KYCPayloadDeserializer {
require(!halted);

bytes32 hash = sha256(dataframe);
var (whitelistedAddress, customerId, minETH, maxETH, pricingInfo) = getKYCPresalePayload(dataframe);
var (whitelistedAddress, customerId, minETH, maxETH, pricingInfo) = getKYCPayload(dataframe);
uint multiplier = 10 ** 18;
address receiver = msg.sender;
uint weiAmount = msg.value;
Expand Down
5 changes: 2 additions & 3 deletions ico/tests/contracts/test_kyc_payload_deserializing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from eth_utils import to_normalized_address, to_checksum_address
from web3.contract import Contract

from ico.kyc import pack_kyc_dataframe
from ico.kyc import pack_kyc_pricing_dataframe


Expand All @@ -27,7 +26,7 @@ def test_roundtrip_kyc_data(kyc_deserializer, whitelisted_address):
"""We correctly encode data in Python side and decode it back in the smart contract."""

customer_id = uuid.uuid4()
dataframe = pack_kyc_dataframe(whitelisted_address, customer_id, int(0.1 * 10000), int(9999 * 10000))
dataframe = pack_kyc_pricing_dataframe(whitelisted_address, customer_id, int(0.1 * 10000), int(9999 * 10000), 1)
tuple_value = kyc_deserializer.call().getKYCPayload(dataframe)

#
Expand All @@ -45,7 +44,7 @@ def test_roundtrip_kyc_presale_data(kyc_deserializer, whitelisted_address):

customer_id = uuid.uuid4()
dataframe = pack_kyc_pricing_dataframe(whitelisted_address, customer_id, int(0.1 * 10000), int(9999 * 10000), 123)
tuple_value = kyc_deserializer.call().getKYCPresalePayload(dataframe)
tuple_value = kyc_deserializer.call().getKYCPayload(dataframe)

#
# Check that the output looks like what we did expect
Expand Down
80 changes: 75 additions & 5 deletions ico/tests/contracts/test_kyccrowdsale.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from ico.state import CrowdsaleState
from ico.sign import get_ethereum_address_from_private_key
from ico.sign import sign
from ico.kyc import pack_kyc_dataframe
from ico.kyc import pack_kyc_pricing_dataframe, unpack_kyc_pricing_dataframe


@pytest.fixture
Expand Down Expand Up @@ -149,7 +149,7 @@ def test_kyc_participate_with_signed_address(chain, kyc_crowdsale, customer, cus
assert not kyc_crowdsale.call().isBreakingCap(wei_value, tokens_per_eth, wei_value, tokens_per_eth)

# KYC limits for this participant: 0...1 ETH
kyc_payload = pack_kyc_dataframe(customer, customer_id, 0, 1*10000)
kyc_payload = pack_kyc_pricing_dataframe(customer, customer_id, 0, 1*10000, 1)
signed_data = sign(kyc_payload, private_key)

kyc_crowdsale.transact({"from": customer, "value": wei_value, "gas": 2222333}).buyWithKYCData(kyc_payload, signed_data["v"], signed_data["r_bytes"], signed_data["s_bytes"])
Expand Down Expand Up @@ -177,7 +177,7 @@ def test_kyc_participate_bad_signature(chain, kyc_crowdsale, customer, customer_
wei_value = to_wei(1, "ether")

# KYC limits for this participant: 0...1 ETH
kyc_payload = pack_kyc_dataframe(customer, customer_id, 0, 1*10000)
kyc_payload = pack_kyc_pricing_dataframe(customer, customer_id, 0, 1*10000, 1)
signed_data = sign(kyc_payload, private_key + "x") # Use bad private key

with pytest.raises(TransactionFailed):
Expand All @@ -194,7 +194,7 @@ def test_kyc_participate_under_payment(chain, kyc_crowdsale, customer, customer_
wei_value = to_wei(0.1, "ether")

# KYC limits for this participant: 0...1 ETH
kyc_payload = pack_kyc_dataframe(customer, customer_id, int(0.5 * 10000), 1*10000)
kyc_payload = pack_kyc_pricing_dataframe(customer, customer_id, int(0.5 * 10000), 1*10000, 1)
signed_data = sign(kyc_payload, private_key) # Use bad private key

with pytest.raises(TransactionFailed):
Expand All @@ -210,7 +210,7 @@ def test_kyc_participate_over_payment(chain, kyc_crowdsale, customer, customer_i
wei_value = to_wei(1, "ether")

# KYC limits for this participant: 0...1 ETH
kyc_payload = pack_kyc_dataframe(customer, customer_id, 0, 10*10000)
kyc_payload = pack_kyc_pricing_dataframe(customer, customer_id, 0, 10*10000, 1)
signed_data = sign(kyc_payload, private_key) # Use bad private key

kyc_crowdsale.transact({"from": customer, "value": wei_value, "gas": 2222333}).buyWithKYCData(kyc_payload, signed_data["v"], signed_data["r_bytes"], signed_data["s_bytes"])
Expand All @@ -225,3 +225,73 @@ def test_kyc_participate_set_signer_only_owner(chain, kyc_crowdsale, malicious_a

with pytest.raises(TransactionFailed):
kyc_crowdsale.transact({"from": malicious_address}).setSignerAddress(signer_address)


def test_kyc_participate_with_different_price(chain, web3, kyc_crowdsale, customer, customer_id, kyc_token, private_key, preico_starts_at, pricing, team_multisig):
"""The same user buys token with two different prices (as given by the server)."""

# Check KYC crowdsale is good to go
whitelisted_address = customer
time_travel(chain, kyc_crowdsale.call().startsAt() + 1)
start_multisig_total = web3.eth.getBalance(team_multisig)

# Check the setup looks good
assert kyc_crowdsale.call().getState() == CrowdsaleState.Funding
assert kyc_crowdsale.call().isFinalizerSane()
assert kyc_crowdsale.call().isPricingSane()
assert kyc_crowdsale.call().beneficiary() == team_multisig
assert kyc_token.call().transferAgents(team_multisig) == True
assert kyc_crowdsale.call().investedAmountOf(whitelisted_address) == 0

# Do a test buy for 1 ETH
wei_value = to_wei(1, "ether")
excepted_token_value = int(0.5 * 10**18)
price = 2 # wei per token

assert kyc_crowdsale.call().calculateTokens(wei_value, price) == excepted_token_value

# Buy with price 1 token = 2 wei
kyc_payload = pack_kyc_pricing_dataframe(whitelisted_address, customer_id, 0, 1*10000, price)
signed_data = sign(kyc_payload, private_key)
unpacked = unpack_kyc_pricing_dataframe(kyc_payload)
assert unpacked["pricing_data"] == price

kyc_crowdsale.transact({"from": whitelisted_address, "value": wei_value, "gas": 2222333}).buyWithKYCData(kyc_payload, signed_data["v"], signed_data["r_bytes"], signed_data["s_bytes"])

# We got credited
assert kyc_token.call().balanceOf(whitelisted_address) == excepted_token_value
assert kyc_crowdsale.call().investedAmountOf(whitelisted_address) == wei_value

# We have tracked the investor id
events = kyc_crowdsale.pastEvents("Invested").get()
assert len(events) == 1
e = events[0]
assert e["args"]["investor"].lower() == whitelisted_address.lower()
assert e["args"]["weiAmount"] == wei_value
assert e["args"]["customerId"] == customer_id.int
assert e["args"]["tokenAmount"] == excepted_token_value

# More tokens, different price this time
wei_value = to_wei(1, "ether")
new_excepted_token_value = int(0.25 * 10**18)
price = 4 # wei per token

# New transaction, increased per person cap to 2 ETH
kyc_payload = pack_kyc_pricing_dataframe(whitelisted_address, customer_id, 0, 2*10000, price)
signed_data = sign(kyc_payload, private_key)
kyc_crowdsale.transact({"from": whitelisted_address, "value": wei_value, "gas": 333444}).buyWithKYCData(kyc_payload, signed_data["v"], signed_data["r_bytes"], signed_data["s_bytes"])

# We got credited
total = wei_value * 2
assert kyc_token.call().balanceOf(whitelisted_address) == excepted_token_value + new_excepted_token_value
assert kyc_crowdsale.call().investedAmountOf(whitelisted_address) == total
assert web3.eth.getBalance(team_multisig) == start_multisig_total + total

# We have another event, this time with new price
events = kyc_crowdsale.pastEvents("Invested").get()
assert len(events) == 2
e = events[-1]
assert e["args"]["investor"].lower() == whitelisted_address.lower()
assert e["args"]["weiAmount"] == wei_value
assert e["args"]["customerId"] == customer_id.int
assert e["args"]["tokenAmount"] == new_excepted_token_value

0 comments on commit e0fea1a

Please sign in to comment.