diff --git a/contracts/vesting.tmy/include/vesting.tmy/vesting.tmy.hpp b/contracts/vesting.tmy/include/vesting.tmy/vesting.tmy.hpp index 22c3a26..752252d 100644 --- a/contracts/vesting.tmy/include/vesting.tmy/vesting.tmy.hpp +++ b/contracts/vesting.tmy/include/vesting.tmy/vesting.tmy.hpp @@ -25,20 +25,33 @@ namespace vestingtoken microseconds cliff_period; microseconds start_delay; microseconds vesting_period; + double tge_unlock; }; static const std::map vesting_categories = { - {1, {days(6 * 30), days(0 * 30), days(2 * 365)}}, // Seed Private Sale, - {2, {days(6 * 30), days(6 * 30), days(2 * 365)}}, // Strategic Partnerships Private Sale, - {3, {days(0 * 30), days(0 * 30), days(0 * 30)}}, // Public Sale (DO NOT USED YET), - {4, {days(0 * 30), days(1 * 365), days(5 * 365)}}, // Team and Advisors, Ecosystem - {5, {days(0 * 30), days(0 * 30), days(1 * 365)}}, // Legal and Compliance - {6, {days(0 * 30), days(0 * 30), days(2 * 365)}}, // Reserves, Partnerships, Liquidly Allocation - {7, {days(0 * 30), days(0 * 30), days(5 * 365)}}, // Community and Marketing, Platform Dev, Infra Rewards - - {999, {eosio::seconds(10), eosio::seconds(10), eosio::seconds(20)}}, // TESTING ONLY + // DEPRECIATED: + {1, {days(6 * 30), days(0 * 30), days(2 * 365), 0.0}}, // Seed Private Sale (DEPRECIATED), + {2, {days(6 * 30), days(6 * 30), days(2 * 365), 0.0}}, // Strategic Partnerships Private Sale (DEPRECIATED), + // Unchanged: + {3, {days(0 * 30), days(0 * 30), days(0 * 30), 0.0}}, // Public Sale (DO NOT USED YET), + {4, {days(0 * 30), days(1 * 365), days(5 * 365), 0.0}}, // Team and Advisors, Ecosystem + {5, {days(0 * 30), days(0 * 30), days(1 * 365), 0.0}}, // Legal and Compliance + {6, {days(0 * 30), days(0 * 30), days(2 * 365), 0.0}}, // Reserves, Partnerships, Liquidly Allocation + {7, {days(0 * 30), days(0 * 30), days(5 * 365), 0.0}}, // Community and Marketing, Platform Dev, Infra Rewards + // New (replacing depreciated): + {8, {days(0 * 30), days(0 * 30), days(2 * 365), 0.05}}, // Seed (Early Bird) + {9, {days(0 * 30), days(0 * 30), days(2 * 365), 0.025}}, // Seed (Last Chance) + {10, {days(0 * 30), days(14), days(0 * 365), 1.0}}, // Public (TGE) + // Public sale has a delay of 14 days to accommodate the "right of withdrawal" under EU's MICA regulations + + // TESTING ONLY: + {997, {days(6 * 30), days(0 * 30), days(2 * 365), 0.0}}, // TESTING ONLY + {998, {eosio::seconds(0), eosio::seconds(10), eosio::seconds(20), 0.5}}, // TESTING ONLY + {999, {eosio::seconds(10), eosio::seconds(10), eosio::seconds(20), 0.0}}, // TESTING ONLY }; + static const std::map depreciated_categories = {{1, true}, {2, true}}; + class [[eosio::contract("vesting.tmy")]] vestingToken : public eosio::contract { public: @@ -105,8 +118,22 @@ namespace vestingtoken */ [[eosio::action]] void withdraw(eosio::name holder); + /** + * @details Migrates an allocation to a new amount and category + * + * @internal Auth required by the contract + * + * @param sender {name} - The account name of the sender who created the allocation. + * @param holder {name} - The account name of the token holder. + * @param allocation_id {uint64_t} - The ID of the allocation to be migrated. + * @param amount {asset} - The new amount of tokens to be assigned. + * @param category_id {int} - The new vesting category for the assigned tokens. + */ + [[eosio::action]] void migratealloc(eosio::name sender, name holder, uint64_t allocation_id, eosio::asset old_amount, eosio::asset new_amount, int old_category_id, int new_category_id); + using setsettings_action = action_wrapper<"setsettings"_n, &vestingToken::setsettings>; using assigntokens_action = action_wrapper<"assigntokens"_n, &vestingToken::assigntokens>; using withdraw_action = action_wrapper<"withdraw"_n, &vestingToken::withdraw>; + using migratealloc_action = action_wrapper<"migratealloc"_n, &vestingToken::migratealloc>; }; } diff --git a/contracts/vesting.tmy/src/vesting.tmy.cpp b/contracts/vesting.tmy/src/vesting.tmy.cpp index 8faddbd..f3fddf3 100644 --- a/contracts/vesting.tmy/src/vesting.tmy.cpp +++ b/contracts/vesting.tmy/src/vesting.tmy.cpp @@ -2,6 +2,20 @@ namespace vestingtoken { + void check_asset(const eosio::asset &asset) + { + auto sym = asset.symbol; + eosio::check(sym.is_valid(), "invalid amount symbol"); + eosio::check(sym == vestingToken::system_resource_currency, "Symbol does not match system resource currency"); + eosio::check(sym.precision() == vestingToken::system_resource_currency.precision(), "Symbol precision does not match"); + eosio::check(asset.amount > 0, "Amount must be greater than 0"); + } + + void check_category(int category_id) + { + eosio::check(vesting_categories.contains(category_id), "Invalid new vesting category"); + eosio::check(!depreciated_categories.contains(category_id), "New category is depreciated"); + } void vestingToken::setsettings(string sales_date_str, string launch_date_str) { @@ -17,15 +31,8 @@ namespace vestingtoken void vestingToken::assigntokens(eosio::name sender, eosio::name holder, eosio::asset amount, int category_id) { - // Check if the provided category exists in the map - eosio::check(vesting_categories.contains(category_id), "Invalid vesting category"); - - // Check the symbol is correct and valid - auto sym = amount.symbol; - eosio::check(sym.is_valid(), "invalid amount symbol"); - eosio::check(sym == system_resource_currency, "Symbol does not match system resource currency"); - eosio::check(sym.precision() == system_resource_currency.precision(), "Symbol precision does not match"); - eosio::check(amount.amount > 0, "Amount must be greater than 0"); + check_category(category_id); + check_asset(amount); // Create a new vesting schedule vesting_allocations vesting_table(get_self(), holder.value); @@ -93,14 +100,14 @@ namespace vestingtoken vesting_category category = vesting_categories.at(vesting_allocation.vesting_category_type); - time_point vesting_start = launch_date + vesting_allocation.time_since_sale_start + category.start_delay; - time_point cliff_end = vesting_start + category.cliff_period; + time_point vesting_start = launch_date + category.start_delay; + time_point cliff_finished = vesting_start + category.cliff_period; // Calculate the vesting end time time_point vesting_end = vesting_start + category.vesting_period; // Check if vesting period after cliff has started - if (now >= cliff_end) + if (now >= cliff_finished) { // Calculate the total claimable amount int64_t claimable = 0; @@ -110,8 +117,14 @@ namespace vestingtoken } else { + // Calculate the percentage of the vesting period that has passed double vesting_finished = static_cast((now - vesting_start).count()) / category.vesting_period.count(); - claimable = vesting_allocation.tokens_allocated.amount * vesting_finished; + // Calculate the claimable amount: + // + tokens allocated * TGE unlock percentage + // + tokens allocated * % of vesting time that has passed * what is left after TGE unlock + claimable = vesting_allocation.tokens_allocated.amount * ((1.0 - category.tge_unlock) * vesting_finished + category.tge_unlock); + // Ensure the claimable amount is not greater than the total allocated amount + claimable = std::min(claimable, vesting_allocation.tokens_allocated.amount); } total_claimable += claimable - vesting_allocation.tokens_claimed.amount; @@ -123,7 +136,6 @@ namespace vestingtoken } } - eosio::print("]"); if (total_claimable > 0) { // Transfer the tokens to the holder @@ -135,4 +147,54 @@ namespace vestingtoken .send(); } } + + // Migrates an allocation to a new amount and category + void vestingToken::migratealloc(eosio::name sender, name holder, uint64_t allocation_id, eosio::asset old_amount, eosio::asset new_amount, int old_category_id, int new_category_id) + { + require_auth(get_self()); + + check_category(new_category_id); + check_asset(new_amount); + + // Get the vesting allocations + vesting_allocations vesting_table(get_self(), holder.value); + auto iter = vesting_table.find(allocation_id); + eosio::check(iter != vesting_table.end(), "Allocation not found"); + + // Checks to verify new allocation is valid + eosio::check(iter->tokens_allocated.amount == old_amount.amount, "Old amount does not match existing allocation"); + eosio::check(iter->vesting_category_type == old_category_id, "Old category does not match existing allocation"); + eosio::check(iter->tokens_claimed.amount < new_amount.amount, "New amount is less than the amount already claimed"); + + // Modify the table row data, and update the table + vesting_table.modify(iter, get_self(), [&](auto &row) + { + row.tokens_allocated = new_amount; + row.vesting_category_type = new_category_id; }); + + // Notify the holder + eosio::require_recipient(holder); + + // // Calculate the change in the allocation amount + int64_t amount_change = new_amount.amount - old_amount.amount; + + // If new tokens were allocated, then send them to the contract + if (amount_change > 0) + { + eosio::action({sender, "active"_n}, + token_contract_name, + "transfer"_n, + std::make_tuple(sender, get_self(), eosio::asset(amount_change, old_amount.symbol), std::string("Re-allocated vested funds"))) + .send(); + } + // If tokens were removed, send them back to the sender + else if (amount_change < 0) + { + eosio::action({get_self(), "active"_n}, + token_contract_name, + "transfer"_n, + std::make_tuple(get_self(), sender, eosio::asset(-amount_change, old_amount.symbol), std::string("Refunded vested funds"))) + .send(); + } + } } \ No newline at end of file