dividends.delta
1 // Copyright (c) 2025 ALPHA/DELTA Network 2 // This file is part of the DeltaVM library. 3 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at: 7 8 // http://www.apache.org/licenses/LICENSE-2.0 9 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 /**********************************************************************************************************************/ 17 // DELTA Dividends Program 18 // 19 // Collects trading fees from exchange and perpetuals programs and distributes 20 // them to staked DX holders at each epoch boundary. 21 // 22 // Revenue Sources: 23 // - Spot trading fees from delta.exchange 24 // - Perpetual trading fees from delta.perpetuals 25 // - Liquidation proceeds from delta.perpetuals 26 // 27 // Distribution Model: 28 // - Per-epoch distribution to staked DX holders 29 // - Pro-rata based on staking weight 30 // - Claimable anytime after distribution 31 // - Unclaimed dividends accrue across epochs 32 // 33 // Fee Allocation: 34 // - 70% to DX stakers (this program) 35 // - 20% to Insurance Fund (retained by perpetuals) 36 // - 10% to Operations (governance controlled) 37 /**********************************************************************************************************************/ 38 39 program delta.dividends; 40 41 /**********************************************************************************************************************/ 42 // CONSTANTS (represented as literals in code) 43 /**********************************************************************************************************************/ 44 45 // STAKER_SHARE_BPS = 7000 (70%) 46 // OPERATIONS_SHARE_BPS = 1000 (10%) 47 // EPOCH_BLOCKS = 8640 (1 day at 10s blocks) 48 // MIN_CLAIM_AMOUNT = 1000 (0.001 sAX in microcredits) 49 50 /**********************************************************************************************************************/ 51 // STRUCTS 52 /**********************************************************************************************************************/ 53 54 // Dividend pool state for an epoch. 55 struct dividend_pool: 56 // Epoch number. 57 epoch as u64; 58 // Total fees collected this epoch (in sAX microcredits). 59 total_fees as u128; 60 // Total staked DX at snapshot. 61 total_staked as u128; 62 // Dividend per DX (scaled by 1e18 for precision). 63 dividend_per_dx as u128; 64 // Block when epoch started. 65 start_block as u32; 66 // Block when epoch ended (0 if current). 67 end_block as u32; 68 // Whether distribution has been finalized. 69 finalized as boolean; 70 71 // User's dividend tracking. 72 struct user_dividends: 73 // User address. 74 user as address; 75 // Total claimed across all epochs. 76 total_claimed as u128; 77 // Last epoch claimed. 78 last_claim_epoch as u64; 79 // Pending (unclaimed) dividends. 80 pending as u128; 81 82 // Fee deposit record. 83 struct fee_deposit: 84 // Deposit ID. 85 deposit_id as u64; 86 // Source: 0 = exchange, 1 = perpetuals, 2 = liquidation. 87 source as u8; 88 // Amount in sAX microcredits. 89 amount as u128; 90 // Block when deposited. 91 deposited_at as u32; 92 // Epoch this deposit belongs to. 93 epoch as u64; 94 95 // Operations wallet for governance-controlled share. 96 struct operations_wallet: 97 // Wallet address (governance controlled). 98 wallet as address; 99 // Total collected. 100 total_collected as u128; 101 // Last updated block. 102 updated_at as u32; 103 104 // Epoch snapshot for staking weights. 105 struct epoch_snapshot: 106 // Epoch number. 107 epoch as u64; 108 // Total staked DX at snapshot. 109 total_staked as u128; 110 // Number of stakers. 111 staker_count as u64; 112 // Block when snapshot was taken. 113 snapshot_block as u32; 114 115 // User stake snapshot for dividend calculation. 116 struct stake_snapshot: 117 // User address. 118 user as address; 119 // Epoch number. 120 epoch as u64; 121 // Staked DX at snapshot. 122 staked_dx as u128; 123 // Weight for dividend calculation. 124 weight as u128; 125 126 // Claim record for audit trail. 127 struct claim_record: 128 // Claim ID. 129 claim_id as u64; 130 // User address. 131 user as address; 132 // Amount claimed in sAX. 133 amount as u128; 134 // Epoch range claimed (from). 135 from_epoch as u64; 136 // Epoch range claimed (to). 137 to_epoch as u64; 138 // Block when claimed. 139 claimed_at as u32; 140 141 // Program configuration. 142 struct dividend_config: 143 // Staker share in basis points (7000 = 70%). 144 staker_share_bps as u16; 145 // Operations share in basis points (1000 = 10%). 146 operations_share_bps as u16; 147 // Minimum claim amount in microcredits. 148 min_claim_amount as u128; 149 // Blocks per epoch. 150 epoch_blocks as u32; 151 // Operations wallet address. 152 operations_wallet as address; 153 // Admin address for configuration. 154 admin as address; 155 // Whether the program is active. 156 active as boolean; 157 158 /**********************************************************************************************************************/ 159 // MAPPINGS 160 /**********************************************************************************************************************/ 161 162 // Current epoch number. 163 mapping current_epoch: 164 key as u8.public; 165 value as u64.public; 166 167 // Dividend pools by epoch. 168 mapping dividend_pools: 169 key as u64.public; 170 value as dividend_pool.public; 171 172 // User dividend tracking. 173 mapping user_dividend_info: 174 key as address.public; 175 value as user_dividends.public; 176 177 // Fee deposit counter. 178 mapping deposit_counter: 179 key as u8.public; 180 value as u64.public; 181 182 // Fee deposits by ID. 183 mapping fee_deposits: 184 key as u64.public; 185 value as fee_deposit.public; 186 187 // Epoch snapshots. 188 mapping epoch_snapshots: 189 key as u64.public; 190 value as epoch_snapshot.public; 191 192 // User stake snapshots (key = hash(user, epoch)). 193 mapping stake_snapshots: 194 key as field.public; 195 value as stake_snapshot.public; 196 197 // Claim counter. 198 mapping claim_counter: 199 key as u8.public; 200 value as u64.public; 201 202 // Claim records by ID. 203 mapping claim_records: 204 key as u64.public; 205 value as claim_record.public; 206 207 // Program configuration. 208 mapping config: 209 key as u8.public; 210 value as dividend_config.public; 211 212 // Operations wallet state. 213 mapping operations: 214 key as u8.public; 215 value as operations_wallet.public; 216 217 // Total fees collected all time. 218 mapping total_stats: 219 key as u8.public; 220 value as u128.public; 221 222 // Authorized fee depositors (exchange, perpetuals). 223 mapping authorized_depositors: 224 key as address.public; 225 value as boolean.public; 226 227 /**********************************************************************************************************************/ 228 // FUNCTIONS 229 /**********************************************************************************************************************/ 230 231 // Initialize the dividends program. 232 function initialize: 233 input r0 as address.public; // Operations wallet address. 234 input r1 as address.public; // Admin address. 235 236 // Create default configuration. 237 // Staker share: 70%, Operations: 10% (20% stays in insurance fund). 238 239 finalize initialize r0 r1 self.caller; 240 241 finalize initialize: 242 input r0 as address.public; 243 input r1 as address.public; 244 input r2 as address.public; 245 246 // Ensure not already initialized. 247 get.or_use config[0u8] dividend_config { 248 staker_share_bps: 0u16, 249 operations_share_bps: 0u16, 250 min_claim_amount: 0u128, 251 epoch_blocks: 0u32, 252 operations_wallet: r0, 253 admin: r1, 254 active: false 255 } into r3; 256 assert.eq r3.active false; 257 258 // Create configuration. 259 cast 7000u16 1000u16 1000u128 8640u32 r0 r1 true into r4 as dividend_config; 260 set r4 into config[0u8]; 261 262 // Initialize epoch to 1. 263 set 1u64 into current_epoch[0u8]; 264 265 // Initialize operations wallet. 266 cast r0 0u128 0u32 into r5 as operations_wallet; 267 set r5 into operations[0u8]; 268 269 // Initialize counters. 270 set 0u64 into deposit_counter[0u8]; 271 set 0u64 into claim_counter[0u8]; 272 set 0u128 into total_stats[0u8]; 273 274 // Collect fees from authorized sources (exchange, perpetuals). 275 function collect_fees: 276 input r0 as u128.public; // Amount in sAX microcredits. 277 input r1 as u8.public; // Source: 0 = exchange, 1 = perpetuals, 2 = liquidation. 278 279 // Only authorized depositors can call this. 280 // Amount must be positive. 281 assert.neq r0 0u128; 282 283 finalize collect_fees r0 r1 self.caller; 284 285 finalize collect_fees: 286 input r0 as u128.public; 287 input r1 as u8.public; 288 input r2 as address.public; 289 290 // Verify caller is authorized depositor. 291 get.or_use authorized_depositors[r2] false into r3; 292 assert.eq r3 true; 293 294 // Get current epoch. 295 get.or_use current_epoch[0u8] 1u64 into r4; 296 297 // Get deposit counter. 298 get.or_use deposit_counter[0u8] 0u64 into r5; 299 add r5 1u64 into r6; 300 set r6 into deposit_counter[0u8]; 301 302 // Create fee deposit record. 303 cast r6 r1 r0 block.height r4 into r7 as fee_deposit; 304 set r7 into fee_deposits[r6]; 305 306 // Update current epoch pool. 307 get.or_use dividend_pools[r4] dividend_pool { 308 epoch: r4, 309 total_fees: 0u128, 310 total_staked: 0u128, 311 dividend_per_dx: 0u128, 312 start_block: block.height, 313 end_block: 0u32, 314 finalized: false 315 } into r8; 316 317 // Calculate staker portion (70%). 318 mul r0 7000u128 into r9; 319 div r9 10000u128 into r10; 320 321 // Add to pool. 322 add r8.total_fees r10 into r11; 323 cast r8.epoch r11 r8.total_staked r8.dividend_per_dx r8.start_block r8.end_block r8.finalized into r12 as dividend_pool; 324 set r12 into dividend_pools[r4]; 325 326 // Calculate operations portion (10%). 327 mul r0 1000u128 into r13; 328 div r13 10000u128 into r14; 329 330 // Update operations wallet. 331 get.or_use operations[0u8] operations_wallet { 332 wallet: r2, 333 total_collected: 0u128, 334 updated_at: 0u32 335 } into r15; 336 add r15.total_collected r14 into r16; 337 cast r15.wallet r16 block.height into r17 as operations_wallet; 338 set r17 into operations[0u8]; 339 340 // Update total stats. 341 get.or_use total_stats[0u8] 0u128 into r18; 342 add r18 r0 into r19; 343 set r19 into total_stats[0u8]; 344 345 // Take epoch snapshot for staking weights. 346 // Called at the start of each epoch by the system. 347 function take_epoch_snapshot: 348 input r0 as u64.public; // Epoch number. 349 input r1 as u128.public; // Total staked DX. 350 input r2 as u64.public; // Number of stakers. 351 352 finalize take_epoch_snapshot r0 r1 r2; 353 354 finalize take_epoch_snapshot: 355 input r0 as u64.public; 356 input r1 as u128.public; 357 input r2 as u64.public; 358 359 // Create epoch snapshot. 360 cast r0 r1 r2 block.height into r3 as epoch_snapshot; 361 set r3 into epoch_snapshots[r0]; 362 363 // Update pool with staking total. 364 get.or_use dividend_pools[r0] dividend_pool { 365 epoch: r0, 366 total_fees: 0u128, 367 total_staked: 0u128, 368 dividend_per_dx: 0u128, 369 start_block: block.height, 370 end_block: 0u32, 371 finalized: false 372 } into r4; 373 cast r4.epoch r4.total_fees r1 r4.dividend_per_dx r4.start_block r4.end_block r4.finalized into r5 as dividend_pool; 374 set r5 into dividend_pools[r0]; 375 376 // Record user's stake for an epoch. 377 function record_stake: 378 input r0 as address.public; // User address. 379 input r1 as u64.public; // Epoch. 380 input r2 as u128.public; // Staked DX amount. 381 382 finalize record_stake r0 r1 r2; 383 384 finalize record_stake: 385 input r0 as address.public; 386 input r1 as u64.public; 387 input r2 as u128.public; 388 389 // Create snapshot key: hash(user, epoch). 390 hash.bhp256 r0 into r3 as field; 391 cast r1 into r4 as field; 392 add r3 r4 into r5; 393 394 // Create stake snapshot. 395 cast r0 r1 r2 r2 into r6 as stake_snapshot; 396 set r6 into stake_snapshots[r5]; 397 398 // Finalize epoch and calculate dividends. 399 function finalize_epoch: 400 input r0 as u64.public; // Epoch to finalize. 401 402 finalize finalize_epoch r0; 403 404 finalize finalize_epoch: 405 input r0 as u64.public; 406 407 // Get pool for this epoch. 408 get dividend_pools[r0] into r1; 409 assert.eq r1.finalized false; 410 411 // Ensure epoch has ended (next epoch started). 412 get current_epoch[0u8] into r2; 413 gt r2 r0 into r3; 414 assert.eq r3 true; 415 416 // Calculate dividend per DX (scaled by 1e18). 417 // dividend_per_dx = total_fees * 1e18 / total_staked 418 gt r1.total_staked 0u128 into r4; 419 ternary r4 r1.total_staked 1u128 into r5; 420 421 mul r1.total_fees 1000000000000000000u128 into r6; 422 div r6 r5 into r7; 423 424 // Mark pool as finalized. 425 cast r1.epoch r1.total_fees r1.total_staked r7 r1.start_block block.height true into r8 as dividend_pool; 426 set r8 into dividend_pools[r0]; 427 428 // Claim pending dividends. 429 function claim_dividends: 430 input r0 as u64.public; // From epoch. 431 input r1 as u64.public; // To epoch (inclusive). 432 433 finalize claim_dividends r0 r1 self.caller; 434 435 finalize claim_dividends: 436 input r0 as u64.public; 437 input r1 as u64.public; 438 input r2 as address.public; 439 440 // Get current epoch. 441 get current_epoch[0u8] into r3; 442 443 // Ensure to_epoch is finalized (less than current). 444 lt r1 r3 into r4; 445 assert.eq r4 true; 446 447 // Get user's dividend info. 448 get.or_use user_dividend_info[r2] user_dividends { 449 user: r2, 450 total_claimed: 0u128, 451 last_claim_epoch: 0u64, 452 pending: 0u128 453 } into r5; 454 455 // From epoch must be after last claimed epoch. 456 gte r0 r5.last_claim_epoch into r6; 457 assert.eq r6 true; 458 459 // Calculate total claimable (simplified - in practice would loop through epochs). 460 // For demonstration, we'll claim for a single epoch. 461 // Real implementation would iterate or use accumulator pattern. 462 463 // Get stake snapshot for from_epoch. 464 hash.bhp256 r2 into r7 as field; 465 cast r0 into r8 as field; 466 add r7 r8 into r9; 467 get.or_use stake_snapshots[r9] stake_snapshot { 468 user: r2, 469 epoch: r0, 470 staked_dx: 0u128, 471 weight: 0u128 472 } into r10; 473 474 // Get pool for from_epoch. 475 get dividend_pools[r0] into r11; 476 assert.eq r11.finalized true; 477 478 // Calculate user's share. 479 // user_dividend = (staked_dx * dividend_per_dx) / 1e18 480 mul r10.staked_dx r11.dividend_per_dx into r12; 481 div r12 1000000000000000000u128 into r13; 482 483 // Check minimum claim amount. 484 get config[0u8] into r14; 485 gte r13 r14.min_claim_amount into r15; 486 assert.eq r15 true; 487 488 // Update user's dividend info. 489 add r5.total_claimed r13 into r16; 490 add r5.pending 0u128 into r17; // Clear pending after claim. 491 cast r2 r16 r1 r17 into r18 as user_dividends; 492 set r18 into user_dividend_info[r2]; 493 494 // Create claim record. 495 get claim_counter[0u8] into r19; 496 add r19 1u64 into r20; 497 set r20 into claim_counter[0u8]; 498 499 cast r20 r2 r13 r0 r1 block.height into r21 as claim_record; 500 set r21 into claim_records[r20]; 501 502 // Note: Actual sAX transfer would be handled by calling delta.sax::transfer. 503 504 // Get pending dividends for a user. 505 function get_pending: 506 input r0 as address.public; // User address. 507 508 // Read-only operation, returns pending amount. 509 finalize get_pending r0; 510 511 finalize get_pending: 512 input r0 as address.public; 513 514 // Get user info. 515 get.or_use user_dividend_info[r0] user_dividends { 516 user: r0, 517 total_claimed: 0u128, 518 last_claim_epoch: 0u64, 519 pending: 0u128 520 } into r1; 521 522 // Note: Actual pending calculation would iterate unclaimed epochs. 523 // This is a simplified read operation. 524 525 // Start new epoch (called by system at epoch boundary). 526 function start_new_epoch: 527 input r0 as u64.public; // New epoch number. 528 529 finalize start_new_epoch r0; 530 531 finalize start_new_epoch: 532 input r0 as u64.public; 533 534 // Get current epoch. 535 get current_epoch[0u8] into r1; 536 537 // New epoch must be current + 1. 538 add r1 1u64 into r2; 539 assert.eq r0 r2; 540 541 // Finalize previous epoch if not already. 542 get.or_use dividend_pools[r1] dividend_pool { 543 epoch: r1, 544 total_fees: 0u128, 545 total_staked: 0u128, 546 dividend_per_dx: 0u128, 547 start_block: 0u32, 548 end_block: 0u32, 549 finalized: false 550 } into r3; 551 552 // Mark previous pool end block. 553 cast r3.epoch r3.total_fees r3.total_staked r3.dividend_per_dx r3.start_block block.height r3.finalized into r4 as dividend_pool; 554 set r4 into dividend_pools[r1]; 555 556 // Set new current epoch. 557 set r0 into current_epoch[0u8]; 558 559 // Initialize new pool. 560 cast r0 0u128 0u128 0u128 block.height 0u32 false into r5 as dividend_pool; 561 set r5 into dividend_pools[r0]; 562 563 // Authorize a fee depositor (admin only). 564 function authorize_depositor: 565 input r0 as address.public; // Depositor address. 566 input r1 as boolean.public; // Authorized status. 567 568 finalize authorize_depositor r0 r1 self.caller; 569 570 finalize authorize_depositor: 571 input r0 as address.public; 572 input r1 as boolean.public; 573 input r2 as address.public; 574 575 // Verify admin. 576 get config[0u8] into r3; 577 assert.eq r2 r3.admin; 578 579 // Set authorization. 580 set r1 into authorized_depositors[r0]; 581 582 // Withdraw operations funds (operations wallet only). 583 function withdraw_operations: 584 input r0 as u128.public; // Amount to withdraw. 585 586 finalize withdraw_operations r0 self.caller; 587 588 finalize withdraw_operations: 589 input r0 as u128.public; 590 input r1 as address.public; 591 592 // Get operations wallet. 593 get operations[0u8] into r2; 594 595 // Verify caller is operations wallet. 596 assert.eq r1 r2.wallet; 597 598 // Verify sufficient balance. 599 gte r2.total_collected r0 into r3; 600 assert.eq r3 true; 601 602 // Update balance. 603 sub r2.total_collected r0 into r4; 604 cast r2.wallet r4 block.height into r5 as operations_wallet; 605 set r5 into operations[0u8]; 606 607 // Note: Actual sAX transfer would be handled by calling delta.sax::transfer. 608 609 // Update configuration (admin only). 610 function update_config: 611 input r0 as u16.public; // New staker share BPS. 612 input r1 as u16.public; // New operations share BPS. 613 input r2 as u128.public; // New min claim amount. 614 input r3 as address.public; // New operations wallet. 615 616 // Validate shares don't exceed 80% (20% stays in insurance fund). 617 add r0 r1 into r4; 618 lte r4 8000u16 into r5; 619 assert.eq r5 true; 620 621 finalize update_config r0 r1 r2 r3 self.caller; 622 623 finalize update_config: 624 input r0 as u16.public; 625 input r1 as u16.public; 626 input r2 as u128.public; 627 input r3 as address.public; 628 input r4 as address.public; 629 630 // Verify admin. 631 get config[0u8] into r5; 632 assert.eq r4 r5.admin; 633 634 // Update configuration. 635 cast r0 r1 r2 r5.epoch_blocks r3 r5.admin r5.active into r6 as dividend_config; 636 set r6 into config[0u8]; 637 638 // Update operations wallet address. 639 get operations[0u8] into r7; 640 cast r3 r7.total_collected block.height into r8 as operations_wallet; 641 set r8 into operations[0u8]; 642 643 // Get dividend pool info for an epoch. 644 function get_pool: 645 input r0 as u64.public; // Epoch number. 646 647 finalize get_pool r0; 648 649 finalize get_pool: 650 input r0 as u64.public; 651 652 // Read-only operation. 653 get dividend_pools[r0] into r1; 654 655 // Get epoch snapshot. 656 function get_snapshot: 657 input r0 as u64.public; // Epoch number. 658 659 finalize get_snapshot r0; 660 661 finalize get_snapshot: 662 input r0 as u64.public; 663 664 // Read-only operation. 665 get epoch_snapshots[r0] into r1;