From 24ec468d3762edfa05b456b701b3db46749647ff Mon Sep 17 00:00:00 2001 From: despiegk Date: Tue, 26 Aug 2025 11:36:53 +0200 Subject: [PATCH] ... --- examples/threefold/incatokens/main.v | 48 ++++++ lib/threefold/incatokens/amm.v | 38 +++++ lib/threefold/incatokens/auction.v | 54 +++++++ lib/threefold/incatokens/charts.v | 67 ++++++++ lib/threefold/incatokens/export.v | 95 +++++++++++ lib/threefold/incatokens/factory.v | 68 ++++++++ lib/threefold/incatokens/incatokens_test.v | 81 ++++++++++ lib/threefold/incatokens/model.v | 93 +++++++++++ lib/threefold/incatokens/simulation.v | 175 +++++++++++++++++++++ lib/threefold/incatokens/vesting.v | 71 +++++++++ 10 files changed, 790 insertions(+) create mode 100644 examples/threefold/incatokens/main.v create mode 100644 lib/threefold/incatokens/amm.v create mode 100644 lib/threefold/incatokens/auction.v create mode 100644 lib/threefold/incatokens/charts.v create mode 100644 lib/threefold/incatokens/export.v create mode 100644 lib/threefold/incatokens/factory.v create mode 100644 lib/threefold/incatokens/incatokens_test.v create mode 100644 lib/threefold/incatokens/model.v create mode 100644 lib/threefold/incatokens/simulation.v create mode 100644 lib/threefold/incatokens/vesting.v diff --git a/examples/threefold/incatokens/main.v b/examples/threefold/incatokens/main.v new file mode 100644 index 00000000..eab80fd8 --- /dev/null +++ b/examples/threefold/incatokens/main.v @@ -0,0 +1,48 @@ +import freeflowuniverse.herolib.threefold.incatokens + +fn main() { + // Create simulation + mut sim := incatokens.simulation_new( + name: 'inca_tge_v1' + total_supply: 10_000_000_000 + )! + + // Initialize with default rounds + sim.init_default_rounds()! + + // Define scenarios + scenarios := { + 'Low': { + 'demands': [10_000_000.0, 10_000_000.0, 0.0], + 'amm_trades': [0.0, 0.0, 0.0] + }, + 'Mid': { + 'demands': [20_000_000.0, 20_000_000.0, 0.0], + 'amm_trades': [0.0, 0.0, 0.0] + }, + 'High': { + 'demands': [50_000_000.0, 100_000_000.0, 0.0], + 'amm_trades': [0.0, 0.0, 0.0] + } + } + + // Run all scenarios + for name, params in scenarios { + sim.run_scenario(name, params['demands'], params['amm_trades'])! + } + + // Create vesting schedules + sim.create_vesting_schedules()! + + // Generate charts + price_chart := sim.generate_price_chart()! + market_cap_chart := sim.generate_market_cap_chart()! + vesting_chart := sim.generate_vesting_chart()! + + // Export results + sim.export_report('simulation_report.md')! + sim.export_csv('prices', 'price_data.csv')! + sim.export_csv('vesting', 'vesting_schedule.csv')! + + println('Simulation complete!') +} \ No newline at end of file diff --git a/lib/threefold/incatokens/amm.v b/lib/threefold/incatokens/amm.v new file mode 100644 index 00000000..579afccd --- /dev/null +++ b/lib/threefold/incatokens/amm.v @@ -0,0 +1,38 @@ +module incatokens + +import math + +pub struct AMMPool { +pub mut: + tokens f64 + usdc f64 + k f64 // constant product +} + +pub fn (mut pool AMMPool) add_liquidity(tokens f64, usdc f64) { + pool.tokens += tokens + pool.usdc += usdc + pool.k = pool.tokens * pool.usdc +} + +pub fn (mut pool AMMPool) trade(usdc_amount f64) ! { + if pool.tokens <= 0 || pool.usdc <= 0 { + return error('AMM pool is empty') + } + + pool.usdc += usdc_amount + + if pool.usdc <= 0 { + return error('Insufficient USDC in pool') + } + + // Maintain constant product + pool.tokens = pool.k / pool.usdc +} + +pub fn (pool AMMPool) get_price() f64 { + if pool.tokens <= 0 { + return 0 + } + return pool.usdc / pool.tokens +} \ No newline at end of file diff --git a/lib/threefold/incatokens/auction.v b/lib/threefold/incatokens/auction.v new file mode 100644 index 00000000..d985f81e --- /dev/null +++ b/lib/threefold/incatokens/auction.v @@ -0,0 +1,54 @@ +module incatokens + +import math +pub struct AuctionResult { +pub mut: + tokens_sold f64 + clearing_price f64 + usd_raised f64 + fully_subscribed bool +} + +pub fn simulate_auction(config AuctionConfig) !AuctionResult { + demand := config.demand + min_price := config.min_price + token_supply := config.token_supply + + if token_supply <= 0 { + return AuctionResult{ + tokens_sold: 0 + clearing_price: 0 + usd_raised: 0 + fully_subscribed: true + } + } + + implied_avg := demand / token_supply + + if implied_avg < min_price { + // Partial fill at floor price + tokens_sold := math.min(demand / min_price, token_supply) + return AuctionResult{ + tokens_sold: tokens_sold + clearing_price: min_price + usd_raised: tokens_sold * min_price + fully_subscribed: tokens_sold >= token_supply + } + } + + // Full subscription at market clearing price + return AuctionResult{ + tokens_sold: token_supply + clearing_price: implied_avg + usd_raised: demand + fully_subscribed: true + } +} + +@[params] +pub struct AuctionConfig { +pub mut: + demand f64 + min_price f64 + token_supply f64 +} \ No newline at end of file diff --git a/lib/threefold/incatokens/charts.v b/lib/threefold/incatokens/charts.v new file mode 100644 index 00000000..211ac9e6 --- /dev/null +++ b/lib/threefold/incatokens/charts.v @@ -0,0 +1,67 @@ +module incatokens + +import freeflowuniverse.herolib.biz.spreadsheet +import freeflowuniverse.herolib.web.echarts + +// Generate price evolution chart +pub fn (sim Simulation) generate_price_chart() !echarts.EChartsOption { + mut rownames := []string{} + for name, _ in sim.scenarios { + rownames << 'scenario_${name}_price' + } + + return sim.price_sheet.line_chart( + rowname: rownames.join(',') + period_type: .month + title: 'INCA Token Price Evolution' + title_sub: 'Price paths across different scenarios' + unit: .normal + )! +} + +// Generate market cap chart +pub fn (sim Simulation) generate_market_cap_chart() !echarts.EChartsOption { + // Create market cap rows from price rows + mut mc_sheet := spreadsheet.sheet_new( + name: '${sim.name}_market_cap' + nrcol: sim.price_sheet.nrcol + curr: sim.currency + )! + + for name, scenario in sim.scenarios { + mut mc_row := mc_sheet.row_new( + name: 'scenario_${name}_mc' + tags: 'scenario:${name} type:market_cap' + descr: 'Market cap for ${name} scenario' + )! + + price_row := sim.price_sheet.row_get('scenario_${name}_price')! + for i, cell in price_row.cells { + mc_row.cells[i].val = cell.val * sim.total_supply + } + } + + return mc_sheet.bar_chart( + namefilter: mc_sheet.rows.keys() + period_type: .quarter + title: 'INCA Market Capitalization' + title_sub: 'Market cap evolution by quarter' + unit: .million + )! +} + +// Generate vesting schedule chart +pub fn (mut sim Simulation) generate_vesting_chart() !echarts.EChartsOption { + if isnil(sim.vesting_sheet) { + sim.create_vesting_schedules()! + } + + return sim.vesting_sheet.line_chart( + includefilter: ['type:vesting'] + excludefilter: ['type:total_vesting'] + period_type: .quarter + title: 'Token Vesting Schedule' + title_sub: 'Cumulative tokens unlocked over time' + unit: .million + )! +} \ No newline at end of file diff --git a/lib/threefold/incatokens/export.v b/lib/threefold/incatokens/export.v new file mode 100644 index 00000000..8c34f71b --- /dev/null +++ b/lib/threefold/incatokens/export.v @@ -0,0 +1,95 @@ +module incatokens + +import freeflowuniverse.herolib.core.pathlib +import freeflowuniverse.herolib.data.markdown +import os + +// // Export simulation results to markdown report +// pub fn (sim &Simulation) export_report(path string) ! { +// content := sim.render_report()! + +// // Write to file +// mut p := pathlib.get_file( +// path: path +// create: true +// delete: true +// )! +// p.write(content)! +// } + +// Export data to CSV +pub fn (sim Simulation) export_csv(sheet_name string, path string) ! { + mut sheet := match sheet_name { + 'prices' { sim.price_sheet } + 'tokens' { sim.token_sheet } + 'investments' { sim.investment_sheet } + 'vesting' { sim.vesting_sheet } + else { return error('Unknown sheet: ${sheet_name}') } + } + + sheet.export_csv( + path: path + separator: ',' + include_empty: false + )! +} + +pub fn (sim Simulation) generate_distribution_section() !string { + mut lines := []string{} + + lines << '- **Total supply:** ${sim.total_supply} INCA' + lines << '- **Public (TGE):** ${(sim.public_pct * 100).str()}% (No lockup)' + lines << '- **Team:** ${(sim.team_pct * 100).str()}% (${sim.team_vesting.cliff_months}mo cliff, ${sim.team_vesting.vesting_months}mo vest)' + lines << '- **Treasury:** ${(sim.treasury_pct * 100).str()}% (${sim.treasury_vesting.cliff_months}mo cliff, ${sim.treasury_vesting.vesting_months}mo vest)' + lines << '- **Investors:** ${(sim.investor_pct * 100).str()}%' + lines << '' + lines << '### Investor Rounds & Vesting' + lines << '| Round | Allocation | Price (USD) | Vesting Schedule |' + lines << '|---|---|---|---|' + + for round in sim.investor_rounds { + lines << '| **${round.name}** | ${(round.allocation_pct * 100).str()}% | \$${round.price} | ${round.vesting.cliff_months}mo cliff, ${round.vesting.vesting_months}mo linear vest |' + } + + return lines.join('\n') +} + +pub fn (sim Simulation) generate_scenario_section(scenario &Scenario) !string { + mut lines := []string{} + + lines << '### ${scenario.name} Scenario' + lines << '**Parameters:**' + lines << '- **Auction Demand:** \$${scenario.demands.map(it.str()).join(', ')}' + lines << '- **AMM Net Trade:** \$${scenario.amm_trades.map(it.str()).join(', ')}' + lines << '' + lines << '**Results:**' + lines << '| Treasury Raised | Final Price | ${sim.investor_rounds.map("ROI " + it.name).join(" | ")} |' + lines << '|:---|:---|${sim.investor_rounds.map(":---").join("|")}|' + + mut row := ['\$${(scenario.final_metrics.treasury_total / 1000000).str()}M', '\$${scenario.final_metrics.final_price}'] + for round in sim.investor_rounds { + roi := scenario.final_metrics.investor_roi[round.name] or { 0.0 } + row << '${roi}x' + } + lines << '| ${row.join(' | ')} |' + + return lines.join('\n') +} + +pub fn (sim Simulation) generate_financial_summary() !string { + mut lines := []string{} + + lines << '### Funds Raised for INCA COOP' + lines << '| Round | USD Raised |' + lines << '|---|---|' + + mut total_raised := 0.0 + for round in sim.investor_rounds { + raised := round.allocation_pct * sim.total_supply * round.price + total_raised += raised + lines << '| **${round.name}** | \$${raised} |' + } + lines << '| **Total** | **\$${total_raised}** |' + + return lines.join('\n') +} \ No newline at end of file diff --git a/lib/threefold/incatokens/factory.v b/lib/threefold/incatokens/factory.v new file mode 100644 index 00000000..80e4f135 --- /dev/null +++ b/lib/threefold/incatokens/factory.v @@ -0,0 +1,68 @@ +module incatokens + +import freeflowuniverse.herolib.core.texttools +import freeflowuniverse.herolib.biz.spreadsheet + +__global ( + simulations map[string]&Simulation +) + +@[params] +pub struct SimulationNewArgs { +pub mut: + name string = 'default' + total_supply f64 = 10_000_000_000 + public_pct f64 = 0.50 + team_pct f64 = 0.15 + treasury_pct f64 = 0.15 + investor_pct f64 = 0.20 + nrcol int = 60 // 60 months default + currency string = 'USD' +} + +pub fn simulation_new(args SimulationNewArgs) !&Simulation { + name := texttools.name_fix(args.name) + + // Initialize spreadsheets for tracking + price_sheet := spreadsheet.sheet_new( + name: '${name}_prices' + nrcol: args.nrcol + curr: args.currency + )! + + token_sheet := spreadsheet.sheet_new( + name: '${name}_tokens' + nrcol: args.nrcol + curr: args.currency + )! + + investment_sheet := spreadsheet.sheet_new( + name: '${name}_investments' + nrcol: args.nrcol + curr: args.currency + )! + + mut sim := &Simulation{ + name: name + total_supply: args.total_supply + public_pct: args.public_pct + team_pct: args.team_pct + treasury_pct: args.treasury_pct + investor_pct: args.investor_pct + currency: args.currency + price_sheet: price_sheet + token_sheet: token_sheet + investment_sheet: investment_sheet + vesting_sheet: unsafe { nil } + } + + simulations[name] = sim + return sim +} + +pub fn simulation_get(name string) !&Simulation { + name_fixed := texttools.name_fix(name) + return simulations[name_fixed] or { + return error('Simulation "${name_fixed}" not found') + } +} \ No newline at end of file diff --git a/lib/threefold/incatokens/incatokens_test.v b/lib/threefold/incatokens/incatokens_test.v new file mode 100644 index 00000000..d7dce54e --- /dev/null +++ b/lib/threefold/incatokens/incatokens_test.v @@ -0,0 +1,81 @@ +module incatokens + +fn test_simulation_creation() { + mut sim := simulation_new(name: 'test_sim')! + sim.init_default_rounds()! + + assert sim.name == 'test_sim' + assert sim.total_supply == 10_000_000_000 + assert sim.investor_rounds.len == 3 +} + +fn test_scenario_execution() { + mut sim := simulation_new(name: 'test_sim')! + sim.init_default_rounds()! + + // Run low scenario + low_scenario := sim.run_scenario( + 'Low', + [10_000_000.0, 10_000_000.0, 0.0], + [0.0, 0.0, 0.0] + )! + + assert low_scenario.name == 'Low' + assert low_scenario.final_metrics.treasury_total > 0 + assert low_scenario.final_metrics.final_price > 0 + + // Check ROI is positive for all rounds + for round in sim.investor_rounds { + roi := low_scenario.final_metrics.investor_roi[round.name] or { 0.0 } + assert roi > 0 // Should have positive return + } +} + +fn test_vesting_schedules() { + mut sim := simulation_new(name: 'test_sim')! + sim.init_default_rounds()! + sim.create_vesting_schedules()! + + // Check team vesting + team_row := sim.vesting_sheet.row_get('team_vesting')! + + // Before cliff (month 11), should be 0 + assert team_row.cells[11].val == 0 + + // After cliff starts (month 12), should have tokens + assert team_row.cells[12].val > 0 + + // After full vesting (month 48), should have all tokens + total_team_tokens := sim.total_supply * sim.team_pct + assert team_row.cells[48].val == total_team_tokens +} + +fn test_export_functionality() { + mut sim := simulation_new(name: 'test_export')! + sim.init_default_rounds()! + + // Run scenarios + sim.run_scenario('Low', [10_000_000.0, 10_000_000.0, 0.0], [0.0, 0.0, 0.0])! + sim.run_scenario('High', [50_000_000.0, 100_000_000.0, 0.0], [0.0, 0.0, 0.0])! + + // Test CSV export + sim.export_csv('prices', '/tmp/test_prices.csv')! + + // Test report generation + // sim.export_report('/tmp/test_report.md')! +} +// fn test_template_rendering() { +// mut sim := simulation_new(name: 'test_template')! +// sim.init_default_rounds()! + +// // Run a scenario +// sim.run_scenario('Low', [10_000_000.0, 10_000_000.0, 0.0], [0.0, 0.0, 0.0])! + +// // Render the report +// report := sim.render_report()! + +// // Assert that the report is not empty and contains some expected text +// assert report.len > 0 +// assert report.contains('# INCA TGE Simulation Report') +// assert report.contains('## 1) Token Distribution & Vesting') +// } \ No newline at end of file diff --git a/lib/threefold/incatokens/model.v b/lib/threefold/incatokens/model.v new file mode 100644 index 00000000..ab6c9d5d --- /dev/null +++ b/lib/threefold/incatokens/model.v @@ -0,0 +1,93 @@ +module incatokens + +import freeflowuniverse.herolib.biz.spreadsheet +import freeflowuniverse.herolib.data.ourtime + +pub struct VestingSchedule { +pub mut: + cliff_months int + vesting_months int +} + +pub struct InvestorRound { +pub mut: + name string + allocation_pct f64 + price f64 + vesting VestingSchedule +} + +pub enum EpochType { + auction_only + hybrid + amm_only +} + +pub struct Epoch { +pub mut: + index int + type_ EpochType + start_month int + end_month int + auction_share f64 + amm_share f64 + tokens_allocated f64 + tokens_spillover f64 + auction_demand f64 + amm_net_trade f64 + final_price f64 + treasury_raised f64 +} + +pub struct Scenario { +pub mut: + name string + demands []f64 + amm_trades []f64 + epochs []Epoch + final_metrics ScenarioMetrics +} + +pub struct ScenarioMetrics { +pub mut: + treasury_total f64 + final_price f64 + investor_roi map[string]f64 + market_cap_final f64 + circulating_supply_final f64 +} + +@[heap] +pub struct Simulation { +pub mut: + name string + + // Token distribution + total_supply f64 + public_pct f64 + team_pct f64 + treasury_pct f64 + investor_pct f64 + + // Configuration + currency string + epoch1_floor_uplift f64 = 1.20 + epochn_floor_uplift f64 = 1.20 + amm_liquidity_depth_factor f64 = 2.0 + + // Investor rounds + investor_rounds []InvestorRound + + // Vesting schedules + team_vesting VestingSchedule + treasury_vesting VestingSchedule + + // Tracking sheets + price_sheet &spreadsheet.Sheet + token_sheet &spreadsheet.Sheet + investment_sheet &spreadsheet.Sheet + vesting_sheet &spreadsheet.Sheet + + // Scenarios + scenarios map[string]&Scenario +} \ No newline at end of file diff --git a/lib/threefold/incatokens/simulation.v b/lib/threefold/incatokens/simulation.v new file mode 100644 index 00000000..a093211d --- /dev/null +++ b/lib/threefold/incatokens/simulation.v @@ -0,0 +1,175 @@ +module incatokens + +import freeflowuniverse.herolib.biz.spreadsheet + +// Initialize default investor rounds +pub fn (mut sim Simulation) init_default_rounds() ! { + sim.investor_rounds = [ + InvestorRound{ + name: 'R1' + allocation_pct: 0.05 + price: 0.005 + vesting: VestingSchedule{cliff_months: 6, vesting_months: 24} + }, + InvestorRound{ + name: 'R2' + allocation_pct: 0.075 + price: 0.009 + vesting: VestingSchedule{cliff_months: 6, vesting_months: 24} + }, + InvestorRound{ + name: 'R3' + allocation_pct: 0.075 + price: 0.0106 + vesting: VestingSchedule{cliff_months: 6, vesting_months: 24} + } + ] + + sim.team_vesting = VestingSchedule{cliff_months: 12, vesting_months: 36} + sim.treasury_vesting = VestingSchedule{cliff_months: 12, vesting_months: 48} +} + +// Run a scenario with given demands and AMM trades +pub fn (mut sim Simulation) run_scenario(name string, demands []f64, amm_trades []f64) !&Scenario { + mut scenario := &Scenario{ + name: name + demands: demands + amm_trades: amm_trades + } + + // Initialize epochs + scenario.epochs = [ + Epoch{index: 0, type_: .auction_only, start_month: 0, end_month: 3, auction_share: 1.0, amm_share: 0.0}, + Epoch{index: 1, type_: .hybrid, start_month: 3, end_month: 6, auction_share: 0.5, amm_share: 0.5}, + Epoch{index: 2, type_: .amm_only, start_month: 6, end_month: 12, auction_share: 0.0, amm_share: 1.0} + ] + + // Track in spreadsheet + mut price_row := sim.price_sheet.row_new( + name: 'scenario_${name}_price' + tags: 'scenario:${name} type:price' + descr: 'Token price evolution for ${name} scenario' + )! + + mut treasury_row := sim.investment_sheet.row_new( + name: 'scenario_${name}_treasury' + tags: 'scenario:${name} type:treasury' + descr: 'Treasury raised for ${name} scenario' + aggregatetype: .sum + )! + + // Calculate public tokens per epoch + total_public := sim.total_supply * sim.public_pct + tokens_per_epoch := total_public / 3.0 + + mut last_auction_price := sim.get_last_investor_price() + mut spillover := 0.0 + mut treasury_total := 0.0 + mut amm_pool := AMMPool{} + + for mut epoch in scenario.epochs { + epoch.tokens_allocated = tokens_per_epoch + spillover + epoch.auction_demand = demands[epoch.index] + epoch.amm_net_trade = amm_trades[epoch.index] + + // Run auction if applicable + if epoch.auction_share > 0 { + auction_tokens := epoch.tokens_allocated * epoch.auction_share + floor_price := sim.calculate_floor_price(epoch.index, last_auction_price) + + auction_result := simulate_auction( + demand: epoch.auction_demand + min_price: floor_price + token_supply: auction_tokens + )! + + epoch.treasury_raised = auction_result.usd_raised + treasury_total += auction_result.usd_raised + last_auction_price = auction_result.clearing_price + epoch.final_price = auction_result.clearing_price + spillover = auction_tokens - auction_result.tokens_sold + + // Record in spreadsheet + treasury_row.cells[epoch.start_month].val = auction_result.usd_raised + } + + // Handle AMM if applicable + if epoch.amm_share > 0 { + amm_tokens := epoch.tokens_allocated * epoch.amm_share + spillover + spillover = 0 + + // Seed AMM pool + amm_usdc_to_add := sim.amm_liquidity_depth_factor * epoch.treasury_raised + amm_pool.add_liquidity(amm_tokens, amm_usdc_to_add) + + // Simulate trading + if epoch.amm_net_trade != 0 { + amm_pool.trade(epoch.amm_net_trade)! + } + + epoch.final_price = amm_pool.get_price() + } + + // Record price in spreadsheet + for month in epoch.start_month .. epoch.end_month { + price_row.cells[month].val = epoch.final_price + } + + epoch.tokens_spillover = spillover + } + + // Calculate final metrics + scenario.final_metrics = sim.calculate_metrics(scenario, treasury_total)! + + sim.scenarios[name] = scenario + return scenario +} + +// Calculate metrics for scenario +fn (sim Simulation) calculate_metrics(scenario &Scenario, treasury_total f64) !ScenarioMetrics { + final_price := scenario.epochs.last().final_price + + mut investor_roi := map[string]f64{} + for round in sim.investor_rounds { + investor_roi[round.name] = final_price / round.price + } + + return ScenarioMetrics{ + treasury_total: treasury_total + final_price: final_price + investor_roi: investor_roi + market_cap_final: final_price * sim.total_supply + circulating_supply_final: sim.calculate_circulating_supply(12) // at month 12 + } +} + +fn (sim Simulation) get_last_investor_price() f64 { + mut max_price := 0.0 + for round in sim.investor_rounds { + if round.price > max_price { + max_price = round.price + } + } + return max_price +} + +fn (sim Simulation) calculate_floor_price(epoch_idx int, last_auction_price f64) f64 { + last_investor_price := sim.get_last_investor_price() + + if epoch_idx == 0 { + return last_investor_price * sim.epoch1_floor_uplift + } + return last_auction_price * sim.epochn_floor_uplift +} + +fn (sim Simulation) calculate_circulating_supply(month int) f64 { + // Base circulation (non-public allocations) + investor_tokens := sim.investor_pct * sim.total_supply + team_tokens := sim.team_pct * sim.total_supply + treasury_tokens := sim.treasury_pct * sim.total_supply + + // For simplicity, assume all public tokens are circulating after TGE + public_tokens := sim.public_pct * sim.total_supply + + return investor_tokens + team_tokens + treasury_tokens + public_tokens +} \ No newline at end of file diff --git a/lib/threefold/incatokens/vesting.v b/lib/threefold/incatokens/vesting.v new file mode 100644 index 00000000..63c2d7be --- /dev/null +++ b/lib/threefold/incatokens/vesting.v @@ -0,0 +1,71 @@ +module incatokens + +import freeflowuniverse.herolib.biz.spreadsheet + +// Create vesting schedule in spreadsheet +pub fn (mut sim Simulation) create_vesting_schedules() ! { + // Create vesting sheet + mut vesting_sheet := spreadsheet.sheet_new( + name: '${sim.name}_vesting' + nrcol: 60 // 60 months + curr: sim.currency + )! + + // Team vesting + team_tokens := sim.total_supply * sim.team_pct + mut team_row := vesting_sheet.row_new( + name: 'team_vesting' + tags: 'category:team type:vesting' + descr: 'Team token vesting schedule' + )! + sim.apply_vesting_schedule(mut team_row, team_tokens, sim.team_vesting)! + + // Treasury vesting + treasury_tokens := sim.total_supply * sim.treasury_pct + mut treasury_row := vesting_sheet.row_new( + name: 'treasury_vesting' + tags: 'category:treasury type:vesting' + descr: 'Treasury token vesting schedule' + )! + sim.apply_vesting_schedule(mut treasury_row, treasury_tokens, sim.treasury_vesting)! + + // Investor rounds vesting + for round in sim.investor_rounds { + round_tokens := sim.total_supply * round.allocation_pct + mut round_row := vesting_sheet.row_new( + name: '${round.name}_vesting' + tags: 'category:investor round:${round.name} type:vesting' + descr: '${round.name} investor vesting schedule' + )! + sim.apply_vesting_schedule(mut round_row, round_tokens, round.vesting)! + } + + // Create total unlocked row + mut total_row := vesting_sheet.group2row( + name: 'total_unlocked' + include: ['type:vesting'] + tags: 'summary type:total_vesting' + descr: 'Total tokens unlocked over time' + aggregatetype: .sum + )! + + sim.vesting_sheet = vesting_sheet +} + +fn (sim Simulation) apply_vesting_schedule(mut row spreadsheet.Row, total_tokens f64, schedule VestingSchedule) ! { + monthly_unlock := total_tokens / f64(schedule.vesting_months) + + for month in 0 .. row.sheet.nrcol { + if month < schedule.cliff_months { + // Before cliff, no tokens unlocked + row.cells[month].val = 0 + } else if month < schedule.cliff_months + schedule.vesting_months { + // During vesting period + months_vested := month - schedule.cliff_months + 1 + row.cells[month].val = monthly_unlock * f64(months_vested) + } else { + // After vesting complete + row.cells[month].val = total_tokens + } + } +} \ No newline at end of file