Skip to content

Commit

Permalink
Merge pull request #106 from TokenMarketNet/feat/rfd-129-kycpresale
Browse files Browse the repository at this point in the history
Feat/rfd 129 kycpresale
  • Loading branch information
miohtama committed Feb 5, 2018
2 parents 2835f33 + 124a42e commit 5c25e9f
Show file tree
Hide file tree
Showing 6 changed files with 490 additions and 33 deletions.
3 changes: 0 additions & 3 deletions contracts/CrowdsaleBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,11 @@ contract CrowdsaleBase is Haltable {
// Crowdsale end time has been changed
event EndsAtChanged(uint newEndsAt);

State public testState;

function CrowdsaleBase(address _token, PricingStrategy _pricingStrategy, address _multisigWallet, uint _start, uint _end, uint _minimumFundingGoal) {

owner = msg.sender;

token = FractionalERC20(_token);

setPricingStrategy(_pricingStrategy);

multisigWallet = _multisigWallet;
Expand Down
51 changes: 21 additions & 30 deletions contracts/KYCPayloadDeserializer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
import "./BytesDeserializer.sol";

/**
* A mix-in contract to decode different AML payloads.
* A mix-in contract to decode different signed KYC payloads.
*
* @notice This should be a library, but for the complexity and toolchain fragility risks involving of linking library inside library, we put this as a mix-in.
* @notice This should be a library, but for the complexity and toolchain fragility risks involving of linking library inside library, we currently use this as a helper method mix-in.
*/
contract KYCPayloadDeserializer {

using BytesDeserializer for bytes;

// @notice this struct describes what kind of data we include in the payload, we do not use this directly
// The bytes payload set on the server side
// total 56 bytes

struct KYCPayload {

/** Customer whitelisted address where the deposit can come from */
Expand All @@ -35,46 +35,37 @@ contract KYCPayloadDeserializer {

/** Max amount this customer can to invest in ETH. Set zero if no maximum. Expressed as parts of 10000. 1 ETH = 10000. */
uint32 maxETH; // 4 bytes

/**
* Information about the price promised for this participant. It can be pricing tier id or directly one token price in weis.
* @notice This is a later addition and not supported in all scenarios yet.
*/
uint256 pricingInfo;
}

/**
* Deconstruct server-side byte data to structured data.
* Same as above, does not seem to cause any issue.
*/

function deserializeKYCPayload(bytes dataframe) internal constant returns(KYCPayload decodedPayload) {
KYCPayload payload;
payload.whitelistedAddress = dataframe.sliceAddress(0);
payload.customerId = uint128(dataframe.slice16(20));
payload.minETH = uint32(dataframe.slice4(36));
payload.maxETH = uint32(dataframe.slice4(40));
return payload;
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);
}

/**
* Helper function to allow us to return the decoded payload to an external caller for testing.
* Same as above, but with pricing information included in the payload as the last integer.
*
* TODO: Some sort of compiler issue (?) with memory keyword. Tested with solc 0.4.16 and solc 0.4.18.
* If used, makes KYCCrowdsale to set itself to a bad state getState() returns 5 (Failure). Overrides some memory?
* @notice In a long run, deprecate the legacy methods above and only use this payload.
*/
/*
function broken_getKYCPayload(bytes dataframe) public constant returns(address whitelistedAddress, uint128 customerId, uint32 minEth, uint32 maxEth) {
KYCPayload memory payload = deserializeKYCPayload(dataframe);
payload.whitelistedAddress = dataframe.sliceAddress(0);
payload.customerId = uint128(dataframe.slice16(20));
payload.minETH = uint32(dataframe.slice4(36));
payload.maxETH = uint32(dataframe.slice4(40));
return (payload.whitelistedAddress, payload.customerId, payload.minETH, payload.maxETH);
}*/

/**
* 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) {
function getKYCPresalePayload(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));
uint32 _maxETH = uint32(dataframe.slice4(40));
return (_whitelistedAddress, _customerId, _minETH, _maxETH);
uint256 _pricingInfo = uint256(dataframe.slice32(44));
return (_whitelistedAddress, _customerId, _minETH, _maxETH, _pricingInfo);
}

}
163 changes: 163 additions & 0 deletions contracts/KYCPresale.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import "./CrowdsaleBase.sol";
import "./KYCPayloadDeserializer.sol";

/**
* A presale smart contract that collects money from SAFT/SAFTE agreed buyers.
*
* Presale contract where we collect money for the token that does not exist yet.
* The same KYC rules apply as in KYCCrowdsale. No tokens are issued in this point,
* but they are delivered to the buyers after the token sale is over.
*
*/
contract KYCPresale is CrowdsaleBase, KYCPayloadDeserializer {

/** The cap of this presale contract in wei */
uint256 public saleWeiCap;

/** Server holds the private key to this address to decide if the AML payload is valid or not. */
address public signerAddress;

/** A new server-side signer key was set to be effective */
event SignerChanged(address signer);

/** An user made a prepurchase through KYC'ed interface. The money has been moved to the token sale multisig wallet. The buyer will receive their tokens in an airdrop after the token sale is over. */
event Prepurchased(address investor, uint weiAmount, uint tokenAmount, uint128 customerId, uint256 pricingInfo);

/** The owner changes the presale ETH cap during the sale */
event CapUpdated(uint256 newCap);

/**
* Constructor.
*
* Presale does not know about token or pricing strategy, as they will be only available during the future airdrop.
*
* @dev The parent contract has some unnecessary variables for our use case. For this round of development, we chose to use null value for token and pricing strategy. In the future versions have a parent sale contract that does not assume an existing token.
*/
function KYCPresale(address _multisigWallet, uint _start, uint _end, uint _saleWeiCap) CrowdsaleBase(FractionalERC20(address(1)), PricingStrategy(address(0)), _multisigWallet, _start, _end, 0) {
saleWeiCap = _saleWeiCap;
}

/**
* A token purchase with anti-money laundering
*
* ©return tokenAmount How many tokens where bought
*/
function buyWithKYCData(bytes dataframe, uint8 v, bytes32 r, bytes32 s) public payable returns(uint tokenAmount) {

// Presale ended / emergency abort
require(!halted);

bytes32 hash = sha256(dataframe);
var (whitelistedAddress, customerId, minETH, maxETH, pricingInfo) = getKYCPresalePayload(dataframe);
uint multiplier = 10 ** 18;
address receiver = msg.sender;
uint weiAmount = msg.value;

// The payload was created by token sale server
require(ecrecover(hash, v, r, s) == signerAddress);

// Determine if it's a good time to accept investment from this participant
if(getState() == State.PreFunding) {
// Are we whitelisted for early deposit
require(earlyParticipantWhitelist[receiver]);
} else if(getState() == State.Funding) {
// Retail participants can only come in when the crowdsale is running
// pass
} else {
// Unwanted state
revert;
}

if(investedAmountOf[receiver] == 0) {
// A new investor
investorCount++;
}

// Update per investor amount
investedAmountOf[receiver] = investedAmountOf[receiver].plus(weiAmount);

// Update totals
weiRaised = weiRaised.plus(weiAmount);

// Check that we did not bust the cap
require(!isBreakingCap(weiAmount, tokenAmount, weiRaised, tokensSold));

require(investedAmountOf[msg.sender] >= minETH * multiplier / 10000);
require(investedAmountOf[msg.sender] <= maxETH * multiplier / 10000);

// Pocket the money, or fail the crowdsale if we for some reason cannot send the money to our multisig
require(multisigWallet.send(weiAmount));

// Tell us invest was success
Prepurchased(receiver, weiAmount, tokenAmount, customerId, pricingInfo);

return 0; // In presale we do not issue actual tokens tyet
}

/// @dev This function can set the server side address
/// @param _signerAddress The address derived from server's private key
function setSignerAddress(address _signerAddress) onlyOwner {
signerAddress = _signerAddress;
SignerChanged(signerAddress);
}

/**
* Called from invest() to confirm if the curret investment does not break our cap rule.
*/
function isBreakingCap(uint weiAmount, uint tokenAmount, uint weiRaisedTotal, uint tokensSoldTotal) constant returns (bool limitBroken) {
if(weiRaisedTotal > saleWeiCap) {
return true;
} else {
return false;
}
}

/**
* We are sold out when our approve pool becomes empty.
*/
function isCrowdsaleFull() public constant returns (bool) {
return weiRaised >= saleWeiCap;
}

/**
* Allow owner to adjust the cap during the presale.
*
* This allows e.g. US dollar pegged caps.
*/
function setWeiCap(uint newCap) public onlyOwner {
saleWeiCap = newCap;
CapUpdated(newCap);
}

/**
* Because this is a presale, we do not issue any tokens yet.
*
* @dev Have this taken away from the parent contract?
*/
function assignTokens(address receiver, uint tokenAmount) internal {
revert;
}

/**
* Allow to (re)set pricing strategy.
*
* @dev Because we do not have token price set in presale, we do nothing. This will be removed in the future versions.
*/
function setPricingStrategy(PricingStrategy _pricingStrategy) onlyOwner {
}

/**
* Presale state machine management.
*
* Presale cannot fail; it is running until manually ended.
*
*/
function getState() public constant returns (State) {
if (block.timestamp < startsAt) {
return State.PreFunding;
} else {
return State.Funding;
}
}

}
34 changes: 34 additions & 0 deletions ico/kyc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ def pack_kyc_dataframe(whitelisted_address: str, customer_id: UUID, min_eth_10k:
See KYCPayloadDeserializer for the matching Solidity code.
.. note ::
In a long term this will be deprecated in the behalf of the function below.
:param whitelisted_address: Must be whitelisted address in a Ethereum checksummed format
:param customer_id: Customer id as UUIDv8
:param min_eth: Min investment for this customer. Expressed as the parts of 1/10000.
Expand All @@ -27,3 +31,33 @@ def pack_kyc_dataframe(whitelisted_address: str, customer_id: UUID, min_eth_10k:
data = addr_b + customer_b + min_b + max_b
assert len(data) == 44, "Got length: {}".format(len(data))
return data


def pack_kyc_pricing_dataframe(whitelisted_address: str, customer_id: UUID, min_eth_10k: int, max_eth_10k: int, pricing_info: int) -> bytes:
"""Pack KYC presale information to the smart contract.
Same as above, but with pricing info included.
See KYCPayloadDeserializer for the matching Solidity code.
:param whitelisted_address: Must be whitelisted address in a Ethereum checksummed format
:param customer_id: Customer id as UUIDv8
:param min_eth: Min investment for this customer. Expressed as the parts of 1/10000.
:param max_eth: Max investment for this customer. Expressed as the parts of 1/10000.
:param pricing_info: Tier identifier or directly one token price in wei.
:return: Raw bytes to send to the contract as a function argument
"""
assert is_checksum_address(whitelisted_address)
assert isinstance(customer_id, UUID)
assert type(min_eth_10k) == int
assert type(max_eth_10k) == int
assert type(pricing_info) == int
addr_value = int(whitelisted_address, 16)
addr_b = addr_value.to_bytes(20, byteorder="big") # Ethereum address is 20 bytes
customer_b = customer_id.bytes
min_b = min_eth_10k.to_bytes(4, byteorder="big")
max_b = max_eth_10k.to_bytes(4, byteorder="big")
pricing_data = pricing_info.to_bytes(32, byteorder="big")
data = addr_b + customer_b + min_b + max_b + pricing_data
assert len(data) == 76, "Got length: {}".format(len(data))
return data
18 changes: 18 additions & 0 deletions ico/tests/contracts/test_kyc_payload_deserializing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from web3.contract import Contract

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


@pytest.fixture()
Expand Down Expand Up @@ -39,3 +40,20 @@ def test_roundtrip_kyc_data(kyc_deserializer, whitelisted_address):
assert tuple_value[2] == 1000
assert tuple_value[3] == 99990000

def test_roundtrip_kyc_presale_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_pricing_dataframe(whitelisted_address, customer_id, int(0.1 * 10000), int(9999 * 10000), 123)
tuple_value = kyc_deserializer.call().getKYCPresalePayload(dataframe)

#
# Check that the output looks like what we did expect
#

assert tuple_value[0].lower() == whitelisted_address.lower()
# Do a raw integer comparison, because web3.py and UUID disagree about left padding zeroes
assert tuple_value[1] == customer_id.int
assert tuple_value[2] == 1000
assert tuple_value[3] == 99990000
assert tuple_value[4] == 123

0 comments on commit 5c25e9f

Please sign in to comment.