public static object HandleHelperOperation(string operation, params object[] args) { switch (operation) { case "BalanceOfVestedAddress": // retrieve the real balance of an address that has been subjected to whitepaper defined vesting period if (!Helpers.RequireArgumentLength(args, 1)) { return(false); } return(Helpers.BalanceOfVestedAddress((byte[])args[0])); case "IsPrivateSaleAllocationLocked": // if the admin method `Administration.AllocatePresalePurchase` is permanently disabled, this method will return // the timestamp the lock was put in place. return(Storage.Get(Storage.CurrentContext, StorageKeys.PrivateSaleAllocationLocked())); case "supportedStandards": // support NEP-10 by responding to supportedStandards // https://github.com/neo-project/proposals/blob/master/nep-10.mediawiki return(ICOTemplate.SupportedStandards()); case "BalanceOfSaleContribution": if (!Helpers.RequireArgumentLength(args, 1)) { return(false); } return(Helpers.BalanceOfSaleContribution((byte[])args[0])); } return(false); }
/// <summary> /// Claims unsold tokens /// </summary> /// <returns></returns> public static bool ClaimUnsoldTokens() { bool UnsoldTokensClaimed = Storage.Get(Storage.CurrentContext, StorageKeys.UnsoldTokensClaimed()).AsString() == "1"; //This method can only be executed by the admin account, after the public sale, and can only be called once (use UnsoldTokensClaimed() storage item) if (Helpers.GetBlockTimestamp() >= ICOTemplate.PublicSaleEndTime() && UnsoldTokensClaimed == false && Helpers.VerifyIsAdminAccount()) { byte[] address = ICOTemplate.AdditionalCompanyTokenFund; //Get amount remaining BigInteger amountRemaining = NEP5.CrowdsaleAvailableAmount(); //Add vested amount to account TokenSale.SetVestingPeriodForAddress(address, "company", amountRemaining); //Set total supply Helpers.SetTotalSupply(amountRemaining); //Set the UnsoldTokensClaimed() storage item so ClaimUnsoldTokens() cannot be called again Storage.Put(Storage.CurrentContext, StorageKeys.UnsoldTokensClaimed(), "1"); transfer(null, address, amountRemaining); Runtime.Notify("ClaimUnsoldTokens() tokens allocated", address, amountRemaining); return(true); } return(false); }
/// <summary> /// mint tokens is called when a user wishes to purchase tokens /// </summary> /// <returns></returns> public static bool MintTokens() { object[] transactionData = Helpers.GetTransactionAndSaleData(); Transaction tx = (Transaction)transactionData[0]; byte[] sender = (byte[])transactionData[1]; byte[] receiver = (byte[])transactionData[2]; ulong receivedNEO = (ulong)transactionData[3]; ulong receivedGAS = (ulong)transactionData[4]; BigInteger whiteListGroupNumber = (BigInteger)transactionData[5]; BigInteger crowdsaleAvailableAmount = (BigInteger)transactionData[6]; BigInteger groupMaximumContribution = (BigInteger)transactionData[7]; BigInteger totalTokensPurchased = (BigInteger)transactionData[8]; BigInteger neoRemainingAfterPurchase = (BigInteger)transactionData[9]; BigInteger gasRemainingAfterPurchase = (BigInteger)transactionData[10]; BigInteger totalContributionBalance = (BigInteger)transactionData[11]; if (Helpers.GetBlockTimestamp() >= ICOTemplate.PublicSaleEndTime()) { Runtime.Notify("MintTokens() failed. Token Sale is closed.", false); return(false); } if (!CanUserParticipateInSale(transactionData)) { Runtime.Notify("MintTokens() CanUserParticipate failed", false); return(false); } byte[] lastTransactionHash = Storage.Get(Storage.CurrentContext, StorageKeys.MintTokensLastTX()); if (lastTransactionHash == tx.Hash) { // ensure that minTokens doesnt process the same transaction more than once Runtime.Notify("MintTokens() not processing duplicate tx.Hash", tx.Hash); return(false); } Storage.Put(Storage.CurrentContext, StorageKeys.MintTokensLastTX(), tx.Hash); Runtime.Notify("MintTokens() receivedNEO / receivedGAS", receivedNEO, receivedGAS); if (neoRemainingAfterPurchase > 0 || gasRemainingAfterPurchase > 0) { // this purchase would have exceed the allowed max supply so we spent what we could and will refund the remainder refund(sender, neoRemainingAfterPurchase, gasRemainingAfterPurchase); } BigInteger senderAmountSubjectToVesting = SubjectToVestingPeriod(sender); BigInteger newTokenBalance = NEP5.BalanceOf(sender) + totalTokensPurchased + senderAmountSubjectToVesting; Helpers.SetBalanceOf(sender, newTokenBalance); Helpers.SetBalanceOfSaleContribution(sender, totalContributionBalance); Helpers.SetTotalSupply(totalTokensPurchased); transfer(null, sender, totalTokensPurchased); return(true); }
/// <summary> /// allow an administrator to request the unlocking of founder tokens /// </summary> /// <param name="address">founders script hash</param> /// <param name="roundNumber">1-7</param> /// <returns></returns> public static bool UnlockFoundersTokens(byte[] address, int roundNumber) { if (address.Length != 20) { Runtime.Log("UnlockFoundersTokens() invalid address supplied"); return(false); } byte[] roundKey = address.Concat(((BigInteger)roundNumber).AsByteArray()); StorageMap unlockedRounds = Storage.CurrentContext.CreateMap(StorageKeys.FounderTokenUnlockRound()); bool roundPreviouslyUnlocked = unlockedRounds.Get(roundKey).AsBigInteger() > 0; if (roundPreviouslyUnlocked) { Runtime.Log("UnlockFoundersTokens() round already unlocked"); return(false); } object[] foundersVestingPeriod = GetCoreTeamVestingSchedule(); uint currentTimestamp = Helpers.GetBlockTimestamp(); int roundIndex = (roundNumber * 2) - 2; int roundValueIndex = roundIndex + 1; if (roundIndex < 0) { Runtime.Log("UnlockFoundersTokens() invalid round index (<0)"); return(false); } uint roundReleaseDate = (uint)foundersVestingPeriod[roundIndex]; BigInteger roundReleaseAmount = (BigInteger)foundersVestingPeriod[roundValueIndex]; if (currentTimestamp < roundReleaseDate) { Runtime.Log("UnlockFoundersTokens() not scheduled for release"); return(false); } object[] founderKeys = ICOTemplate.MoonlightFounderKeys(); for (int i = 0; i < founderKeys.Length; i++) { byte[] founderKey = (byte[])founderKeys[i]; if (founderKey == address) { Runtime.Notify("UnlockFoundersTokens() releasing funds. currentTimestamp / roundReleaseDate / roundReleaseAmount", currentTimestamp, roundReleaseDate, roundReleaseAmount); Helpers.SetBalanceOf(founderKey, NEP5.BalanceOf(founderKey) + roundReleaseAmount); // set new balance for destination account unlockedRounds.Put(roundKey, "1"); return(true); } } return(false); }
/// <summary> /// get the maximum number of LX that can be purchased by groupNumber during the public sale /// </summary> /// <param name="groupNumber"></param> /// <returns></returns> public static BigInteger GetGroupMaxContribution(BigInteger groupNumber) { StorageMap contributionLimits = Storage.CurrentContext.CreateMap(StorageKeys.GroupContributionAmountPrefix()); BigInteger maxContribution = contributionLimits.Get(groupNumber.AsByteArray()).AsBigInteger(); if (maxContribution > 0) { return(maxContribution); } return(ICOTemplate.MaximumContributionAmount()); }
/// <summary> /// helper method to retrieve the stored group unlock block height /// </summary> /// <param name="groupNumber"></param> /// <returns></returns> public static uint GetGroupUnlockTime(BigInteger groupNumber) { BigInteger unlockTime = 0; if (groupNumber <= 0 || groupNumber > 4) { return(0); } else if (groupNumber > 0 && groupNumber <= 4) { unlockTime = (uint)ICOTemplate.PresaleStartTime(); } return((uint)unlockTime); }
/// <summary> /// MintTokensEth is called when a the ETH contribution listener server triggers an Ether receive event /// </summary> /// <returns></returns> public static bool MintTokensEth(string ethAddress, byte[] neoAddress, ulong ethReceived) { object[] transactionData = Helpers.GetEthTransactionAndSaleData(ethReceived, ethAddress, neoAddress); Transaction tx = (Transaction)transactionData[0]; byte[] sender = (byte[])transactionData[1]; byte[] receiver = (byte[])transactionData[2]; BigInteger whiteListGroupNumber = (BigInteger)transactionData[5]; BigInteger crowdsaleAvailableAmount = (BigInteger)transactionData[6]; BigInteger groupMaximumContribution = (BigInteger)transactionData[7]; BigInteger totalTokensPurchased = (BigInteger)transactionData[8] * NEP5.factor; BigInteger totalContributionBalance = (BigInteger)transactionData[9]; if (!CanETHUserParticipateInSale(transactionData)) { refundEth(ethAddress, ethReceived); Runtime.Notify("MintTokensEth() CanUserParticipate failed", false); return(false); } if (Helpers.GetBlockTimestamp() >= ICOTemplate.PublicSaleEndTime()) { refundEth(ethAddress, ethReceived); Runtime.Notify("MintTokensEth() failed. Token Sale is closed.", false); return(false); } byte[] lastTransactionHash = Storage.Get(Storage.CurrentContext, StorageKeys.MintTokensEthLastTX()); if (lastTransactionHash == tx.Hash) { // ensure that minTokens doesnt process the same transaction more than once Runtime.Notify("MintTokensEth() not processing duplicate tx.Hash", tx.Hash); return(false); } BigInteger tokenTotalSupply = NEP5.TotalSupply(); Storage.Put(Storage.CurrentContext, StorageKeys.MintTokensEthLastTX(), tx.Hash); Runtime.Notify("MintTokensEth() receivedETH", ethReceived); BigInteger senderAmountSubjectToVesting = TokenSale.SubjectToVestingPeriod(sender); BigInteger newTokenBalance = NEP5.BalanceOf(sender) + totalTokensPurchased + senderAmountSubjectToVesting; Helpers.SetBalanceOf(sender, newTokenBalance); Helpers.SetBalanceOfSaleContribution(sender, totalContributionBalance); Helpers.SetTotalSupply(totalTokensPurchased); transfer(null, sender, totalTokensPurchased); return(true); }
/// <summary> /// initialise the smart contract for use /// </summary> /// <returns></returns> public static bool InitSmartContract() { if (Helpers.ContractInitialised()) { // contract can only be initialised once Runtime.Log("InitSmartContract() contract already initialised"); return(false); } uint ContractInitTime = Helpers.GetBlockTimestamp(); Storage.Put(Storage.CurrentContext, StorageKeys.ContractInitTime(), ContractInitTime); // assign pre-allocated tokens to the project object[] immediateAllocation = ICOTemplate.ImmediateProjectGrowthAllocation(); object[] vestedAllocation = ICOTemplate.VestedProjectGrowthAllocation(); BigInteger immediateProjectAllocationValue = ((ICOTemplate.TokenMaxSupply * (BigInteger)immediateAllocation[0]) / 100) * NEP5.factor; BigInteger vestedProjectAllocationValue = ((ICOTemplate.TokenMaxSupply * (BigInteger)vestedAllocation[0]) / 100) * NEP5.factor; Helpers.SetBalanceOf(ICOTemplate.MoonlightProjectKey(), immediateProjectAllocationValue + vestedProjectAllocationValue); Helpers.SetBalanceOfVestedAmount(ICOTemplate.MoonlightProjectKey(), immediateProjectAllocationValue + vestedProjectAllocationValue); // lockup a portion of the tokens to be released in the future uint vestedGrowthReleaseDate = (uint)vestedAllocation[1] + ContractInitTime; object[] vestedTokenPeriod = new object[] { vestedGrowthReleaseDate, vestedProjectAllocationValue }; StorageMap vestingData = Storage.CurrentContext.CreateMap(StorageKeys.VestedTokenPrefix()); vestingData.Put(ICOTemplate.MoonlightProjectKey(), vestedTokenPeriod.Serialize()); // token allocation to MoonlightFounderKeys - update the total supply to include balance - these funds will be unlocked gradually BigInteger founderTokenAllocation = ((ICOTemplate.TokenMaxSupply * (BigInteger)ICOTemplate.MoonlightFoundersAllocationPercentage()) / 100) * NEP5.factor; // token allocated to presale BigInteger presaleAllocationMaxValue = ((ICOTemplate.TokenMaxSupply * (BigInteger)ICOTemplate.PresaleAllocationPercentage()) / 100) * NEP5.factor; // update the total supply to reflect the project allocated tokens BigInteger totalSupply = immediateProjectAllocationValue + vestedProjectAllocationValue + founderTokenAllocation + presaleAllocationMaxValue; Helpers.SetTotalSupply(totalSupply); UpdateAdminAddress(ICOTemplate.InitialAdminAccount); EnableDEXWhitelisting(ICOTemplate.WhitelistDEXListings()); Runtime.Log("InitSmartContract() contract initialisation complete"); return(true); }
/// <summary> /// set a vesting schedule, as defined in the whitepaper, for tokens purchased during the presale /// </summary> /// <param name="address"></param> /// <param name="tokenBalance"></param> /// <returns></returns> public static bool SetVestingPeriodForAddress(byte[] address, BigInteger tokensPurchased) { if (!ICOTemplate.UseTokenVestingPeriod()) { return(false); } if (address.Length != 20) { return(false); } object[] vestingOne = ICOTemplate.VestingBracketOne(); object[] vestingTwo = ICOTemplate.VestingBracketTwo(); BigInteger bracketOneThreshold = (BigInteger)vestingOne[0] * NEP5.factor; BigInteger bracketTwoThreshold = (BigInteger)vestingTwo[0] * NEP5.factor; BigInteger currentAvailableBalance = 0; // how many tokens will be immediately available to the owner uint currentTimestamp = Helpers.GetContractInitTime(); uint bracketOneReleaseDate = (uint)vestingOne[1] + currentTimestamp; uint bracketTwoReleaseDate = (uint)vestingTwo[1] + currentTimestamp; StorageMap vestingData = Storage.CurrentContext.CreateMap(StorageKeys.VestedTokenPrefix()); if (tokensPurchased > bracketTwoThreshold) { // user has purchased enough tokens to fall under the second vesting period restriction // calculate the difference between the bracketOne and bracketTwo thresholds to calculate how much should be released after bracketOne lapses BigInteger bracketOneReleaseAmount = bracketTwoThreshold - bracketOneThreshold; // the remainder will be released after the bracket two release date BigInteger bracketTwoReleaseAmount = tokensPurchased - bracketOneReleaseAmount - bracketOneThreshold; object[] lockoutTimes = new object[] { bracketOneReleaseDate, bracketOneReleaseAmount, bracketTwoReleaseDate, bracketTwoReleaseAmount }; vestingData.Put(address, lockoutTimes.Serialize()); } else { // user has purchased enough tokens to fall under the first vesting period restriction // calculate the difference between amount purchased and bracketOne threshold to calculate how much should be released after the bracketOne lapses BigInteger bracketOneReleaseAmount = tokensPurchased - bracketOneThreshold; object[] lockoutTimes = new object[] { bracketOneReleaseDate, bracketOneReleaseAmount }; vestingData.Put(address, lockoutTimes.Serialize()); } // ensure the total amount purchased is saved Helpers.SetBalanceOf(address, tokensPurchased); Helpers.SetBalanceOfVestedAmount(address, tokensPurchased); return(true); }
/// <summary> /// retrieve information for the received transaction /// </summary> /// <returns>object[] { /// (Transaction)tx, (byte[])sender, (byte)receiver, ulong receivedNEO, ulong receivedGAS, /// (BigInteger)whiteListGroupNumber, (BigInteger)crowdsaleAvailableAmount, (BigInteger)groupMaximumContribution /// (BigInteger)totalTokensPurchased, (BigInteger)neoRemainingAfterPurchase, (BigInteger)gasRemainingAfterPurchase /// (BigInteger)totalContributionBalance /// } /// </returns> public static object[] GetEthTransactionAndSaleData(ulong receivedETH, string ethAddress, byte[] neoAddress) { Transaction tx = (Transaction)ExecutionEngine.ScriptContainer; byte[] sender = neoAddress; byte[] receiver = neoAddress; // only add funds to total received value if receiver is the recipient of the output Runtime.Notify("GetEthTransactionData() Received ETH Deposit type", receiver); BigInteger whiteListGroupNumber = KYC.GetWhitelistGroupNumber(sender); BigInteger crowdsaleAvailableAmount = NEP5.CrowdsaleAvailableAmount(); BigInteger groupMaximumContribution = KYC.GetGroupMaxContribution(whiteListGroupNumber) * NEP5.factor; BigInteger totalTokensPurchased = 0; //ETH minimum must be 0.1 eth if (ICOTemplate.ICOAllowsETH() && receivedETH >= ICOTemplate.EthMinimumContribution()) { //Get the amount of tokens in exchange for contributed ETH. receivedETH is with 18 decimals so divide by 1000000000000000000. BigInteger ethTokenValue = receivedETH * ICOTemplate.ICOEthToTokenExchangeRate() / 1000000000000000000; // there is enough NOS left for this purchase to complete totalTokensPurchased = ethTokenValue; // ensure amountAvailable now reflects number of tokens purchased with ETH } BigInteger totalContributionBalance = BalanceOfSaleContribution(sender) + (totalTokensPurchased * NEP5.factor); return(new object[] { tx, // neo transaction object sender, // who initiated the transfer receiver, // who the assets were sent to ethAddress, // ETH address of contributor receivedETH, // how many neo were transferred whiteListGroupNumber, // what whitelist group is the sender in crowdsaleAvailableAmount, // how many tokens are left to be purchased groupMaximumContribution, // how many tokens can members of this whitelist group purchase totalTokensPurchased, // the total number of tokens purchased in this transaction totalContributionBalance // the total amount of tokens sender has purchased during public sale }); }
/// <summary> /// get the maximum number of NOS that can be purchased by groupNumber during the public sale /// </summary> /// <param name="groupNumber"></param> /// <returns></returns> public static BigInteger GetGroupMaxContribution(BigInteger groupNumber) { BigInteger maxContribution = 0; uint latestTimeStamp = Helpers.GetBlockTimestamp(); uint publicSaleMaxContribution = (uint)ICOTemplate.MaximumContributionAmount(); uint publicSaleEndTime = (uint)ICOTemplate.PublicSaleEndTime(); //If latest block timestamp is larger than presale start and smaller than presale end: check presale tier contributions. if (latestTimeStamp >= (uint)ICOTemplate.PresaleStartTime() && latestTimeStamp <= (uint)ICOTemplate.PresaleEndTime()) { //Presale has not ended. Only presale can participate. if (groupNumber == 1) { //Pre-sale tier 1. maxContribution = (uint)ICOTemplate.PresaleTierOne(); } else if (groupNumber == 2) { //Pre-sale tier 2. maxContribution = (uint)ICOTemplate.PresaleTierTwo(); } else if (groupNumber == 3) { //Pre-sale tier 3. maxContribution = (uint)ICOTemplate.PresaleTierThree(); } else if (groupNumber == 4) { //Tier 4 maxContribution = (uint)ICOTemplate.PresaleTierFour(); } } //Otherwise we're in the public sale; get the publicSaleMaxContribution //publicSaleMaxContribution returns the max contribution based on the presale phase using Helpers.GetPublicSaleMaxContribution() else if (groupNumber > 0 && groupNumber <= 4 && latestTimeStamp >= (uint)ICOTemplate.PublicSaleStartTime() && latestTimeStamp <= publicSaleEndTime) { maxContribution = publicSaleMaxContribution; } return(maxContribution); }
/// <summary> /// allow allocation of presale purchases by contract administrator. this allows the nOS team to allocate the nOS tokens from the private sale, company reserve, and locked incentive reserve. /// This method will not allow the private allocations to exceed the defined amount /// the state of the `LockPrivateSaleAllocation` can be determined by the public using the method `IsPrivateSaleAllocationLocked` (returns timestamp that lock was put in place) /// </summary> /// <param name="address"></param> /// <param name="amountPurchased"></param> /// <returns></returns> public static bool AllocatePrivateSalePurchase(byte[] address, string allocationType, BigInteger amountPurchased) { amountPurchased = amountPurchased * NEP5.factor; bool privateSaleLocked = Storage.Get(Storage.CurrentContext, StorageKeys.PrivateSaleAllocationLocked()).AsBigInteger() > 0; if (privateSaleLocked) { Runtime.Notify("AllocatePrivateSalePurchase() privateSaleLocked, can't allocate"); return(false); } if (allocationType != "incentive" && allocationType != "privateSale" && allocationType != "company") { return(false); } BigInteger presaleAllocationMaxValue = ICOTemplate.LockedTokenAllocationAmount() * NEP5.factor; BigInteger presaleAllocatedValue = Storage.Get(Storage.CurrentContext, StorageKeys.PresaleAllocatedValue()).AsBigInteger(); if ((presaleAllocatedValue + amountPurchased) > presaleAllocationMaxValue) { // this purchase will exceed the presale cap.. dont allow Runtime.Notify("AllocatePrivateSalePurchase() purchase will exceed max allocation"); return(false); } if (!TokenSale.SetVestingPeriodForAddress(address, allocationType, amountPurchased)) { Runtime.Notify("SetVestingPeriodForAddress() failed."); return(false); } Storage.Put(Storage.CurrentContext, StorageKeys.PresaleAllocatedValue(), presaleAllocatedValue + amountPurchased); transfer(null, address, amountPurchased); Runtime.Notify("AllocatePrivateSalePurchase() tokens allocated", address, amountPurchased, allocationType); return(true); }
/// <summary> /// initialise the smart contract for use /// </summary> /// <returns></returns> public static bool InitSmartContract() { if (Helpers.ContractInitialised()) { // contract can only be initialised once Runtime.Log("InitSmartContract() contract already initialised"); return(false); } uint ContractInitTime = Helpers.GetBlockTimestamp(); Storage.Put(Storage.CurrentContext, StorageKeys.ContractInitTime(), ContractInitTime); // assign pre-allocated tokens to the NosProjectKey() (10,000,000 tokens) BigInteger immediateProjectAllocationValue = ICOTemplate.ImmediateCompanyReserve() * NEP5.factor; Helpers.SetBalanceOf(ICOTemplate.NosProjectKey, immediateProjectAllocationValue); transfer(null, ICOTemplate.NosProjectKey, immediateProjectAllocationValue); // token allocated to private sale & vested reserves & incentives BigInteger presaleAllocationMaxValue = ICOTemplate.LockedTokenAllocationAmount() * NEP5.factor; // update the total supply to reflect the project allocated tokens BigInteger totalSupply = immediateProjectAllocationValue + presaleAllocationMaxValue; Helpers.SetTotalSupply(totalSupply); UpdateAdminAddress(ICOTemplate.InitialAdminAccount); EnableTransferFromWhitelisting(ICOTemplate.WhitelistTransferFromListings()); Runtime.Log("InitSmartContract() contract initialisation complete"); return(true); }
/// <summary> /// the core teams token allocation follow a linear quarterly maturation over 18 months beginning after 6 months /// </summary> /// <returns></returns> public static object[] GetCoreTeamVestingSchedule() { // calculate the allocation given to each team member object[] founderKeys = ICOTemplate.MoonlightFounderKeys(); BigInteger founderTokenAllocation = ((ICOTemplate.TokenMaxSupply * (BigInteger)ICOTemplate.MoonlightFoundersAllocationPercentage()) / 100) * NEP5.factor; BigInteger individualAllocation = founderTokenAllocation / founderKeys.Length; uint ContractInitTime = Helpers.GetContractInitTime(); // determine vesting schedule details for core teams token allocation // there will be 7 releases, one each quarter ending 2 years from contract init int numberOfTokenReleases = 7; BigInteger tokensPerRelease = individualAllocation / numberOfTokenReleases; object[] vestingPeriod = new object[14]; object[] founderReleaseSchedule = ICOTemplate.MoonlightFoundersAllocationReleaseSchedule(); uint initialReleaseDate = ContractInitTime + (uint)founderReleaseSchedule[0]; uint releaseFrequency = (uint)founderReleaseSchedule[1]; BigInteger tokensReleased = tokensPerRelease; // this is not the nicest way to populate the vesting schedule array, but it is much cheaper (in terms of processing/gas price) than looping vestingPeriod[0] = initialReleaseDate; vestingPeriod[1] = tokensPerRelease; // 3 months later release another batch of tokens tokensReleased += tokensPerRelease; vestingPeriod[2] = initialReleaseDate + (releaseFrequency * 1); vestingPeriod[3] = tokensPerRelease; // 3 months later release another batch of tokens tokensReleased += tokensPerRelease; vestingPeriod[4] = initialReleaseDate + (releaseFrequency * 2); vestingPeriod[5] = tokensPerRelease; // 3 months later release another batch of tokens tokensReleased += tokensPerRelease; vestingPeriod[6] = initialReleaseDate + (releaseFrequency * 3); vestingPeriod[7] = tokensPerRelease; // 3 months later release another batch of tokens tokensReleased += tokensPerRelease; vestingPeriod[8] = initialReleaseDate + (releaseFrequency * 4); vestingPeriod[9] = tokensPerRelease; // 3 months later release another batch of tokens tokensReleased += tokensPerRelease; vestingPeriod[10] = initialReleaseDate + (releaseFrequency * 5); vestingPeriod[11] = tokensPerRelease; // 3 months later release the last of the tokens vestingPeriod[12] = initialReleaseDate + (releaseFrequency * 6); vestingPeriod[13] = individualAllocation - tokensReleased; /* * Runtime.Notify("VestingSchedule", Helpers.SerializeArray(vestingPeriod)); * a serialised copy of this array ends up with values such as (dates subject to change): * 0e * 04 292cf05b 5bf02c29 1542466601 Saturday, November 17, 2018 2:56:41 PM * 07 6ddb810adb0301 0103db0a81db6d 285714285714285 * 04 0979685c 5c687909 1550350601 Saturday, February 16, 2019 8:56:41 PM * 07 dab60315b60702 0207b61503b6da 571428571428570 * 04 e9c5e05c 5ce0c5e9 1558234601 Sunday, May 19, 2019 2:56:41 AM * 07 4792851f910b03 030b911f859247 857142857142855 * 04 c912595d 5d5912c9 1566118601 Sunday, August 18, 2019 8:56:41 AM * 07 b46d072a6c0f04 040f6c2a076db4 1142857142857140 * 04 a95fd15d 5dd15fa9 1574002601 Sunday, November 17, 2019 2:56:41 PM * 07 21498934471305 05134734894921 1428571428571425 * 04 89ac495e 5e49ac89 1581886601 Sunday, February 16, 2020 8:56:41 PM * 07 8e240b3f221706 0617223f0b248e 1714285714285710 * 04 69f9c15e 5ec1f969 1589770601 Monday, May 18, 2020 2:56:41 AM * 07 00008d49fd1a07 071afd498d0000 2000000000000000 */ return(vestingPeriod); }
/// <summary> /// NEP5: Transfer tokens from one account to another /// </summary> /// <param name="from">sender address</param> /// <param name="to">recipient address</param> /// <param name="amount">number of tokens to transfer</param> /// <param name="caller"></param> /// <param name="entry"></param> /// <returns></returns> public static bool Transfer(byte[] from, byte[] to, BigInteger amount, byte[] caller, byte[] entry) { if (Helpers.GetBlockTimestamp() < ICOTemplate.PublicSaleEndTime()) { Runtime.Log("Transfer() not available before ICOTemplate.PublicSaleEndTime()"); return(false); } if (caller != entry && !Helpers.IsContractWhitelistedTransferFrom(caller)) { from = caller; } if (from.Length != 20 || to.Length != 20) { Runtime.Log("Transfer() (from|to).Length != 20"); return(false); } if (amount < 0) { Runtime.Log("Transfer() invalid transfer amount must be >= 0"); throw new Exception(); } BigInteger fromBalance = BalanceOf(from); // retrieve balance of originating account if (fromBalance < amount) { Runtime.Log("Transfer() fromBalance < transferValue"); // don't transfer if funds not available return(false); } if (amount == 0 || from == to) { // don't accept a meaningless value Runtime.Log("Transfer() empty transfer amount or from==to"); transfer(from, to, amount); return(true); // as per nep5 standard - return true when amount is 0 or from == to } if (!Runtime.CheckWitness(from)) { // ensure transaction is signed properly by the owner of the tokens Runtime.Log("Transfer() CheckWitness failed"); return(false); } BigInteger recipientBalance = BalanceOf(to); BigInteger recipientAmountSubjectToVesting = TokenSale.SubjectToVestingPeriod(to); BigInteger senderAmountSubjectToVesting = TokenSale.SubjectToVestingPeriod(from); BigInteger newBalance = fromBalance - amount; Helpers.SetBalanceOf(from, newBalance + senderAmountSubjectToVesting); // remove balance from originating account Helpers.SetBalanceOf(to, recipientBalance + recipientAmountSubjectToVesting + amount); // set new balance for destination account transfer(from, to, amount); return(true); }
/// <summary> /// transfer an amount from the "from" account to the "to" acount if the originator has been approved to transfer the requested amount. /// </summary> /// <param name="originator"></param> /// <param name="from"></param> /// <param name="to"></param> /// <param name="amount"></param> /// <param name="caller"></param> /// <param name="entry"></param> /// <returns></returns> public static bool TransferFrom(byte[] originator, byte[] from, byte[] to, BigInteger amount, byte[] caller, byte[] entry) { if (Helpers.GetBlockTimestamp() < ICOTemplate.PublicSaleEndTime()) { Runtime.Log("TransferFrom() not available before ICOTemplate.PublicSaleEndTime()"); return(false); } if (caller != entry && !Helpers.IsContractWhitelistedTransferFrom(caller)) { originator = caller; } if (originator.Length != 20 || from.Length != 20 || to.Length != 20) { Runtime.Log("TransferFrom() (originator|from|to).Length != 20"); return(false); } if (amount < 0) { Runtime.Log("TransferFrom() invalid transfer amount must be >= 0"); throw new Exception(); } BigInteger approvedTransferAmount = Allowance(from, to); // how many tokens is this address authorised to transfer BigInteger fromBalance = BalanceOf(from); // retrieve balance of authorised account if (approvedTransferAmount < amount || fromBalance < amount) { // don't transfer if funds not available Runtime.Notify("TransferFrom() (authorisedAmount|fromBalance) < transferValue", approvedTransferAmount, fromBalance, amount); return(false); } if (amount == 0 || from == to) { // don't accept a meaningless value Runtime.Log("TransferFrom() empty transfer amount or from==to"); transfer(from, to, amount); return(true); // as per nep5 standard - return true when amount is 0 or from == to } if (!Runtime.CheckWitness(originator)) { // ensure transaction is signed properly by the request originator Runtime.Log("TransferFrom() CheckWitness failed"); return(false); } BigInteger recipientBalance = BalanceOf(to); BigInteger recipientAmountSubjectToVesting = TokenSale.SubjectToVestingPeriod(to); BigInteger senderAmountSubjectToVesting = TokenSale.SubjectToVestingPeriod(from); BigInteger newBalance = fromBalance - amount; Helpers.SetBalanceOf(from, newBalance + senderAmountSubjectToVesting); // remove balance from originating account Helpers.SetBalanceOf(to, recipientBalance + recipientAmountSubjectToVesting + amount); // set new balance for destination account Helpers.SetAllowanceAmount(from.Concat(originator), approvedTransferAmount - amount); // deduct transferred amount from allowance transfer(from, to, amount); return(true); }
/// <summary> /// retrieve information for the received transaction /// </summary> /// <returns>object[] { /// (Transaction)tx, (byte[])sender, (byte)receiver, ulong receivedNEO, ulong receivedGAS, /// (BigInteger)whiteListGroupNumber, (BigInteger)crowdsaleAvailableAmount, (BigInteger)groupMaximumContribution /// (BigInteger)totalTokensPurchased, (BigInteger)neoRemainingAfterPurchase, (BigInteger)gasRemainingAfterPurchase /// (BigInteger)totalContributionBalance /// } /// </returns> public static object[] GetTransactionAndSaleData() { Transaction tx = (Transaction)ExecutionEngine.ScriptContainer; TransactionOutput[] inputs = tx.GetReferences(); TransactionOutput reference = inputs[0]; TransactionOutput[] outputs = tx.GetOutputs(); byte[] sender = reference.ScriptHash; byte[] receiver = ExecutionEngine.ExecutingScriptHash; ulong receivedNEO = 0; ulong receivedGAS = 0; foreach (var input in inputs) { // ensure that the provided inputs are valid if (input.ScriptHash == receiver) { throw new System.Exception(); } } foreach (TransactionOutput output in outputs) { if (output.ScriptHash == receiver) { // only add funds to total received value if receiver is the recipient of the output ulong receivedValue = (ulong)output.Value; Runtime.Notify("GetTransactionData() Received Deposit type", receiver, reference.AssetId); if (reference.AssetId == NEP5.NEO) { receivedNEO += receivedValue; } else if (reference.AssetId == NEP5.GAS) { receivedGAS += receivedValue; } } } BigInteger whiteListGroupNumber = KYC.GetWhitelistGroupNumber(sender); BigInteger crowdsaleAvailableAmount = NEP5.CrowdsaleAvailableAmount(); BigInteger groupMaximumContribution = KYC.GetGroupMaxContribution(whiteListGroupNumber) * NEP5.factor; BigInteger totalTokensPurchased = 0; BigInteger neoRemainingAfterPurchase = 0; BigInteger gasRemainingAfterPurchase = 0; BigInteger runningCrowdsaleAmount = crowdsaleAvailableAmount; if (ICOTemplate.ICOAllowsNEO() && receivedNEO > 0) { BigInteger neoTokenValue = receivedNEO * ICOTemplate.ICONeoToTokenExchangeRate(); if (neoTokenValue > runningCrowdsaleAmount) { // the user is trying to purchase more tokens than are available // figure out how much NOS can be purchased without exceeding the cap neoRemainingAfterPurchase = (neoTokenValue - runningCrowdsaleAmount) / (ICOTemplate.ICONeoToTokenExchangeRate()); totalTokensPurchased = runningCrowdsaleAmount; } else { // there is enough NOS left for this purchase to complete totalTokensPurchased = neoTokenValue; } // ensure amountAvailable now reflects number of tokens purchased with NEO runningCrowdsaleAmount -= totalTokensPurchased; } if (ICOTemplate.ICOAllowsGAS() && receivedGAS > 0) { BigInteger gasTokenValue = receivedGAS * ICOTemplate.ICOGasToTokenExchangeRate(); if (gasTokenValue > runningCrowdsaleAmount) { // the user is trying to purchase more tokens than are available // figure out how much NOS can be purchased without exceeding the cap gasRemainingAfterPurchase = (gasTokenValue - runningCrowdsaleAmount) / (ICOTemplate.ICOGasToTokenExchangeRate()); totalTokensPurchased = totalTokensPurchased + runningCrowdsaleAmount; } else { totalTokensPurchased = totalTokensPurchased + gasTokenValue; } } BigInteger totalContributionBalance = BalanceOfSaleContribution(sender) + totalTokensPurchased; return(new object[] { tx, // neo transaction object sender, // who initiated the transfer receiver, // who the assets were sent to receivedNEO, // how many neo were transferred receivedGAS, // how many gas were transferred whiteListGroupNumber, // what whitelist group is the sender in crowdsaleAvailableAmount, // how many tokens are left to be purchased groupMaximumContribution, // how many tokens can members of this whitelist group purchase totalTokensPurchased, // the total number of tokens purchased in this transaction neoRemainingAfterPurchase, // how much neo is left after purchase of tokens gasRemainingAfterPurchase, // how much gas is left after purchase of tokens totalContributionBalance // the total amount of tokens sender has purchased during public sale }); }
public static object HandleNEP5Operation(string operation, object[] args, byte[] caller, byte[] entry) { //{ "name", "symbol", "decimals", "totalSupply", "balanceOf", "transfer", "transferFrom", "approve", "allowance" }; if (operation == "name") { // the name of the token return(ICOTemplate.TokenName()); } if (operation == "symbol") { // the symbol of the token return(ICOTemplate.TokenSymbol()); } if (operation == "decimals") { // decimals to determine fractions of tokens return(TokenDecimals()); } if (operation == "totalSupply") { // the total number of tokens minted return(TotalSupply()); } if (operation == "balanceOf") { // retreive the balance of an address if (!Helpers.RequireArgumentLength(args, 1)) { // BalanceOf() requires at least 1 argument - the address to check the balance of return(false); } return(BalanceOf((byte[])args[0])); } if (operation == "transfer") { // transfer tokens from one address to another if (!Helpers.RequireArgumentLength(args, 3)) { // Transfer() requires 3 arguments: from, to, amount return(false); } return(Transfer((byte[])args[0], (byte[])args[1], (BigInteger)args[2], caller, entry)); } if (operation == "transferFrom") { // transfer tokens from one address to another if (!Helpers.RequireArgumentLength(args, 4)) { // TransferFrom() requires 4 arguments: spender, from, to, amount return(false); } return(TransferFrom((byte[])args[0], (byte[])args[1], (byte[])args[2], (BigInteger)args[3], caller, entry)); } if (operation == "approve") { // approve a third party to transfer tokens from one address to another if (!Helpers.RequireArgumentLength(args, 3)) { // Approve() requires 3 arguments: from, spender, amount return(false); } return(Approve((byte[])args[0], (byte[])args[1], (BigInteger)args[2], caller, entry)); } if (operation == "allowance") { // retreive the authorised balance of an address if (!Helpers.RequireArgumentLength(args, 2)) { // Allowance() requires 2 arguments: from, to return(false); } return(Allowance((byte[])args[0], (byte[])args[1])); } // check how many tokens left for purchase if (operation == "crowdsale_available_amount") { return(CrowdsaleAvailableAmount()); } if (operation == "mintTokens") { return(TokenSale.MintTokens()); } return(false); }
/// <summary> /// set a vesting schedule, as defined in the whitepaper, for tokens purchased during the presale /// </summary> /// <param name="address"></param> /// <param name="tokenBalance"></param> /// <returns></returns> public static bool SetVestingPeriodForAddress(byte[] address, string allocationType, BigInteger tokensPurchased) { if (allocationType != "incentive" && allocationType != "privateSale" && allocationType != "company") { return(false); } if (address.Length != 20) { return(false); } BigInteger currentAvailableBalance = 0; // how many tokens will be immediately available to the owner uint contractInitTime = Helpers.GetContractInitTime(); uint currentTimestamp = Helpers.GetBlockTimestamp(); StorageMap vestingData = Storage.CurrentContext.CreateMap(StorageKeys.VestedTokenPrefix()); uint initialReleaseDate = 0; uint releaseFrequency = 0; object[] vestingObj = new object[0]; if (allocationType == "incentive") { vestingObj = ICOTemplate.VestingIncentive(); initialReleaseDate = (uint)vestingObj[0] + contractInitTime; releaseFrequency = (uint)vestingObj[1]; } else if (allocationType == "privateSale") { vestingObj = ICOTemplate.VestingPrivateSale(); initialReleaseDate = contractInitTime; releaseFrequency = (uint)vestingObj[0]; } else if (allocationType == "company") { vestingObj = ICOTemplate.VestingCompany(); initialReleaseDate = (uint)vestingObj[0] + contractInitTime; releaseFrequency = (uint)vestingObj[0]; } object[] releasePeriod = new object[4]; releasePeriod[0] = initialReleaseDate; releasePeriod[1] = initialReleaseDate + releaseFrequency; releasePeriod[2] = initialReleaseDate + (releaseFrequency * 2); releasePeriod[3] = initialReleaseDate + (releaseFrequency * 3); // calculate how much should be released BigInteger releaseAmount = tokensPurchased * ICOTemplate.DistributionPercentage() / 100; object[] lockoutTimes = new object[] { releasePeriod[0], releaseAmount, releasePeriod[1], releaseAmount, releasePeriod[2], releaseAmount, releasePeriod[3], releaseAmount }; vestingData.Put(address, lockoutTimes.Serialize()); // ensure the total amount purchased is saved Helpers.SetBalanceOf(address, tokensPurchased); Helpers.SetBalanceOfVestedAmount(address, tokensPurchased, allocationType); transfer(null, address, tokensPurchased); return(true); }
/// <summary> /// allow allocation of presale purchases by contract administrator. this allows the moonlight team to allocate the 25% of LX tokens sold in the private presale. /// as we accepted ETH in addition to NEO&GAS, using a mintTokens method here is not practical. /// 1. this method will not allow the presale allocation to exceed the defined amount /// 2. this method is permanently disabled once the method `LockPresaleAllocation` has been called. /// 3. the state of the `LockPresaleAllocation` can be determined by the public using the method `IsPresaleAllocationLocked` (returns timestamp that lock was put in place) /// </summary> /// <param name="address"></param> /// <param name="amountPurchased"></param> /// <returns></returns> public static bool AllocatePresalePurchase(byte[] address, BigInteger amountPurchased) { bool presaleLocked = Storage.Get(Storage.CurrentContext, StorageKeys.PresaleAllocationLocked()).AsBigInteger() > 0; if (presaleLocked) { Runtime.Notify("AllocatePresalePurchase() presaleLocked, can't allocate"); return(false); } BigInteger presaleAllocationMaxValue = ((ICOTemplate.TokenMaxSupply * (BigInteger)ICOTemplate.PresaleAllocationPercentage()) / 100) * NEP5.factor; BigInteger presaleAllocatedValue = Storage.Get(Storage.CurrentContext, StorageKeys.PresaleAllocatedValue()).AsBigInteger(); if ((presaleAllocatedValue + amountPurchased) > presaleAllocationMaxValue) { // this purchase will exceed the presale cap.. dont allow Runtime.Notify("AllocatePresalePurchase() purchase will exceed presale max allocation"); return(false); } TokenSale.SetVestingPeriodForAddress(address, amountPurchased); Storage.Put(Storage.CurrentContext, StorageKeys.PresaleAllocatedValue(), presaleAllocatedValue + amountPurchased); Runtime.Notify("AllocatePresalePurchase() tokens allocated", address, amountPurchased); return(true); }