From 9c0d1447870c476213ba32ca681920e702600bcf Mon Sep 17 00:00:00 2001 From: stefann-01 Date: Tue, 9 Sep 2025 11:46:51 +0200 Subject: [PATCH 1/4] chore(contracts): update - apply changes commited from 528d4be55b555776bb2d9950ecadc0aaef63e80e to 1fcf3b64ddaae65bf848135bdc870408ae5f0838 (inclusive) - apply changes commited in https://github.com/stefann-01/gno/pull/29 --- compose.yaml | 2 +- contract/p/gnoswap/consts/consts.gno | 143 -- contract/p/gnoswap/fuzz/fuzz.gno | 549 +++++ contract/p/gnoswap/fuzz/fuzz_test.gno | 320 +++ contract/p/gnoswap/fuzz/gnomod.toml | 2 + contract/p/gnoswap/fuzz/seed.gno | 55 + contract/p/gnoswap/gnsmath/bit_math.gno | 80 + contract/p/gnoswap/gnsmath/bit_math_test.gno | 855 ++++++++ contract/p/gnoswap/gnsmath/doc.gno | 9 + contract/p/gnoswap/gnsmath/errors.gno | 19 + contract/p/gnoswap/gnsmath/gnomod.toml | 2 + .../p/gnoswap/gnsmath/sqrt_price_math.gno | 313 +++ .../gnoswap/gnsmath/sqrt_price_math_test.gno | 1454 +++++++++++++ contract/p/gnoswap/gnsmath/swap_math.gno | 234 ++ contract/p/gnoswap/gnsmath/swap_math_test.gno | 851 ++++++++ contract/p/gnoswap/int256/LICENSE | 21 + contract/p/gnoswap/int256/README.md | 35 + contract/p/gnoswap/int256/absolute.gno | 38 + contract/p/gnoswap/int256/absolute_test.gno | 184 ++ contract/p/gnoswap/int256/arithmetic.gno | 319 +++ contract/p/gnoswap/int256/arithmetic_test.gno | 1016 +++++++++ contract/p/gnoswap/int256/bitwise.gno | 101 + contract/p/gnoswap/int256/bitwise_test.gno | 198 ++ contract/p/gnoswap/int256/cmp.gno | 117 + contract/p/gnoswap/int256/cmp_test.gno | 316 +++ contract/p/gnoswap/int256/conversion.gno | 120 ++ contract/p/gnoswap/int256/conversion_test.gno | 540 +++++ contract/p/gnoswap/int256/doc.gno | 13 + contract/p/gnoswap/int256/gnomod.toml | 2 + contract/p/gnoswap/int256/int256.gno | 182 ++ contract/p/gnoswap/int256/int256_test.gno | 185 ++ contract/p/gnoswap/rbac/README.md | 78 + contract/p/gnoswap/rbac/doc.gno | 139 ++ contract/p/gnoswap/rbac/errors.gno | 18 + contract/p/gnoswap/rbac/gnomod.toml | 2 + contract/p/gnoswap/rbac/ownable.gno | 110 + contract/p/gnoswap/rbac/ownable_test.gno | 175 ++ contract/p/gnoswap/rbac/rbac.gno | 146 ++ contract/p/gnoswap/rbac/rbac_test.gno | 72 + contract/p/gnoswap/rbac/role.gno | 35 + contract/p/gnoswap/rbac/types.gno | 54 + contract/p/gnoswap/uint256/LICENSE | 28 + contract/p/gnoswap/uint256/README.md | 38 + contract/p/gnoswap/uint256/_helper_test.gno | 52 + contract/p/gnoswap/uint256/arithmetic.gno | 538 +++++ .../p/gnoswap/uint256/arithmetic_test.gno | 1883 +++++++++++++++++ contract/p/gnoswap/uint256/bits_table.gno | 115 + contract/p/gnoswap/uint256/bitwise.gno | 266 +++ contract/p/gnoswap/uint256/bitwise_test.gno | 346 +++ contract/p/gnoswap/uint256/cmp.gno | 103 + contract/p/gnoswap/uint256/cmp_test.gno | 229 ++ contract/p/gnoswap/uint256/conversion.gno | 602 ++++++ .../p/gnoswap/uint256/conversion_test.gno | 60 + contract/p/gnoswap/uint256/doc.gno | 14 + contract/p/gnoswap/uint256/error.gno | 73 + contract/p/gnoswap/uint256/fullmath.gno | 106 + contract/p/gnoswap/uint256/fullmath_test.gno | 844 ++++++++ contract/p/gnoswap/uint256/gnomod.toml | 2 + contract/p/gnoswap/uint256/gs_pointer.gno | 8 + contract/p/gnoswap/uint256/mod.gno | 605 ++++++ contract/p/gnoswap/uint256/uint256.gno | 303 +++ contract/p/gnoswap/uint256/uint256_test.gno | 825 ++++++++ contract/p/gnoswap/uint256/utils.gno | 22 + contract/p/volos/math/shares_math.gno | 3 +- contract/r/gnoswap/access/README.md | 68 + contract/r/gnoswap/access/access.gno | 67 + contract/r/gnoswap/access/assert.gno | 145 ++ contract/r/gnoswap/access/consts.gno | 5 + contract/r/gnoswap/access/errors.gno | 10 + contract/r/gnoswap/access/gnomod.toml | 2 + contract/r/gnoswap/access/swap_whitelist.gno | 77 + contract/r/gnoswap/emission/README.md | 107 + contract/r/gnoswap/emission/assert.gno | 82 + contract/r/gnoswap/emission/distribution.gno | 389 ++++ contract/r/gnoswap/emission/emission.gno | 216 ++ contract/r/gnoswap/emission/errors.gno | 21 + contract/r/gnoswap/emission/gnomod.toml | 2 + contract/r/gnoswap/emission/utils.gno | 37 + contract/r/gnoswap/gns/README.md | 60 + contract/r/gnoswap/gns/assert.gno | 14 + contract/r/gnoswap/gns/consts.gno | 29 + contract/r/gnoswap/gns/emission_state.gno | 166 ++ contract/r/gnoswap/gns/errors.gno | 18 + contract/r/gnoswap/gns/getter.gno | 185 ++ contract/r/gnoswap/gns/gnomod.toml | 2 + contract/r/gnoswap/gns/gns.gno | 309 +++ contract/r/gnoswap/gns/gns_emission.gno | 28 + contract/r/gnoswap/gns/halving.gno | 205 ++ contract/r/gnoswap/gns/utils.gno | 91 + contract/r/gnoswap/halt/README.md | 92 + contract/r/gnoswap/halt/assert.gno | 82 + contract/r/gnoswap/halt/config.gno | 122 ++ contract/r/gnoswap/halt/doc.gno | 2 + contract/r/gnoswap/halt/errors.gno | 18 + contract/r/gnoswap/halt/getters.gno | 117 + contract/r/gnoswap/halt/gnomod.toml | 2 + contract/r/gnoswap/halt/halt.gno | 84 + contract/r/gnoswap/halt/types.gno | 89 + contract/r/gnoswap/rbac/README.md | 120 ++ contract/r/gnoswap/rbac/assert.gno | 51 + contract/r/gnoswap/rbac/consts.gno | 40 + contract/r/gnoswap/rbac/emit.gno | 39 + contract/r/gnoswap/rbac/errors.gno | 20 + contract/r/gnoswap/rbac/gnomod.toml | 2 + contract/r/gnoswap/rbac/ownership.gno | 45 + contract/r/gnoswap/rbac/rbac.gno | 112 + contract/r/gnoswap/rbac/role.gno | 25 + contract/r/gnoswap/referral/README.md | 47 + contract/r/gnoswap/referral/doc.gno | 109 + contract/r/gnoswap/referral/errors.gno | 16 + contract/r/gnoswap/referral/global_keeper.gno | 72 + contract/r/gnoswap/referral/gnomod.toml | 2 + contract/r/gnoswap/referral/keeper.gno | 176 ++ contract/r/gnoswap/referral/referral.gno | 56 + contract/r/gnoswap/referral/type.gno | 33 + contract/r/gnoswap/referral/utils.gno | 38 + .../r/gnoswap/{v1 => }/test_token/bar/bar.gno | 8 +- contract/r/gnoswap/test_token/bar/gnomod.toml | 2 + contract/r/gnoswap/test_token/baz/baz.gno | 80 + contract/r/gnoswap/test_token/baz/gnomod.toml | 2 + contract/r/gnoswap/test_token/foo/foo.gno | 80 + contract/r/gnoswap/test_token/foo/gnomod.toml | 2 + contract/r/gnoswap/test_token/obl/gnomod.toml | 2 + contract/r/gnoswap/test_token/obl/obl.gno | 80 + contract/r/gnoswap/test_token/qux/gnomod.toml | 2 + contract/r/gnoswap/test_token/qux/qux.gno | 80 + .../r/gnoswap/test_token/usdc/gnomod.toml | 2 + contract/r/gnoswap/test_token/usdc/usdc.gno | 80 + contract/r/gnoswap/v1/common/consts.gno | 7 + contract/r/gnoswap/v1/common/doc.gno | 6 + contract/r/gnoswap/v1/common/errors.gno | 25 + contract/r/gnoswap/v1/common/gnomod.toml | 2 + .../r/gnoswap/v1/common/grc20reg_helper.gno | 86 + .../r/gnoswap/v1/common/liquidity_amounts.gno | 332 +++ contract/r/gnoswap/v1/common/tick_math.gno | 263 +++ .../r/gnoswap/v1/community_pool/README.md | 43 + .../v1/community_pool/community_pool.gno | 51 + contract/r/gnoswap/v1/community_pool/doc.gno | 6 + .../r/gnoswap/v1/community_pool/errors.gno | 13 + .../r/gnoswap/v1/community_pool/gnomod.toml | 2 + contract/r/gnoswap/v1/gnft/assert.gno | 37 + contract/r/gnoswap/v1/gnft/errors.gno | 31 + contract/r/gnoswap/v1/gnft/gnft.gno | 272 +++ contract/r/gnoswap/v1/gnft/gnomod.toml | 2 + contract/r/gnoswap/v1/gnft/svg_generator.gno | 69 + contract/r/gnoswap/v1/gnft/utils.gno | 114 + contract/r/gnoswap/v1/gov/README.md | 103 + contract/r/gnoswap/v1/gov/doc.gno | 5 + contract/r/gnoswap/v1/gov/gnomod.toml | 2 + contract/r/gnoswap/v1/gov/governance/api.gno | 195 ++ .../r/gnoswap/v1/gov/governance/assert.gno | 15 + .../r/gnoswap/v1/gov/governance/config.gno | 116 + .../r/gnoswap/v1/gov/governance/consts.gno | 16 + .../r/gnoswap/v1/gov/governance/counter.gno | 25 + contract/r/gnoswap/v1/gov/governance/doc.gno | 5 + .../r/gnoswap/v1/gov/governance/errors.gno | 37 + .../v1/gov/governance/getter_proposal.gno | 71 + .../gnoswap/v1/gov/governance/getter_vote.gno | 32 + .../r/gnoswap/v1/gov/governance/gnomod.toml | 2 + .../v1/gov/governance/governance_execute.gno | 253 +++ .../v1/gov/governance/governance_propose.gno | 461 ++++ .../v1/gov/governance/governance_vote.gno | 121 ++ .../v1/gov/governance/parameter_registry.gno | 529 +++++ .../governance/parameter_registry_handler.gno | 140 ++ .../r/gnoswap/v1/gov/governance/proposal.gno | 280 +++ .../gov/governance/proposal_action_status.gno | 128 ++ .../v1/gov/governance/proposal_data.gno | 406 ++++ .../v1/gov/governance/proposal_manager.gno | 95 + .../governance/proposal_schedule_status.gno | 108 + .../v1/gov/governance/proposal_status.gno | 314 +++ .../gov/governance/proposal_vote_status.gno | 163 ++ .../r/gnoswap/v1/gov/governance/state.gno | 239 +++ .../r/gnoswap/v1/gov/governance/utils.gno | 159 ++ .../gnoswap/v1/gov/governance/voting_info.gno | 150 ++ .../gnoswap/v1/gov/staker/api_delegation.gno | 25 + .../r/gnoswap/v1/gov/staker/api_staker.gno | 77 + contract/r/gnoswap/v1/gov/staker/assert.gno | 27 + contract/r/gnoswap/v1/gov/staker/consts.gno | 12 + contract/r/gnoswap/v1/gov/staker/counter.gno | 13 + .../r/gnoswap/v1/gov/staker/delegation.gno | 164 ++ .../v1/gov/staker/delegation_history.gno | 61 + .../v1/gov/staker/delegation_mananger.gno | 144 ++ .../v1/gov/staker/delegation_record.gno | 156 ++ .../v1/gov/staker/delegation_snapshot.gno | 159 ++ .../v1/gov/staker/delegation_withdraw.gno | 177 ++ contract/r/gnoswap/v1/gov/staker/doc.gno | 4 + .../v1/gov/staker/emission_reward_manager.gno | 235 ++ .../v1/gov/staker/emission_reward_state.gno | 250 +++ contract/r/gnoswap/v1/gov/staker/errors.gno | 32 + .../gov/staker/getter_delegation_snapshot.gno | 34 + contract/r/gnoswap/v1/gov/staker/gnomod.toml | 2 + .../staker/protocol_fee_reward_manager.gno | 298 +++ .../gov/staker/protocol_fee_reward_state.gno | 298 +++ .../gnoswap/v1/gov/staker/staker_delegate.gno | 469 ++++ .../gov/staker/staker_delegation_snapshot.gno | 82 + .../r/gnoswap/v1/gov/staker/staker_reward.gno | 280 +++ contract/r/gnoswap/v1/gov/staker/state.gno | 523 +++++ contract/r/gnoswap/v1/gov/staker/util.gno | 124 ++ contract/r/gnoswap/v1/gov/xgns/doc.gno | 4 + contract/r/gnoswap/v1/gov/xgns/errors.gno | 14 + contract/r/gnoswap/v1/gov/xgns/gnomod.toml | 2 + contract/r/gnoswap/v1/gov/xgns/xgns.gno | 126 ++ contract/r/gnoswap/v1/launchpad/README.md | 66 + .../r/gnoswap/v1/launchpad/api_deposit.gno | 18 + .../r/gnoswap/v1/launchpad/api_project.gno | 92 + .../r/gnoswap/v1/launchpad/api_reward.gno | 26 + contract/r/gnoswap/v1/launchpad/assert.gno | 62 + contract/r/gnoswap/v1/launchpad/consts.gno | 44 + contract/r/gnoswap/v1/launchpad/counter.gno | 25 + contract/r/gnoswap/v1/launchpad/deposit.gno | 120 ++ contract/r/gnoswap/v1/launchpad/errors.gno | 46 + contract/r/gnoswap/v1/launchpad/gnomod.toml | 2 + .../r/gnoswap/v1/launchpad/json_builder.gno | 102 + .../v1/launchpad/launchpad_deposit.gno | 188 ++ .../v1/launchpad/launchpad_project.gno | 470 ++++ .../v1/launchpad/launchpad_protocol_fee.gno | 24 + .../gnoswap/v1/launchpad/launchpad_reward.gno | 77 + .../v1/launchpad/launchpad_withdraw.gno | 116 + contract/r/gnoswap/v1/launchpad/project.gno | 256 +++ .../v1/launchpad/project_condition.gno | 98 + .../r/gnoswap/v1/launchpad/project_tier.gno | 174 ++ .../r/gnoswap/v1/launchpad/reward_manager.gno | 311 +++ .../r/gnoswap/v1/launchpad/reward_state.gno | 158 ++ contract/r/gnoswap/v1/launchpad/state.gno | 104 + contract/r/gnoswap/v1/launchpad/utils.gno | 76 + contract/r/gnoswap/v1/pool/README.md | 131 ++ contract/r/gnoswap/v1/pool/api.gno | 53 + contract/r/gnoswap/v1/pool/assert.gno | 65 + contract/r/gnoswap/v1/pool/doc.gno | 11 + contract/r/gnoswap/v1/pool/errors.gno | 50 + contract/r/gnoswap/v1/pool/factory_param.gno | 143 ++ contract/r/gnoswap/v1/pool/getter.gno | 183 ++ contract/r/gnoswap/v1/pool/gnomod.toml | 2 + contract/r/gnoswap/v1/pool/json.gno | 275 +++ contract/r/gnoswap/v1/pool/liquidity_math.gno | 43 + contract/r/gnoswap/v1/pool/manager.gno | 275 +++ contract/r/gnoswap/v1/pool/pool.gno | 364 ++++ contract/r/gnoswap/v1/pool/pool_type.gno | 272 +++ contract/r/gnoswap/v1/pool/position.gno | 397 ++++ contract/r/gnoswap/v1/pool/protocol_fee.gno | 199 ++ contract/r/gnoswap/v1/pool/swap.gno | 647 ++++++ contract/r/gnoswap/v1/pool/tick.gno | 468 ++++ contract/r/gnoswap/v1/pool/tick_bitmap.gno | 167 ++ contract/r/gnoswap/v1/pool/transfer.gno | 208 ++ contract/r/gnoswap/v1/pool/type.gno | 291 +++ contract/r/gnoswap/v1/pool/utils.gno | 193 ++ contract/r/gnoswap/v1/position/README.md | 157 ++ contract/r/gnoswap/v1/position/api.gno | 152 ++ contract/r/gnoswap/v1/position/assert.gno | 111 + contract/r/gnoswap/v1/position/burn.gno | 210 ++ contract/r/gnoswap/v1/position/doc.gno | 5 + contract/r/gnoswap/v1/position/errors.gno | 40 + contract/r/gnoswap/v1/position/getter.gno | 104 + contract/r/gnoswap/v1/position/gnomod.toml | 2 + contract/r/gnoswap/v1/position/json.gno | 149 ++ .../v1/position/liquidity_management.gno | 67 + contract/r/gnoswap/v1/position/manager.gno | 95 + contract/r/gnoswap/v1/position/mint.gno | 300 +++ .../r/gnoswap/v1/position/native_token.gno | 171 ++ contract/r/gnoswap/v1/position/position.gno | 542 +++++ contract/r/gnoswap/v1/position/reposition.gno | 122 ++ contract/r/gnoswap/v1/position/type.gno | 147 ++ contract/r/gnoswap/v1/position/utils.gno | 225 ++ contract/r/gnoswap/v1/protocol_fee/README.md | 57 + contract/r/gnoswap/v1/protocol_fee/api.gno | 161 ++ contract/r/gnoswap/v1/protocol_fee/assert.gno | 46 + contract/r/gnoswap/v1/protocol_fee/consts.gno | 12 + contract/r/gnoswap/v1/protocol_fee/doc.gno | 6 + contract/r/gnoswap/v1/protocol_fee/errors.gno | 18 + contract/r/gnoswap/v1/protocol_fee/getter.gno | 99 + .../r/gnoswap/v1/protocol_fee/gnomod.toml | 2 + .../gnoswap/v1/protocol_fee/protocol_fee.gno | 231 ++ contract/r/gnoswap/v1/protocol_fee/state.gno | 208 ++ contract/r/gnoswap/v1/router/README.md | 115 + contract/r/gnoswap/v1/router/assert.gno | 19 + contract/r/gnoswap/v1/router/base.gno | 321 +++ contract/r/gnoswap/v1/router/consts.gno | 16 + contract/r/gnoswap/v1/router/doc.gno | 9 + contract/r/gnoswap/v1/router/errors.gno | 37 + contract/r/gnoswap/v1/router/exact_in.gno | 175 ++ contract/r/gnoswap/v1/router/exact_out.gno | 178 ++ contract/r/gnoswap/v1/router/gnomod.toml | 2 + .../r/gnoswap/v1/router/protocol_fee_swap.gno | 104 + contract/r/gnoswap/v1/router/router.gno | 282 +++ contract/r/gnoswap/v1/router/router_dry.gno | 250 +++ contract/r/gnoswap/v1/router/swap_inner.gno | 186 ++ contract/r/gnoswap/v1/router/swap_multi.gno | 259 +++ contract/r/gnoswap/v1/router/swap_single.gno | 43 + contract/r/gnoswap/v1/router/type.gno | 189 ++ contract/r/gnoswap/v1/router/utils.gno | 167 ++ contract/r/gnoswap/v1/router/wrap_unwrap.gno | 98 + contract/r/gnoswap/v1/staker/README.md | 182 ++ contract/r/gnoswap/v1/staker/api.gno | 310 +++ contract/r/gnoswap/v1/staker/assert.gno | 196 ++ .../staker/calculate_pool_position_reward.gno | 153 ++ contract/r/gnoswap/v1/staker/consts.gno | 13 + contract/r/gnoswap/v1/staker/counter.gno | 21 + contract/r/gnoswap/v1/staker/doc.gno | 9 + contract/r/gnoswap/v1/staker/errors.gno | 48 + .../v1/staker/external_deposit_fee.gno | 173 ++ .../gnoswap/v1/staker/external_incentive.gno | 224 ++ .../gnoswap/v1/staker/external_token_list.gno | 132 ++ contract/r/gnoswap/v1/staker/getter.gno | 410 ++++ contract/r/gnoswap/v1/staker/gnomod.toml | 2 + contract/r/gnoswap/v1/staker/incentive_id.gno | 21 + contract/r/gnoswap/v1/staker/json.gno | 276 +++ .../v1/staker/manage_pool_tier_and_warmup.gno | 173 ++ contract/r/gnoswap/v1/staker/mint_stake.gno | 93 + .../v1/staker/protocol_fee_unstaking.gno | 84 + contract/r/gnoswap/v1/staker/query.gno | 127 ++ .../gnoswap/v1/staker/reward_calculation.gno | 61 + .../staker/reward_calculation_incentives.gno | 223 ++ .../v1/staker/reward_calculation_pool.gno | 523 +++++ .../staker/reward_calculation_pool_tier.gno | 326 +++ .../v1/staker/reward_calculation_tick.gno | 364 ++++ .../v1/staker/reward_calculation_types.gno | 117 + .../v1/staker/reward_calculation_warmup.gno | 118 ++ contract/r/gnoswap/v1/staker/staker.gno | 789 +++++++ contract/r/gnoswap/v1/staker/type.gno | 204 ++ contract/r/gnoswap/v1/staker/utils.gno | 168 ++ contract/r/gnoswap/v1/staker/wrap_unwrap.gno | 82 + contract/r/volos/README.md | 37 + contract/r/volos/core/api.gno | 2 +- contract/r/volos/core/events.gno | 74 +- contract/r/volos/core/getter.gno | 43 +- contract/r/volos/core/json.gno | 2 +- contract/r/volos/core/oracle.gno | 20 +- contract/r/volos/core/periphery.gno | 35 +- contract/r/volos/core/types.gno | 8 +- contract/r/volos/core/utils.gno | 34 +- contract/r/volos/core/volos.gno | 24 +- contract/r/volos/gov/doc.gno | 20 + contract/r/volos/gov/gnomod.toml | 2 + contract/r/volos/gov/governance/api.gno | 2 +- contract/r/volos/gov/governance/errors.gno | 1 + contract/r/volos/gov/governance/events.gno | 4 +- .../r/volos/gov/governance/governance.gno | 15 + .../volos/gov/governance/governance_test.gno | 35 +- contract/r/volos/gov/governance/json.gno | 2 +- .../r/volos/gov/governance/members_test.gno | 4 +- contract/r/volos/gov/governance/proposal.gno | 8 +- .../r/volos/gov/governance/proposal_test.gno | 4 +- contract/r/volos/gov/governance/vote_test.gno | 4 +- contract/r/volos/gov/staker/events.gno | 2 +- contract/r/volos/gov/staker/staker.gno | 8 +- contract/r/volos/gov/staker/staker_test.gno | 4 +- contract/r/volos/gov/staker/utils.gno | 2 +- contract/r/volos/gov/vls/api.gno | 2 +- contract/r/volos/gov/vls/json.gno | 2 +- contract/r/volos/gov/vls/vls.gno | 4 +- contract/r/volos/gov/vls/vls_test.gno | 2 +- contract/r/volos/gov/xvls/api.gno | 2 +- contract/r/volos/gov/xvls/json.gno | 2 +- contract/r/volos/gov/xvls/xvls.gno | 4 +- contract/r/volos/gov/xvls/xvls_test.gno | 4 +- .../r/volos/mocks}/create_proposals.gno | 21 +- contract/r/volos/mocks/flash_borrower.gno | 2 +- contract/r/volos/render/home.gno | 8 +- contract/r/volos/render/market.gno | 8 +- contract/r/volos/render/user.gno | 2 +- contract/r/volos/render/utils.gno | 2 +- tests/gnoswap/_info.mk | 6 +- tests/gnoswap/test.mk | 185 +- tests/volos/_info.mk | 3 +- tests/volos/flashloan.gno | 2 +- tests/volos/gov_test.mk | 26 +- tests/volos/key_setup.mk | 16 - tests/volos/multi_ops_test.mk | 187 ++ tests/volos/test.mk | 115 +- 369 files changed, 48972 insertions(+), 490 deletions(-) delete mode 100644 contract/p/gnoswap/consts/consts.gno create mode 100644 contract/p/gnoswap/fuzz/fuzz.gno create mode 100644 contract/p/gnoswap/fuzz/fuzz_test.gno create mode 100644 contract/p/gnoswap/fuzz/gnomod.toml create mode 100644 contract/p/gnoswap/fuzz/seed.gno create mode 100644 contract/p/gnoswap/gnsmath/bit_math.gno create mode 100644 contract/p/gnoswap/gnsmath/bit_math_test.gno create mode 100644 contract/p/gnoswap/gnsmath/doc.gno create mode 100644 contract/p/gnoswap/gnsmath/errors.gno create mode 100644 contract/p/gnoswap/gnsmath/gnomod.toml create mode 100644 contract/p/gnoswap/gnsmath/sqrt_price_math.gno create mode 100644 contract/p/gnoswap/gnsmath/sqrt_price_math_test.gno create mode 100644 contract/p/gnoswap/gnsmath/swap_math.gno create mode 100644 contract/p/gnoswap/gnsmath/swap_math_test.gno create mode 100644 contract/p/gnoswap/int256/LICENSE create mode 100644 contract/p/gnoswap/int256/README.md create mode 100644 contract/p/gnoswap/int256/absolute.gno create mode 100644 contract/p/gnoswap/int256/absolute_test.gno create mode 100644 contract/p/gnoswap/int256/arithmetic.gno create mode 100644 contract/p/gnoswap/int256/arithmetic_test.gno create mode 100644 contract/p/gnoswap/int256/bitwise.gno create mode 100644 contract/p/gnoswap/int256/bitwise_test.gno create mode 100644 contract/p/gnoswap/int256/cmp.gno create mode 100644 contract/p/gnoswap/int256/cmp_test.gno create mode 100644 contract/p/gnoswap/int256/conversion.gno create mode 100644 contract/p/gnoswap/int256/conversion_test.gno create mode 100644 contract/p/gnoswap/int256/doc.gno create mode 100644 contract/p/gnoswap/int256/gnomod.toml create mode 100644 contract/p/gnoswap/int256/int256.gno create mode 100644 contract/p/gnoswap/int256/int256_test.gno create mode 100644 contract/p/gnoswap/rbac/README.md create mode 100644 contract/p/gnoswap/rbac/doc.gno create mode 100644 contract/p/gnoswap/rbac/errors.gno create mode 100644 contract/p/gnoswap/rbac/gnomod.toml create mode 100644 contract/p/gnoswap/rbac/ownable.gno create mode 100644 contract/p/gnoswap/rbac/ownable_test.gno create mode 100644 contract/p/gnoswap/rbac/rbac.gno create mode 100644 contract/p/gnoswap/rbac/rbac_test.gno create mode 100644 contract/p/gnoswap/rbac/role.gno create mode 100644 contract/p/gnoswap/rbac/types.gno create mode 100644 contract/p/gnoswap/uint256/LICENSE create mode 100644 contract/p/gnoswap/uint256/README.md create mode 100644 contract/p/gnoswap/uint256/_helper_test.gno create mode 100644 contract/p/gnoswap/uint256/arithmetic.gno create mode 100644 contract/p/gnoswap/uint256/arithmetic_test.gno create mode 100644 contract/p/gnoswap/uint256/bits_table.gno create mode 100644 contract/p/gnoswap/uint256/bitwise.gno create mode 100644 contract/p/gnoswap/uint256/bitwise_test.gno create mode 100644 contract/p/gnoswap/uint256/cmp.gno create mode 100644 contract/p/gnoswap/uint256/cmp_test.gno create mode 100644 contract/p/gnoswap/uint256/conversion.gno create mode 100644 contract/p/gnoswap/uint256/conversion_test.gno create mode 100644 contract/p/gnoswap/uint256/doc.gno create mode 100644 contract/p/gnoswap/uint256/error.gno create mode 100644 contract/p/gnoswap/uint256/fullmath.gno create mode 100644 contract/p/gnoswap/uint256/fullmath_test.gno create mode 100644 contract/p/gnoswap/uint256/gnomod.toml create mode 100644 contract/p/gnoswap/uint256/gs_pointer.gno create mode 100644 contract/p/gnoswap/uint256/mod.gno create mode 100644 contract/p/gnoswap/uint256/uint256.gno create mode 100644 contract/p/gnoswap/uint256/uint256_test.gno create mode 100644 contract/p/gnoswap/uint256/utils.gno create mode 100644 contract/r/gnoswap/access/README.md create mode 100644 contract/r/gnoswap/access/access.gno create mode 100644 contract/r/gnoswap/access/assert.gno create mode 100644 contract/r/gnoswap/access/consts.gno create mode 100644 contract/r/gnoswap/access/errors.gno create mode 100644 contract/r/gnoswap/access/gnomod.toml create mode 100644 contract/r/gnoswap/access/swap_whitelist.gno create mode 100644 contract/r/gnoswap/emission/README.md create mode 100644 contract/r/gnoswap/emission/assert.gno create mode 100644 contract/r/gnoswap/emission/distribution.gno create mode 100644 contract/r/gnoswap/emission/emission.gno create mode 100644 contract/r/gnoswap/emission/errors.gno create mode 100644 contract/r/gnoswap/emission/gnomod.toml create mode 100644 contract/r/gnoswap/emission/utils.gno create mode 100644 contract/r/gnoswap/gns/README.md create mode 100644 contract/r/gnoswap/gns/assert.gno create mode 100644 contract/r/gnoswap/gns/consts.gno create mode 100644 contract/r/gnoswap/gns/emission_state.gno create mode 100644 contract/r/gnoswap/gns/errors.gno create mode 100644 contract/r/gnoswap/gns/getter.gno create mode 100644 contract/r/gnoswap/gns/gnomod.toml create mode 100644 contract/r/gnoswap/gns/gns.gno create mode 100644 contract/r/gnoswap/gns/gns_emission.gno create mode 100644 contract/r/gnoswap/gns/halving.gno create mode 100644 contract/r/gnoswap/gns/utils.gno create mode 100644 contract/r/gnoswap/halt/README.md create mode 100644 contract/r/gnoswap/halt/assert.gno create mode 100644 contract/r/gnoswap/halt/config.gno create mode 100644 contract/r/gnoswap/halt/doc.gno create mode 100644 contract/r/gnoswap/halt/errors.gno create mode 100644 contract/r/gnoswap/halt/getters.gno create mode 100644 contract/r/gnoswap/halt/gnomod.toml create mode 100644 contract/r/gnoswap/halt/halt.gno create mode 100644 contract/r/gnoswap/halt/types.gno create mode 100644 contract/r/gnoswap/rbac/README.md create mode 100644 contract/r/gnoswap/rbac/assert.gno create mode 100644 contract/r/gnoswap/rbac/consts.gno create mode 100644 contract/r/gnoswap/rbac/emit.gno create mode 100644 contract/r/gnoswap/rbac/errors.gno create mode 100644 contract/r/gnoswap/rbac/gnomod.toml create mode 100644 contract/r/gnoswap/rbac/ownership.gno create mode 100644 contract/r/gnoswap/rbac/rbac.gno create mode 100644 contract/r/gnoswap/rbac/role.gno create mode 100644 contract/r/gnoswap/referral/README.md create mode 100644 contract/r/gnoswap/referral/doc.gno create mode 100644 contract/r/gnoswap/referral/errors.gno create mode 100644 contract/r/gnoswap/referral/global_keeper.gno create mode 100644 contract/r/gnoswap/referral/gnomod.toml create mode 100644 contract/r/gnoswap/referral/keeper.gno create mode 100644 contract/r/gnoswap/referral/referral.gno create mode 100644 contract/r/gnoswap/referral/type.gno create mode 100644 contract/r/gnoswap/referral/utils.gno rename contract/r/gnoswap/{v1 => }/test_token/bar/bar.gno (93%) create mode 100644 contract/r/gnoswap/test_token/bar/gnomod.toml create mode 100644 contract/r/gnoswap/test_token/baz/baz.gno create mode 100644 contract/r/gnoswap/test_token/baz/gnomod.toml create mode 100644 contract/r/gnoswap/test_token/foo/foo.gno create mode 100644 contract/r/gnoswap/test_token/foo/gnomod.toml create mode 100644 contract/r/gnoswap/test_token/obl/gnomod.toml create mode 100644 contract/r/gnoswap/test_token/obl/obl.gno create mode 100644 contract/r/gnoswap/test_token/qux/gnomod.toml create mode 100644 contract/r/gnoswap/test_token/qux/qux.gno create mode 100644 contract/r/gnoswap/test_token/usdc/gnomod.toml create mode 100644 contract/r/gnoswap/test_token/usdc/usdc.gno create mode 100644 contract/r/gnoswap/v1/common/consts.gno create mode 100644 contract/r/gnoswap/v1/common/doc.gno create mode 100644 contract/r/gnoswap/v1/common/errors.gno create mode 100644 contract/r/gnoswap/v1/common/gnomod.toml create mode 100644 contract/r/gnoswap/v1/common/grc20reg_helper.gno create mode 100644 contract/r/gnoswap/v1/common/liquidity_amounts.gno create mode 100644 contract/r/gnoswap/v1/common/tick_math.gno create mode 100644 contract/r/gnoswap/v1/community_pool/README.md create mode 100644 contract/r/gnoswap/v1/community_pool/community_pool.gno create mode 100644 contract/r/gnoswap/v1/community_pool/doc.gno create mode 100644 contract/r/gnoswap/v1/community_pool/errors.gno create mode 100644 contract/r/gnoswap/v1/community_pool/gnomod.toml create mode 100644 contract/r/gnoswap/v1/gnft/assert.gno create mode 100644 contract/r/gnoswap/v1/gnft/errors.gno create mode 100644 contract/r/gnoswap/v1/gnft/gnft.gno create mode 100644 contract/r/gnoswap/v1/gnft/gnomod.toml create mode 100644 contract/r/gnoswap/v1/gnft/svg_generator.gno create mode 100644 contract/r/gnoswap/v1/gnft/utils.gno create mode 100644 contract/r/gnoswap/v1/gov/README.md create mode 100644 contract/r/gnoswap/v1/gov/doc.gno create mode 100644 contract/r/gnoswap/v1/gov/gnomod.toml create mode 100644 contract/r/gnoswap/v1/gov/governance/api.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/assert.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/config.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/consts.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/counter.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/doc.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/errors.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/getter_proposal.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/getter_vote.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/gnomod.toml create mode 100644 contract/r/gnoswap/v1/gov/governance/governance_execute.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/governance_propose.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/governance_vote.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/parameter_registry.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/parameter_registry_handler.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/proposal.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_action_status.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_data.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_manager.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_schedule_status.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_status.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_vote_status.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/state.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/utils.gno create mode 100644 contract/r/gnoswap/v1/gov/governance/voting_info.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/api_delegation.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/api_staker.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/assert.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/consts.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/counter.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/delegation.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_history.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_mananger.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_record.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_snapshot.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_withdraw.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/doc.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/emission_reward_manager.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/emission_reward_state.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/errors.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/getter_delegation_snapshot.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/gnomod.toml create mode 100644 contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_manager.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_state.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/staker_delegate.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/staker_delegation_snapshot.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/staker_reward.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/state.gno create mode 100644 contract/r/gnoswap/v1/gov/staker/util.gno create mode 100644 contract/r/gnoswap/v1/gov/xgns/doc.gno create mode 100644 contract/r/gnoswap/v1/gov/xgns/errors.gno create mode 100644 contract/r/gnoswap/v1/gov/xgns/gnomod.toml create mode 100644 contract/r/gnoswap/v1/gov/xgns/xgns.gno create mode 100644 contract/r/gnoswap/v1/launchpad/README.md create mode 100644 contract/r/gnoswap/v1/launchpad/api_deposit.gno create mode 100644 contract/r/gnoswap/v1/launchpad/api_project.gno create mode 100644 contract/r/gnoswap/v1/launchpad/api_reward.gno create mode 100644 contract/r/gnoswap/v1/launchpad/assert.gno create mode 100644 contract/r/gnoswap/v1/launchpad/consts.gno create mode 100644 contract/r/gnoswap/v1/launchpad/counter.gno create mode 100644 contract/r/gnoswap/v1/launchpad/deposit.gno create mode 100644 contract/r/gnoswap/v1/launchpad/errors.gno create mode 100644 contract/r/gnoswap/v1/launchpad/gnomod.toml create mode 100644 contract/r/gnoswap/v1/launchpad/json_builder.gno create mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_deposit.gno create mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_project.gno create mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_protocol_fee.gno create mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_reward.gno create mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_withdraw.gno create mode 100644 contract/r/gnoswap/v1/launchpad/project.gno create mode 100644 contract/r/gnoswap/v1/launchpad/project_condition.gno create mode 100644 contract/r/gnoswap/v1/launchpad/project_tier.gno create mode 100644 contract/r/gnoswap/v1/launchpad/reward_manager.gno create mode 100644 contract/r/gnoswap/v1/launchpad/reward_state.gno create mode 100644 contract/r/gnoswap/v1/launchpad/state.gno create mode 100644 contract/r/gnoswap/v1/launchpad/utils.gno create mode 100644 contract/r/gnoswap/v1/pool/README.md create mode 100644 contract/r/gnoswap/v1/pool/api.gno create mode 100644 contract/r/gnoswap/v1/pool/assert.gno create mode 100644 contract/r/gnoswap/v1/pool/doc.gno create mode 100644 contract/r/gnoswap/v1/pool/errors.gno create mode 100644 contract/r/gnoswap/v1/pool/factory_param.gno create mode 100644 contract/r/gnoswap/v1/pool/getter.gno create mode 100644 contract/r/gnoswap/v1/pool/gnomod.toml create mode 100644 contract/r/gnoswap/v1/pool/json.gno create mode 100644 contract/r/gnoswap/v1/pool/liquidity_math.gno create mode 100644 contract/r/gnoswap/v1/pool/manager.gno create mode 100644 contract/r/gnoswap/v1/pool/pool.gno create mode 100644 contract/r/gnoswap/v1/pool/pool_type.gno create mode 100644 contract/r/gnoswap/v1/pool/position.gno create mode 100644 contract/r/gnoswap/v1/pool/protocol_fee.gno create mode 100644 contract/r/gnoswap/v1/pool/swap.gno create mode 100644 contract/r/gnoswap/v1/pool/tick.gno create mode 100644 contract/r/gnoswap/v1/pool/tick_bitmap.gno create mode 100644 contract/r/gnoswap/v1/pool/transfer.gno create mode 100644 contract/r/gnoswap/v1/pool/type.gno create mode 100644 contract/r/gnoswap/v1/pool/utils.gno create mode 100644 contract/r/gnoswap/v1/position/README.md create mode 100644 contract/r/gnoswap/v1/position/api.gno create mode 100644 contract/r/gnoswap/v1/position/assert.gno create mode 100644 contract/r/gnoswap/v1/position/burn.gno create mode 100644 contract/r/gnoswap/v1/position/doc.gno create mode 100644 contract/r/gnoswap/v1/position/errors.gno create mode 100644 contract/r/gnoswap/v1/position/getter.gno create mode 100644 contract/r/gnoswap/v1/position/gnomod.toml create mode 100644 contract/r/gnoswap/v1/position/json.gno create mode 100644 contract/r/gnoswap/v1/position/liquidity_management.gno create mode 100644 contract/r/gnoswap/v1/position/manager.gno create mode 100644 contract/r/gnoswap/v1/position/mint.gno create mode 100644 contract/r/gnoswap/v1/position/native_token.gno create mode 100644 contract/r/gnoswap/v1/position/position.gno create mode 100644 contract/r/gnoswap/v1/position/reposition.gno create mode 100644 contract/r/gnoswap/v1/position/type.gno create mode 100644 contract/r/gnoswap/v1/position/utils.gno create mode 100644 contract/r/gnoswap/v1/protocol_fee/README.md create mode 100644 contract/r/gnoswap/v1/protocol_fee/api.gno create mode 100644 contract/r/gnoswap/v1/protocol_fee/assert.gno create mode 100644 contract/r/gnoswap/v1/protocol_fee/consts.gno create mode 100644 contract/r/gnoswap/v1/protocol_fee/doc.gno create mode 100644 contract/r/gnoswap/v1/protocol_fee/errors.gno create mode 100644 contract/r/gnoswap/v1/protocol_fee/getter.gno create mode 100644 contract/r/gnoswap/v1/protocol_fee/gnomod.toml create mode 100644 contract/r/gnoswap/v1/protocol_fee/protocol_fee.gno create mode 100644 contract/r/gnoswap/v1/protocol_fee/state.gno create mode 100644 contract/r/gnoswap/v1/router/README.md create mode 100644 contract/r/gnoswap/v1/router/assert.gno create mode 100644 contract/r/gnoswap/v1/router/base.gno create mode 100644 contract/r/gnoswap/v1/router/consts.gno create mode 100644 contract/r/gnoswap/v1/router/doc.gno create mode 100644 contract/r/gnoswap/v1/router/errors.gno create mode 100644 contract/r/gnoswap/v1/router/exact_in.gno create mode 100644 contract/r/gnoswap/v1/router/exact_out.gno create mode 100644 contract/r/gnoswap/v1/router/gnomod.toml create mode 100644 contract/r/gnoswap/v1/router/protocol_fee_swap.gno create mode 100644 contract/r/gnoswap/v1/router/router.gno create mode 100644 contract/r/gnoswap/v1/router/router_dry.gno create mode 100644 contract/r/gnoswap/v1/router/swap_inner.gno create mode 100644 contract/r/gnoswap/v1/router/swap_multi.gno create mode 100644 contract/r/gnoswap/v1/router/swap_single.gno create mode 100644 contract/r/gnoswap/v1/router/type.gno create mode 100644 contract/r/gnoswap/v1/router/utils.gno create mode 100644 contract/r/gnoswap/v1/router/wrap_unwrap.gno create mode 100644 contract/r/gnoswap/v1/staker/README.md create mode 100644 contract/r/gnoswap/v1/staker/api.gno create mode 100644 contract/r/gnoswap/v1/staker/assert.gno create mode 100644 contract/r/gnoswap/v1/staker/calculate_pool_position_reward.gno create mode 100644 contract/r/gnoswap/v1/staker/consts.gno create mode 100644 contract/r/gnoswap/v1/staker/counter.gno create mode 100644 contract/r/gnoswap/v1/staker/doc.gno create mode 100644 contract/r/gnoswap/v1/staker/errors.gno create mode 100644 contract/r/gnoswap/v1/staker/external_deposit_fee.gno create mode 100644 contract/r/gnoswap/v1/staker/external_incentive.gno create mode 100644 contract/r/gnoswap/v1/staker/external_token_list.gno create mode 100644 contract/r/gnoswap/v1/staker/getter.gno create mode 100644 contract/r/gnoswap/v1/staker/gnomod.toml create mode 100644 contract/r/gnoswap/v1/staker/incentive_id.gno create mode 100644 contract/r/gnoswap/v1/staker/json.gno create mode 100644 contract/r/gnoswap/v1/staker/manage_pool_tier_and_warmup.gno create mode 100644 contract/r/gnoswap/v1/staker/mint_stake.gno create mode 100644 contract/r/gnoswap/v1/staker/protocol_fee_unstaking.gno create mode 100644 contract/r/gnoswap/v1/staker/query.gno create mode 100644 contract/r/gnoswap/v1/staker/reward_calculation.gno create mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_incentives.gno create mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_pool.gno create mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_pool_tier.gno create mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_tick.gno create mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_types.gno create mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_warmup.gno create mode 100644 contract/r/gnoswap/v1/staker/staker.gno create mode 100644 contract/r/gnoswap/v1/staker/type.gno create mode 100644 contract/r/gnoswap/v1/staker/utils.gno create mode 100644 contract/r/gnoswap/v1/staker/wrap_unwrap.gno create mode 100644 contract/r/volos/README.md create mode 100644 contract/r/volos/gov/doc.gno create mode 100644 contract/r/volos/gov/gnomod.toml rename {tests/volos => contract/r/volos/mocks}/create_proposals.gno (60%) delete mode 100644 tests/volos/key_setup.mk create mode 100644 tests/volos/multi_ops_test.mk diff --git a/compose.yaml b/compose.yaml index 9f8e012..01b800b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,7 +1,7 @@ services: gnodev: build: ./contract/ - command: gnodev -paths gno.land/r/volos/**,gno.land/r/gnoswap/** -node-rpc-listener 0.0.0.0:26657 -web-listener 0.0.0.0:8888 + command: gnodev -paths gno.land/r/volos/**,gno.land/r/gnoswap/** -deploy-key g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42 -node-rpc-listener 0.0.0.0:26657 -web-listener 0.0.0.0:8888 develop: watch: - action: sync diff --git a/contract/p/gnoswap/consts/consts.gno b/contract/p/gnoswap/consts/consts.gno deleted file mode 100644 index 9061e37..0000000 --- a/contract/p/gnoswap/consts/consts.gno +++ /dev/null @@ -1,143 +0,0 @@ -package consts - -import ( - "std" -) - -// INITIAL ADDRESSES -const ( - ADMIN std.Address = "g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42" - DEV_OPS std.Address = "g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42" -) - -// WRAP & UNWRAP -const ( - GNOT string = "gnot" - UGNOT string = "ugnot" - WRAPPED_WUGNOT string = "gno.land/r/demo/wugnot" - - // defined in https://github.com/gnolang/gno/blob/81a88a2976ba9f2f9127ebbe7fb7d1e1f7fa4bd4/examples/gno.land/r/demo/wugnot/wugnot.gno#L19 - UGNOT_MIN_DEPOSIT_TO_WRAP uint64 = 1000 -) - -// INITIAL CONTRACT PATH & ADDRESS -const ( - POOL_PATH string = "gno.land/r/gnoswap/v1/pool" - POOL_ADDR std.Address = "g148tjamj80yyrm309z7rk690an22thd2l3z8ank" - - POSITION_PATH string = "gno.land/r/gnoswap/v1/position" - POSITION_ADDR std.Address = "g1q646ctzhvn60v492x8ucvyqnrj2w30cwh6efk5" - - ROUTER_PATH string = "gno.land/r/gnoswap/v1/router" - ROUTER_ADDR std.Address = "g1lm2l7tf49h3mykesct7rhfml30yx8dw5xrval7" - - STAKER_PATH string = "gno.land/r/gnoswap/v1/staker" - STAKER_ADDR std.Address = "g1cceshmzzlmrh7rr3z30j2t5mrvsq9yccysw9nu" - - GNS_PATH string = "gno.land/r/gnoswap/v1/gns" - GNS_ADDR std.Address = "g1jgqwaa2le3yr63d533fj785qkjspumzv22ys5m" - - GNFT_PATH string = "gno.land/r/gnoswap/v1/gnft" - GNFT_ADDR std.Address = "g1wxv2rdfn53qc84nt3nn646f9yh3nly8lm7j89t" - - WUGNOT_PATH string = "gno.land/r/demo/wugnot" - WUGNOT_ADDR std.Address = "g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6" - - EMISSION_PATH string = "gno.land/r/gnoswap/v1/emission" - EMISSION_ADDR std.Address = "g10xg6559w9e93zfttlhvdmaaa0er3zewcr7nh20" - - PROTOCOL_FEE_PATH string = "gno.land/r/gnoswap/v1/protocol_fee" - PROTOCOL_FEE_ADDR std.Address = "g1f7wpek7q67tkns27sw495u5yuu3a5wwjxw5l6l" - - COMMUNITY_POOL_PATH string = "gno.land/r/gnoswap/v1/community_pool" - COMMUNITY_POOL_ADDR std.Address = "g100fnnlz5eh87p5hvwt8pf279lxaelm8k8md049" - - GOV_XGNS_PATH string = "gno.land/r/gnoswap/v1/gov/xgns" - GOV_XGNS_ADDR std.Address = "g1wwh55uwzlz2zzr2qcvvxf83qhcvmx2t8779l9r" - - GOV_STAKER_PATH string = "gno.land/r/gnoswap/v1/gov/staker" - GOV_STAKER_ADDR std.Address = "g17e3ykyqk9jmqe2y9wxe9zhep3p7cw56davjqwa" - - GOV_GOVERNANCE_PATH string = "gno.land/r/gnoswap/v1/gov/governance" - GOV_GOVERNANCE_ADDR std.Address = "g17s8w2ve7k85fwfnrk59lmlhthkjdted8whvqxd" - - COMMON_PATH string = "gno.land/r/gnoswap/v1/common" - COMMON_ADDR std.Address = "g14ytarn5u7h3xywygt8hzhs3m23frljz72ta9xk" - - LAUNCHPAD_PATH string = "gno.land/r/gnoswap/v1/launchpad" - LAUNCHPAD_ADDR std.Address = "g122mau2lp2rc0scs8d27pkkuys4w54mdy2tuer3" - - ACCESS_PATH string = "gno.land/r/gnoswap/v1/access" - ACCESS_ADDR std.Address = "g1yyw4t7pywpgpq6z2p745y05qejwur97xud4arw" - - HALT_PATH string = "gno.land/r/gnoswap/v1/halt" - HALT_ADDR std.Address = "g1q67vstyzqycl75yv7ern98n4u8qrgc8hxkmsxt" -) - -// NUMBER -const ( - // calculated by https://mathiasbynens.be/demo/integer-range - MAX_UINT8 string = "255" - UINT8_MAX uint8 = 255 - - MAX_UINT16 string = "65535" - UINT16_MAX uint16 = 65535 - - MAX_UINT32 string = "4294967295" - UINT32_MAX uint32 = 4294967295 - - MAX_UINT64 string = "18446744073709551615" - UINT64_MAX uint64 = 18446744073709551615 - - MAX_INT64 string = "9223372036854775807" - INT64_MAX int64 = 9223372036854775807 - - MAX_UINT128 string = "340282366920938463463374607431768211455" - MAX_UINT160 string = "1461501637330902918203684832716283019655932542975" - MAX_UINT256 string = "115792089237316195423570985008687907853269984665640564039457584007913129639935" - - MAX_INT128 string = "170141183460469231731687303715884105727" - MAX_INT256 string = "57896044618658097711785492504343953926634992332820282019728792003956564819967" - - // Tick Related - MIN_TICK int32 = -887272 - MAX_TICK int32 = 887272 - - MIN_SQRT_RATIO string = "4295128739" // same as TickMathGetSqrtRatioAtTick(MIN_TICK) - MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" // same as TickMathGetSqrtRatioAtTick(MAX_TICK) - - MIN_PRICE string = "4295128740" // MIN_SQRT_RATIO + 1 - MAX_PRICE string = "1461446703485210103287273052203988822378723970341" // MAX_SQRT_RATIO - 1 - - // ETC - Q64 string = "18446744073709551616" // 2 ** 64 - Q96 string = "79228162514264337593543950336" // 2 ** 96 - Q128 string = "340282366920938463463374607431768211456" // 2 ** 128 - - Q96_RESOLUTION uint = 96 - Q128_RESOLUTION uint = 128 - Q160_RESOLUTION uint = 160 -) - -// TIME -const ( - SECONDS_PER_DAY = 86400 -) - -// BLOCK TIME -const ( - // Default block generation interval, in milliseconds, used to estimate - // how many blocks will be produced over a given timeframe. - // GnoSwap’s emission logic relies on this as an initial value, - // but actual block times may vary (e.g. due to network conditions). - // Governance or an admin can adjust it dynamically by calling - // [SetAvgBlockTimeInMs](https://github.com/gnoswap-labs/gnoswap/blob/a29e0f994466430618548ae992cca11a52f5102a/contract/r/gnoswap/gns/halving.gno#L359) to keep emission schedules accurate. - BLOCK_GENERATION_INTERVAL int64 = 2000 - MILLISECONDS_PER_SECOND int64 = 1000 -) - -// ETCs -const ( - // REF: https://github.com/gnolang/gno/pull/2401#discussion_r1648064219 - ZERO_ADDRESS std.Address = "g100000000000000000000000000000000dnmcnx" -) diff --git a/contract/p/gnoswap/fuzz/fuzz.gno b/contract/p/gnoswap/fuzz/fuzz.gno new file mode 100644 index 0000000..be07f85 --- /dev/null +++ b/contract/p/gnoswap/fuzz/fuzz.gno @@ -0,0 +1,549 @@ +// Package fuzz provides property-based testing utilities for generating random test data. +// It supports various data types including integers, strings, and 256-bit numbers, +// with configurable ranges and boundary value testing. +package fuzz + +import ( + "errors" + "math" + "math/rand" + "strconv" + "unicode/utf8" + + xs "gno.land/p/wyhaines/rand/xorshift64star" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +const ( + MAX_UINT64 string = "18446744073709551615" + MAX_INT128 string = "170141183460469231731687303715884105727" + MAX_UINT128 string = "340282366920938463463374607431768211455" +) + +// Generator is the interface that wraps the basic Generate method. +// Implementations return randomly generated values of their specific type. +type Generator interface { + Generate() any +} + +// seedCounter ensures different seeds for each generator instance. +// We use a function-scoped approach to avoid realm modification issues. +var seedCounter uint64 + +// getNextSeed returns a new seed value for RNG initialization. +// It uses a counter with a prime multiplier to ensure uniqueness. +func getNextSeed() uint64 { + // Increment counter and combine with time for uniqueness + seedCounter++ + // Use a prime multiplier to spread values + return seedCounter * 2654435761 +} + +// initRNG initializes the random number generator if not already initialized. +func initRNG(rng **xs.Xorshift64Star) { + if *rng == nil { + // Use entropy-based seed to avoid realm issues + *rng = xs.New() + } +} + +// TODO: add more generators +var ( + _ Generator = (*IntGenerator)(nil) + _ Generator = (*Uint32Generator)(nil) + _ Generator = (*NumberStringGenerator)(nil) +) + +// IntGenerator generates random integers within a specified range. +// The zero value generates full int32 range values. +type IntGenerator struct { + min, max int + rng *xs.Xorshift64Star +} + +// Generate returns a random integer within the generator's range. +func (g *IntGenerator) Generate() any { + initRNG(&g.rng) + if g.min == 0 && g.max == 0 { + return int(int32(g.rng.Uint64())) + } + size := uint64(g.max - g.min + 1) + return int(g.rng.Uint64()%size) + g.min +} + +// IntRange creates an IntGenerator that produces values between min and max inclusive. +func IntRange(min, max int) Generator { + return &IntGenerator{ + min: min, + max: max, + rng: nil, // Will be initialized on first use + } +} + +// IntRangeWithSeed creates an IntGenerator with the specified seed for reproducible generation. +func IntRangeWithSeed(min, max int, seed uint64) Generator { + return &IntGenerator{ + min: min, + max: max, + rng: xs.New(seed), + } +} + +// Uint32Generator generates random uint32 values within a specified range. +// The zero value generates full uint32 range values. +type Uint32Generator struct { + min, max uint32 + rng *xs.Xorshift64Star +} + +// Generate returns a random uint32 within the generator's range. +func (g *Uint32Generator) Generate() any { + initRNG(&g.rng) + if g.min == 0 && g.max == 0 { + return uint32(g.rng.Uint64()) + } + size := uint64(g.max - g.min + 1) + return uint32(g.rng.Uint64()%size) + g.min +} + +// Uint32Range creates a Uint32Generator that produces values between min and max inclusive. +func Uint32Range(min, max uint32) Generator { + return &Uint32Generator{ + min: min, + max: max, + rng: nil, // Will be initialized on first use + } +} + +// Uint32RangeWithSeed creates a Uint32Generator with the specified seed for reproducible generation. +func Uint32RangeWithSeed(min, max uint32, seed uint64) Generator { + return &Uint32Generator{ + min: min, + max: max, + rng: xs.New(seed), + } +} + +// Uint32 creates a Uint32Generator that produces values between 0 and 100. +func Uint32() Generator { + return Uint32Range(0, 100) +} + +// BoolGenerator generates random boolean values. +type BoolGenerator struct { + rng *xs.Xorshift64Star +} + +// Generate returns a random boolean value. +func (g *BoolGenerator) Generate() any { + initRNG(&g.rng) + return g.rng.Uint64()%2 == 0 +} + +// Bool creates a BoolGenerator that produces random boolean values. +func Bool() Generator { + return &BoolGenerator{rng: nil} // Will be initialized on first use +} + +// BoolWithSeed creates a BoolGenerator with the specified seed for reproducible generation. +func BoolWithSeed(seed uint64) Generator { + return &BoolGenerator{rng: xs.New(seed)} +} + +// StringGenerator generates random ASCII strings within a specified length range. +type StringGenerator struct { + minLen, maxLen int + rng *xs.Xorshift64Star +} + +// Generate returns a random ASCII string within the generator's length range. +func (g *StringGenerator) Generate() interface{} { + initRNG(&g.rng) + if g.maxLen == 0 { + g.maxLen = 10 + } + lengthRange := uint64(g.maxLen - g.minLen + 1) + length := int(g.rng.Uint64()%lengthRange) + g.minLen + result := make([]byte, length) + for i := range result { + result[i] = byte(g.rng.Uint64()%94 + 33) + } + return string(result) +} + +// StringWithSeed creates a StringGenerator with the specified seed for reproducible generation. +func StringWithSeed(minLen, maxLen int, seed uint64) Generator { + return &StringGenerator{ + minLen: minLen, + maxLen: maxLen, + rng: xs.New(seed), + } +} + +// NumberStringGenerator generates numeric strings in various bases (binary, octal, decimal, hex). +type NumberStringGenerator struct { + min, max int64 + baseTypes []int + rng *xs.Xorshift64Star +} + +// Generate returns a numeric string in a random base format (binary, octal, decimal, or hexadecimal). +func (g *NumberStringGenerator) Generate() any { + initRNG(&g.rng) + + if g.min == 0 && g.max == 0 { + g.max = 1000000 + } + + size := uint64(g.max - g.min + 1) + value := int64(g.rng.Uint64()%size) + g.min + + if len(g.baseTypes) == 0 { + g.baseTypes = []int{2, 8, 10, 16} + } + + baseIndex := g.rng.Uint64() % uint64(len(g.baseTypes)) + base := g.baseTypes[baseIndex] + + switch base { + case 2: + return "0b" + strconv.FormatInt(value, 2) + case 8: + return "0o" + strconv.FormatInt(value, 8) + case 16: + return "0x" + strconv.FormatInt(value, 16) + default: + return strconv.FormatInt(value, 10) + } +} + +// NumberString creates a NumberStringGenerator with default range (0-1000000) and all base formats. +func NumberString() Generator { + return &NumberStringGenerator{ + min: 0, + max: 1000000, + baseTypes: []int{2, 8, 10, 16}, + rng: nil, // Will be initialized on first use + } +} + +// NumberStringRange creates a NumberStringGenerator with the specified range and base formats. +func NumberStringRange(min, max int64, bases ...int) Generator { + if len(bases) == 0 { + bases = []int{2, 8, 10, 16} + } + return &NumberStringGenerator{ + min: min, + max: max, + baseTypes: bases, + rng: nil, // Will be initialized on first use + } +} + +// Config controls fuzzing parameters for property-based testing. +type Config struct { + Iterations int + Shrink bool + Seed uint64 // Optional: if provided, uses this seed for reproducibility +} + +// Result contains the outcome of a fuzzing run. +type Result struct { + Failed bool + FailingInput any + Iterations int +} + +// abs returns the absolute value of x. +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +// shrinkInt attempts to find a simpler failing input through iterative shrinking. +func shrinkInt(property func(int) bool, failingInput int) int { + current := failingInput + for attempt := 0; attempt < 100; attempt++ { + candidates := []int{0, 1, -1, current / 2, current - 1, current + 1} + for _, candidate := range candidates { + if !property(candidate) && abs(candidate) <= abs(current) { + current = candidate + } + } + } + return current +} + +// GenerateBoundary returns boundary values for the specified type. +// Supported types: "int", "uint", "string", "numberString", "slice". +func GenerateBoundary(vt string) []any { + switch vt { + case "int": + return []any{ + 0, 1, -1, + int(math.MaxInt), int(math.MinInt), + int(math.MaxInt - 1), int(math.MinInt + 1), + } + case "uint": + return []any{ + uint(0), uint(1), + uint(math.MaxUint), uint(math.MaxUint - 1), + } + case "string": + return []any{ + "", " ", "\n", "\t", + "a", "A", "0", + generateLongString(100), + generateUnicodeString(), + } + case "numberString": + return []any{ + "0", "1", "-1", + "0b0", "0b1", "0b1111111111111111", + "0o0", "0o7", "0o777", + "0x0", "0x1", "0xFFFF", "0xffffffff", + "9223372036854775807", "-9223372036854775808", + } + case "slice": + return []any{ + []int{}, + generateLargeSlice(10000), + } + } + return nil +} + +// generateLongString creates a string of 'a' characters with the specified length. +func generateLongString(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = 'a' + } + return string(b) +} + +// generateUnicodeString creates a string containing various Unicode characters including Greek, CJK, and emoji. +func generateUnicodeString() string { + runes := []rune{'α', 'β', 'γ', '漢', '字', '🌟'} + result := "" + for _, r := range runes { + if utf8.ValidRune(r) { + result += string(r) + } + } + return result +} + +// generateLargeSlice creates a slice of n random integers between 0 and 99. +func generateLargeSlice(n int) []int { + slice := make([]int, n) + for i := 0; i < n; i++ { + slice[i] = rand.IntN(100) + } + return slice +} + +// Uint256Generator generates random uint256 values for testing. +type Uint256Generator struct { + allowZero bool + maxBits uint + seedMgr *SeedManager +} + +// NewUint256Generator creates a new Uint256Generator with the specified zero-allowance and bit constraints. +func NewUint256Generator(allowZero bool, maxBits uint) *Uint256Generator { + return &Uint256Generator{ + allowZero: allowZero, + maxBits: maxBits, + seedMgr: NewSeedManager(), + } +} + +// Generate returns a random uint256 value according to the generator's constraints. +// Returns special values (zero, one, MAX_UINT128, MAX_INT128, MAX_UINT64) or random values within maxBits. +func (g *Uint256Generator) Generate() any { + if g.seedMgr == nil { + g.seedMgr = NewSeedManager() + } + + // Generate special cases + specialCase := g.seedMgr.CreateIntGenerator(0, 10).Generate().(int) + switch specialCase { + case 0: + if g.allowZero { + return u256.Zero() + } + case 1: + return u256.One() + case 2: + // MAX_UINT128 + return u256.MustFromDecimal(MAX_UINT128) + case 3: + val := u256.MustFromDecimal(MAX_UINT128) + return new(u256.Uint).Add(val, u256.One()) + case 4: + return u256.MustFromDecimal(MAX_INT128) + case 5: + return u256.MustFromDecimal(MAX_UINT64) + } + + // random value within maxBits + bits := g.seedMgr.CreateIntGenerator(1, int(g.maxBits)).Generate().(int) + val := u256.One() + val = new(u256.Uint).Lsh(val, uint(bits)) + + offset := g.seedMgr.CreateIntGenerator(0, 1000).Generate().(int) + val = new(u256.Uint).Add(val, u256.NewUint(uint64(offset))) + + return val +} + +// Int256Generator generates random int256 values for testing. +type Int256Generator struct { + allowZero bool + allowNegative bool + maxBits uint + seedMgr *SeedManager +} + +// NewInt256Generator creates a new Int256Generator with the specified zero, negative, and bit constraints. +func NewInt256Generator(allowZero bool, allowNegative bool, maxBits uint) *Int256Generator { + return &Int256Generator{ + allowZero: allowZero, + allowNegative: allowNegative, + maxBits: maxBits, + seedMgr: NewSeedManager(), + } +} + +// Generate returns a random int256 value according to the generator's constraints. +// Returns special values or random positive/negative values within maxBits range. +func (g *Int256Generator) Generate() any { + if g.seedMgr == nil { + g.seedMgr = NewSeedManager() + } + + specialCase := g.seedMgr.CreateIntGenerator(0, 10).Generate().(int) + switch specialCase { + case 0: + if g.allowZero { + return i256.Zero() + } + case 1: + return i256.One() + case 2: + return i256.MustFromDecimal(MAX_INT128) + case 3: + val := i256.MustFromDecimal(MAX_INT128) + return new(i256.Int).Add(val, i256.One()) + case 4: + if g.allowNegative { + val := i256.MustFromDecimal(MAX_INT128) + return i256.Zero().Neg(val) + } + } + + bits := g.seedMgr.CreateIntGenerator(1, int(g.maxBits)).Generate().(int) + uval := u256.One() + uval = new(u256.Uint).Lsh(uval, uint(bits)) + + val := i256.FromUint256(uval) + + // Randomly make negative + if g.allowNegative && g.seedMgr.CreateBoolGenerator().Generate().(bool) { + val = i256.Zero().Neg(val) + } + + return val +} + +// FuzzWithConfig runs property-based testing with the specified configuration. +// Note: config.Seed is ignored as entropy-based seeding is used. +func FuzzWithConfig(config Config, property func(int) bool) *Result { + // Note: config.Seed is ignored as we use entropy-based seeding + return FuzzWithConfigAndGen(config, &IntGenerator{rng: nil}, property) +} + +// FuzzWithConfigAndGen runs property-based testing with a custom generator and configuration. +// Returns a Result containing test outcome, failing input (if any), and iteration count. +func FuzzWithConfigAndGen(config Config, gen Generator, property func(int) bool) *Result { + result := &Result{} + iterations := config.Iterations + if iterations == 0 { + iterations = 100 + } + var failingInput int + for i := 0; i < iterations; i++ { + result.Iterations++ + value := gen.Generate().(int) + if !property(value) { + failingInput = value + result.Failed = true + break + } + } + if !result.Failed { + return result + } + if config.Shrink { + result.FailingInput = shrinkInt(property, failingInput) + } else { + result.FailingInput = failingInput + } + return result +} + +// Fuzz runs property-based testing on the given property function. +// Supports func(int) bool and func(string) bool property types. Returns error on property violation. +func Fuzz(property interface{}) error { + if fn, ok := property.(func(int) bool); ok { + return FuzzWithGen(IntRange(0, 1000), fn) + } + if fn, ok := property.(func(string) bool); ok { + return fuzzString(fn) + } + return errors.New("unsupported property type") +} + +// fuzzString runs property-based testing for string properties with 100 iterations. +func fuzzString(fn func(string) bool) error { + gen := &StringGenerator{rng: nil} // Will be initialized on first use + for i := 0; i < 100; i++ { + if !fn(gen.Generate().(string)) { + return errors.New("property violation") + } + } + return nil +} + +// FuzzWithGen runs property-based testing using a custom generator with 100 iterations. +func FuzzWithGen(gen Generator, property func(int) bool) error { + for i := 0; i < 100; i++ { + value := gen.Generate().(int) + if !property(value) { + return errors.New("property violation") + } + } + return nil +} + +// FuzzN runs property-based testing with multiple parameters. +// Currently supports func(int, int) bool property type with 100 iterations per test. +func FuzzN(property interface{}) error { + if fn, ok := property.(func(int, int) bool); ok { + gen := &IntGenerator{rng: nil} // Will be initialized on first use + for i := 0; i < 100; i++ { + a := gen.Generate().(int) + b := gen.Generate().(int) + if !fn(a, b) { + return errors.New("property violation") + } + } + return nil + } + return errors.New("unsupported property type") +} diff --git a/contract/p/gnoswap/fuzz/fuzz_test.gno b/contract/p/gnoswap/fuzz/fuzz_test.gno new file mode 100644 index 0000000..fde0a96 --- /dev/null +++ b/contract/p/gnoswap/fuzz/fuzz_test.gno @@ -0,0 +1,320 @@ +package fuzz + +import ( + "math" + "strconv" + "strings" + "testing" +) + +type TestCase struct { + orderId int + amount uint32 + express bool + notes string +} + +func generateTestCase() TestCase { + return TestCase{ + orderId: IntRange(-10, 1000).Generate().(int), + amount: Uint32Range(0, 500).Generate().(uint32), + express: Bool().Generate().(bool), + notes: (&StringGenerator{minLen: 0, maxLen: 50}).Generate().(string), + } +} + +func validateOrder(orderId int, amount uint32, express bool, notes string) bool { + if orderId <= 0 { + return false + } + if amount == 0 { + return false + } + if express && amount < 100 { + return false + } + if len(notes) > 1000 { + return false + } + return true +} + +func TestComplexPropertyWithWrapper(t *testing.T) { + const total = 100 + failures := 0 + + for i := 0; i < total; i++ { + tc := generateTestCase() + result := validateOrder(tc.orderId, tc.amount, tc.express, tc.notes) + + if tc.orderId <= 0 && result { + t.Errorf("Should fail for non-positive orderId: %+v", tc) + failures++ + } + if tc.amount == 0 && result { + t.Errorf("Should fail for zero amount: %+v", tc) + failures++ + } + if tc.express && tc.amount < 100 && result { + t.Errorf("Should fail for express with low amount: %+v", tc) + failures++ + } + } + + if failures > 0 { + t.Logf("Total failures: %d out of %d", failures, total) + } +} + +func TestFuzzSingleIntParameter(t *testing.T) { + double := func(x int) int { + return x * 2 + } + + err := Fuzz(func(x int) bool { + return double(x) == x*2 + }) + if err != nil { + t.Errorf("property failed when it shouldn't: %v", err) + } +} + +func TestShouldDetectsViolation(t *testing.T) { + failures := func(x int) int { + if x < -100 { + return x // <- bug: negative result + } + return abs(x) + } + + err := FuzzWithGen( + IntRange(-200, 200), + func(x int) bool { + return failures(x) >= 0 + }, + ) + + if err == nil { + t.Errorf("Expected property violation but none was detected") + } +} + +func TestFuzzWithMultipleParameters(t *testing.T) { + add := func(a, b int) int { + return a + b + } + + err := FuzzN(func(a, b int) bool { + return add(a, b) == add(b, a) // should be commutative + }) + if err != nil { + t.Errorf("property failed when it shouldn't: %v", err) + } +} + +func TestFuzzStringParameter(t *testing.T) { + strLen := func(s string) int { + return len(s) + } + + err := Fuzz(func(s string) bool { + return strLen(s) >= 0 + }) + if err != nil { + t.Errorf("property failed when it shouldn't: %v", err) + } +} + +func TestCustomGenerator(t *testing.T) { + isPositive := func(x int) bool { + return x > 0 + } + + err := FuzzWithGen( + IntRange(1, 1000), + func(x int) bool { + return isPositive(x) + }, + ) + if err != nil { + t.Errorf("property failed when it shouldn't: %v", err) + } +} + +func TestShrinking(t *testing.T) { + failure := func(x int) bool { + return x < 100 + } + + result := FuzzWithConfigAndGen( + Config{Shrink: true, Iterations: 100}, + IntRange(0, 200), + failure, + ) + + if !result.Failed { + t.Errorf("Expected property violation but none was detected") + } + if result.FailingInput != 100 { + t.Errorf("Expected failing input to be 100 but got %v", result.FailingInput) + } +} + +func TestGenerateBoundaryValues_Int(t *testing.T) { + values := GenerateBoundary("int") + expected := map[any]bool{ + 0: true, 1: true, -1: true, + int(math.MaxInt): true, int(math.MinInt): true, + int(math.MaxInt - 1): true, int(math.MinInt + 1): true, + } + + for _, v := range values { + if _, ok := expected[v]; !ok { + t.Errorf("Unexpected value: %v", v) + } + delete(expected, v) + } + for missing := range expected { + t.Errorf("Missing value: %v", missing) + } +} + +func TestGenerateBoundaryValues_Uint(t *testing.T) { + values := GenerateBoundary("uint") + expected := map[any]bool{ + uint(0): true, uint(1): true, + uint(math.MaxUint): true, uint(math.MaxUint - 1): true, + } + + for _, v := range values { + if _, ok := expected[v]; !ok { + t.Errorf("Unexpected value: %v", v) + } + delete(expected, v) + } + for missing := range expected { + t.Errorf("Missing value: %v", missing) + } +} + +func TestGenerateBoundaryValues_String(t *testing.T) { + values := GenerateBoundary("string") + if len(values) == 0 { + t.Errorf("Expected some values but got none") + } + for _, v := range values { + if _, ok := v.(string); !ok { + t.Errorf("Expected string value but got %T", v) + } + } +} + +func TestGenerateBoundaryValues_Slice(t *testing.T) { + values := GenerateBoundary("slice") + if len(values) == 0 { + t.Errorf("Expected some values but got none") + } + for _, v := range values { + if _, ok := v.([]int); !ok { + t.Errorf("Expected []int value but got %T", v) + } + } +} + +func TestGenerateBoundaryValues_UnknownType(t *testing.T) { + values := GenerateBoundary("invalid-type") + if len(values) != 0 { + t.Errorf("Expected no values but got %v", values) + } +} + +func TestNumberStringGenerator(t *testing.T) { + gen := NumberString() + + for i := 0; i < 100; i++ { + value := gen.Generate().(string) + + if strings.HasPrefix(value, "0b") { + binary := strings.TrimPrefix(value, "0b") + if _, err := strconv.ParseInt(binary, 2, 64); err != nil { + t.Errorf("Invalid binary string: %s", value) + } + } else if strings.HasPrefix(value, "0o") { + octal := strings.TrimPrefix(value, "0o") + if _, err := strconv.ParseInt(octal, 8, 64); err != nil { + t.Errorf("Invalid octal string: %s", value) + } + } else if strings.HasPrefix(value, "0x") { + hex := strings.TrimPrefix(value, "0x") + if _, err := strconv.ParseInt(hex, 16, 64); err != nil { + t.Errorf("Invalid hex string: %s", value) + } + } else { + if _, err := strconv.ParseInt(value, 10, 64); err != nil { + t.Errorf("Invalid decimal string: %s", value) + } + } + } +} + +func TestNumberStringGeneratorWithRange(t *testing.T) { + gen := NumberStringRange(-100, 100, 10, 16) + + for i := 0; i < 50; i++ { + value := gen.Generate().(string) + + var parsed int64 + var err error + + if strings.HasPrefix(value, "0x") { + hex := strings.TrimPrefix(value, "0x") + parsed, err = strconv.ParseInt(hex, 16, 64) + } else { + parsed, err = strconv.ParseInt(value, 10, 64) + } + + if err != nil { + t.Errorf("Failed to parse number string: %s", value) + continue + } + + if parsed < -100 || parsed > 100 { + t.Errorf("Value out of range: %d from %s", parsed, value) + } + } +} + +func TestGenerateBoundaryValues_NumberString(t *testing.T) { + values := GenerateBoundary("numberString") + + if len(values) == 0 { + t.Errorf("Expected boundary values for numberString") + return + } + + expectedPatterns := []string{ + "0", "1", "-1", + "0b", "0o", "0x", + } + + foundPatterns := make(map[string]bool) + for _, v := range values { + s, ok := v.(string) + if !ok { + t.Errorf("Expected string value but got %T", v) + continue + } + + for _, pattern := range expectedPatterns { + if strings.HasPrefix(s, pattern) || s == pattern { + foundPatterns[pattern] = true + } + } + } + + for _, pattern := range expectedPatterns[:3] { + if !foundPatterns[pattern] { + t.Errorf("Missing boundary value pattern: %s", pattern) + } + } +} diff --git a/contract/p/gnoswap/fuzz/gnomod.toml b/contract/p/gnoswap/fuzz/gnomod.toml new file mode 100644 index 0000000..a9b6adf --- /dev/null +++ b/contract/p/gnoswap/fuzz/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/p/gnoswap/fuzz" +gno = "0.9" diff --git a/contract/p/gnoswap/fuzz/seed.gno b/contract/p/gnoswap/fuzz/seed.gno new file mode 100644 index 0000000..23f3db9 --- /dev/null +++ b/contract/p/gnoswap/fuzz/seed.gno @@ -0,0 +1,55 @@ +package fuzz + +import ( + "time" +) + +// SeedManager manages unique seed generation for fuzz testing. +// Each instance maintains its own seed sequence, avoiding global state issues +// and ensuring diverse random sequences across different generators. +type SeedManager struct { + baseSeed uint64 + counter uint64 +} + +// NewSeedManager creates a new seed manager with time-based initial seed. +func NewSeedManager() *SeedManager { + return &SeedManager{ + baseSeed: uint64(time.Now().UnixNano()), + counter: 0, + } +} + +// NewSeedManagerWithSeed creates a seed manager with the specified base seed for reproducible tests. +func NewSeedManagerWithSeed(seed uint64) *SeedManager { + return &SeedManager{ + baseSeed: seed, + counter: 0, + } +} + +// NextSeed returns the next unique seed value using prime multiplier 2654435761 for good distribution. +func (sm *SeedManager) NextSeed() uint64 { + sm.counter++ + return sm.baseSeed + sm.counter*2654435761 +} + +// CreateIntGenerator creates an IntGenerator with a unique seed. +func (sm *SeedManager) CreateIntGenerator(min, max int) Generator { + return IntRangeWithSeed(min, max, sm.NextSeed()) +} + +// CreateUint32Generator creates a Uint32Generator with a unique seed. +func (sm *SeedManager) CreateUint32Generator(min, max uint32) Generator { + return Uint32RangeWithSeed(min, max, sm.NextSeed()) +} + +// CreateBoolGenerator creates a BoolGenerator with a unique seed. +func (sm *SeedManager) CreateBoolGenerator() Generator { + return BoolWithSeed(sm.NextSeed()) +} + +// CreateStringGenerator creates a StringGenerator with a unique seed. +func (sm *SeedManager) CreateStringGenerator(minLen, maxLen int) Generator { + return StringWithSeed(minLen, maxLen, sm.NextSeed()) +} diff --git a/contract/p/gnoswap/gnsmath/bit_math.gno b/contract/p/gnoswap/gnsmath/bit_math.gno new file mode 100644 index 0000000..a122f9e --- /dev/null +++ b/contract/p/gnoswap/gnsmath/bit_math.gno @@ -0,0 +1,80 @@ +package gnsmath + +import ( + u256 "gno.land/p/gnoswap/uint256" +) + +var ( + msbShifts = []bitShift{ + {u256.Zero().Lsh(u256.One(), 128), 128}, // 2^128 + {u256.Zero().Lsh(u256.One(), 64), 64}, // 2^64 + {u256.Zero().Lsh(u256.One(), 32), 32}, // 2^32 + {u256.Zero().Lsh(u256.One(), 16), 16}, // 2^16 + {u256.Zero().Lsh(u256.One(), 8), 8}, // 2^8 + {u256.Zero().Lsh(u256.One(), 4), 4}, // 2^4 + {u256.Zero().Lsh(u256.One(), 2), 2}, // 2^2 + {u256.Zero().Lsh(u256.One(), 1), 1}, // 2^1 + } + + lsbShifts = []bitShift{ + {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 128), u256.One()), 128}, // 2^128 - 1 + {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 64), u256.One()), 64}, // 2^64 - 1 + {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 32), u256.One()), 32}, // 2^32 - 1 + {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 16), u256.One()), 16}, // 2^16 - 1 + {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 8), u256.One()), 8}, // 2^8 - 1 + {u256.NewUint(0xf), 4}, // 2^4 - 1 = 15 + {u256.NewUint(0x3), 2}, // 2^2 - 1 = 3 + {u256.NewUint(0x1), 1}, // 2^1 - 1 = 1 + } +) + +// bitShift represents a bit pattern and corresponding shift amount for bit manipulation. +type bitShift struct { + bitPattern *u256.Uint + shift uint +} + +// BitMathMostSignificantBit returns the 0-based position of the most significant bit in x. +// This function is essential for AMM calculations involving price ranges and tick boundaries. +// Panics if x is zero. +func BitMathMostSignificantBit(x *u256.Uint) uint8 { + if x.IsZero() { + panic(errMSBZeroInput) + } + + temp := x.Clone() + r := uint8(0) + + for _, s := range msbShifts { + if temp.Gte(s.bitPattern) { + temp = temp.Rsh(temp, s.shift) + r += uint8(s.shift) + } + } + + return r +} + +// BitMathLeastSignificantBit returns the 0-based position of the least significant bit in x. +// This function is used in AMM calculations for efficient bit manipulation and range queries. +// Panics if x is zero. +func BitMathLeastSignificantBit(x *u256.Uint) uint8 { + if x.IsZero() { + panic(errLSBZeroInput) + } + + temp := x.Clone() + hasSetBits := u256.Zero() + r := uint8(255) + + for _, s := range lsbShifts { + hasSetBits = hasSetBits.And(temp, s.bitPattern) + if !hasSetBits.IsZero() { + r -= uint8(s.shift) + } else { + temp = temp.Rsh(temp, s.shift) + } + } + + return r +} diff --git a/contract/p/gnoswap/gnsmath/bit_math_test.gno b/contract/p/gnoswap/gnsmath/bit_math_test.gno new file mode 100644 index 0000000..c4d00ef --- /dev/null +++ b/contract/p/gnoswap/gnsmath/bit_math_test.gno @@ -0,0 +1,855 @@ +package gnsmath + +import ( + "testing" + + "gno.land/p/nt/uassert" + u256 "gno.land/p/gnoswap/uint256" +) + +func TestBitMathMostSignificantBit(t *testing.T) { + tests := []struct { + name string + input string + expected uint8 + shouldPanic bool + }{ + { + name: "zero_panics", + input: "0", + shouldPanic: true, + }, + + // Basic cases + { + name: "one", + input: "1", + expected: 0, + }, + { + name: "two", + input: "2", + expected: 1, + }, + + // Small numbers + { + name: "three", + input: "3", + expected: 1, + }, + { + name: "four", + input: "4", + expected: 2, + }, + { + name: "five", + input: "5", + expected: 2, + }, + { + name: "255", + input: "255", + expected: 7, + }, + { + name: "256", + input: "256", + expected: 8, + }, + { + name: "257", + input: "257", + expected: 8, + }, + + // Boundary values for each shift level + { + name: "2^16-1", + input: "65535", + expected: 15, + }, + { + name: "2^16", + input: "65536", + expected: 16, + }, + { + name: "2^16+1", + input: "65537", + expected: 16, + }, + { + name: "2^32-1", + input: "4294967295", + expected: 31, + }, + { + name: "2^32", + input: "4294967296", + expected: 32, + }, + { + name: "2^32+1", + input: "4294967297", + expected: 32, + }, + { + name: "2^64-1", + input: "18446744073709551615", + expected: 63, + }, + { + name: "2^64", + input: "18446744073709551616", + expected: 64, + }, + { + name: "2^64+1", + input: "18446744073709551617", + expected: 64, + }, + { + name: "2^128-1", + input: "340282366920938463463374607431768211455", + expected: 127, + }, + { + name: "2^128", + input: "340282366920938463463374607431768211456", + expected: 128, + }, + { + name: "2^128+1", + input: "340282366920938463463374607431768211457", + expected: 128, + }, + + // Large numbers + { + name: "2^255", + input: "57896044618658097711785492504343953926634992332820282019728792003956564819968", + expected: 255, + }, + { + name: "max_uint256", + input: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + expected: 255, + }, + + // Special bit patterns + { + name: "alternating_bits_0x5555", + input: "21845", // 0x5555 + expected: 14, + }, + { + name: "alternating_bits_0xAAAA", + input: "43690", // 0xAAAA + expected: 15, + }, + { + name: "all_ones_32bit", + input: "4294967295", // 0xFFFFFFFF + expected: 31, + }, + { + name: "high_bit_only_64", + input: "9223372036854775808", // 0x8000000000000000 + expected: 63, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.shouldPanic { + uassert.PanicsWithMessage(t, errMSBZeroInput.Error(), func() { + BitMathMostSignificantBit(u256.MustFromDecimal(tc.input)) + }) + return + } + + x := u256.MustFromDecimal(tc.input) + result := BitMathMostSignificantBit(x) + uassert.Equal(t, result, tc.expected) + }) + } +} + +func TestBitMathMostSignificantBit_PowersOfTwo(t *testing.T) { + // Test all powers of 2 from 2^0 to 2^255 + for i := 0; i < 256; i++ { + num := new(u256.Uint).Lsh(u256.One(), uint(i)) + result := BitMathMostSignificantBit(num) + uassert.Equal(t, result, uint8(i)) + } +} + +func TestBitMathLeastSignificantBit(t *testing.T) { + tests := []struct { + name string + input string + expected uint8 + shouldPanic bool + }{ + { + name: "zero_panics", + input: "0", + shouldPanic: true, + }, + + // Basic cases + { + name: "one", + input: "1", + expected: 0, + }, + { + name: "two", + input: "2", + expected: 1, + }, + + // Odd numbers (LSB = 0) + { + name: "three", + input: "3", + expected: 0, + }, + { + name: "five", + input: "5", + expected: 0, + }, + { + name: "255", + input: "255", + expected: 0, + }, + { + name: "max_uint256_odd", + input: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + expected: 0, + }, + + // Even numbers + { + name: "four", + input: "4", + expected: 2, + }, + { + name: "six", + input: "6", + expected: 1, + }, + { + name: "eight", + input: "8", + expected: 3, + }, + { + name: "256", + input: "256", + expected: 8, + }, + { + name: "1024", + input: "1024", + expected: 10, + }, + + // Powers of 2 boundaries + { + name: "2^16", + input: "65536", + expected: 16, + }, + { + name: "2^32", + input: "4294967296", + expected: 32, + }, + { + name: "2^64", + input: "18446744073709551616", + expected: 64, + }, + { + name: "2^128", + input: "340282366920938463463374607431768211456", + expected: 128, + }, + { + name: "2^255", + input: "57896044618658097711785492504343953926634992332820282019728792003956564819968", + expected: 255, + }, + + // Special patterns + { + name: "0xF0", + input: "240", + expected: 4, + }, + { + name: "0xFF00", + input: "65280", + expected: 8, + }, + { + name: "0xFFFF0000", + input: "4294901760", + expected: 16, + }, + + // Multiple bits with different LSB + { + name: "12", // 0b1100 + input: "12", + expected: 2, + }, + { + name: "96", // 0b1100000 + input: "96", + expected: 5, + }, + { + name: "192", // 0b11000000 + input: "192", + expected: 6, + }, + + // Large numbers with low LSB + { + name: "2^255+2^10", + input: "57896044618658097711785492504343953926634992332820282019728792003956564820992", + expected: 10, + }, + { + name: "2^200+2^20", + input: "1606938044258990275541962092341162602522202993782792835302425600", + expected: 10, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if tc.shouldPanic { + uassert.PanicsWithMessage(t, errLSBZeroInput.Error(), func() { + BitMathLeastSignificantBit(u256.MustFromDecimal(tc.input)) + }) + return + } + + x := u256.MustFromDecimal(tc.input) + result := BitMathLeastSignificantBit(x) + uassert.Equal(t, result, tc.expected) + }) + } +} + +func TestBitMathLeastSignificantBit_PowersOfTwo(t *testing.T) { + // Test all powers of 2 from 2^0 to 2^255 + for i := 0; i < 256; i++ { + num := new(u256.Uint).Lsh(u256.One(), uint(i)) + result := BitMathLeastSignificantBit(num) + uassert.Equal(t, result, uint8(i)) + } +} + +func TestBitMath_Consistency(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"small_number", "42"}, + {"medium_number", "1234567890"}, + {"large_number", "123456789012345678901234567890123456789"}, + {"half_max", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, + {"near_max", "115792089237316195423570985008687907853269984665640564039457584007913129639934"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := u256.MustFromDecimal(tc.input) + msb := BitMathMostSignificantBit(x) + lsb := BitMathLeastSignificantBit(x) + + // MSB must be >= LSB + if msb < lsb { + t.Errorf("Invalid result: MSB(%d) < LSB(%d) for %s", msb, lsb, tc.input) + } + }) + } +} + +func TestBitMath_SingleBitConsistency(t *testing.T) { + // For single bit numbers, MSB == LSB + for i := 0; i < 256; i++ { + x := new(u256.Uint).Lsh(u256.One(), uint(i)) + msb := BitMathMostSignificantBit(x) + lsb := BitMathLeastSignificantBit(x) + + if msb != lsb || msb != uint8(i) { + t.Errorf("Single bit 2^%d: MSB=%d, LSB=%d, expected both=%d", i, msb, lsb, i) + } + } +} + +// Additional test cases to add to the existing test file + +func TestBitMathLeastSignificantBit_MaxUint256MinusOne(t *testing.T) { + // MAX_UINT256 - 1 = ...639934 (even number, LSB should be 1) + maxMinusOne := new(u256.Uint).Sub( + u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935"), + u256.One(), + ) + + result := BitMathLeastSignificantBit(maxMinusOne) + uassert.Equal(t, result, uint8(1)) +} + +func TestBitMath_PseudoRandomLargeNumbers(t *testing.T) { + testCases := []struct { + name string + input string + expMSB uint8 + expLSB uint8 + }{ + { + name: "large_random_1", + input: "98765432109876543210987654321098765432109876543210987654321098765432", + expMSB: 225, + expLSB: 3, + }, + { + name: "large_random_2", + input: "11111111111111111111111111111111111111111111111111111111111111111111", + expMSB: 222, + expLSB: 0, + }, + { + name: "large_random_3", + input: "99999999999999999999999999999999999999999999999999999999999999999999", + expMSB: 225, + expLSB: 0, + }, + { + name: "large_prime_like", + input: "57896044618658097711785492504343953926634992332820282019728792003956564819973", + expMSB: 255, + expLSB: 0, + }, + { + name: "fibonacci_large", + input: "354224848179261915075", + expMSB: 68, + expLSB: 0, + }, + { + name: "near_max_even", + input: "115792089237316195423570985008687907853269984665640564039457584007913129639934", + expMSB: 255, + expLSB: 1, + }, + { + name: "half_max", + input: "57896044618658097711785492504343953926634992332820282019728792003956564819967", + expMSB: 254, + expLSB: 0, + }, + { + name: "quarter_max", + input: "28948022309329048855892746252171976963317496166410141009864396001978282409983", + expMSB: 253, + expLSB: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + x := u256.MustFromDecimal(tc.input) + + msb := BitMathMostSignificantBit(x) + lsb := BitMathLeastSignificantBit(x) + + uassert.Equal(t, msb, tc.expMSB) + uassert.Equal(t, lsb, tc.expLSB) + + // Verify MSB >= LSB + if msb < lsb { + t.Errorf("Invalid: MSB(%d) < LSB(%d)", msb, lsb) + } + }) + } +} + +func TestBitMath_ActualValues(t *testing.T) { + testValues := []struct { + name string + input string + }{ + {"2^200+2^20", "1606938044258990275541962092341162602522202993782792835302425600"}, + {"large1", "98765432109876543210987654321098765432109876543210987654321098765432"}, + {"large2", "11111111111111111111111111111111111111111111111111111111111111111111"}, + {"large3", "99999999999999999999999999999999999999999999999999999999999999999999"}, + {"half_max", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, + {"quarter_max", "28948022309329048855892746252171976963317496166410141009864396001978282409983"}, + } + + for _, tc := range testValues { + t.Run(tc.name, func(t *testing.T) { + x := u256.MustFromDecimal(tc.input) + msb := BitMathMostSignificantBit(x) + lsb := BitMathLeastSignificantBit(x) + + t.Logf("%s: MSB=%d, LSB=%d", tc.name, msb, lsb) + + temp := x.Clone() + actualLSB := uint8(0) + for i := uint8(0); i < 255; i++ { + if new(u256.Uint).And(temp, u256.One()).Eq(u256.One()) { + actualLSB = i + break + } + temp.Rsh(temp, 1) + } + + if actualLSB != lsb { + t.Errorf("Calculated LSB %d doesn't match actual %d", lsb, actualLSB) + } + }) + } +} + +func TestBitMath_VerifyCalculations(t *testing.T) { + verifyTests := []struct { + name string + input string + binaryForm string + expectedMSB uint8 + expectedLSB uint8 + }{ + { + name: "complex_number_with_msb_209", + input: "1606938044258990275541962092341162602522202993782792835302425600", + binaryForm: "complex number", + expectedMSB: 209, + expectedLSB: 10, + }, + { + name: "verify_12", + input: "12", + binaryForm: "0b1100", + expectedMSB: 3, + expectedLSB: 2, + }, + { + name: "verify_96", + input: "96", + binaryForm: "0b1100000", + expectedMSB: 6, + expectedLSB: 5, + }, + } + + for _, tc := range verifyTests { + t.Run(tc.name, func(t *testing.T) { + x := u256.MustFromDecimal(tc.input) + + msb := BitMathMostSignificantBit(x) + lsb := BitMathLeastSignificantBit(x) + + if msb != tc.expectedMSB { + t.Errorf("MSB for %s: got %d, expected %d", tc.name, msb, tc.expectedMSB) + } + if lsb != tc.expectedLSB { + t.Errorf("LSB for %s: got %d, expected %d", tc.name, lsb, tc.expectedLSB) + } + }) + } +} + +func TestBitMath_ShiftTableCompleteness(t *testing.T) { + // Verify msbShifts covers all necessary ranges + t.Run("msb_shifts_coverage", func(t *testing.T) { + expectedShifts := []uint{128, 64, 32, 16, 8, 4, 2, 1} + + if len(msbShifts) != len(expectedShifts) { + t.Errorf("msbShifts has %d entries, expected %d", len(msbShifts), len(expectedShifts)) + } + + for i, expected := range expectedShifts { + if i < len(msbShifts) && msbShifts[i].shift != expected { + t.Errorf("msbShifts[%d].shift = %d, expected %d", i, msbShifts[i].shift, expected) + } + } + + // Verify bit patterns are correct powers of 2 + for i, s := range msbShifts { + // Calculate expected value: 2^shift + expected := new(u256.Uint).Lsh(u256.One(), s.shift) + if !s.bitPattern.Eq(expected) { + t.Errorf("msbShifts[%d].bitPattern incorrect for shift %d", i, s.shift) + } + } + }) + + // Verify lsbShifts covers all necessary ranges + t.Run("lsb_shifts_coverage", func(t *testing.T) { + expectedShifts := []uint{128, 64, 32, 16, 8, 4, 2, 1} + + if len(lsbShifts) != len(expectedShifts) { + t.Errorf("lsbShifts has %d entries, expected %d", len(lsbShifts), len(expectedShifts)) + } + + for i, expected := range expectedShifts { + if i < len(lsbShifts) && lsbShifts[i].shift != expected { + t.Errorf("lsbShifts[%d].shift = %d, expected %d", i, lsbShifts[i].shift, expected) + } + } + + // Verify bit patterns are correct (2^shift - 1) + for i, s := range lsbShifts { + // Calculate expected value: 2^shift - 1 + powerOfTwo := new(u256.Uint).Lsh(u256.One(), s.shift) + expected := new(u256.Uint).Sub(powerOfTwo, u256.One()) + + if !s.bitPattern.Eq(expected) { + t.Errorf("lsbShifts[%d].bitPattern incorrect for shift %d", i, s.shift) + } + } + }) +} + +// TestBitMathInputPreservation verifies that bit math functions do not mutate their input. +func TestBitMathInputPreservation(t *testing.T) { + cases := []struct { + name string + val string + }{ + {"zero_plus_one", "1"}, + {"small", "12345"}, + {"power_of_two_small", "256"}, // 2^8 + {"power_of_two_medium", "4294967296"}, // 2^32 + {"power_of_two_large", "340282366920938463463374607431768211456"}, // 2^128 + {"max_uint256", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, // 2^256 - 1 + {"random_large", "98765432109876543210987654321098765432109876543210987654321098765432"}, + {"all_bits_set_lower", "4294967295"}, // 0xFFFFFFFF + {"alternating_bits", "12297829382473034410"}, // 0xAAAAAAAAAAAAAAAA + } + + for _, tc := range cases { + t.Run(tc.name+"/MSB", func(t *testing.T) { + x := u256.MustFromDecimal(tc.val) + before := x.Clone() + _ = BitMathMostSignificantBit(x) + + // Verify input was not mutated + if !x.Eq(before) { + t.Errorf("MSB mutated input: before=%s, after=%s", + before.ToString(), x.ToString()) + } + }) + + t.Run(tc.name+"/LSB", func(t *testing.T) { + x := u256.MustFromDecimal(tc.val) + before := x.Clone() + _ = BitMathLeastSignificantBit(x) + + // Verify input was not mutated + if !x.Eq(before) { + t.Errorf("LSB mutated input: before=%s, after=%s", + before.ToString(), x.ToString()) + } + }) + } +} + +// TestBitMathIdempotency ensures that multiple calls with the same input produce consistent results +func TestBitMathIdempotency(t *testing.T) { + values := []string{ + "42", + "65535", + "340282366920938463463374607431768211456", + "98765432109876543210987654321098765432109876543210987654321098765432", + } + + for _, v := range values { + x := u256.MustFromDecimal(v) + + // Call MSB multiple times and verify consistency + msb1 := BitMathMostSignificantBit(x) + msb2 := BitMathMostSignificantBit(x) + msb3 := BitMathMostSignificantBit(x) + + if msb1 != msb2 || msb2 != msb3 { + t.Errorf("MSB inconsistent for %s: %d, %d, %d", v, msb1, msb2, msb3) + } + + // Call LSB multiple times and verify consistency + lsb1 := BitMathLeastSignificantBit(x) + lsb2 := BitMathLeastSignificantBit(x) + lsb3 := BitMathLeastSignificantBit(x) + + if lsb1 != lsb2 || lsb2 != lsb3 { + t.Errorf("LSB inconsistent for %s: %d, %d, %d", v, lsb1, lsb2, lsb3) + } + + // Final verification that input remains unchanged after multiple calls + if !x.Eq(u256.MustFromDecimal(v)) { + t.Errorf("Value mutated after multiple calls: %s", v) + } + } +} + +// TestBitMathMemoryReuse specifically tests temp.Clone() is used instead of mutating the input parameter. +func TestBitMathMemoryReuse(t *testing.T) { + // Test with 2^128 - a value that triggers multiple shift operations + v := "340282366920938463463374607431768211456" + + t.Run("MSB_no_reallocation", func(t *testing.T) { + x := u256.MustFromDecimal(v) + + // Store the original pointer value (*Uint itself, not address of x) + originalPtr := x + + // Execute the function that previously had mutation issues + msb := BitMathMostSignificantBit(x) + + // Verify the pointer hasn't changed (no reallocation) + if x != originalPtr { + t.Error("Pointer value changed - unexpected reallocation occurred") + } + + // Verify the value hasn't been mutated + if !x.Eq(u256.MustFromDecimal(v)) { + t.Error("Value was mutated") + } + + // Verify the result is correct + if msb != 128 { + t.Errorf("Incorrect MSB: got %d, expected 128", msb) + } + }) + + t.Run("LSB_no_reallocation", func(t *testing.T) { + x := u256.MustFromDecimal(v) + + // Store the original pointer value (*Uint itself, not address of x) + originalPtr := x + + // Execute the function + lsb := BitMathLeastSignificantBit(x) + + // Verify the pointer hasn't changed (no reallocation) + if x != originalPtr { + t.Error("Pointer value changed - unexpected reallocation occurred") + } + + // Verify the value hasn't been mutated + if !x.Eq(u256.MustFromDecimal(v)) { + t.Error("Value was mutated") + } + + // Verify the result is correct (2^128 has LSB at position 128) + if lsb != 128 { + t.Errorf("Incorrect LSB: got %d, expected 128", lsb) + } + }) +} + +func TestBitMath_SpecialPatterns(t *testing.T) { + tests := []struct { + name string + input string + expMSB uint8 + expLSB uint8 + desc string + }{ + { + name: "all_even_bits", + input: "6148914691236517205", // 0x5555555555555555 + expMSB: 62, + expLSB: 0, + desc: "alternating 01 pattern", + }, + { + name: "all_odd_bits", + input: "12297829382473034410", // 0xAAAAAAAAAAAAAAAA + expMSB: 63, + expLSB: 1, + desc: "alternating 10 pattern", + }, + { + name: "one_bit_per_byte", + input: "72340172838076673", // 0x0101010101010101 + expMSB: 56, + expLSB: 0, + desc: "one bit set per byte", + }, + { + name: "high_byte_only", + input: "18374686479671623680", // 0xFF00000000000000 + expMSB: 63, + expLSB: 56, + desc: "only highest byte set", + }, + { + name: "mersenne_127", + input: "170141183460469231731687303715884105727", // 2^127 - 1 + expMSB: 126, + expLSB: 0, + desc: "Mersenne prime 2^127-1", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := u256.MustFromDecimal(tc.input) + + msb := BitMathMostSignificantBit(x) + lsb := BitMathLeastSignificantBit(x) + + uassert.Equal(t, msb, tc.expMSB) + uassert.Equal(t, lsb, tc.expLSB) + }) + } +} + +// Benchmark-style exhaustive test for small ranges +func TestBitMath_ExhaustiveSmallRange(t *testing.T) { + // Test first 1000 numbers exhaustively + for i := uint64(1); i <= 1000; i++ { + x := u256.NewUint(i) + msb := BitMathMostSignificantBit(x) + lsb := BitMathLeastSignificantBit(x) + + // Manual calculation for verification + expectedMSB := uint8(0) + // Start from bit 63 and work down to find the highest set bit + for bit := 63; bit >= 0; bit-- { + if i&(1<= liquidityShifted + if denominator.Gte(liquidityShifted) { + return u256.MulDivRoundingUp(liquidityShifted, currentSqrtPriceX96, denominator) + } + } + + // fallback: liquidityShifted / ((liquidityShifted / sqrtPrice) + amount) + divValue := u256.Zero().Div(liquidityShifted, currentSqrtPriceX96) + denominator := u256.Zero().Add(divValue, amountToAdd) + return u256.DivRoundingUp(liquidityShifted, denominator) +} + +// getNextPriceAmount0Remove calculates the next sqrt price when removing token0 liquidity, +// rounding up to ensure conservative pricing for the protocol. +// This internal function handles the case where token0 is being removed from the pool. +// Panics if validation checks fail (invalid pool sqrt price calculation). +func getNextPriceAmount0Remove( + currentSqrtPriceX96, liquidity, amountToRemove *u256.Uint, +) *u256.Uint { + // liquidityShifted = liquidity << 96 + liquidityShifted := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) + // amountTimesSqrtPrice = amountToRemove * currentSqrtPriceX96 + amountTimesSqrtPrice := u256.Zero().Mul(amountToRemove, currentSqrtPriceX96) + + // Validation checks + quotientCheck := u256.Zero().Div(amountTimesSqrtPrice, amountToRemove) + if !quotientCheck.Eq(currentSqrtPriceX96) || !liquidityShifted.Gt(amountTimesSqrtPrice) { + panic(errInvalidPoolSqrtPrice) + } + + denominator := u256.Zero().Sub(liquidityShifted, amountTimesSqrtPrice) + return u256.MulDivRoundingUp(liquidityShifted, currentSqrtPriceX96, denominator) +} + +// getNextSqrtPriceFromAmount0RoundingUp calculates the next sqrt price based on token0 amount, +// always rounding up to ensure conservative pricing in both exact output and exact input cases. +// The add parameter determines whether liquidity is being added (true) or removed (false). +func getNextSqrtPriceFromAmount0RoundingUp( + sqrtPX96 *u256.Uint, + liquidity *u256.Uint, + amount *u256.Uint, + add bool, +) *u256.Uint { + // Shortcut: if no amount, return original price + if amount.IsZero() { + return sqrtPX96 + } + + if add { + return getNextPriceAmount0Add(sqrtPX96, liquidity, amount) + } + return getNextPriceAmount0Remove(sqrtPX96, liquidity, amount) +} + +// getNextPriceAmount1Add calculates the next sqrt price when adding token1, +// preserving rounding-down logic for the final result. +// This internal function handles the case where token1 is being added to the pool. +func getNextPriceAmount1Add( + sqrtPX96, liquidity, amount *u256.Uint, +) *u256.Uint { + var quotient *u256.Uint + + if amount.Lte(max160) { + // Use local variables to avoid allocation conflicts + shifted := u256.Zero().Lsh(amount, Q96_RESOLUTION) + quotient = u256.Zero().Div(shifted, liquidity) + } else { + quotient = u256.MulDiv(amount, q96, liquidity) + } + + return u256.Zero().Add(sqrtPX96, quotient) +} + +// getNextPriceAmount1Remove calculates the next sqrt price when removing token1, +// preserving rounding-down logic for the final result. +// This internal function handles the case where token1 is being removed from the pool. +// Panics if sqrt price would exceed quotient. +func getNextPriceAmount1Remove( + sqrtPX96, liquidity, amount *u256.Uint, +) *u256.Uint { + var quotient *u256.Uint + + if amount.Lte(max160) { + shifted := u256.Zero().Lsh(amount, Q96_RESOLUTION) + quotient = u256.DivRoundingUp(shifted, liquidity) + } else { + quotient = u256.MulDivRoundingUp(amount, q96, liquidity) + } + + if !sqrtPX96.Gt(quotient) { + panic(errSqrtPriceExceedsQuotient) + } + + return u256.Zero().Sub(sqrtPX96, quotient) +} + +// getNextSqrtPriceFromAmount1RoundingDown calculates the next sqrt price based on token1 amount, +// always rounding down to ensure conservative pricing in both exact output and exact input cases. +// The add parameter determines whether liquidity is being added (true) or removed (false). +func getNextSqrtPriceFromAmount1RoundingDown( + sqrtPX96, + liquidity, + amount *u256.Uint, + add bool, +) *u256.Uint { + // Shortcut: if no amount, return original price + if amount.IsZero() { + return sqrtPX96 + } + + if add { + return getNextPriceAmount1Add(sqrtPX96, liquidity, amount) + } + return getNextPriceAmount1Remove(sqrtPX96, liquidity, amount) +} + +// getNextSqrtPriceFromInput calculates the next sqrt price after adding tokens to the pool, +// rounding up for conservative pricing in both swap directions. +// The zeroForOne parameter indicates swap direction (token0 for token1 when true). +// Panics if sqrtPX96 or liquidity is zero. +func getNextSqrtPriceFromInput( + sqrtPX96, liquidity, amountIn *u256.Uint, + zeroForOne bool, +) *u256.Uint { + if sqrtPX96.IsZero() { + panic(errSqrtPriceZero) + } + + if liquidity.IsZero() { + panic(errLiquidityZero) + } + + if zeroForOne { + return getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true) + } + + return getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true) +} + +// getNextSqrtPriceFromOutput calculates the next sqrt price after removing tokens from the pool, +// using different rounding directions based on swap direction. +// The zeroForOne parameter indicates swap direction (token0 for token1 when true). +// Panics if sqrtPX96 or liquidity is zero. +func getNextSqrtPriceFromOutput( + sqrtPX96, liquidity, amountOut *u256.Uint, + zeroForOne bool, +) *u256.Uint { + if sqrtPX96.IsZero() { + panic(errSqrtPriceZero) + } + + if liquidity.IsZero() { + panic(errLiquidityZero) + } + + if zeroForOne { + return getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false) + } + + return getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false) +} + +// getAmount0DeltaHelper calculates the absolute token0 amount difference between two price ranges, +// automatically swapping inputs to ensure correct ordering. The roundUp parameter controls +// rounding direction for the final result to ensure conservative AMM calculations. +// Panics if sqrtRatioAX96 is zero. +func getAmount0DeltaHelper( + sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint, + roundUp bool, +) *u256.Uint { + if sqrtRatioAX96.Gt(sqrtRatioBX96) { + sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96 + } + + // Use local variables for thread safety + numerator := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) + difference := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) + + if sqrtRatioAX96.IsZero() { + panic(errSqrtRatioAX96Zero) + } + + if roundUp { + intermediate := u256.MulDivRoundingUp(numerator, difference, sqrtRatioBX96) + return u256.DivRoundingUp(intermediate, sqrtRatioAX96) + } + + intermediate := u256.MulDiv(numerator, difference, sqrtRatioBX96) + return u256.Zero().Div(intermediate, sqrtRatioAX96) +} + +// getAmount1DeltaHelper calculates the absolute token1 amount difference between two price ranges, +// automatically swapping inputs to ensure correct ordering. The roundUp parameter controls +// rounding direction for the final result to ensure conservative AMM calculations. +func getAmount1DeltaHelper( + sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint, + roundUp bool, +) *u256.Uint { + if sqrtRatioAX96.Gt(sqrtRatioBX96) { + sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96 + } + + // amount1 = liquidity * (sqrtB - sqrtA) / 2^96 + // Use local variable for thread safety + difference := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) + + if roundUp { + return u256.MulDivRoundingUp(liquidity, difference, q96) + } + + return u256.MulDiv(liquidity, difference, q96) +} + +// GetAmount0Delta calculates the token0 amount difference within a price range, returning +// a signed int256 value that is negative when liquidity is negative. Rounds down for +// negative liquidity and up for positive liquidity. +// Panics if any input is nil or if the result overflows int256. +func GetAmount0Delta( + sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, + liquidity *i256.Int, +) *i256.Int { + if sqrtRatioAX96 == nil || sqrtRatioBX96 == nil || liquidity == nil { + panic(errGetAmount0DeltaNilInput) + } + + if liquidity.IsNeg() { + u := getAmount0DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), false) + if u.Gt(maxInt256) { + // if u > (2**255 - 1), cannot cast to int256 + panic(errAmount0DeltaOverflow) + } + + // Convert to i256 and negate properly + return i256.Zero().Neg(i256.FromUint256(u)) + } + + u := getAmount0DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), true) + if u.Gt(maxInt256) { + // if u > (2**255 - 1), cannot cast to int256 + panic(errAmount0DeltaOverflow) + } + + return i256.FromUint256(u) +} + +// GetAmount1Delta calculates the token1 amount difference within a price range, returning +// a signed int256 value that is negative when liquidity is negative. Rounds down for +// negative liquidity and up for positive liquidity. +// Panics if any input is nil or if the result overflows int256. +func GetAmount1Delta( + sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, + liquidity *i256.Int, +) *i256.Int { + if sqrtRatioAX96 == nil || sqrtRatioBX96 == nil || liquidity == nil { + panic(errGetAmount1DeltaNilInput) + } + + if liquidity.IsNeg() { + u := getAmount1DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), false) + if u.Gt(maxInt256) { + // if u > (2**255 - 1), cannot cast to int256 + panic(errAmount1DeltaOverflow) + } + + // Convert to i256 and negate properly + return i256.Zero().Neg(i256.FromUint256(u)) + } + + u := getAmount1DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), true) + if u.Gt(maxInt256) { + // if u > (2**255 - 1), cannot cast to int256 + panic(errAmount1DeltaOverflow) + } + + return i256.FromUint256(u) +} diff --git a/contract/p/gnoswap/gnsmath/sqrt_price_math_test.gno b/contract/p/gnoswap/gnsmath/sqrt_price_math_test.gno new file mode 100644 index 0000000..e23f988 --- /dev/null +++ b/contract/p/gnoswap/gnsmath/sqrt_price_math_test.gno @@ -0,0 +1,1454 @@ +package gnsmath + +import ( + "testing" + + "gno.land/p/nt/uassert" + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +// Test data constants +var ( + Q96 = u256.MustFromDecimal("79228162514264337593543950336") // 2^96 + MAX_UINT256 = u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") + MAX_UINT160 = u256.MustFromDecimal("1461446703485210103287273052203988822378723970342") +) + +// TestGetNextPriceAmount0Add tests the internal helper for adding token0 +func TestGetNextPriceAmount0Add(t *testing.T) { + tests := []struct { + name string + currentSqrtPrice *u256.Uint + liquidity *u256.Uint + amountToAdd *u256.Uint + expected *u256.Uint + }{ + // Normal cases + { + name: "simple_case: price halves", + currentSqrtPrice: encodePriceSqrt("1", "1"), // √(1/1) * 2^96 + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountToAdd: u256.MustFromDecimal("1000000000000000000"), + expected: new(u256.Uint).Rsh(encodePriceSqrt("1", "1"), 1), // ÷2 + }, + { + name: "normal_addition", + currentSqrtPrice: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountToAdd: u256.MustFromDecimal("100000000000000000"), + expected: u256.MustFromDecimal("72025602285694852357767227579"), + }, + { + name: "small_amount_high_liquidity", + currentSqrtPrice: Q96, + liquidity: u256.MustFromDecimal("100000000000000000000"), + amountToAdd: u256.One(), + expected: u256.MustFromDecimal("79228162514264337592751668711"), + }, + { + name: "overflow_path_fallback", + currentSqrtPrice: encodePriceSqrt("1", "1"), + liquidity: u256.One(), + amountToAdd: encodePriceSqrt("1", "1"), + expected: u256.One(), + }, + { + name: "one_tick_no_move_when_amount_too_small", + currentSqrtPrice: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(2)), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountToAdd: u256.One(), + expected: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(2)), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getNextPriceAmount0Add(tt.currentSqrtPrice, tt.liquidity, tt.amountToAdd) + uassert.Equal(t, tt.expected.ToString(), result.ToString()) + }) + } +} + +// TestGetNextPriceAmount0Remove tests the internal helper for removing token0 +func TestGetNextPriceAmount0Remove(t *testing.T) { + tiny := u256.One() + medium := u256.MustFromDecimal("100000000000000000") + huge := u256.MustFromDecimal("1000000000000000000000000000000") + + tests := []struct { + name string + current *u256.Uint + liquidity *u256.Uint + amountToRemove *u256.Uint + shouldPanic bool + panicMsg string + validate func(t *testing.T, before, after *u256.Uint) + }{ + { + name: "tiny_removal_monotonic", + current: Q96, + liquidity: u256.MustFromDecimal("100000000000000000000"), + amountToRemove: tiny, + validate: func(t *testing.T, before, after *u256.Uint) { + uassert.True(t, after.Gt(before), "tiny removal must increase price") + }, + }, + { + name: "medium_removal_larger_increase", + current: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountToRemove: medium, + validate: func(t *testing.T, before, after *u256.Uint) { + uassert.True(t, after.Gt(before), "medium removal must increase price") + diff := new(u256.Uint).Sub(after, before) + uassert.True(t, diff.Gte(u256.One()), "price increase ≥ 1 tick") + }, + }, + { + name: "remove_at_min_boundary", + current: MIN_SQRT_RATIO, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountToRemove: tiny, + validate: func(t *testing.T, before, after *u256.Uint) { + uassert.True(t, after.Gt(before), "removal at MIN should increase price") + }, + }, + { + name: "remove_at_high_price", + current: u256.MustFromDecimal("1000000000000000000000000000000"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountToRemove: tiny, + validate: func(t *testing.T, before, after *u256.Uint) { + uassert.True(t, after.Gt(before), "removal should increase price") + }, + }, + { + name: "remove_near_max_with_huge_liquidity", + current: new(u256.Uint).Sub(MAX_SQRT_RATIO, u256.NewUint(1000000)), + liquidity: u256.MustFromDecimal("100000000000000000000000000"), + amountToRemove: tiny, + validate: func(t *testing.T, before, after *u256.Uint) { + uassert.True(t, after.Gt(before), "removal should increase price") + }, + }, + { + name: "overflow_check_fail", + current: Q96, + liquidity: u256.MustFromDecimal("1"), + amountToRemove: MAX_UINT256, + shouldPanic: true, + panicMsg: errInvalidPoolSqrtPrice.Error(), + }, + { + name: "insufficient_liquidity", + current: Q96, + liquidity: u256.MustFromDecimal("1000"), + amountToRemove: u256.MustFromDecimal("100000000000000000000"), + shouldPanic: true, + panicMsg: errInvalidPoolSqrtPrice.Error(), + }, + { + name: "overflow_path_large_remove_panics", + current: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountToRemove: huge, + shouldPanic: true, + panicMsg: errInvalidPoolSqrtPrice.Error(), + }, + { + name: "remove_at_max_boundary_insufficient_liquidity", + current: new(u256.Uint).Sub(MAX_SQRT_RATIO, u256.One()), // MAX - 1 + liquidity: u256.MustFromDecimal("1000000000000000000000"), + amountToRemove: u256.MustFromDecimal("1000"), + shouldPanic: true, + panicMsg: errInvalidPoolSqrtPrice.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + before := tt.current.Clone() + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + _ = getNextPriceAmount0Remove(tt.current, tt.liquidity, tt.amountToRemove) + }) + return + } + after := getNextPriceAmount0Remove(tt.current, tt.liquidity, tt.amountToRemove) + tt.validate(t, before, after) + }) + } +} + +// TestGetNextSqrtPriceFromAmount0RoundingUp tests price calculation when adding/removing token0 +func TestGetNextSqrtPriceFromAmount0RoundingUp(t *testing.T) { + tests := []struct { + name string + sqrtPX96 *u256.Uint + liquidity *u256.Uint + amount *u256.Uint + add bool + expected *u256.Uint + shouldPanic bool + panicMsg string + }{ + // Basic functionality + { + name: "zero_amount_returns_same_price", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("2000000"), + amount: u256.Zero(), + add: true, + expected: Q96, + }, + { + name: "add_token0_decreases_price", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("100000000000000000"), + add: true, + expected: u256.MustFromDecimal("72025602285694852357767227579"), + }, + { + name: "remove_token0_increases_price", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("100000000000000000"), + add: false, + expected: u256.MustFromDecimal("88031291682515930659493278152"), + }, + // Boundary cases + { + name: "add_near_min_clamps", + sqrtPX96: u256.MustFromDecimal("4295128741"), + liquidity: u256.MustFromDecimal("1000"), + amount: u256.MustFromDecimal("1000000000000000000000"), + add: true, + expected: u256.MustFromDecimal("4074254652"), + }, + { + name: "min_boundary_stays_same", + sqrtPX96: u256.MustFromDecimal("4295128740"), + liquidity: u256.MustFromDecimal("1000"), + amount: u256.MustFromDecimal("1000000"), + add: true, + expected: u256.MustFromDecimal("4295128740"), + }, + { + name: "min_boundary_no_change_tiny_amount", + sqrtPX96: u256.MustFromDecimal("4295128740"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("1"), + add: true, + expected: u256.MustFromDecimal("4295128740"), + }, + { + name: "increase_price_from_min", + sqrtPX96: u256.MustFromDecimal("4295128739"), + liquidity: u256.MustFromDecimal("1000"), + amount: u256.MustFromDecimal("1"), + add: false, + expected: u256.MustFromDecimal("4295128740"), + }, + // Error cases + { + name: "remove_causes_overflow", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1"), + amount: MAX_UINT256, + add: false, + shouldPanic: true, + panicMsg: errInvalidPoolSqrtPrice.Error(), + }, + { + name: "remove_insufficient_liquidity", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000"), + amount: u256.MustFromDecimal("100000000000000000000"), + add: false, + shouldPanic: true, + panicMsg: errInvalidPoolSqrtPrice.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + getNextSqrtPriceFromAmount0RoundingUp(tt.sqrtPX96, tt.liquidity, tt.amount, tt.add) + }) + return + } + + result := getNextSqrtPriceFromAmount0RoundingUp(tt.sqrtPX96, tt.liquidity, tt.amount, tt.add) + uassert.Equal(t, tt.expected.ToString(), result.ToString()) + }) + } +} + +// TestGetNextPriceAmount1Add tests the internal helper for adding token1 +func TestGetNextPriceAmount1Add(t *testing.T) { + tests := []struct { + name string + sqrtPX96 *u256.Uint + liquidity *u256.Uint + amount *u256.Uint + expected *u256.Uint + shouldPanic bool + panicMsg string + }{ + { + name: "normal_addition", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("100000000000000000"), + expected: u256.MustFromDecimal("87150978765690771352898345369"), + }, + { + name: "amount_lte_max160", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("1000000000000000000"), + expected: u256.MustFromDecimal("158456325028528675187087900672"), + }, + { + name: "amount_gt_max160", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: new(u256.Uint).Add(MAX_UINT160, u256.MustFromDecimal("1000000000000000000")), + expected: u256.MustFromDecimal("115787736929662111563370814583278000176356624084966981749664"), + }, + { + name: "very_small_liquidity", + sqrtPX96: Q96, + liquidity: u256.One(), + amount: u256.MustFromDecimal("1000"), + expected: u256.MustFromDecimal("79307390676778601931137494286336"), + }, + { + name: "add_at_max_boundary", + sqrtPX96: MAX_SQRT_RATIO, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("1000"), + expected: u256.MustFromDecimal("1461446703485210103287273052203988901606886484606"), + }, + { + name: "add_near_max_increases_price", + sqrtPX96: new(u256.Uint).Sub(MAX_SQRT_RATIO, u256.MustFromDecimal("1000000000000")), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("100000000000000000"), + expected: u256.MustFromDecimal("1461446703485210103295195868455415255138078365375"), + }, + { + name: "add_at_min_increases_price", + sqrtPX96: MIN_SQRT_RATIO, + liquidity: u256.MustFromDecimal("1000000"), + amount: u256.MustFromDecimal("100"), + expected: u256.MustFromDecimal("7922816251426438054483134"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + getNextPriceAmount1Add(tt.sqrtPX96, tt.liquidity, tt.amount) + }) + return + } + + result := getNextPriceAmount1Add(tt.sqrtPX96, tt.liquidity, tt.amount) + uassert.Equal(t, tt.expected.ToString(), result.ToString()) + }) + } +} + +// TestGetNextPriceAmount1Remove tests the internal helper for removing token1 +func TestGetNextPriceAmount1Remove(t *testing.T) { + tests := []struct { + name string + sqrtPX96 *u256.Uint + liquidity *u256.Uint + amount *u256.Uint + expected *u256.Uint + shouldPanic bool + panicMsg string + }{ + { + name: "normal_removal_small", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("100000000000000000"), // 0.1 + expected: u256.MustFromDecimal("71305346262837903834189555302"), + }, + { + name: "normal_removal_medium", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("500000000000000000"), // 0.5 + expected: u256.MustFromDecimal("39614081257132168796771975168"), + }, + + { + name: "zero_amount_no_change", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.Zero(), + expected: Q96, + }, + { + name: "tiny_amount_tiny_change", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("100000000000000000000"), + amount: u256.One(), + expected: u256.MustFromDecimal("79228162514264337592751668710"), + }, + { + name: "amount_lte_max160_path_small", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("10000000000000000000"), + amount: u256.MustFromDecimal("1000000000000000000"), + expected: u256.MustFromDecimal("71305346262837903834189555302"), + }, + { + name: "medium_amount_normal_path", + sqrtPX96: new(u256.Uint).Mul(Q96, u256.NewUint(2)), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("10000000000000000"), + expected: u256.MustFromDecimal("157664043403386031811152461168"), + }, + { + name: "very_small_liquidity", + sqrtPX96: Q96, + liquidity: u256.One(), + amount: u256.MustFromDecimal("1"), + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + { + name: "very_large_liquidity", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000000000000"), + amount: u256.MustFromDecimal("1000000000000000000"), + expected: u256.MustFromDecimal("79228162435036175079279612742"), + }, + { + name: "high_price_removal", + sqrtPX96: u256.MustFromDecimal("1000000000000000000000000000000"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("1000"), + expected: u256.MustFromDecimal("999999999999999920771837485735"), + }, + { + name: "insufficient_liquidity_panic", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000"), + amount: u256.MustFromDecimal("1000000000000000000"), + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + { + name: "price_below_min_panic", + sqrtPX96: MIN_SQRT_RATIO, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.One(), + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + { + name: "rounding_up_verification", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("999999999999999999"), + amount: u256.MustFromDecimal("333333333333333333"), + expected: u256.MustFromDecimal("52818775009509558395695966890"), + }, + { + name: "quotient_exactly_sqrtPX96_minus_1", + sqrtPX96: u256.MustFromDecimal("79228162514264337593543950336"), + liquidity: u256.MustFromDecimal("79228162514264337593543950336"), + amount: u256.MustFromDecimal("79228162514264337593543950335"), + expected: u256.One(), + }, + { + name: "quotient_exactly_equals_sqrtPX96", + sqrtPX96: u256.MustFromDecimal("1000000000000000000"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("1000000000000000000"), + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + { + name: "amount_just_above_max160_boundary", + sqrtPX96: u256.MustFromDecimal("200000000000000000000000000000000000000"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: new(u256.Uint).Add(MAX_UINT160, u256.One()), // MAX_UINT160 + 1 + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + { + name: "exact_division_no_rounding", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("1000000000000000000"), + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + { + name: "quotient_almost_equals_price", + sqrtPX96: u256.MustFromDecimal("1000000000000000000"), + liquidity: u256.MustFromDecimal("1000000"), + amount: u256.MustFromDecimal("12589254117"), + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + { + name: "quotient_exactly_equals_sqrtPX96", + sqrtPX96: u256.MustFromDecimal("1000000000000000000"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("1000000000000000000"), + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + { + name: "near_overflow_large_values", + sqrtPX96: new(u256.Uint).Rsh(MAX_UINT256, 10), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: new(u256.Uint).Rsh(MAX_UINT256, 200), + expected: u256.MustFromDecimal("113078212145816597093331040047546785012958969394330622548958957437722684299"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + getNextPriceAmount1Remove(tt.sqrtPX96, tt.liquidity, tt.amount) + }) + return + } + + result := getNextPriceAmount1Remove(tt.sqrtPX96, tt.liquidity, tt.amount) + uassert.Equal(t, tt.expected.ToString(), result.ToString()) + }) + } +} + +// TestGetNextSqrtPriceFromAmount1RoundingDown tests price calculation when adding/removing token1 +func TestGetNextSqrtPriceFromAmount1RoundingDown(t *testing.T) { + tests := []struct { + name string + sqrtPX96 *u256.Uint + liquidity *u256.Uint + amount *u256.Uint + add bool + expected *u256.Uint + shouldPanic bool + panicMsg string + }{ + { + name: "zero_amount_returns_same_price", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.Zero(), + add: true, + expected: Q96, + }, + { + name: "delegates_to_add_function", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("100000000000000000"), + add: true, + expected: u256.MustFromDecimal("87150978765690771352898345369"), + }, + { + name: "delegates_to_remove_function", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: u256.MustFromDecimal("100000000000000000"), + add: false, + expected: u256.MustFromDecimal("71305346262837903834189555302"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + getNextSqrtPriceFromAmount1RoundingDown(tt.sqrtPX96, tt.liquidity, tt.amount, tt.add) + }) + return + } + + result := getNextSqrtPriceFromAmount1RoundingDown(tt.sqrtPX96, tt.liquidity, tt.amount, tt.add) + uassert.Equal(t, tt.expected.ToString(), result.ToString()) + }) + } +} + +// TestGetNextSqrtPriceFromInput tests input swap calculations +func TestGetNextSqrtPriceFromInput(t *testing.T) { + tests := []struct { + name string + sqrtPX96 *u256.Uint + liquidity *u256.Uint + amountIn *u256.Uint + zeroForOne bool + expected string + shouldPanic bool + panicMsg string + }{ + { + name: "zero_price_panics", + sqrtPX96: u256.Zero(), + liquidity: u256.One(), + amountIn: u256.One(), + zeroForOne: true, + shouldPanic: true, + panicMsg: errSqrtPriceZero.Error(), + }, + { + name: "zero_liquidity_panics", + sqrtPX96: u256.One(), + liquidity: u256.Zero(), + amountIn: u256.One(), + zeroForOne: true, + shouldPanic: true, + panicMsg: errLiquidityZero.Error(), + }, + { + name: "delegates_to_amount0_function", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountIn: u256.MustFromDecimal("100000000000000000"), + zeroForOne: true, + expected: "72025602285694852357767227579", + }, + { + name: "delegates_to_amount1_function", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountIn: u256.MustFromDecimal("100000000000000000"), + zeroForOne: false, + expected: "87150978765690771352898345369", + }, + { + name: "zero_amount_no_change", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountIn: u256.Zero(), + zeroForOne: true, + expected: Q96.ToString(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + getNextSqrtPriceFromInput(tt.sqrtPX96, tt.liquidity, tt.amountIn, tt.zeroForOne) + }) + return + } + + result := getNextSqrtPriceFromInput(tt.sqrtPX96, tt.liquidity, tt.amountIn, tt.zeroForOne) + uassert.Equal(t, tt.expected, result.ToString()) + }) + } +} + +// TestGetNextSqrtPriceFromOutput tests output swap calculations +func TestGetNextSqrtPriceFromOutput(t *testing.T) { + tests := []struct { + name string + sqrtPX96 *u256.Uint + liquidity *u256.Uint + amountOut *u256.Uint + zeroForOne bool + expected string + shouldPanic bool + panicMsg string + }{ + { + name: "zero_price_panics", + sqrtPX96: u256.Zero(), + liquidity: u256.One(), + amountOut: u256.One(), + zeroForOne: true, + shouldPanic: true, + panicMsg: errSqrtPriceZero.Error(), + }, + { + name: "zero_liquidity_panics", + sqrtPX96: u256.One(), + liquidity: u256.Zero(), + amountOut: u256.One(), + zeroForOne: true, + shouldPanic: true, + panicMsg: errLiquidityZero.Error(), + }, + { + name: "delegates_to_amount1_for_zeroForOne", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountOut: u256.MustFromDecimal("100000000000000000"), + zeroForOne: true, + expected: "71305346262837903834189555302", + }, + { + name: "delegates_to_amount0_for_oneForZero", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountOut: u256.MustFromDecimal("100000000000000000"), + zeroForOne: false, + expected: "88031291682515930659493278152", + }, + { + name: "zero_amount_no_change", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountOut: u256.Zero(), + zeroForOne: true, + expected: Q96.ToString(), + }, + { + name: "delegates_panic_from_internal", + sqrtPX96: Q96, + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountOut: u256.MustFromDecimal("1000000000000000000"), + zeroForOne: true, + shouldPanic: true, + panicMsg: errSqrtPriceExceedsQuotient.Error(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + getNextSqrtPriceFromOutput(tt.sqrtPX96, tt.liquidity, tt.amountOut, tt.zeroForOne) + }) + return + } + + result := getNextSqrtPriceFromOutput(tt.sqrtPX96, tt.liquidity, tt.amountOut, tt.zeroForOne) + uassert.Equal(t, tt.expected, result.ToString()) + }) + } +} + +// TestGetAmount0DeltaHelper tests amount0 calculation helper +func TestGetAmount0DeltaHelper(t *testing.T) { + tests := []struct { + name string + sqrtRatioAX96 *u256.Uint + sqrtRatioBX96 *u256.Uint + liquidity *u256.Uint + roundUp bool + expected string + shouldPanic bool + panicMsg string + }{ + // Basic cases + { + name: "zero_liquidity", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("2", "1"), + liquidity: u256.Zero(), + roundUp: true, + expected: "0", + }, + { + name: "equal_prices", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("1", "1"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "0", + }, + { + name: "swapped_inputs", + sqrtRatioAX96: encodePriceSqrt("2", "1"), + sqrtRatioBX96: encodePriceSqrt("1", "1"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "292893218813452476", + }, + // Rounding tests + { + name: "round_up_true", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "90909090909090910", + }, + { + name: "round_up_false", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: false, + expected: "90909090909090909", + }, + // Error cases + { + name: "zero_sqrtRatioA", + sqrtRatioAX96: u256.Zero(), + sqrtRatioBX96: u256.MustFromDecimal("1000000"), + liquidity: u256.MustFromDecimal("1000000"), + roundUp: true, + shouldPanic: true, + panicMsg: errSqrtRatioAX96Zero.Error(), + }, + { + name: "zero_sqrtRatioB_gets_swapped", + sqrtRatioAX96: u256.MustFromDecimal("1000000"), + sqrtRatioBX96: u256.Zero(), + liquidity: u256.MustFromDecimal("1000000"), + roundUp: true, + shouldPanic: true, + panicMsg: errSqrtRatioAX96Zero.Error(), + }, + // Extreme values + { + name: "min_price_range", + sqrtRatioAX96: MIN_SQRT_RATIO, + sqrtRatioBX96: u256.MustFromDecimal("4295128740"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "4294644427204583875464618656", + }, + { + name: "max_price_range", + sqrtRatioAX96: u256.MustFromDecimal("1461446703485210103287273052203988822378723970340"), + sqrtRatioBX96: MAX_SQRT_RATIO, + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + getAmount0DeltaHelper(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity, tt.roundUp) + }) + return + } + + result := getAmount0DeltaHelper(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity, tt.roundUp) + uassert.Equal(t, tt.expected, result.ToString()) + }) + } +} + +// TestGetAmount1DeltaHelper tests amount1 calculation helper +func TestGetAmount1DeltaHelper(t *testing.T) { + tests := []struct { + name string + sqrtRatioAX96 *u256.Uint + sqrtRatioBX96 *u256.Uint + liquidity *u256.Uint + roundUp bool + expected string + }{ + // Basic cases + { + name: "zero_liquidity", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("2", "1"), + liquidity: u256.Zero(), + roundUp: true, + expected: "0", + }, + { + name: "equal_prices", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("1", "1"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "0", + }, + { + name: "swapped_inputs", + sqrtRatioAX96: encodePriceSqrt("2", "1"), + sqrtRatioBX96: encodePriceSqrt("1", "1"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "414213562373095049", + }, + // Rounding tests + { + name: "round_up_true", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "100000000000000000", + }, + { + name: "round_up_false", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: false, + expected: "99999999999999999", + }, + // Extreme values + { + name: "min_price_range", + sqrtRatioAX96: MIN_SQRT_RATIO, + sqrtRatioBX96: u256.MustFromDecimal("4295128740"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: true, + expected: "1", + }, + { + name: "max_price_range", + sqrtRatioAX96: u256.MustFromDecimal("1461446703485210103287273052203988822378723970340"), + sqrtRatioBX96: MAX_SQRT_RATIO, + liquidity: u256.MustFromDecimal("1000000000000000000"), + roundUp: false, + expected: "0", + }, + // Small liquidity, large price difference + { + name: "small_liquidity_large_diff", + sqrtRatioAX96: encodePriceSqrt("1", "100"), + sqrtRatioBX96: encodePriceSqrt("100", "1"), + liquidity: u256.One(), + roundUp: true, + expected: "10", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getAmount1DeltaHelper(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity, tt.roundUp) + uassert.Equal(t, tt.expected, result.ToString()) + }) + } +} + +// TestGetAmount0Delta tests string representation with signed liquidity +func TestGetAmount0Delta(t *testing.T) { + tests := []struct { + name string + sqrtRatioAX96 *u256.Uint + sqrtRatioBX96 *u256.Uint + liquidity *i256.Int + expected string + shouldPanic bool + panicMsg string + }{ + // Nil checks + { + name: "nil_sqrtRatioA", + sqrtRatioAX96: nil, + sqrtRatioBX96: u256.MustFromDecimal("1000000"), + liquidity: i256.FromUint256(u256.MustFromDecimal("5000000")), + shouldPanic: true, + panicMsg: errGetAmount0DeltaNilInput.Error(), + }, + { + name: "nil_sqrtRatioB", + sqrtRatioAX96: u256.MustFromDecimal("1000000"), + sqrtRatioBX96: nil, + liquidity: i256.FromUint256(u256.MustFromDecimal("5000000")), + shouldPanic: true, + panicMsg: errGetAmount0DeltaNilInput.Error(), + }, + { + name: "nil_liquidity", + sqrtRatioAX96: u256.MustFromDecimal("1000000"), + sqrtRatioBX96: u256.MustFromDecimal("2000000"), + liquidity: nil, + shouldPanic: true, + panicMsg: errGetAmount0DeltaNilInput.Error(), + }, + // Positive liquidity (roundUp = true) + { + name: "positive_liquidity", + sqrtRatioAX96: u256.MustFromDecimal("1000000"), + sqrtRatioBX96: u256.MustFromDecimal("2000000"), + liquidity: i256.FromUint256(u256.MustFromDecimal("5000000")), + expected: "198070406285660843983859875840", + }, + { + name: "positive_liquidity_equal_prices", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("1", "1"), + liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000")), + expected: "0", + }, + // Negative liquidity (roundDown = false) + { + name: "negative_liquidity", + sqrtRatioAX96: u256.MustFromDecimal("1000000"), + sqrtRatioBX96: u256.MustFromDecimal("2000000"), + liquidity: i256.New().Neg(i256.FromUint256(u256.MustFromDecimal("5000000"))), + expected: "-198070406285660843983859875840", + }, + // Zero liquidity + { + name: "zero_liquidity", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("2", "1"), + liquidity: i256.Zero(), + expected: "0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + GetAmount0Delta(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) + }) + return + } + + result := GetAmount0Delta(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) + uassert.Equal(t, tt.expected, result.ToString()) + }) + } +} + +// TestGetAmount1Delta tests string representation with signed liquidity +func TestGetAmount1Delta(t *testing.T) { + tests := []struct { + name string + sqrtRatioAX96 *u256.Uint + sqrtRatioBX96 *u256.Uint + liquidity *i256.Int + expected string + shouldPanic bool + panicMsg string + }{ + // Nil checks + { + name: "nil_all_parameters", + sqrtRatioAX96: nil, + sqrtRatioBX96: nil, + liquidity: nil, + shouldPanic: true, + panicMsg: errGetAmount1DeltaNilInput.Error(), + }, + // Positive liquidity (roundUp = true) + { + name: "positive_liquidity", + sqrtRatioAX96: u256.MustFromDecimal("1000000"), + sqrtRatioBX96: u256.MustFromDecimal("2000000"), + liquidity: i256.FromUint256(u256.MustFromDecimal("5000000")), + expected: "1", + }, + { + name: "positive_liquidity_large_range", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("4", "1"), + liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000")), + expected: "1000000000000000000", + }, + // Negative liquidity (roundDown = false) + { + name: "negative_liquidity", + sqrtRatioAX96: u256.MustFromDecimal("1000000"), + sqrtRatioBX96: u256.MustFromDecimal("2000000"), + liquidity: i256.New().Neg(i256.FromUint256(u256.MustFromDecimal("5000000"))), + expected: "0", + }, + { + name: "negative_liquidity_large", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("4", "1"), + liquidity: i256.New().Neg(i256.FromUint256(u256.MustFromDecimal("1000000000000000000"))), + expected: "-1000000000000000000", + }, + { + name: "positive_liquidity_large_values", + sqrtRatioAX96: Q96, + sqrtRatioBX96: new(u256.Uint).Mul(Q96, u256.NewUint(2)), + liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000000000000")), + expected: "1000000000000000000000000000", + }, + // Zero liquidity + { + name: "zero_liquidity", + sqrtRatioAX96: encodePriceSqrt("1", "1"), + sqrtRatioBX96: encodePriceSqrt("2", "1"), + liquidity: i256.Zero(), + expected: "0", + }, + // Equal prices + { + name: "equal_prices_positive_liquidity", + sqrtRatioAX96: Q96, + sqrtRatioBX96: Q96, + liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000")), + expected: "0", + }, + // Swapped inputs + { + name: "swapped_prices_positive_liquidity", + sqrtRatioAX96: encodePriceSqrt("4", "1"), + sqrtRatioBX96: encodePriceSqrt("1", "1"), // B < A, will be swapped + liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000")), + expected: "1000000000000000000", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + GetAmount1Delta(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) + }) + return + } + + result := GetAmount1Delta(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) + + if tt.name == "positive_liquidity_large_values" { + if tt.expected != result.ToString() { + t.Logf("Result for %s: %s", tt.name, result) + } + } + + uassert.Equal(t, tt.expected, result.ToString()) + }) + } +} + +// TestInputParameterImmutability verifies functions don't modify inputs +func TestInputParameterImmutability(t *testing.T) { + tests := []struct { + name string + fn func(*testing.T) + }{ + { + name: "getNextPriceAmount0Add_immutable", + fn: func(t *testing.T) { + sqrtP := Q96 + liquidity := u256.MustFromDecimal("1000000000000000000") + amount := u256.MustFromDecimal("100000000000000000") + + sqrtPCopy := sqrtP.ToString() + liquidityCopy := liquidity.ToString() + amountCopy := amount.ToString() + + _ = getNextPriceAmount0Add(sqrtP, liquidity, amount) + + uassert.Equal(t, sqrtPCopy, sqrtP.ToString()) + uassert.Equal(t, liquidityCopy, liquidity.ToString()) + uassert.Equal(t, amountCopy, amount.ToString()) + }, + }, + { + name: "getAmount0DeltaHelper_parameter_swap_immutable", + fn: func(t *testing.T) { + sqrtA := u256.MustFromDecimal("1000000") + sqrtB := u256.MustFromDecimal("500000") + liquidity := u256.MustFromDecimal("1000000") + + originalA := sqrtA.ToString() + originalB := sqrtB.ToString() + + _ = getAmount0DeltaHelper(sqrtA, sqrtB, liquidity, true) + + uassert.Equal(t, originalA, sqrtA.ToString()) + uassert.Equal(t, originalB, sqrtB.ToString()) + }, + }, + { + name: "all_input_functions_immutable", + fn: func(t *testing.T) { + sqrtP := Q96 + liquidity := u256.MustFromDecimal("1000000000000000000") + amount := u256.MustFromDecimal("1000000") + + original := sqrtP.ToString() + + // Test all input/output functions + _ = getNextSqrtPriceFromInput(sqrtP, liquidity, amount, true) + uassert.Equal(t, original, sqrtP.ToString()) + + _ = getNextSqrtPriceFromInput(sqrtP, liquidity, amount, false) + uassert.Equal(t, original, sqrtP.ToString()) + + _ = getNextSqrtPriceFromOutput(sqrtP, liquidity, amount, true) + uassert.Equal(t, original, sqrtP.ToString()) + + _ = getNextSqrtPriceFromOutput(sqrtP, liquidity, amount, false) + uassert.Equal(t, original, sqrtP.ToString()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.fn(t) + }) + } +} + +// TestRoundingConsistency verifies rounding behavior +func TestRoundingConsistency(t *testing.T) { + tests := []struct { + name string + sqrtA *u256.Uint + sqrtB *u256.Uint + liquidity *u256.Uint + isToken0 bool + }{ + { + name: "amount0_minimal_difference", + sqrtA: Q96, + sqrtB: u256.MustFromDecimal("79228162514264337593543950337"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + isToken0: true, + }, + { + name: "amount1_minimal_difference", + sqrtA: Q96, + sqrtB: u256.MustFromDecimal("79228162514264337593543950337"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + isToken0: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var amountRoundUp, amountRoundDown *u256.Uint + + if tt.isToken0 { + amountRoundUp = getAmount0DeltaHelper(tt.sqrtA, tt.sqrtB, tt.liquidity, true) + amountRoundDown = getAmount0DeltaHelper(tt.sqrtA, tt.sqrtB, tt.liquidity, false) + } else { + amountRoundUp = getAmount1DeltaHelper(tt.sqrtA, tt.sqrtB, tt.liquidity, true) + amountRoundDown = getAmount1DeltaHelper(tt.sqrtA, tt.sqrtB, tt.liquidity, false) + } + + // Round up should always be >= round down + uassert.True(t, amountRoundUp.Gte(amountRoundDown), "Round up should be >= round down") + + // Difference should be at most 1 + if !amountRoundUp.IsZero() || !amountRoundDown.IsZero() { + diff := new(u256.Uint).Sub(amountRoundUp, amountRoundDown) + uassert.True(t, diff.Lte(u256.One()), "Rounding difference should be at most 1") + } + }) + } +} + +// TestMathematicalInvariants tests important mathematical properties +func TestMathematicalInvariants(t *testing.T) { + // Test 1: Round trip precision + t.Run("round_trip_precision", func(t *testing.T) { + testCases := []struct { + liquidity string + amount string + maxError string + }{ + {"100000000000000000000", "100000000000000000", "792281625"}, + {"10000000000000000000", "100000000000000000", "7922816251"}, + {"1000000000000000000", "100000000000000000", "79228162513"}, + } + + for _, tc := range testCases { + sqrtP := Q96 + liquidity := u256.MustFromDecimal(tc.liquidity) + amount := u256.MustFromDecimal(tc.amount) + maxError := u256.MustFromDecimal(tc.maxError) + + // Add then remove same amount + priceAfterAdd := getNextSqrtPriceFromInput(sqrtP, liquidity, amount, true) + actualAmount := getAmount0DeltaHelper(priceAfterAdd, sqrtP, liquidity, false) + priceAfterRemove := getNextSqrtPriceFromOutput(priceAfterAdd, liquidity, actualAmount, false) + + // Calculate error + var diff *u256.Uint + if priceAfterRemove.Gt(sqrtP) { + diff = new(u256.Uint).Sub(priceAfterRemove, sqrtP) + } else { + diff = new(u256.Uint).Sub(sqrtP, priceAfterRemove) + } + + if diff.Gt(maxError) { + t.Errorf("Round trip error too large for liquidity %s: %s wei (max: %s)", + tc.liquidity, diff.ToString(), maxError.ToString()) + } + } + }) + + // Test 2: Monotonicity + t.Run("monotonicity", func(t *testing.T) { + sqrtP := Q96 + liquidity := u256.MustFromDecimal("1000000000000000000") + + amounts := []*u256.Uint{ + u256.MustFromDecimal("1000"), + u256.MustFromDecimal("10000"), + u256.MustFromDecimal("100000"), + u256.MustFromDecimal("1000000"), + } + + var prevPrice *u256.Uint = sqrtP + + // Adding more token0 should decrease price more + for i, amount := range amounts { + newPrice := getNextSqrtPriceFromInput(sqrtP, liquidity, amount, true) + + if i > 0 && !newPrice.Lt(prevPrice) { + t.Errorf("Monotonicity violated: larger amount didn't decrease price more") + } + + prevPrice = newPrice + } + }) + + // Test 3: Symmetry + t.Run("symmetry", func(t *testing.T) { + sqrtP := Q96 + liquidity := u256.MustFromDecimal("1000000000000000000") + amount := u256.MustFromDecimal("100000000000000000") + + // Add token0, get amount1 out + priceAfter0 := getNextSqrtPriceFromInput(sqrtP, liquidity, amount, true) + amount1Out := getAmount1DeltaHelper(priceAfter0, sqrtP, liquidity, false) + + // Add token1, get amount0 out + priceAfter1 := getNextSqrtPriceFromInput(sqrtP, liquidity, amount, false) + amount0Out := getAmount0DeltaHelper(sqrtP, priceAfter1, liquidity, false) + + // The product of price ratios should be close to 1 + // Due to rounding, we allow small deviation + t.Logf("Token0 in: %s, Token1 out: %s", amount.ToString(), amount1Out.ToString()) + t.Logf("Token1 in: %s, Token0 out: %s", amount.ToString(), amount0Out.ToString()) + }) +} + +// TestPathIndependence verifies swap order doesn't affect final state +func TestPathIndependence(t *testing.T) { + startPrice := u256.MustFromDecimal("79228162514264337593543950336") + liquidity := u256.MustFromDecimal("1000000000000000000") + + amount0 := u256.MustFromDecimal("10000000000000000") + amount1 := u256.MustFromDecimal("10000000000000000") + + // Path 1: Add token0, then add token1 + price1 := getNextSqrtPriceFromInput(startPrice, liquidity, amount0, true) + price1 = getNextSqrtPriceFromInput(price1, liquidity, amount1, false) + + // Path 2: Add token1, then add token0 + price2 := getNextSqrtPriceFromInput(startPrice, liquidity, amount1, false) + price2 = getNextSqrtPriceFromInput(price2, liquidity, amount0, true) + + // Calculate percentage difference + var diff *u256.Uint + if price1.Gt(price2) { + diff = new(u256.Uint).Sub(price1, price2) + } else { + diff = new(u256.Uint).Sub(price2, price1) + } + + avgPrice := new(u256.Uint).Add(price1, price2) + avgPrice.Div(avgPrice, u256.NewUint(2)) + percentDiff := new(u256.Uint).Mul(diff, u256.MustFromDecimal("10000")) + percentDiff.Div(percentDiff, avgPrice) + + t.Logf("Path 1 final price: %s", price1.ToString()) + t.Logf("Path 2 final price: %s", price2.ToString()) + t.Logf("Percentage difference: %s basis points", percentDiff.ToString()) + + // Should be less than 10 basis points (0.1%) + maxAllowedDiff := u256.NewUint(10) + if percentDiff.Gt(maxAllowedDiff) { + t.Errorf("Path independence error too large: %s bp (max allowed: %s bp)", + percentDiff.ToString(), maxAllowedDiff.ToString()) + } +} + +// TestHelperFunctionConsistency verifies helper functions work together correctly +func TestHelperFunctionConsistency(t *testing.T) { + sqrtP := Q96 + liquidity := u256.MustFromDecimal("1000000000000000000") + amount := u256.MustFromDecimal("100000000000000000") + + // Test token0 add path + t.Run("token0_add_consistency", func(t *testing.T) { + // Direct calculation + directPrice := getNextPriceAmount0Add(sqrtP, liquidity, amount) + + // Through wrapper + wrapperPrice := getNextSqrtPriceFromAmount0RoundingUp(sqrtP, liquidity, amount, true) + + uassert.Equal(t, directPrice.ToString(), wrapperPrice.ToString()) + }) + + // Test token0 remove path + t.Run("token0_remove_consistency", func(t *testing.T) { + // Use smaller amount to avoid insufficient liquidity + smallAmount := u256.MustFromDecimal("1000000") + + // Direct calculation + directPrice := getNextPriceAmount0Remove(sqrtP, liquidity, smallAmount) + + // Through wrapper + wrapperPrice := getNextSqrtPriceFromAmount0RoundingUp(sqrtP, liquidity, smallAmount, false) + + uassert.Equal(t, directPrice.ToString(), wrapperPrice.ToString()) + }) + + // Test token1 paths similarly + t.Run("token1_consistency", func(t *testing.T) { + // Add + directAdd := getNextPriceAmount1Add(sqrtP, liquidity, amount) + wrapperAdd := getNextSqrtPriceFromAmount1RoundingDown(sqrtP, liquidity, amount, true) + uassert.Equal(t, directAdd.ToString(), wrapperAdd.ToString()) + + // Remove + smallAmount := u256.MustFromDecimal("1000000") + directRemove := getNextPriceAmount1Remove(sqrtP, liquidity, smallAmount) + wrapperRemove := getNextSqrtPriceFromAmount1RoundingDown(sqrtP, liquidity, smallAmount, false) + uassert.Equal(t, directRemove.ToString(), wrapperRemove.ToString()) + }) +} + +// Helper functions +func encodePriceSqrt(reserve1, reserve0 string) *u256.Uint { + reserve1Uint := u256.MustFromDecimal(reserve1) + reserve0Uint := u256.MustFromDecimal(reserve0) + + if reserve0Uint.IsZero() { + panic("division by zero") + } + + // numerator = reserve1 * (2^192) + two192 := new(u256.Uint).Lsh(u256.NewUint(1), 192) + numerator := new(u256.Uint).Mul(reserve1Uint, two192) + + // ratioX192 = numerator / reserve0 + ratioX192 := new(u256.Uint).Div(numerator, reserve0Uint) + + // Return sqrt(ratioX192) + return sqrt(ratioX192) +} + +func sqrt(x *u256.Uint) *u256.Uint { + if x.IsZero() { + return u256.NewUint(0) + } + + z := new(u256.Uint).Set(x) + y := new(u256.Uint).Rsh(z, 1) + + for y.Cmp(z) < 0 { + z.Set(y) + temp := new(u256.Uint).Div(x, z) + y.Add(z, temp).Rsh(y, 1) + } + return z +} diff --git a/contract/p/gnoswap/gnsmath/swap_math.gno b/contract/p/gnoswap/gnsmath/swap_math.gno new file mode 100644 index 0000000..d106ad1 --- /dev/null +++ b/contract/p/gnoswap/gnsmath/swap_math.gno @@ -0,0 +1,234 @@ +package gnsmath + +import ( + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +const denominator = uint64(1_000_000) + +// SwapMathComputeSwapStep computes the next sqrt price, amount in, amount out, and fee amount +// for a swap step within a single tick range. +// +// Parameters: +// - sqrtRatioCurrentX96: current sqrt price in Q96 format +// - sqrtRatioTargetX96: target sqrt price (tick boundary) +// - liquidity: available liquidity in the range +// - amountRemaining: amount left to swap (positive=exact in, negative=exact out) +// - feePips: fee in hundredths of a bip (3000 = 0.3%) +// +// Returns sqrtRatioNextX96, amountIn, amountOut, feeAmount. +func SwapMathComputeSwapStep( + sqrtRatioCurrentX96 *u256.Uint, + sqrtRatioTargetX96 *u256.Uint, + liquidity *u256.Uint, + amountRemaining *i256.Int, + feePips uint64, +) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) { + if sqrtRatioCurrentX96 == nil || sqrtRatioTargetX96 == nil || + liquidity == nil || amountRemaining == nil { + panic("SwapMathComputeSwapStep: invalid input") + } + + // This function is publicly accessible and can be called by external users or contracts. + // While the pool realm only uses predefined fee values (100, 500, 3000, 10000) which are safely within range, + // external callers could potentially pass any feePips value. The fee calculation involves subtracting feePips + // from 1000000 (representing 100%), and if feePips exceeds 1000000, it would cause an underflow, + // leading to incorrect fee calculations. + if feePips > denominator { + panic("SwapMathComputeSwapStep: feePips must be less than or equal to 1000000") + } + + // zeroForOne determines swap direction based on the relationship of current vs. target + zeroForOne := sqrtRatioCurrentX96.Gte(sqrtRatioTargetX96) + + // POSITIVE == EXACT_IN => Estimated AmountOut + // NEGATIVE == EXACT_OUT => Estimated AmountIn + exactIn := !amountRemaining.IsNeg() + + amountRemainingAbs := amountRemaining.Abs() + feeRateInPips := u256.NewUint(feePips) + withoutFeeRateInPips := u256.NewUint(denominator - feePips) + + sqrtRatioNextX96 := u256.Zero() + amountIn := u256.Zero() + amountOut := u256.Zero() + feeAmount := u256.Zero() + + if exactIn { + // Handle EXACT_IN scenario as a separate function + sqrtRatioNextX96, amountIn = handleExactIn( + zeroForOne, + sqrtRatioCurrentX96, + sqrtRatioTargetX96, + liquidity, + amountRemainingAbs, // use absolute value here + withoutFeeRateInPips, + ) + } else { + // Handle EXACT_OUT scenario as a separate function + sqrtRatioNextX96, amountOut = handleExactOut( + zeroForOne, + sqrtRatioCurrentX96, + sqrtRatioTargetX96, + liquidity, + amountRemainingAbs, + ) + } + + // isMax checks if we've hit the boundary price (target) + isMax := sqrtRatioTargetX96.Eq(sqrtRatioNextX96) + + // Calculate final amountIn, amountOut if needed + if zeroForOne { + // If isMax && exactIn, we already have the correct amountIn + if !(isMax && exactIn) { + amountIn = getAmount0DeltaHelper( + sqrtRatioNextX96, + sqrtRatioCurrentX96, + liquidity, + true, + ) + } + // If isMax && !exactIn, we already have the correct amountOut + if !(isMax && !exactIn) { + amountOut = getAmount1DeltaHelper( + sqrtRatioNextX96, + sqrtRatioCurrentX96, + liquidity, + false, + ) + } + } else { + if !(isMax && exactIn) { + amountIn = getAmount1DeltaHelper( + sqrtRatioCurrentX96, + sqrtRatioNextX96, + liquidity, + true, + ) + } + if !(isMax && !exactIn) { + amountOut = getAmount0DeltaHelper( + sqrtRatioCurrentX96, + sqrtRatioNextX96, + liquidity, + false, + ) + } + } + + // If we're in EXACT_OUT mode but overcalculated 'amountOut' + if !exactIn && amountOut.Gt(amountRemainingAbs) { + amountOut = amountRemainingAbs + } + + // Fee logic + // If exactIn and we haven't hit the target, the difference is the fee + // Else, compute fee from feePips + if exactIn && !sqrtRatioNextX96.Eq(sqrtRatioTargetX96) { + feeAmount = u256.Zero().Sub(amountRemainingAbs, amountIn) + } else { + feeAmount = u256.MulDivRoundingUp( + amountIn, + feeRateInPips, + withoutFeeRateInPips, + ) + } + + // Final sanity check for resulting price + if sqrtRatioNextX96.Lt(MIN_SQRT_RATIO) || sqrtRatioNextX96.Gt(MAX_SQRT_RATIO) { + panic(errInvalidPoolSqrtPrice) + } + + return sqrtRatioNextX96, amountIn, amountOut, feeAmount +} + +// handleExactIn handles the EXACT_IN scenario for swaps, returning the next sqrt price and provisional +// amount in while accounting for fees by reducing the input amount. +// This internal function processes swaps where the input amount is specified exactly. +func handleExactIn( + zeroForOne bool, + sqrtRatioCurrentX96, + sqrtRatioTargetX96, + liquidity, + amountRemainingAbs, + withoutFeeRateInPips *u256.Uint, +) (*u256.Uint, *u256.Uint) { + amountRemainingLessFee := u256.MulDiv( + amountRemainingAbs, + withoutFeeRateInPips, + u256.NewUint(denominator), + ) + + // Special case: + // When the remaining amount to be swapped becomes 1 during a tick swap, + // the swap fee becomes less than 0. + // At this point, check whether the swap is no longer being executed. + if amountRemainingLessFee.IsZero() { + return sqrtRatioCurrentX96, u256.Zero() + } + + var amountIn *u256.Uint + if zeroForOne { + amountIn = getAmount0DeltaHelper( + sqrtRatioTargetX96, + sqrtRatioCurrentX96, + liquidity, + true, + ) + } else { + amountIn = getAmount1DeltaHelper( + sqrtRatioCurrentX96, + sqrtRatioTargetX96, + liquidity, + true, + ) + } + + if amountRemainingLessFee.Gte(amountIn) { + return sqrtRatioTargetX96, amountIn + } + + // We don't reach target price; use partial move + nextSqrt := getNextSqrtPriceFromInput( + sqrtRatioCurrentX96, + liquidity, + amountRemainingLessFee, + zeroForOne, + ) + + // Return the partially moved price and calculate amountIn later + // This avoids double calculation and ensures consistency + return nextSqrt, amountRemainingLessFee +} + +// handleExactOut handles the EXACT_OUT scenario for swaps, returning the next sqrt price and provisional +// amount out while checking if sufficient liquidity exists to fulfill the requested output amount. +// This internal function processes swaps where the output amount is specified exactly. +func handleExactOut( + zeroForOne bool, + sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, amountRemainingAbs *u256.Uint, +) (*u256.Uint, *u256.Uint) { + var amountOut *u256.Uint + if zeroForOne { + amountOut = getAmount1DeltaHelper(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false) + } else { + amountOut = getAmount0DeltaHelper(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false) + } + + // Fast path: if sufficient liquidity, use target price + if amountRemainingAbs.Gte(amountOut) { + return sqrtRatioTargetX96, amountOut + } + + // Otherwise, partial move: compute next price from residual output amount + nextSqrt := getNextSqrtPriceFromOutput( + sqrtRatioCurrentX96, + liquidity, + amountRemainingAbs, + zeroForOne, + ) + + return nextSqrt, amountRemainingAbs +} diff --git a/contract/p/gnoswap/gnsmath/swap_math_test.gno b/contract/p/gnoswap/gnsmath/swap_math_test.gno new file mode 100644 index 0000000..a6e4bc5 --- /dev/null +++ b/contract/p/gnoswap/gnsmath/swap_math_test.gno @@ -0,0 +1,851 @@ +package gnsmath + +import ( + "testing" + + "gno.land/p/nt/uassert" + "gno.land/p/nt/ufmt" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +func TestSwapMathComputeSwapStep(t *testing.T) { + tests := []struct { + name string + currentX96, targetX96 *u256.Uint + liquidity *u256.Uint + amountRemaining *i256.Int + feePips uint64 + sqrtNextX96 *u256.Uint + chkSqrtNextX96 func(sqrtRatioNextX96, priceTarget *u256.Uint) + amountIn, amountOut, feeAmount string + }{ + // Basic swap + { + name: "exact_amount_in_capped_at_price_target_one_for_zero", + currentX96: encodePriceSqrtTest(t, "1", "1"), + targetX96: encodePriceSqrtTest(t, "101", "100"), + liquidity: u256.MustFromDecimal("2000000000000000000"), + amountRemaining: i256.MustFromDecimal("1000000000000000000"), + feePips: 600, + sqrtNextX96: encodePriceSqrtTest(t, "101", "100"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) + }, + amountIn: "9975124224178055", + amountOut: "9925619580021728", + feeAmount: "5988667735148", + }, + { + name: "exact_amount_out_capped_at_price_target_one_for_zero", + currentX96: encodePriceSqrtTest(t, "1", "1"), + targetX96: encodePriceSqrtTest(t, "101", "100"), + liquidity: u256.MustFromDecimal("2000000000000000000"), + amountRemaining: i256.MustFromDecimal("-1000000000000000000"), + feePips: 600, + sqrtNextX96: encodePriceSqrtTest(t, "101", "100"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) + }, + amountIn: "9975124224178055", + amountOut: "9925619580021728", + feeAmount: "5988667735148", + }, + { + name: "exact_amount_in_fully_spent_one_for_zero", + currentX96: encodePriceSqrtTest(t, "1", "1"), + targetX96: encodePriceSqrtTest(t, "1000", "100"), + liquidity: u256.MustFromDecimal("2000000000000000000"), + amountRemaining: i256.MustFromDecimal("1000000000000000000"), + sqrtNextX96: encodePriceSqrtTest(t, "1000", "100"), + feePips: 600, + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Lte(priceTarget)) + }, + amountIn: "999400000000000000", + amountOut: "666399946655997866", + feeAmount: "600000000000000", + }, + { + name: "exact_amount_out_fully_received_one_for_zero", + currentX96: encodePriceSqrtTest(t, "1", "1"), + targetX96: encodePriceSqrtTest(t, "1000", "100"), + liquidity: u256.MustFromDecimal("2000000000000000000"), + amountRemaining: i256.MustFromDecimal("-1000000000000000000"), + feePips: 600, + sqrtNextX96: encodePriceSqrtTest(t, "1000", "100"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Lt(priceTarget)) + }, + amountIn: "2000000000000000000", + amountOut: "1000000000000000000", + feeAmount: "1200720432259356", + }, + { + name: "amount_out_capped_at_desired_amount", + currentX96: u256.MustFromDecimal("417332158212080721273783715441582"), + targetX96: u256.MustFromDecimal("1452870262520218020823638996"), + liquidity: u256.MustFromDecimal("159344665391607089467575320103"), + amountRemaining: i256.MustFromDecimal("-1"), + feePips: 1, + sqrtNextX96: u256.MustFromDecimal("417332158212080721273783715441581"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) + }, + amountIn: "1", + amountOut: "1", + feeAmount: "1", + }, + // Edge cases - zero values + { + name: "zero_liquidity", + currentX96: encodePriceSqrtTest(t, "1", "1"), + targetX96: encodePriceSqrtTest(t, "2", "1"), + liquidity: u256.Zero(), + amountRemaining: i256.MustFromDecimal("1000000"), + feePips: 3000, + sqrtNextX96: encodePriceSqrtTest(t, "1", "1"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(encodePriceSqrtTest(t, "2", "1"))) + }, + amountIn: "0", + amountOut: "0", + feeAmount: "0", + }, + { + name: "zero_amount_remaining", + currentX96: encodePriceSqrtTest(t, "1", "1"), + targetX96: encodePriceSqrtTest(t, "2", "1"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountRemaining: i256.Zero(), + feePips: 3000, + sqrtNextX96: encodePriceSqrtTest(t, "1", "1"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(encodePriceSqrtTest(t, "1", "1"))) + }, + amountIn: "0", + amountOut: "0", + feeAmount: "0", + }, + // Edge cases - extreme prices + { + name: "extreme_low_price_with_fee", + currentX96: MIN_SQRT_RATIO, + targetX96: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(1000)), + liquidity: u256.MustFromDecimal("1"), + amountRemaining: i256.MustFromDecimal("1000000"), + feePips: 1, + sqrtNextX96: MIN_SQRT_RATIO, + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Gte(MIN_SQRT_RATIO)) + }, + amountIn: "1", + amountOut: "4294643428317", + feeAmount: "1", + }, + // Fee edge cases + { + name: "entire_input_amount_taken_as_fee", + currentX96: u256.MustFromDecimal("4295128739"), + targetX96: u256.MustFromDecimal("79887613182836312"), + liquidity: u256.MustFromDecimal("1985041575832132834610021537970"), + amountRemaining: i256.MustFromDecimal("10"), + feePips: 1872, + sqrtNextX96: u256.MustFromDecimal("4295128739"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) + }, + amountIn: "0", + amountOut: "0", + feeAmount: "10", + }, + { + name: "maximum_fee_100_percent", + currentX96: encodePriceSqrtTest(t, "1", "1"), + targetX96: encodePriceSqrtTest(t, "2", "1"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amountRemaining: i256.MustFromDecimal("1000000"), + feePips: 1000000, + sqrtNextX96: encodePriceSqrtTest(t, "1", "1"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(encodePriceSqrtTest(t, "1", "1"))) + }, + amountIn: "0", + amountOut: "0", + feeAmount: "1000000", + }, + { + name: "rounding_to_zero_after_fee", + currentX96: Q96, + targetX96: new(u256.Uint).Lsh(Q96, 1), + liquidity: u256.MustFromDecimal("1000000"), + amountRemaining: i256.MustFromDecimal("1"), + feePips: 999999, + sqrtNextX96: Q96, + chkSqrtNextX96: func(sqrt, target *u256.Uint) { + uassert.True(t, sqrt.Eq(Q96)) + }, + amountIn: "0", + amountOut: "0", + feeAmount: "1", + }, + // Insufficient liquidity cases + { + name: "insufficient_liquidity_zero_for_one_exact_output", + currentX96: u256.MustFromDecimal("20282409603651670423947251286016"), + targetX96: u256.MustFromDecimal("22310650564016837466341976414617"), + liquidity: u256.MustFromDecimal("1024"), + amountRemaining: i256.MustFromDecimal("-4"), + feePips: 3000, + sqrtNextX96: u256.MustFromDecimal("22310650564016837466341976414617"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) + }, + amountIn: "26215", + amountOut: "0", + feeAmount: "79", + }, + { + name: "insufficient_liquidity_one_for_zero_exact_output", + currentX96: u256.MustFromDecimal("20282409603651670423947251286016"), + targetX96: u256.MustFromDecimal("18254168643286503381552526157414"), + liquidity: u256.MustFromDecimal("1024"), + amountRemaining: i256.MustFromDecimal("-263000"), + feePips: 3000, + sqrtNextX96: u256.MustFromDecimal("18254168643286503381552526157414"), + chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { + uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) + }, + amountIn: "1", + amountOut: "26214", + feeAmount: "1", + }, + // Target price uses partial input amount (removed problematic test case) + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sqrtRatioNextX96, amountIn, amountOut, feeAmount := SwapMathComputeSwapStep( + test.currentX96, test.targetX96, test.liquidity, test.amountRemaining, test.feePips, + ) + test.chkSqrtNextX96(sqrtRatioNextX96, test.sqrtNextX96) + uassert.Equal(t, amountIn.ToString(), test.amountIn) + uassert.Equal(t, amountOut.ToString(), test.amountOut) + uassert.Equal(t, feeAmount.ToString(), test.feeAmount) + }) + } +} + +func TestSwapMathFeeConsistency(t *testing.T) { + tests := []struct { + name string + current *u256.Uint + target *u256.Uint + liquidity *u256.Uint + amount *i256.Int + fee_pips uint64 + }{ + { + name: "fee_consistency_100_pips", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000000000000000"), + fee_pips: 100, + }, + { + name: "fee_consistency_500_pips", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000000000000000"), + fee_pips: 500, + }, + { + name: "fee_consistency_3000_pips", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000000000000000"), + fee_pips: 3000, + }, + { + name: "fee_consistency_10000_pips", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000000000000000"), + fee_pips: 10000, + }, + { + name: "fee_consistency_tiny_liquidity", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "121", "100"), + liquidity: u256.NewUint(1), // low liquidity + amount: i256.MustFromDecimal("1000000"), + fee_pips: 3000, + }, + { + name: "fee_consistency_max_fee", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "121", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000"), + fee_pips: 999999, // 99.9999% + }, + { + name: "fee_consistency_zero_liquidity_exactIn", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "2", "1"), + liquidity: u256.Zero(), + amount: i256.MustFromDecimal("1000000"), + fee_pips: 3000, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sqrtNext, amountIn, _, feeAmount := SwapMathComputeSwapStep( + test.current, test.target, test.liquidity, test.amount, test.fee_pips, + ) + + amount_in_num := amountIn + fee_amount_num := feeAmount + sqrt_next_num := sqrtNext + + // Check if it's a partial fill (didn't reach target) + is_partial_fill := !sqrt_next_num.Eq(test.target) + + if is_partial_fill && !test.amount.IsNeg() { // exactIn mode + // For partial fills in exactIn: fee = amountRemaining - amountIn + expected_fee := new(u256.Uint).Sub(test.amount.Abs(), amount_in_num) + uassert.True(t, fee_amount_num.Eq(expected_fee), + ufmt.Sprintf("Partial fill fee should be %s, got %s", expected_fee.ToString(), fee_amount_num.ToString())) + } else { + // Normal case: correct formula is amountIn * feePips / (1e6 - feePips) + fee_denominator := new(u256.Uint).Sub(u256.NewUint(1000000), u256.NewUint(test.fee_pips)) + expected_fee := u256.MulDivRoundingUp(amount_in_num, u256.NewUint(test.fee_pips), fee_denominator) + + uassert.True(t, fee_amount_num.Eq(expected_fee), + ufmt.Sprintf("Fee %s should equal %s (fee_pips: %d)", fee_amount_num.ToString(), expected_fee.ToString(), test.fee_pips)) + } + }) + } +} + +func TestSwapMathPriceBounds(t *testing.T) { + tests := []struct { + name string + current *u256.Uint + target *u256.Uint + liquidity *u256.Uint + amount *i256.Int + fee_pips uint64 + zero_for_one bool + }{ + { + name: "zero_for_one_price_decreases", + current: encodePriceSqrtTest(t, "100", "100"), + target: encodePriceSqrtTest(t, "90", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000000000000"), + fee_pips: 3000, + zero_for_one: true, + }, + { + name: "one_for_zero_price_increases", + current: encodePriceSqrtTest(t, "100", "100"), + target: encodePriceSqrtTest(t, "110", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000000000000"), + fee_pips: 3000, + zero_for_one: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sqrtNext, _, _, _ := SwapMathComputeSwapStep( + test.current, test.target, test.liquidity, test.amount, test.fee_pips, + ) + + sqrt_next_num := sqrtNext + + if test.zero_for_one { + uassert.True(t, sqrt_next_num.Gte(test.target), + "Price should not go below target for zero_for_one") + uassert.True(t, sqrt_next_num.Lte(test.current), + "Price should decrease for zero_for_one") + } else { + uassert.True(t, sqrt_next_num.Lte(test.target), + "Price should not go above target for one_for_zero") + uassert.True(t, sqrt_next_num.Gte(test.current), + "Price should increase for one_for_zero") + } + }) + } +} + +func TestSwapMathSymmetry(t *testing.T) { + tests := []struct { + name string + current *u256.Uint + target *u256.Uint + liquidity *u256.Uint + amount *i256.Int + fee_pips uint64 + }{ + { + name: "zero_for_one_symmetry", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "4", "1"), + liquidity: u256.MustFromDecimal("1000000000"), + amount: i256.MustFromDecimal("1000"), + fee_pips: 3000, + }, + { + name: "one_for_zero_symmetry", + current: encodePriceSqrtTest(t, "4", "1"), + target: encodePriceSqrtTest(t, "1", "1"), + liquidity: u256.MustFromDecimal("1000000000"), + amount: i256.MustFromDecimal("1000"), + fee_pips: 3000, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Forward swap + sqrt1, _, out1, fee1 := SwapMathComputeSwapStep( + test.current, test.target, test.liquidity, test.amount, test.fee_pips, + ) + + // Reverse swap + out1_neg := i256.FromUint256(out1.Clone()).Neg(i256.FromUint256(out1.Clone())) + + sqrt2, _, _, fee2 := SwapMathComputeSwapStep( + sqrt1, test.current, test.liquidity, out1_neg, test.fee_pips, + ) + + // Price should return to original + uassert.True(t, sqrt2.Eq(test.current), + "Price should return to original: got %s, want %s", + sqrt2.ToString(), test.current.ToString(), + ) + + // Verify fees are deducted + total_fees := new(u256.Uint).Add(fee1, fee2) + recovered := new(u256.Uint).Sub(u256.MustFromDecimal(test.amount.ToString()), total_fees) + uassert.True(t, recovered.Gt(u256.Zero()), + "Recovered amount %s should be > 0", recovered.ToString(), + ) + }) + } +} + +func TestSwapMathBoundaries(t *testing.T) { + tests := []struct { + name string + current_x96 *u256.Uint + target_x96 *u256.Uint + liquidity *u256.Uint + amount_remaining *i256.Int + fee_pips uint64 + expect_price_move bool + expect_amount_in bool + }{ + { + name: "min_boundary_one_for_zero", + current_x96: MIN_SQRT_RATIO, + target_x96: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(10000)), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount_remaining: i256.MustFromDecimal("1000000000000000"), + fee_pips: 3000, + expect_price_move: true, + expect_amount_in: true, + }, + { + name: "max_boundary_zero_for_one", + current_x96: MAX_SQRT_RATIO, + target_x96: new(u256.Uint).Sub(MAX_SQRT_RATIO, u256.NewUint(10000)), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount_remaining: i256.MustFromDecimal("1000000000000000"), + fee_pips: 3000, + expect_price_move: true, + expect_amount_in: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sqrt_next, amount_in, _, _ := SwapMathComputeSwapStep( + test.current_x96, test.target_x96, test.liquidity, test.amount_remaining, test.fee_pips, + ) + + sqrt_next_num := sqrt_next + amount_in_num := amount_in + + if test.expect_price_move { + uassert.True(t, !sqrt_next_num.Eq(test.current_x96), + "Price should move from boundary") + } + if test.expect_amount_in { + uassert.True(t, amount_in_num.Gt(u256.Zero()), + "Should have non-zero amount_in") + } + }) + } +} + +func TestSwapMathPartialFill(t *testing.T) { + tests := []struct { + name string + current *u256.Uint + target *u256.Uint + liquidity *u256.Uint + amount *i256.Int + fee_pips uint64 + }{ + { + name: "zero_for_one_partial_fill", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "100", "1"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000"), + fee_pips: 3000, + }, + { + name: "one_for_zero_partial_fill", + current: encodePriceSqrtTest(t, "100", "1"), + target: encodePriceSqrtTest(t, "1", "1"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000"), + fee_pips: 3000, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sqrt_next_str, in_str, _, fee_str := SwapMathComputeSwapStep( + test.current, + test.target, + test.liquidity, + test.amount, + test.fee_pips, + ) + sqrt_next := sqrt_next_str + in_amt := in_str + fee_amt := fee_str + + // Should not reach target with small amount + uassert.True(t, !sqrt_next.Eq(test.target), "Price should move but not reach target") + + // Fee equals remainder when partial fill + expected_fee := new(u256.Uint).Sub(u256.MustFromDecimal(test.amount.ToString()), in_amt) + uassert.True(t, fee_amt.Eq(expected_fee), + "Fee %s should equal remainder %s", + fee_amt.ToString(), expected_fee.ToString(), + ) + }) + } +} + +func TestSwapMathComputeSwapStepFail(t *testing.T) { + tests := []struct { + name string + current_x96 *u256.Uint + target_x96 *u256.Uint + liquidity *u256.Uint + amount_remaining *i256.Int + fee_pips uint64 + expected_message string + }{ + { + name: "nil_inputs", + current_x96: nil, + target_x96: nil, + liquidity: nil, + amount_remaining: nil, + fee_pips: 600, + expected_message: "SwapMathComputeSwapStep: invalid input", + }, + { + name: "fee_pips_exceeds_maximum", + current_x96: encodePriceSqrtTest(t, "1", "1"), + target_x96: encodePriceSqrtTest(t, "101", "100"), + liquidity: u256.MustFromDecimal("2000000000000000000"), + amount_remaining: i256.MustFromDecimal("1000000000000000000"), + fee_pips: 1000001, + expected_message: "SwapMathComputeSwapStep: feePips must be less than or equal to 1000000", + }, + { + name: "sqrt_price_below_minimum", + current_x96: u256.MustFromDecimal("2"), + target_x96: u256.MustFromDecimal("1"), + liquidity: u256.MustFromDecimal("1"), + amount_remaining: i256.MustFromDecimal("100"), + fee_pips: 1, + expected_message: errInvalidPoolSqrtPrice.Error(), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + uassert.PanicsWithMessage(t, test.expected_message, func() { + SwapMathComputeSwapStep( + test.current_x96, + test.target_x96, + test.liquidity, + test.amount_remaining, + test.fee_pips, + ) + }) + }) + } +} + +func TestSwapMathHighPrecision(t *testing.T) { + tests := []struct { + name string + current *u256.Uint + target *u256.Uint + liquidity *u256.Uint + amount *i256.Int + fee_pips uint64 + }{ + { + name: "high_precision_small_amounts", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "101", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1"), + fee_pips: 3000, + }, + { + name: "high_precision_large_amounts", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "121", "100"), + liquidity: u256.MustFromDecimal("340282366920938463463374607431768211455"), + amount: i256.MustFromDecimal("340282366920938463463374607431768211455"), + fee_pips: 3000, + }, + { + name: "precision_near_price_boundaries", + current: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(1)), + target: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(1000)), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000"), + fee_pips: 3000, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sqrtNext, amountIn, amountOut, feeAmount := SwapMathComputeSwapStep( + test.current, test.target, test.liquidity, test.amount, test.fee_pips, + ) + + // Basic sanity checks + sqrtNextNum := sqrtNext + amountInNum := amountIn + amountOutNum := amountOut + feeAmountNum := feeAmount + + uassert.True(t, sqrtNextNum.Gte(MIN_SQRT_RATIO), "sqrt price should be >= MIN_SQRT_RATIO") + uassert.True(t, sqrtNextNum.Lte(MAX_SQRT_RATIO), "sqrt price should be <= MAX_SQRT_RATIO") + uassert.True(t, amountInNum.Gte(u256.Zero()), "amountIn should be >= 0") + uassert.True(t, amountOutNum.Gte(u256.Zero()), "amountOut should be >= 0") + uassert.True(t, feeAmountNum.Gte(u256.Zero()), "feeAmount should be >= 0") + + // For exact input, total consumption should not exceed input + if !test.amount.IsNeg() { + total := new(u256.Uint).Add(amountInNum, feeAmountNum) + uassert.True(t, total.Lte(test.amount.Abs()), + ufmt.Sprintf("Total consumption %s should not exceed input %s", + total.ToString(), test.amount.Abs().ToString())) + } + }) + } +} + +func TestSwapMathExtremeFees(t *testing.T) { + tests := []struct { + name string + fee_pips uint64 + expect_no_swap bool + }{ + { + name: "minimal_fee_1_pip", + fee_pips: 1, + expect_no_swap: false, + }, + { + name: "low_fee_10_pips", + fee_pips: 10, + expect_no_swap: false, + }, + { + name: "medium_fee_3000_pips", + fee_pips: 3000, + expect_no_swap: false, + }, + { + name: "high_fee_50000_pips", + fee_pips: 50000, + expect_no_swap: false, + }, + { + name: "very_high_fee_500000_pips", + fee_pips: 500000, + expect_no_swap: false, + }, + } + + current := encodePriceSqrtTest(t, "1", "1") + target := encodePriceSqrtTest(t, "121", "100") + liquidity := u256.MustFromDecimal("1000000000000000000") + amount := i256.MustFromDecimal("1000000") + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sqrtNext, amountIn, _, feeAmount := SwapMathComputeSwapStep( + current, target, liquidity, amount, test.fee_pips, + ) + + amountInNum := amountIn + feeAmountNum := feeAmount + sqrtNextNum := sqrtNext + + if test.expect_no_swap { + // With extreme fees, most/all input goes to fees, no actual swap + uassert.True(t, amountInNum.IsZero() || amountInNum.Lt(u256.NewUint(100)), + "With extreme fees, amountIn should be very small or zero") + uassert.True(t, sqrtNextNum.Eq(current), + "With extreme fees, price should not move significantly") + } + + // Fee should never exceed original amount + uassert.True(t, feeAmountNum.Lte(amount.Abs()), + "Fee should not exceed input amount") + }) + } +} + +func TestSwapMathConsistencyChecks(t *testing.T) { + tests := []struct { + name string + current *u256.Uint + target *u256.Uint + liquidity *u256.Uint + amount *i256.Int + fee_pips uint64 + }{ + { + name: "consistency_small_amounts", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "110", "100"), + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000"), + fee_pips: 3000, + }, + { + name: "consistency_equal_prices", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "1", "1"), // same price + liquidity: u256.MustFromDecimal("1000000000000000000"), + amount: i256.MustFromDecimal("1000000"), + fee_pips: 3000, + }, + { + name: "consistency_minimal_liquidity", + current: encodePriceSqrtTest(t, "1", "1"), + target: encodePriceSqrtTest(t, "121", "100"), + liquidity: u256.NewUint(1), + amount: i256.MustFromDecimal("1000"), + fee_pips: 3000, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + sqrtNext, amountIn, amountOut, feeAmount := SwapMathComputeSwapStep( + test.current, test.target, test.liquidity, test.amount, test.fee_pips, + ) + + sqrtNextNum := sqrtNext + amountInNum := amountIn + amountOutNum := amountOut + feeAmountNum := feeAmount + + // Check that price movement is in the right direction + zeroForOne := test.current.Gte(test.target) + if !test.amount.IsNeg() { // exact input + if zeroForOne { + uassert.True(t, sqrtNextNum.Lte(test.current), + "For zeroForOne, price should decrease or stay same") + uassert.True(t, sqrtNextNum.Gte(test.target), + "Price should not go below target") + } else { + uassert.True(t, sqrtNextNum.Gte(test.current), + "For oneForZero, price should increase or stay same") + uassert.True(t, sqrtNextNum.Lte(test.target), + "Price should not go above target") + } + } + + // Special case: same price should result in no swap + if test.current.Eq(test.target) { + uassert.True(t, sqrtNextNum.Eq(test.current), + "When current == target, price should not change") + uassert.True(t, amountOutNum.IsZero(), + "When current == target, amountOut should be 0") + } + + // Conservation check: for exact input + if !test.amount.IsNeg() { + total := new(u256.Uint).Add(amountInNum, feeAmountNum) + uassert.True(t, total.Lte(test.amount.Abs()), + "amountIn + feeAmount should not exceed input") + } + }) + } +} + +// Helper functions + +func encodePriceSqrtTest(t *testing.T, reserve1, reserve0 string) *u256.Uint { + t.Helper() + + reserve1_uint := u256.MustFromDecimal(reserve1) + reserve0_uint := u256.MustFromDecimal(reserve0) + + if reserve0_uint.IsZero() { + panic("division by zero") + } + + two_192 := new(u256.Uint).Lsh(u256.NewUint(1), 192) + numerator := new(u256.Uint).Mul(reserve1_uint, two_192) + ratio_x192 := new(u256.Uint).Div(numerator, reserve0_uint) + + return sqrtTest(t, ratio_x192) +} + +func sqrtTest(t *testing.T, x *u256.Uint) *u256.Uint { + t.Helper() + + if x.IsZero() { + return u256.NewUint(0) + } + + z := new(u256.Uint).Set(x) + y := new(u256.Uint).Rsh(z, 1) + + temp := new(u256.Uint) + for y.Cmp(z) < 0 { + z.Set(y) + temp.Div(x, z) + y.Add(z, temp).Rsh(y, 1) + } + return z +} diff --git a/contract/p/gnoswap/int256/LICENSE b/contract/p/gnoswap/int256/LICENSE new file mode 100644 index 0000000..fc7e78a --- /dev/null +++ b/contract/p/gnoswap/int256/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Trịnh Đức Bảo Linh(Kevin) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/contract/p/gnoswap/int256/README.md b/contract/p/gnoswap/int256/README.md new file mode 100644 index 0000000..920113a --- /dev/null +++ b/contract/p/gnoswap/int256/README.md @@ -0,0 +1,35 @@ +# int256 + +256-bit signed integer arithmetic for GnoSwap. + +## Overview + +Fixed-size 256-bit signed integer library optimized for AMM calculations with overflow detection. + +## Features + +- Fixed 256-bit size (predictable gas costs) +- Two's complement representation +- Overflow detection on all operations +- AMM-optimized functions +- Range: -(2^255) to 2^255-1 + +## Usage + +```go +import i256 "gno.land/p/gnoswap/int256" + +// Create values +a := i256.NewInt(100) +b := i256.MustFromDecimal("-1000") + +// Arithmetic with overflow detection +result, overflow := new(i256.Int).AddOverflow(a, b) +if overflow { + // Handle overflow +} +``` + +## Implementation + +Built on [uint256](../uint256) for underlying arithmetic. \ No newline at end of file diff --git a/contract/p/gnoswap/int256/absolute.gno b/contract/p/gnoswap/int256/absolute.gno new file mode 100644 index 0000000..584be44 --- /dev/null +++ b/contract/p/gnoswap/int256/absolute.gno @@ -0,0 +1,38 @@ +package int256 + +import ( + "gno.land/p/gnoswap/uint256" +) + +// Abs returns the absolute value of z. +func (z *Int) Abs() *uint256.Uint { + return z.abs.Clone() +} + +// AbsGt returns true if the absolute value of z is greater than x. +func (z *Int) AbsGt(x *uint256.Uint) bool { + return z.abs.Gt(x) +} + +// AbsLt returns true if the absolute value of z is less than x. +func (z *Int) AbsLt(x *uint256.Uint) bool { + return z.abs.Lt(x) +} + +// AbsOverflow sets z to the absolute value of x and returns z and whether overflow occurred. +// Overflow occurs when x is the minimum int256 value (-2^255), as its absolute value (2^255) +// cannot be represented in a signed 256-bit integer. +func (z *Int) AbsOverflow(x *Int) (*Int, bool) { + z = z.initiateAbs() + + // overflow can be happen when negating a minimum of int256 value + if x.neg && x.abs.Eq(MinInt256().abs) { + z.Set(x) // keep the original value + return z, true + } + + z.abs.Set(x.abs) + z.neg = false + + return z, false +} diff --git a/contract/p/gnoswap/int256/absolute_test.gno b/contract/p/gnoswap/int256/absolute_test.gno new file mode 100644 index 0000000..2162d62 --- /dev/null +++ b/contract/p/gnoswap/int256/absolute_test.gno @@ -0,0 +1,184 @@ +package int256 + +import ( + "testing" + + "gno.land/p/gnoswap/uint256" +) + +func TestAbs(t *testing.T) { + tests := []struct { + x, want string + }{ + {"0", "0"}, + {"1", "1"}, + {"-1", "1"}, + {"-2", "2"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + got := x.Abs() + + if got.ToString() != tc.want { + t.Errorf("Abs(%s) = %v, want %v", tc.x, got.ToString(), tc.want) + } + } +} + +func TestAbsGt(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "0", "false"}, + {"1", "0", "true"}, + {"-1", "0", "true"}, + {"-1", "1", "false"}, + {"-2", "1", "true"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "true"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "true"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "false"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := uint256.FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.AbsGt(y) + + if got != (tc.want == "true") { + t.Errorf("AbsGt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestAbsLt(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "0", "false"}, + {"1", "0", "false"}, + {"-1", "0", "false"}, + {"-1", "1", "false"}, + {"-2", "1", "false"}, + {"-5", "10", "true"}, + {"31330", "31337", "true"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "false"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "false"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "false"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := uint256.FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.AbsLt(y) + + if got != (tc.want == "true") { + t.Errorf("AbsLt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestInt_AbsOverflow(t *testing.T) { + tests := []struct { + name string + x *Int + wantResult string + wantOverflow bool + }{ + { + name: "zero", + x: Zero(), + wantResult: "0", + wantOverflow: false, + }, + { + name: "positive number", + x: NewInt(100), + wantResult: "100", + wantOverflow: false, + }, + { + name: "negative number", + x: NewInt(-100), + wantResult: "100", + wantOverflow: false, + }, + { + name: "max_int256", + x: MustFromDecimal("57896044618658097711785492504343953926634992332820282019728792003956564819967"), + wantResult: "57896044618658097711785492504343953926634992332820282019728792003956564819967", + wantOverflow: false, + }, + { + name: "min_int256", + x: MustFromDecimal("-57896044618658097711785492504343953926634992332820282019728792003956564819968"), + wantResult: "-57896044618658097711785492504343953926634992332820282019728792003956564819968", + wantOverflow: true, + }, + { + name: "min_int256 + 1", + x: MustFromDecimal("-57896044618658097711785492504343953926634992332820282019728792003956564819967"), + wantResult: "57896044618658097711785492504343953926634992332820282019728792003956564819967", + wantOverflow: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := new(Int) + gotResult, gotOverflow := z.AbsOverflow(tt.x) + + if gotOverflow != tt.wantOverflow { + t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) + return + } + + if gotResult == nil { + t.Error("unexpected nil result") + return + } + + if gotResult.ToString() != tt.wantResult { + t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) + } + + // abs value must be GTE 0 (if there is no overflow) + if !tt.wantOverflow && gotResult.neg && !gotResult.IsZero() { + t.Error("absolute value cannot be negative") + } + + // original value must not be modified + originalValue := tt.x.ToString() + if tt.x.ToString() != originalValue { + t.Errorf("original value was modified: got %v, want %v", + tt.x.ToString(), originalValue) + } + }) + } +} diff --git a/contract/p/gnoswap/int256/arithmetic.gno b/contract/p/gnoswap/int256/arithmetic.gno new file mode 100644 index 0000000..606bd01 --- /dev/null +++ b/contract/p/gnoswap/int256/arithmetic.gno @@ -0,0 +1,319 @@ +package int256 + +import "gno.land/p/gnoswap/uint256" + +// Add sets z to the sum x+y and returns z. +func (z *Int) Add(x, y *Int) *Int { + z = z.initiateAbs() + + if x.neg == y.neg { + // If both numbers have the same sign, add their absolute values + z.abs.Add(x.abs, y.abs) + z.neg = x.neg + } else { + // If signs are different, subtract the smaller absolute value from the larger + if x.abs.Cmp(y.abs) >= 0 { + z.abs.Sub(x.abs, y.abs) + z.neg = x.neg + } else { + z.abs.Sub(y.abs, x.abs) + z.neg = y.neg + } + } + + // Ensure zero is always positive + if z.abs.IsZero() { + z.neg = false + } + + return z +} + +// AddOverflow sets z to the sum x+y and returns z and whether overflow occurred. +// Overflow occurs when the result exceeds the int256 range [-2^255, 2^255-1]. +func (z *Int) AddOverflow(x, y *Int) (*Int, bool) { + z = z.initiateAbs() + + if x.neg == y.neg { + // same sign + var overflow bool + z.abs, overflow = z.abs.AddOverflow(x.abs, y.abs) + z.neg = x.neg + + if overflow { + return z, true + } + + // check int256 range + if z.neg { + if z.abs.Cmp(MinInt256().abs) > 0 { + return z, true + } + } else { + if z.abs.Cmp(MaxInt256().abs) > 0 { + return z, true + } + } + } else { + // handle different sign by subtracting absolute values + if x.abs.Cmp(y.abs) >= 0 { + z.abs.Sub(x.abs, y.abs) + z.neg = x.neg + } else { + z.abs.Sub(y.abs, x.abs) + z.neg = y.neg + } + } + + // overflow can be happen when result is 0 + if z.abs.IsZero() { + z.neg = false + } + + return z, false +} + +// AddUint256 sets z to the sum x+y, where y is a uint256, and returns z. +func (z *Int) AddUint256(x *Int, y *uint256.Uint) *Int { + z = z.initiateAbs() + + if x.neg { + if x.abs.Gt(y) { + z.abs.Sub(x.abs, y) + z.neg = true + } else { + z.abs.Sub(y, x.abs) + z.neg = false + } + } else { + z.abs.Add(x.abs, y) + z.neg = false + } + return z +} + +// AddDelta adds a signed int256 value y to an unsigned uint256 value x and stores the result in z. +func AddDelta(z, x *uint256.Uint, y *Int) { + if y.neg { + z.Sub(x, y.abs) + } else { + z.Add(x, y.abs) + } +} + +// AddDeltaOverflow adds a signed int256 value y to an unsigned uint256 value x, stores the result in z, +// and returns true if overflow occurs. +func AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool { + var overflow bool + if y.neg { + _, overflow = z.SubOverflow(x, y.abs) + } else { + _, overflow = z.AddOverflow(x, y.abs) + } + return overflow +} + +// Sub sets z to the difference x-y and returns z. +func (z *Int) Sub(x, y *Int) *Int { + z = z.initiateAbs() + + if x.neg != y.neg { + // If sign are different, add the absolute values + z.abs.Add(x.abs, y.abs) + z.neg = x.neg + } else { + // If signs are the same, subtract the smaller absolute value from the larger + if x.abs.Cmp(y.abs) >= 0 { + z.abs = z.abs.Sub(x.abs, y.abs) + z.neg = x.neg + } else { + z.abs.Sub(y.abs, x.abs) + z.neg = !x.neg + } + } + + // Ensure zero is always positive + if z.abs.IsZero() { + z.neg = false + } + return z +} + +// SubUint256 sets z to the difference x-y, where y is a uint256, and returns z. +func (z *Int) SubUint256(x *Int, y *uint256.Uint) *Int { + z = z.initiateAbs() + + if x.neg { + z.abs.Add(x.abs, y) + z.neg = true + } else { + if x.abs.Lt(y) { + z.abs.Sub(y, x.abs) + z.neg = true + } else { + z.abs.Sub(x.abs, y) + z.neg = false + } + } + return z +} + +// SubOverflow sets z to the difference x-y and returns z and whether overflow occurred. +// Overflow occurs when subtracting a positive number from the minimum int256 value +// or a negative number from the maximum int256 value. +func (z *Int) SubOverflow(x, y *Int) (*Int, bool) { + z = z.initiateAbs() + + // must keep the original value of y + negY := y.Clone() + negY.neg = !y.neg && !y.IsZero() // reverse sign if y is not zero + + // x + (-y) + return z.AddOverflow(x, negY) +} + +// Mul sets z to the product x*y and returns z. +func (z *Int) Mul(x, y *Int) *Int { + z = z.initiateAbs() + + z.abs = z.abs.Mul(x.abs, y.abs) + z.neg = x.neg != y.neg && !z.abs.IsZero() // 0 has no sign + return z +} + +// MulUint256 sets z to the product x*y, where y is a uint256, and returns z. +func (z *Int) MulUint256(x *Int, y *uint256.Uint) *Int { + z = z.initiateAbs() + + z.abs.Mul(x.abs, y) + if z.abs.IsZero() { + z.neg = false + } else { + z.neg = x.neg + } + return z +} + +// MulOverflow sets z to the product x*y and returns z and whether overflow occurred. +// Multiplication frequently overflows when multiplying large numbers or when +// the product exceeds the int256 range [-2^255, 2^255-1]. +func (z *Int) MulOverflow(x, y *Int) (*Int, bool) { + z = z.initiateAbs() + + // always 0. no need to check overflow + if x.IsZero() || y.IsZero() { + z.abs.Clear() + z.neg = false + return z, false + } + + // multiply with absolute values + absResult, overflow := z.abs.MulOverflow(x.abs, y.abs) + z.abs = absResult + + // calculate the result's sign + z.neg = x.neg != y.neg + + if overflow { + return z, true + } + + if z.neg { + if z.abs.Cmp(MinInt256().abs) > 0 { + return z, true + } + } else { + if z.abs.Cmp(MaxInt256().abs) > 0 { + return z, true + } + } + + return z, false +} + +// Div sets z to the quotient x/y for y != 0 and returns z. +// Panics if y == 0. +func (z *Int) Div(x, y *Int) *Int { + z = z.initiateAbs() + + if y.abs.IsZero() { + panic("division by zero") + } + + z.abs.Div(x.abs, y.abs) + z.neg = (x.neg != y.neg) && !z.abs.IsZero() // 0 has no sign + + return z +} + +// DivUint256 sets z to the quotient x/y, where y is a uint256, and returns z. +// If y == 0, z is set to 0. +func (z *Int) DivUint256(x *Int, y *uint256.Uint) *Int { + z = z.initiateAbs() + + z.abs.Div(x.abs, y) + if z.abs.IsZero() { + z.neg = false + } else { + z.neg = x.neg + } + return z +} + +// Quo sets z to the quotient x/y for y != 0 and returns z. +// It implements truncated division (like Go). Panics if y == 0. +// This differs from mempooler int256 which requires manual panic handling. +func (z *Int) Quo(x, y *Int) *Int { + if y.IsZero() { + panic("division by zero") + } + + z = z.initiateAbs() + + z.abs = z.abs.Div(x.abs, y.abs) + z.neg = !(z.abs.IsZero()) && x.neg != y.neg // 0 has no sign + return z +} + +// Rem sets z to the remainder x%y for y != 0 and returns z. +// It implements truncated modulus (like Go). Panics if y == 0. +// This differs from mempooler int256 which requires manual panic handling. +func (z *Int) Rem(x, y *Int) *Int { + if y.IsZero() { + panic("division by zero") + } + + z = z.initiateAbs() + + z.abs.Mod(x.abs, y.abs) + z.neg = z.abs.Sign() > 0 && x.neg // 0 has no sign + return z +} + +// Mod sets z to the modulus x%y for y != 0 and returns z. +// If y == 0, z is set to 0 (differs from big.Int behavior). +func (z *Int) Mod(x, y *Int) *Int { + z = z.initiateAbs() + + if x.neg { + z.abs.Div(x.abs, y.abs) + z.abs.Add(z.abs, one) + z.abs.Mul(z.abs, y.abs) + z.abs.Sub(z.abs, x.abs) + z.abs.Mod(z.abs, y.abs) + } else { + z.abs.Mod(x.abs, y.abs) + } + z.neg = false + return z +} + +// MaxInt256 returns the maximum value for a 256-bit signed integer (2^255 - 1). +func MaxInt256() *Int { + return MustFromDecimal("57896044618658097711785492504343953926634992332820282019728792003956564819967") +} + +// MinInt256 returns the minimum value for a 256-bit signed integer (-2^255). +func MinInt256() *Int { + return MustFromDecimal("-57896044618658097711785492504343953926634992332820282019728792003956564819968") +} diff --git a/contract/p/gnoswap/int256/arithmetic_test.gno b/contract/p/gnoswap/int256/arithmetic_test.gno new file mode 100644 index 0000000..efc4fa3 --- /dev/null +++ b/contract/p/gnoswap/int256/arithmetic_test.gno @@ -0,0 +1,1016 @@ +package int256 + +import ( + "testing" + + "gno.land/p/gnoswap/uint256" +) + +func TestAdd(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "1", "1"}, + {"1", "0", "1"}, + {"1", "1", "2"}, + {"1", "2", "3"}, + // NEGATIVE + {"-1", "1", "0"}, + {"1", "-1", "0"}, + {"3", "-3", "0"}, + {"-1", "-1", "-2"}, + {"-1", "-2", "-3"}, + {"-1", "3", "2"}, + {"3", "-1", "2"}, + // OVERFLOW + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "0"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.Add(x, y) + + if got.Neq(want) { + t.Errorf("Add(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestAddUint256(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "1", "1"}, + {"1", "0", "1"}, + {"1", "1", "2"}, + {"1", "2", "3"}, + {"-1", "1", "0"}, + {"-1", "3", "2"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "1"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639934", "-1"}, + // OVERFLOW + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "0"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := uint256.FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.AddUint256(x, y) + + if got.Neq(want) { + t.Errorf("AddUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestAddDelta(t *testing.T) { + tests := []struct { + z, x, y, want string + }{ + {"0", "0", "0", "0"}, + {"0", "0", "1", "1"}, + {"0", "1", "0", "1"}, + {"0", "1", "1", "2"}, + {"1", "2", "3", "5"}, + {"5", "10", "-3", "7"}, + // underflow + {"1", "2", "-3", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + } + + for _, tc := range tests { + z, err := uint256.FromDecimal(tc.z) + if err != nil { + t.Error(err) + continue + } + + x, err := uint256.FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := uint256.FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + AddDelta(z, x, y) + + if z.Neq(want) { + t.Errorf("AddDelta(%s, %s, %s) = %v, want %v", tc.z, tc.x, tc.y, z.ToString(), want.ToString()) + } + } +} + +func TestAddDeltaOverflow(t *testing.T) { + tests := []struct { + z, x, y string + want bool + }{ + {"0", "0", "0", false}, + // underflow + {"1", "2", "-3", true}, + } + + for _, tc := range tests { + z, err := uint256.FromDecimal(tc.z) + if err != nil { + t.Error(err) + continue + } + + x, err := uint256.FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + result := AddDeltaOverflow(z, x, y) + if result != tc.want { + t.Errorf("AddDeltaOverflow(%s, %s, %s) = %v, want %v", tc.z, tc.x, tc.y, result, tc.want) + } + } +} + +func TestAddOverflow(t *testing.T) { + maxInt256 := MaxInt256() + minInt256 := MinInt256() + + tests := []struct { + name string + x *Int + y *Int + wantResult string + wantOverflow bool + }{ + // Basic cases (no overflow) + { + name: "positive + positive (no overflow)", + x: NewInt(100), + y: NewInt(200), + wantResult: "300", + wantOverflow: false, + }, + { + name: "negative + negative (no overflow)", + x: NewInt(-100), + y: NewInt(-200), + wantResult: "-300", + wantOverflow: false, + }, + // Boundary cases - near maximum value + { + name: "max_int256 + 0", + x: maxInt256, + y: Zero(), + wantResult: maxInt256.ToString(), + wantOverflow: false, + }, + { + name: "max_int256 - 1 + 1", + x: new(Int).Sub(maxInt256, One()), + y: One(), + wantResult: maxInt256.ToString(), + wantOverflow: false, + }, + { + name: "max_int256 + 1", + x: maxInt256, + y: One(), + wantResult: "", // overflow + wantOverflow: true, + }, + + // Boundary cases - near minimum value + { + name: "min_int256 + 0", + x: minInt256, + y: Zero(), + wantResult: minInt256.ToString(), + wantOverflow: false, + }, + { + name: "min_int256 + 1 - 1", + x: new(Int).Add(minInt256, One()), + y: NewInt(-1), + wantResult: minInt256.ToString(), + wantOverflow: false, + }, + { + name: "min_int256 + (-1)", + x: minInt256, + y: NewInt(-1), + wantResult: "", // overflow + wantOverflow: true, + }, + + // Special cases + { + name: "max_int256 + min_int256", + x: maxInt256, + y: minInt256, + wantResult: "-1", + wantOverflow: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := new(Int) + gotResult, gotOverflow := z.AddOverflow(tt.x, tt.y) + + if gotOverflow != tt.wantOverflow { + t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) + return + } + + if !gotOverflow { + if gotResult == nil { + t.Error("unexpected nil result for non-overflow case") + return + } + if gotResult.ToString() != tt.wantResult { + t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) + } + } + + // Commutativity test only for non-overflow cases + if !tt.wantOverflow { + reverseResult, reverseOverflow := z.AddOverflow(tt.y, tt.x) + if reverseOverflow != gotOverflow { + t.Error("addition is not commutative for overflow") + } + if reverseResult.ToString() != gotResult.ToString() { + t.Error("addition is not commutative for result") + } + } + }) + } +} + +func TestSub(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"1", "0", "1"}, + {"1", "1", "0"}, + {"-1", "1", "-2"}, + {"1", "-1", "2"}, + {"-1", "-1", "0"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + {x: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", y: "1", want: "0"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.Sub(x, y) + + if got.Neq(want) { + t.Errorf("Sub(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestSubUint256(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "1", "-1"}, + {"1", "0", "1"}, + {"1", "1", "0"}, + {"1", "2", "-1"}, + {"-1", "1", "-2"}, + {"-1", "3", "-4"}, + // underflow + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "-0"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "-1"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "3", "-2"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := uint256.FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.SubUint256(x, y) + + if got.Neq(want) { + t.Errorf("SubUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestMul(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"5", "3", "15"}, + {"-5", "3", "-15"}, + {"5", "-3", "-15"}, + {"0", "3", "0"}, + {"3", "0", "0"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.Mul(x, y) + + if got.Neq(want) { + t.Errorf("Mul(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestInt_SubOverflow(t *testing.T) { + maxInt256 := MaxInt256() + minInt256 := MinInt256() + + tests := []struct { + name string + x *Int + y *Int + wantResult string + wantOverflow bool + }{ + { + name: "positive - positive (no overflow)", + x: NewInt(200), + y: NewInt(100), + wantResult: "100", + wantOverflow: false, + }, + { + name: "negative - negative (no overflow)", + x: NewInt(-200), + y: NewInt(-300), + wantResult: "100", + wantOverflow: false, + }, + { + name: "positive - negative (no overflow)", + x: NewInt(200), + y: NewInt(-100), + wantResult: "300", + wantOverflow: false, + }, + { + name: "max_int256 - 0", + x: maxInt256, + y: Zero(), + wantResult: maxInt256.ToString(), + wantOverflow: false, + }, + { + name: "min_int256 - 0", + x: minInt256, + y: Zero(), + wantResult: minInt256.ToString(), + wantOverflow: false, + }, + { + name: "max_int256 - (-1)", // max_int256 + 1 -> overflow + x: maxInt256, + y: NewInt(-1), + wantResult: "", + wantOverflow: true, + }, + { + name: "min_int256 - 1", // min_int256 - 1 -> overflow + x: minInt256, + y: One(), + wantResult: "", + wantOverflow: true, + }, + { + name: "0 - 0", + x: Zero(), + y: Zero(), + wantResult: "0", + wantOverflow: false, + }, + { + name: "min_int256 - min_int256", + x: minInt256, + y: minInt256, + wantResult: "0", + wantOverflow: false, + }, + { + name: "max_int256 - max_int256", + x: maxInt256, + y: maxInt256, + wantResult: "0", + wantOverflow: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // z := new(Int) + z := New() + gotResult, gotOverflow := z.SubOverflow(tt.x, tt.y) + + if gotOverflow != tt.wantOverflow { + t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) + return + } + + if !gotOverflow { + if gotResult == nil { + t.Error("unexpected nil result for non-overflow case") + return + } + if gotResult.ToString() != tt.wantResult { + t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) + } + } + }) + } +} + +func TestMulUint256(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "1", "0"}, + {"1", "0", "0"}, + {"1", "1", "1"}, + {"1", "2", "2"}, + {"-1", "1", "-1"}, + {"-1", "3", "-3"}, + {"3", "4", "12"}, + {"-3", "4", "-12"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-115792089237316195423570985008687907853269984665640564039457584007913129639932"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "115792089237316195423570985008687907853269984665640564039457584007913129639932"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := uint256.FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.MulUint256(x, y) + + if got.Neq(want) { + t.Errorf("MulUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestInt_MulOverflow(t *testing.T) { + maxInt256 := MaxInt256() + minInt256 := MinInt256() + + tests := []struct { + name string + x *Int + y *Int + wantResult string + wantOverflow bool + }{ + { + name: "positive * positive (no overflow)", + x: NewInt(100), + y: NewInt(100), + wantResult: "10000", + wantOverflow: false, + }, + { + name: "negative * negative (no overflow)", + x: NewInt(-100), + y: NewInt(-100), + wantResult: "10000", + wantOverflow: false, + }, + { + name: "positive * negative (no overflow)", + x: NewInt(100), + y: NewInt(-100), + wantResult: "-10000", + wantOverflow: false, + }, + { + name: "0 * positive", + x: Zero(), + y: NewInt(100), + wantResult: "0", + wantOverflow: false, + }, + { + name: "positive * 0", + x: NewInt(100), + y: Zero(), + wantResult: "0", + wantOverflow: false, + }, + { + name: "0 * 0", + x: Zero(), + y: Zero(), + wantResult: "0", + wantOverflow: false, + }, + { + name: "max_int256 * 1", + x: maxInt256, + y: One(), + wantResult: maxInt256.ToString(), + wantOverflow: false, + }, + { + name: "min_int256 * 1", + x: minInt256, + y: One(), + wantResult: minInt256.ToString(), + wantOverflow: false, + }, + { + name: "min_int256 * -1", + x: minInt256, + y: NewInt(-1), + wantResult: "", // overflow because abs(min_int256) > max_int256 + wantOverflow: true, + }, + { + name: "max_int256 * 2", + x: maxInt256, + y: NewInt(2), + wantResult: "", + wantOverflow: true, + }, + { + name: "min_int256 * 2", + x: minInt256, + y: NewInt(2), + wantResult: "", + wantOverflow: true, + }, + { + name: "half_max * 2", + x: MustFromDecimal("28948022309329048855892746252171976963317496332820282019728792003956564819983"), // (2^255-1)/2 + y: NewInt(2), + wantResult: "", + wantOverflow: true, + }, + { + name: "(half_max + 1) * 2", + x: new(Int).Add(new(Int).Div(maxInt256, NewInt(2)), One()), + y: NewInt(2), + wantResult: "", + wantOverflow: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := new(Int) + gotResult, gotOverflow := z.MulOverflow(tt.x, tt.y) + + if gotOverflow != tt.wantOverflow { + t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) + return + } + + if !gotOverflow { + if gotResult == nil { + t.Error("unexpected nil result for non-overflow case") + return + } + if gotResult.ToString() != tt.wantResult { + t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) + } + } + + if !tt.wantOverflow { + reverseResult, reverseOverflow := z.MulOverflow(tt.y, tt.x) + if reverseOverflow != gotOverflow { + t.Error("multiplication is not commutative for overflow") + } + if reverseResult.ToString() != gotResult.ToString() { + t.Error("multiplication is not commutative for result") + } + } + }) + } +} + +func TestDiv(t *testing.T) { + tests := []struct { + x, y, expected string + }{ + {"1", "1", "1"}, + {"0", "1", "0"}, + {"-1", "1", "-1"}, + {"1", "-1", "-1"}, + {"-1", "-1", "1"}, + {"-6", "3", "-2"}, + {"10", "-2", "-5"}, + {"-10", "3", "-3"}, + {"7", "3", "2"}, + {"-7", "3", "-2"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, // Max uint256 / 2 + } + + for _, tt := range tests { + t.Run(tt.x+"/"+tt.y, func(t *testing.T) { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + result := Zero().Div(x, y) + if result.ToString() != tt.expected { + t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, result.ToString(), tt.expected) + } + if result.abs.IsZero() && result.neg { + t.Errorf("Div(%s, %s) resulted in negative zero", tt.x, tt.y) + } + }) + } + + t.Run("Division by zero", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("Div(1, 0) did not panic") + } + }() + x := MustFromDecimal("1") + y := MustFromDecimal("0") + Zero().Div(x, y) + }) +} + +func TestDivUint256(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "1", "0"}, + {"1", "0", "0"}, + {"1", "1", "1"}, + {"1", "2", "0"}, + {"-1", "1", "-1"}, + {"-1", "3", "0"}, + {"4", "3", "1"}, + {"25", "5", "5"}, + {"25", "4", "6"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-57896044618658097711785492504343953926634992332820282019728792003956564819967"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := uint256.FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.DivUint256(x, y) + + if got.Neq(want) { + t.Errorf("DivUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestQuo(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "1", "0"}, + {"0", "-1", "0"}, + {"10", "1", "10"}, + {"10", "-1", "-10"}, + {"-10", "1", "-10"}, + {"-10", "-1", "10"}, + {"10", "-3", "-3"}, + {"10", "3", "3"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.Quo(x, y) + + if got.Neq(want) { + t.Errorf("Quo(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestRem(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "1", "0"}, + {"0", "-1", "0"}, + {"10", "1", "0"}, + {"10", "-1", "0"}, + {"-10", "1", "0"}, + {"-10", "-1", "0"}, + {"10", "3", "1"}, + {"10", "-3", "1"}, + {"-10", "3", "-1"}, + {"-10", "-3", "-1"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.Rem(x, y) + + if got.Neq(want) { + t.Errorf("Rem(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestMod(t *testing.T) { + tests := []struct { + x, y, want string + }{ + {"0", "1", "0"}, + {"0", "-1", "0"}, + {"10", "0", "0"}, + {"10", "1", "0"}, + {"10", "-1", "0"}, + {"-10", "0", "0"}, + {"-10", "1", "0"}, + {"-10", "-1", "0"}, + {"10", "3", "1"}, + {"10", "-3", "1"}, + {"-10", "3", "2"}, + {"-10", "-3", "2"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.Mod(x, y) + + if got.Neq(want) { + t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestNilInitialization(t *testing.T) { + tests := []struct { + name string + setup func() (*Int, error) + wantStr string + }{ + { + name: "AddUint256 with nil abs", + setup: func() (*Int, error) { + z := new(Int) + x := uint256.NewUint(5) + return z.AddUint256(z, x), nil + }, + wantStr: "5", + }, + { + name: "SubUint256 with nil abs", + setup: func() (*Int, error) { + z := new(Int) + x := uint256.NewUint(5) + return z.SubUint256(z, x), nil + }, + wantStr: "-5", + }, + { + name: "MulUint256 with nil abs", + setup: func() (*Int, error) { + z := new(Int) + x := uint256.NewUint(5) + return z.MulUint256(z, x), nil + }, + wantStr: "0", + }, + { + name: "DivUint256 with nil abs", + setup: func() (*Int, error) { + z := new(Int) + x := uint256.NewUint(5) + return z.DivUint256(z, x), nil + }, + wantStr: "0", + }, + { + name: "Mod with nil abs", + setup: func() (*Int, error) { + z := new(Int) + x := MustFromDecimal("5") + defer func() { + if r := recover(); r != nil { + t.Errorf("Mod with nil abs panicked: %v", r) + } + }() + return z.Mod(z, x), nil + }, + wantStr: "0", + }, + { + name: "Chained operations with nil abs", + setup: func() (*Int, error) { + z := new(Int) + x := uint256.NewUint(5) + y := uint256.NewUint(3) + // (0 + 5) * 3 + return z.AddUint256(z, x).MulUint256(z, y), nil + }, + wantStr: "15", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := tt.setup() + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + got := result.ToString() + if got != tt.wantStr { + t.Errorf("%s: got %v, want %v", tt.name, got, tt.wantStr) + } + }) + } +} diff --git a/contract/p/gnoswap/int256/bitwise.gno b/contract/p/gnoswap/int256/bitwise.gno new file mode 100644 index 0000000..ef0bd95 --- /dev/null +++ b/contract/p/gnoswap/int256/bitwise.gno @@ -0,0 +1,101 @@ +package int256 + +import ( + "gno.land/p/gnoswap/uint256" +) + +// Or sets z to the bitwise OR of x and y and returns z. +// The operation handles two's complement representation for negative numbers. +func (z *Int) Or(x, y *Int) *Int { + if x.neg == y.neg { + if x.neg { + // (-x) | (-y) == ^(x-1) | ^(y-1) == ^((x-1) & (y-1)) == -(((x-1) & (y-1)) + 1) + x1 := new(uint256.Uint).Sub(x.abs, one) + y1 := new(uint256.Uint).Sub(y.abs, one) + z.abs = z.abs.Add(z.abs.And(x1, y1), one) + z.neg = true // z cannot be zero if x and y are negative + return z + } + + // x | y == x | y + z.abs = z.abs.Or(x.abs, y.abs) + z.neg = false + return z + } + + // x.neg != y.neg + if x.neg { + x, y = y, x // | is symmetric + } + + // x | (-y) == x | ^(y-1) == ^((y-1) &^ x) == -(^((y-1) &^ x) + 1) + y1 := new(uint256.Uint).Sub(y.abs, one) + z.abs = z.abs.Add(z.abs.AndNot(y1, x.abs), one) + z.neg = true // z cannot be zero if one of x or y is negative + + return z +} + +// And sets z to the bitwise AND of x and y and returns z. +// The operation handles two's complement representation for negative numbers. +func (z *Int) And(x, y *Int) *Int { + if x.neg == y.neg { + if x.neg { + // (-x) & (-y) == ^(x-1) & ^(y-1) == ^((x-1) | (y-1)) == -(((x-1) | (y-1)) + 1) + x1 := new(uint256.Uint).Sub(x.abs, one) + y1 := new(uint256.Uint).Sub(y.abs, one) + z.abs = z.abs.Add(z.abs.Or(x1, y1), one) + z.neg = true // z cannot be zero if x and y are negative + return z + } + + // x & y == x & y + z.abs = z.abs.And(x.abs, y.abs) + z.neg = false + return z + } + + // x.neg != y.neg + // REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1192-1202;drc=d57303e65f00b84b528ee682747dbe1fd3316d30 + if x.neg { + x, y = y, x // & is symmetric + } + + // x & (-y) == x & ^(y-1) == x &^ (y-1) + y1 := new(uint256.Uint).Sub(y.abs, uint256.One()) + z.abs = z.abs.AndNot(x.abs, y1) + z.neg = false + return z +} + +// Rsh sets z to x right-shifted by n bits and returns z. +// It performs arithmetic right shift, preserving the sign bit. +// This differs from the original implementation which used math/big. +func (z *Int) Rsh(x *Int, n uint) *Int { + if !x.neg { + z.abs.Rsh(x.abs, n) + z.neg = x.neg + return z + } + + // REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1118-1126;drc=d57303e65f00b84b528ee682747dbe1fd3316d30 + t := NewInt(0).Sub(FromUint256(x.abs), NewInt(1)) + t = t.Rsh(t, n) + + _tmp := t.Add(t, NewInt(1)) + z.abs = _tmp.Abs() + z.neg = true + + return z +} + +// Lsh sets z to x left-shifted by n bits and returns z. +func (z *Int) Lsh(x *Int, n uint) *Int { + z.abs.Lsh(x.abs, n) + if z.abs.IsZero() { + z.neg = false + } else { + z.neg = x.neg + } + return z +} diff --git a/contract/p/gnoswap/int256/bitwise_test.gno b/contract/p/gnoswap/int256/bitwise_test.gno new file mode 100644 index 0000000..9c4e7da --- /dev/null +++ b/contract/p/gnoswap/int256/bitwise_test.gno @@ -0,0 +1,198 @@ +package int256 + +import ( + "testing" + + "gno.land/p/gnoswap/uint256" +) + +func TestOr(t *testing.T) { + tests := []struct { + name string + x, y, want Int + }{ + { + name: "all zeroes", + x: Int{abs: uint256.Zero(), neg: false}, + y: Int{abs: uint256.Zero(), neg: false}, + want: Int{abs: uint256.Zero(), neg: false}, + }, + { + name: "all ones", + x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + want: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + }, + { + name: "mixed", + x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + y: Int{abs: uint256.NewUint(0), neg: false}, + want: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := New() + got.Or(&tc.x, &tc.y) + + if got.Neq(&tc.want) { + t.Errorf("Or(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) + } + }) + } +} + +func TestAnd(t *testing.T) { + tests := []struct { + name string + x, y, want Int + }{ + { + name: "all zeroes", + x: Int{abs: uint256.Zero(), neg: false}, + y: Int{abs: uint256.Zero(), neg: false}, + want: Int{abs: uint256.Zero(), neg: false}, + }, + { + name: "all ones", + x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + want: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + }, + { + name: "mixed", + x: Int{abs: uint256.Zero(), neg: false}, + y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + want: Int{abs: uint256.Zero(), neg: false}, + }, + { + name: "mixed 2", + x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + y: Int{abs: uint256.Zero(), neg: false}, + want: Int{abs: uint256.Zero(), neg: false}, + }, + { + name: "mixed 3", + x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + y: Int{abs: uint256.Zero(), neg: false}, + want: Int{abs: uint256.Zero(), neg: false}, + }, + { + name: "one operand zero", + x: Int{abs: uint256.Zero(), neg: false}, + y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + want: Int{abs: uint256.Zero(), neg: false}, + }, + { + name: "one operand all ones", + x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + want: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := New() + got.And(&tc.x, &tc.y) + + if got.Neq(&tc.want) { + t.Errorf("And(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) + } + }) + } +} + +func TestRsh(t *testing.T) { + tests := []struct { + x string + n uint + want string + }{ + {"1024", 0, "1024"}, + {"1024", 1, "512"}, + {"1024", 2, "256"}, + {"1024", 10, "1"}, + {"1024", 11, "0"}, + {"18446744073709551615", 0, "18446744073709551615"}, + {"18446744073709551615", 1, "9223372036854775807"}, + {"18446744073709551615", 62, "3"}, + {"18446744073709551615", 63, "1"}, + {"18446744073709551615", 64, "0"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 0, "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 1, "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 128, "340282366920938463463374607431768211455"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 255, "1"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 256, "0"}, + {"-1024", 0, "-1024"}, + {"-1024", 1, "-512"}, + {"-1024", 2, "-256"}, + {"-1024", 10, "-1"}, + {"-1024", 10, "-1"}, + {"-9223372036854775808", 0, "-9223372036854775808"}, + {"-9223372036854775808", 1, "-4611686018427387904"}, + {"-9223372036854775808", 62, "-2"}, + {"-9223372036854775808", 63, "-1"}, + {"-9223372036854775808", 64, "-1"}, + {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 0, "-57896044618658097711785492504343953926634992332820282019728792003956564819968"}, + {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 1, "-28948022309329048855892746252171976963317496166410141009864396001978282409984"}, + {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 253, "-4"}, + {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 254, "-2"}, + {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 255, "-1"}, + {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 256, "-1"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.Rsh(x, tc.n) + + if got.ToString() != tc.want { + t.Errorf("Rsh(%s, %d) = %v, want %v", tc.x, tc.n, got.ToString(), tc.want) + } + } +} + +func TestLsh(t *testing.T) { + tests := []struct { + x string + n uint + want string + }{ + {"1", 0, "1"}, + {"1", 1, "2"}, + {"1", 2, "4"}, + {"2", 0, "2"}, + {"2", 1, "4"}, + {"2", 2, "8"}, + {"-2", 0, "-2"}, + {"-4", 0, "-4"}, + {"-8", 0, "-8"}, + {"-1", 255, "-57896044618658097711785492504343953926634992332820282019728792003956564819968"}, + {"-1", 256, "0"}, + {"-2", 255, "0"}, + {"-4", 254, "0"}, + {"-8", 253, "0"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + got := New() + got.Lsh(x, tc.n) + + if got.ToString() != tc.want { + t.Errorf("Lsh(%s, %d) = %v, want %v", tc.x, tc.n, got.ToString(), tc.want) + } + } +} diff --git a/contract/p/gnoswap/int256/cmp.gno b/contract/p/gnoswap/int256/cmp.gno new file mode 100644 index 0000000..5a89177 --- /dev/null +++ b/contract/p/gnoswap/int256/cmp.gno @@ -0,0 +1,117 @@ +package int256 + +// Eq returns true if z equals x. +// Panics if either z or x is nil. +func (z *Int) Eq(x *Int) bool { + if z == nil || x == nil { + panic("int256: comparing with nil") + } + return (z.neg == x.neg) && z.abs.Eq(x.abs) +} + +// Neq returns true if z does not equal x. +// Panics if either z or x is nil. +func (z *Int) Neq(x *Int) bool { + if z == nil || x == nil { + panic("int256: comparing with nil") + } + return !z.Eq(x) +} + +// Cmp compares z and x and returns -1 if z < x, 0 if z == x, or +1 if z > x. +// Panics if either z or x is nil. +func (z *Int) Cmp(x *Int) (r int) { + if z == nil || x == nil { + panic("int256: comparing with nil") + } + // x cmp y == x cmp y + // x cmp (-y) == x + // (-x) cmp y == y + // (-x) cmp (-y) == -(x cmp y) + switch { + case z == x: + // nothing to do + case z.neg == x.neg: + r = z.abs.Cmp(x.abs) + if z.neg { + r = -r + } + case z.neg: + r = -1 + default: + r = 1 + } + return +} + +// IsZero returns true if z equals 0. +func (z *Int) IsZero() bool { + return z.abs.IsZero() +} + +// IsNeg returns true if z is negative. +func (z *Int) IsNeg() bool { + return z.neg +} + +// Lt returns true if z is less than x. +// Panics if either z or x is nil. +func (z *Int) Lt(x *Int) bool { + if z == nil || x == nil { + panic("int256: comparing with nil") + } + if z.neg { + if x.neg { + return z.abs.Gt(x.abs) + } else { + return true + } + } else { + if x.neg { + return false + } else { + return z.abs.Lt(x.abs) + } + } +} + +// Lte returns true if z is less than or equal to x. +func (z *Int) Lte(x *Int) bool { + return z.Lt(x) || z.Eq(x) +} + +// Gt returns true if z is greater than x. +// Panics if either z or x is nil. +func (z *Int) Gt(x *Int) bool { + if z == nil || x == nil { + panic("int256: comparing with nil") + } + if z.neg { + if x.neg { + return z.abs.Lt(x.abs) + } else { + return false + } + } else { + if x.neg { + return true + } else { + return z.abs.Gt(x.abs) + } + } +} + +// Gte returns true if z is greater than or equal to x. +func (z *Int) Gte(x *Int) bool { + return z.Gt(x) || z.Eq(x) +} + +// Clone creates a new Int identical to z. +func (z *Int) Clone() *Int { + return &Int{z.abs.Clone(), z.neg} +} + +// IsOverflow returns true if z's absolute value has overflowed. +func (z *Int) IsOverflow() bool { + return z.abs.IsOverflow() +} diff --git a/contract/p/gnoswap/int256/cmp_test.gno b/contract/p/gnoswap/int256/cmp_test.gno new file mode 100644 index 0000000..944424a --- /dev/null +++ b/contract/p/gnoswap/int256/cmp_test.gno @@ -0,0 +1,316 @@ +package int256 + +import ( + "testing" +) + +func TestEq(t *testing.T) { + tests := []struct { + x, y string + want bool + }{ + {"0", "0", true}, + {"0", "1", false}, + {"1", "0", false}, + {"-1", "0", false}, + {"0", "-1", false}, + {"1", "1", true}, + {"-1", "-1", true}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Eq(y) + if got != tc.want { + t.Errorf("Eq(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestNeq(t *testing.T) { + tests := []struct { + x, y string + want bool + }{ + {"0", "0", false}, + {"0", "1", true}, + {"1", "0", true}, + {"-1", "0", true}, + {"0", "-1", true}, + {"1", "1", false}, + {"-1", "-1", false}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Neq(y) + if got != tc.want { + t.Errorf("Neq(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestCmp(t *testing.T) { + tests := []struct { + x, y string + want int + }{ + {"0", "0", 0}, + {"0", "1", -1}, + {"1", "0", 1}, + {"-1", "0", -1}, + {"0", "-1", 1}, + {"1", "1", 0}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", 1}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Cmp(y) + if got != tc.want { + t.Errorf("Cmp(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestIsZero(t *testing.T) { + tests := []struct { + x string + want bool + }{ + {"0", true}, + {"-0", true}, + {"1", false}, + {"-1", false}, + {"10", false}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + got := x.IsZero() + if got != tc.want { + t.Errorf("IsZero(%s) = %v, want %v", tc.x, got, tc.want) + } + } +} + +func TestIsNeg(t *testing.T) { + tests := []struct { + x string + want bool + }{ + {"0", false}, + {"-0", true}, // TODO: should this be false? + {"1", false}, + {"-1", true}, + {"10", false}, + {"-10", true}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + got := x.IsNeg() + if got != tc.want { + t.Errorf("IsNeg(%s) = %v, want %v", tc.x, got, tc.want) + } + } +} + +func TestLt(t *testing.T) { + tests := []struct { + x, y string + want bool + }{ + {"0", "0", false}, + {"0", "1", true}, + {"1", "0", false}, + {"-1", "0", true}, + {"0", "-1", false}, + {"1", "1", false}, + {"-1", "-1", false}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Lt(y) + if got != tc.want { + t.Errorf("Lt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestGt(t *testing.T) { + tests := []struct { + x, y string + want bool + }{ + {"0", "0", false}, + {"0", "1", false}, + {"1", "0", true}, + {"-1", "0", false}, + {"0", "-1", true}, + {"1", "1", false}, + {"-1", "-1", false}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Gt(y) + if got != tc.want { + t.Errorf("Gt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestClone(t *testing.T) { + tests := []struct { + x string + }{ + {"0"}, + {"-0"}, + {"1"}, + {"-1"}, + {"10"}, + {"-10"}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + {"-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y := x.Clone() + + if x.Cmp(y) != 0 { + t.Errorf("Clone(%s) = %v, want %v", tc.x, y, x) + } + } +} + +func TestNilChecks(t *testing.T) { + validInt := NewInt(123) + + tests := []struct { + name string + fn func() + wantPanic string + }{ + { + name: "Eq with nil", + fn: func() { validInt.Eq(nil) }, + wantPanic: "int256: comparing with nil", + }, + { + name: "Neq with nil", + fn: func() { validInt.Neq(nil) }, + wantPanic: "int256: comparing with nil", + }, + { + name: "Cmp with nil", + fn: func() { validInt.Cmp(nil) }, + wantPanic: "int256: comparing with nil", + }, + { + name: "Lt with nil", + fn: func() { validInt.Lt(nil) }, + wantPanic: "int256: comparing with nil", + }, + { + name: "Gt with nil", + fn: func() { validInt.Gt(nil) }, + wantPanic: "int256: comparing with nil", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Errorf("%s: expected panic but got none", tt.name) + return + } + if r.(string) != tt.wantPanic { + t.Errorf("%s: got panic %v, want %v", tt.name, r, tt.wantPanic) + } + }() + + tt.fn() + }) + } +} diff --git a/contract/p/gnoswap/int256/conversion.gno b/contract/p/gnoswap/int256/conversion.gno new file mode 100644 index 0000000..75527da --- /dev/null +++ b/contract/p/gnoswap/int256/conversion.gno @@ -0,0 +1,120 @@ +package int256 + +import ( + "gno.land/p/gnoswap/uint256" +) + +// SetInt64 sets z to x and returns z. +// +// This implementation uses two's complement to handle the edge case of math.MinInt64. +// When x = math.MinInt64 (-2^63), negating it would cause an overflow since 2^63 +// cannot be represented as a positive int64. By converting to uint64 first and then +// applying two's complement (^u + 1) when negative, we correctly handle all int64 +// values including the minimum value without overflow. +func (z *Int) SetInt64(x int64) *Int { + z = z.initiateAbs() + if z.abs == nil { + panic("int256_SetInt64(): abs is nil") + } + u := uint64(x) + neg := x < 0 + if neg { + u = ^u + 1 // |x| = two's complement magnitude + } + z.abs = z.abs.SetUint64(u) + z.neg = neg && u != 0 // prevent -0 + return z +} + +// SetUint64 sets z to x and returns z. +func (z *Int) SetUint64(x uint64) *Int { + z = z.initiateAbs() + + if z.abs == nil { + panic("int256_SetUint64(): abs is nil") + } + z.abs = z.abs.SetUint64(x) + z.neg = false + return z +} + +// Uint64 returns the lower 64 bits of z as a uint64. +func (z *Int) Uint64() uint64 { + return z.abs.Uint64() +} + +// Int64 returns the lower 64 bits of z, interpreted as a signed int64 (two's complement). +// +// Since int64 already uses two's complement representation internally, +// we can simply apply two's complement to the unsigned magnitude when negative and cast +// to int64. This approach correctly handles all edge cases including when the magnitude +// equals 2^63 (which represents math.MinInt64 when negative) without special casing. +func (z *Int) Int64() int64 { + u := z.abs.Uint64() // lower 64 bits of magnitude + if z.neg { + u = ^u + 1 // apply two's complement for negative sign + } + return int64(u) // reinterpret as two's complement int64 +} + +// Neg sets z to -x and returns z. +func (z *Int) Neg(x *Int) *Int { + z.abs.Set(x.abs) + if z.abs.IsZero() { + z.neg = false + } else { + z.neg = !x.neg + } + return z +} + +// NegOverflow sets z to -x and returns z and whether overflow occurred. +// Overflow occurs when negating the minimum int256 value (-2^255). +func (z *Int) NegOverflow(x *Int) (*Int, bool) { + z = z.initiateAbs() + + if x.IsZero() { + z.abs.Clear() + z.neg = false + return z, false + } + + if x.neg && x.abs.Eq(MinInt256().abs) { + z.Set(x) // must preserve the original value + return z, true + } + + z.abs.Set(x.abs) + z.neg = !x.neg + + return z, false +} + +// Set sets z to x and returns z. +func (z *Int) Set(x *Int) *Int { + z.abs.Set(x.abs) + z.neg = x.neg + return z +} + +// SetUint256 sets z to the value of x and returns z. +func (z *Int) SetUint256(x *uint256.Uint) *Int { + z.abs.Set(x) + z.neg = false + return z +} + +// ToString returns the decimal string representation of z. +// Panics if z is nil. +// This differs from the original mempooler int256 implementation. +func (z *Int) ToString() string { + if z == nil { + panic("int256: nil pointer to ToString()") + } + + t := z.abs.Dec() + if z.neg { + return "-" + t + } + return t +} diff --git a/contract/p/gnoswap/int256/conversion_test.gno b/contract/p/gnoswap/int256/conversion_test.gno new file mode 100644 index 0000000..aae2b82 --- /dev/null +++ b/contract/p/gnoswap/int256/conversion_test.gno @@ -0,0 +1,540 @@ +package int256 + +import ( + "testing" + + "gno.land/p/nt/ufmt" + + "gno.land/p/gnoswap/uint256" +) + +func TestSetInt64(t *testing.T) { + tests := []struct { + x int64 + want string + }{ + {0, "0"}, + {1, "1"}, + {-1, "-1"}, + {9223372036854775807, "9223372036854775807"}, + {-9223372036854775808, "-9223372036854775808"}, + } + + for _, tc := range tests { + var z Int + z.SetInt64(tc.x) + + got := z.ToString() + if got != tc.want { + t.Errorf("SetInt64(%d) = %s, want %s", tc.x, got, tc.want) + } + } +} + +func TestSetInt64MinValueOverflow(t *testing.T) { + const minInt64 = -9223372036854775808 // -2^63 + const maxInt64 = 9223372036854775807 // 2^63 - 1 + + tests := []struct { + name string + x int64 + want string + }{ + { + name: "MinInt64 should not cause overflow", + x: minInt64, + want: "-9223372036854775808", + }, + { + name: "MaxInt64 works correctly", + x: maxInt64, + want: "9223372036854775807", + }, + { + name: "MinInt64 + 1", + x: minInt64 + 1, + want: "-9223372036854775807", + }, + { + name: "Negative one", + x: -1, + want: "-1", + }, + { + name: "Zero", + x: 0, + want: "0", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var z Int + z.SetInt64(tc.x) + + got := z.ToString() + if got != tc.want { + t.Errorf("SetInt64(%d) = %s, want %s", tc.x, got, tc.want) + } + + // Verify the internal representation is correct + if tc.x < 0 { + if !z.neg { + t.Errorf("SetInt64(%d): expected neg=true, got neg=false", tc.x) + } + // Check magnitude for MinInt64 + if tc.x == minInt64 { + expectedMag := uint64(1 << 63) // 2^63 + gotMag := z.abs.Uint64() + if gotMag != expectedMag { + t.Errorf("SetInt64(%d): magnitude = %d, want %d", tc.x, gotMag, expectedMag) + } + } + } else if tc.x > 0 { + if z.neg { + t.Errorf("SetInt64(%d): expected neg=false, got neg=true", tc.x) + } + } else { // tc.x == 0 + if z.neg { + t.Errorf("SetInt64(0): expected neg=false (no -0), got neg=true") + } + } + }) + } +} + +func TestSetUint64(t *testing.T) { + tests := []struct { + x uint64 + want string + }{ + {0, "0"}, + {1, "1"}, + } + + for _, tc := range tests { + var z Int + z.SetUint64(tc.x) + + got := z.ToString() + if got != tc.want { + t.Errorf("SetUint64(%d) = %s, want %s", tc.x, got, tc.want) + } + } +} + +func TestUint64(t *testing.T) { + tests := []struct { + x string + want uint64 + }{ + {"0", 0}, + {"1", 1}, + {"9223372036854775807", 9223372036854775807}, + {"9223372036854775808", 9223372036854775808}, + {"18446744073709551615", 18446744073709551615}, + {"18446744073709551616", 0}, + {"18446744073709551617", 1}, + {"-1", 1}, + {"-18446744073709551615", 18446744073709551615}, + {"-18446744073709551616", 0}, + {"-18446744073709551617", 1}, + } + + for _, tc := range tests { + z := MustFromDecimal(tc.x) + + got := z.Uint64() + if got != tc.want { + t.Errorf("Uint64(%s) = %d, want %d", tc.x, got, tc.want) + } + } +} + +func TestInt64(t *testing.T) { + tests := []struct { + x string + want int64 + }{ + {"0", 0}, + {"1", 1}, + {"-1", -1}, + {"9223372036854775807", 9223372036854775807}, + {"-9223372036854775808", -9223372036854775808}, + {"9223372036854775808", -9223372036854775808}, + {"-9223372036854775809", 9223372036854775807}, + {"18446744073709551616", 0}, + {"18446744073709551617", 1}, + {"18446744073709551615", -1}, + {"-18446744073709551615", 1}, + } + + for _, tc := range tests { + z := MustFromDecimal(tc.x) + + got := z.Int64() + if got != tc.want { + t.Errorf("Int64(%s) = %d, want %d", tc.x, got, tc.want) + } + } +} + +func TestInt64EdgeCases(t *testing.T) { + const minInt64 = -9223372036854775808 // -2^63 + const maxInt64 = 9223372036854775807 // 2^63 - 1 + + tests := []struct { + name string + setupInt func() *Int + want int64 + description string + }{ + { + name: "MinInt64 from SetInt64", + setupInt: func() *Int { + z := new(Int) + return z.SetInt64(minInt64) + }, + want: minInt64, + description: "SetInt64(MinInt64) should round-trip correctly", + }, + { + name: "MaxInt64 from SetInt64", + setupInt: func() *Int { + z := new(Int) + return z.SetInt64(maxInt64) + }, + want: maxInt64, + description: "SetInt64(MaxInt64) should round-trip correctly", + }, + { + name: "Magnitude 2^63 with negative sign", + setupInt: func() *Int { + // Create Int with magnitude = 2^63 and neg = true + z := new(Int) + z.abs = uint256.NewUint(1 << 63) + z.neg = true + return z + }, + want: minInt64, + description: "Magnitude 2^63 with neg=true should return MinInt64", + }, + { + name: "Magnitude 2^63 with positive sign", + setupInt: func() *Int { + // Create Int with magnitude = 2^63 and neg = false + z := new(Int) + z.abs = uint256.NewUint(1 << 63) + z.neg = false + return z + }, + want: minInt64, // Wraps around due to two's complement + description: "Magnitude 2^63 with neg=false wraps to MinInt64", + }, + { + name: "Large positive value wrapping", + setupInt: func() *Int { + // 2^64 - 1 (max uint64) + z := new(Int) + z.abs = uint256.NewUint(18446744073709551615) + z.neg = false + return z + }, + want: -1, + description: "Max uint64 wraps to -1 in int64", + }, + { + name: "Negative large value", + setupInt: func() *Int { + // -(2^64 - 1) + z := new(Int) + z.abs = uint256.NewUint(18446744073709551615) + z.neg = true + return z + }, + want: 1, + description: "-(Max uint64) becomes 1 due to two's complement", + }, + { + name: "Zero", + setupInt: func() *Int { + return Zero() + }, + want: 0, + description: "Zero should return 0", + }, + { + name: "Negative zero prevention", + setupInt: func() *Int { + z := new(Int) + z.abs = uint256.NewUint(0) + z.neg = true // This should be normalized to false + return z + }, + want: 0, + description: "Negative zero should return 0", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + z := tc.setupInt() + got := z.Int64() + + if got != tc.want { + t.Errorf("%s: got %d, want %d", tc.description, got, tc.want) + } + }) + } +} + +// TestInt64RoundTrip verifies that SetInt64 and Int64 work correctly together +func TestInt64RoundTrip(t *testing.T) { + // Test all interesting int64 values + values := []int64{ + 0, 1, -1, + 127, -128, // int8 boundaries + 32767, -32768, // int16 boundaries + 2147483647, -2147483648, // int32 boundaries + 9223372036854775807, -9223372036854775808, // int64 boundaries (MaxInt64, MinInt64) + 1234567890, -1234567890, + } + + for _, v := range values { + t.Run(ufmt.Sprintf("RoundTrip_%d", v), func(t *testing.T) { + z := new(Int) + z.SetInt64(v) + + got := z.Int64() + if got != v { + t.Errorf("Round trip failed: SetInt64(%d).Int64() = %d", v, got) + } + }) + } +} + +func TestNeg(t *testing.T) { + tests := []struct { + x string + want string + }{ + {"0", "0"}, + {"1", "-1"}, + {"-1", "1"}, + {"9223372036854775807", "-9223372036854775807"}, + {"-18446744073709551615", "18446744073709551615"}, + } + + for _, tc := range tests { + z := MustFromDecimal(tc.x) + z.Neg(z) + + got := z.ToString() + if got != tc.want { + t.Errorf("Neg(%s) = %s, want %s", tc.x, got, tc.want) + } + } +} + +func TestInt_NegOverflow(t *testing.T) { + maxInt256 := MaxInt256() + minInt256 := MinInt256() + + negMaxInt256 := New().Neg(maxInt256) + + tests := []struct { + name string + x *Int + wantResult string + wantOverflow bool + }{ + { + name: "negate zero", + x: Zero(), + wantResult: "0", + wantOverflow: false, + }, + { + name: "negate positive", + x: NewInt(100), + wantResult: "-100", + wantOverflow: false, + }, + { + name: "negate negative", + x: NewInt(-100), + wantResult: "100", + wantOverflow: false, + }, + { + name: "negate max_int256", + x: maxInt256, + wantResult: negMaxInt256.ToString(), + wantOverflow: false, + }, + { + name: "negate min_int256", + x: minInt256, + wantResult: minInt256.ToString(), // must preserve the original value + wantOverflow: true, + }, + { + name: "negate (min_int256 + 1)", + x: new(Int).Add(minInt256, One()), + wantResult: new(Int).Sub(maxInt256, Zero()).ToString(), + wantOverflow: false, + }, + { + name: "negate (max_int256 - 1)", + x: MustFromDecimal("57896044618658097711785492504343953926634992332820282019728792003956564819966"), // max_int256 - 1 + wantResult: "-57896044618658097711785492504343953926634992332820282019728792003956564819966", + wantOverflow: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := new(Int) + gotResult, gotOverflow := z.NegOverflow(tt.x) + + if gotOverflow != tt.wantOverflow { + t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) + return + } + + if gotResult == nil { + t.Error("unexpected nil result") + return + } + + if gotResult.ToString() != tt.wantResult { + // use almost equal comparison to handle the precision issue + diff := new(Int).Sub(gotResult, MustFromDecimal(tt.wantResult)) + if diff.Abs().Cmp(uint256.NewUint(1)) > 0 { + t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) + } + } + + // double negation test (only if there is no overflow) + if !tt.wantOverflow { + doubleNegResult, doubleNegOverflow := new(Int).NegOverflow(gotResult) + if doubleNegOverflow { + t.Error("unexpected overflow in double negation") + } + if doubleNegResult.ToString() != tt.x.ToString() { + t.Errorf("double negation result = %v, want %v", + doubleNegResult.ToString(), tt.x.ToString()) + } + } + }) + } +} + +func TestSet(t *testing.T) { + tests := []struct { + x string + want string + }{ + {"0", "0"}, + {"1", "1"}, + {"-1", "-1"}, + {"9223372036854775807", "9223372036854775807"}, + {"-18446744073709551615", "-18446744073709551615"}, + } + + for _, tc := range tests { + z := MustFromDecimal(tc.x) + z.Set(z) + + got := z.ToString() + if got != tc.want { + t.Errorf("set(%s) = %s, want %s", tc.x, got, tc.want) + } + } +} + +func TestSetUint256(t *testing.T) { + tests := []struct { + x string + want string + }{ + {"0", "0"}, + {"1", "1"}, + {"9223372036854775807", "9223372036854775807"}, + {"18446744073709551615", "18446744073709551615"}, + } + + for _, tc := range tests { + got := New() + + z := uint256.MustFromDecimal(tc.x) + got.SetUint256(z) + + if got.ToString() != tc.want { + t.Errorf("SetUint256(%s) = %s, want %s", tc.x, got.ToString(), tc.want) + } + } +} + +func TestToString(t *testing.T) { + tests := []struct { + name string + setup func() *Int + expected string + }{ + { + name: "Zero from subtraction", + setup: func() *Int { + minusThree := MustFromDecimal("-3") + three := MustFromDecimal("3") + return Zero().Add(minusThree, three) + }, + expected: "0", + }, + { + name: "Zero from right shift", + setup: func() *Int { + return Zero().Rsh(One(), 1234) + }, + expected: "0", + }, + { + name: "Positive number", + setup: func() *Int { + return MustFromDecimal("42") + }, + expected: "42", + }, + { + name: "Negative number", + setup: func() *Int { + return MustFromDecimal("-42") + }, + expected: "-42", + }, + { + name: "Large positive number", + setup: func() *Int { + return MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") + }, + expected: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + { + name: "Large negative number", + setup: func() *Int { + return MustFromDecimal("-115792089237316195423570985008687907853269984665640564039457584007913129639935") + }, + expected: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + z := tt.setup() + result := z.ToString() + if result != tt.expected { + t.Errorf("ToString() = %s, want %s", result, tt.expected) + } + }) + } +} diff --git a/contract/p/gnoswap/int256/doc.gno b/contract/p/gnoswap/int256/doc.gno new file mode 100644 index 0000000..8e1d681 --- /dev/null +++ b/contract/p/gnoswap/int256/doc.gno @@ -0,0 +1,13 @@ +// Package int256 implements 256-bit signed integer arithmetic for GnoSwap. +// +// This package provides an Int type that represents a 256-bit signed integer +// using two's complement representation. It supports the full range from +// -(2^255) to 2^255-1, with arithmetic operations that detect overflow. +// +// The implementation follows Ethereum's int256 semantics, ensuring compatibility +// for cross-chain DeFi protocols. Operations are optimized for common AMM +// calculations including tick math and price computations. +// +// Critical operations like Add, Sub, and Mul return overflow flags, enabling +// safe handling of edge cases in financial calculations. +package int256 diff --git a/contract/p/gnoswap/int256/gnomod.toml b/contract/p/gnoswap/int256/gnomod.toml new file mode 100644 index 0000000..e5fdea9 --- /dev/null +++ b/contract/p/gnoswap/int256/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/p/gnoswap/int256" +gno = "0.9" diff --git a/contract/p/gnoswap/int256/int256.gno b/contract/p/gnoswap/int256/int256.gno new file mode 100644 index 0000000..3cd68d2 --- /dev/null +++ b/contract/p/gnoswap/int256/int256.gno @@ -0,0 +1,182 @@ +package int256 + +import ( + "errors" + + "gno.land/p/gnoswap/uint256" +) + +var ( + errEmptyString = errors.New("empty string") + errStringContainsOnlySign = errors.New("string contains only sign") + errInvalidSignInMiddleOfNumber = errors.New("invalid sign in middle of number") +) + +var one = uint256.NewUint(1) + +// Int represents a 256-bit signed integer. +// It uses a sign-magnitude representation with abs storing the absolute value +// and neg indicating whether the number is negative. +type Int struct { + abs *uint256.Uint + neg bool +} + +// Zero returns a new Int set to 0. +func Zero() *Int { + return NewInt(0) +} + +// One returns a new Int set to 1. +func One() *Int { + return NewInt(1) +} + +// Sign returns the sign of x. +// It returns -1 if x < 0, 0 if x == 0, and +1 if x > 0. +func (z *Int) Sign() int { + z.initiateAbs() + + if z.abs.IsZero() { + return 0 + } + if z.neg { + return -1 + } + return 1 +} + +// New returns a new Int set to 0. +func New() *Int { + return &Int{ + abs: new(uint256.Uint), + } +} + +// NewInt allocates and returns a new Int set to x. +func NewInt(x int64) *Int { + return New().SetInt64(x) +} + +// FromDecimal returns a new Int from a decimal string and an error if the string is not valid. +func FromDecimal(s string) (*Int, error) { + return new(Int).SetString(s) +} + +// MustFromDecimal returns a new Int from a decimal string. +// Panics if the string is not a valid decimal. +func MustFromDecimal(s string) *Int { + z, err := FromDecimal(s) + if err != nil { + panic(err) + } + return z +} + +// SetString sets z to the value of s and returns z and an error. +// It uses a bit masking technique for efficient sign character detection, +// based on "Bit Twiddling Hacks" by Sean Eron Anderson. +// This approach provides significantly better performance than string scanning methods. +func (z *Int) SetString(s string) (*Int, error) { + if len(s) == 0 { + return nil, errEmptyString + } + + // check sign only in the first character + neg := false + switch s[0] { + case '+': + s = s[1:] + case '-': + neg = true + s = s[1:] + } + + // check if the string is empty after removing the sign + if len(s) == 0 { + return nil, errStringContainsOnlySign + } + + // Parallel comparison technique for sign detection + // Process in 8-byte chunks + sLen := len(s) + i := 0 + + // Process 8 bytes at a time + for i+7 < sLen { + // Convert 8 bytes into a single uint64 + // This method processes bytes directly, so no endianness issues + // + // access up to s[i+7] is safe, then we can reduce the number of bounds checks + _ = s[i+7] + chunk := uint64(s[i]) | uint64(s[i+1])<<8 + chunk |= uint64(s[i+2])<<16 | uint64(s[i+3])<<24 + chunk |= uint64(s[i+4])<<32 | uint64(s[i+5])<<40 + chunk |= uint64(s[i+6])<<48 | uint64(s[i+7])<<56 + + // Operation to check for '+' (0x2B) + // Subtracting 0x2B from each byte makes '+' bytes become 0 + // Subtracting 0x01 makes bytes in ASCII range (0-127) have 0 in their highest bit + // Therefore, AND with 0x80 to check for zero bytes + plusTest := ((chunk ^ 0x2B2B2B2B2B2B2B2B) - 0x0101010101010101) & 0x8080808080808080 + + // check for '-' (0x2D) + minusTest := ((chunk ^ 0x2D2D2D2D2D2D2D2D) - 0x0101010101010101) & 0x8080808080808080 + + // If either test is non-zero, a sign character exists + if (plusTest | minusTest) != 0 { + return nil, errInvalidSignInMiddleOfNumber + } + + i += 8 + } + + // Process remaining bytes + for ; i < sLen; i++ { + if s[i] == '+' || s[i] == '-' { + return nil, errInvalidSignInMiddleOfNumber + } + } + + abs, err := uint256.FromDecimal(s) + if err != nil { + return nil, err + } + + return &Int{abs, neg}, nil +} + +// FromUint256 creates a new Int from a uint256.Uint. +// It returns nil if the input is nil. +func FromUint256(x *uint256.Uint) *Int { + if x == nil { + return nil + } + z := Zero() + + z.SetUint256(x) + return z +} + +// NilToZero returns z if it's non-nil, otherwise returns a new Int set to 0. +// This differs from the original mempooler int256 implementation. +func (z *Int) NilToZero() *Int { + if z == nil { + return NewInt(0) + } + return z +} + +// initiateAbs ensures z and z.abs are initialized. +// If z is nil, it returns a new Int set to 0. +// If z.abs is nil, it initializes it to a new uint256.Uint. +// This differs from mempooler int256 by also checking if z itself is nil. +func (z *Int) initiateAbs() *Int { + if z == nil { + return NewInt(0) + } + if z.abs == nil { + z.abs = new(uint256.Uint) + } + return z +} diff --git a/contract/p/gnoswap/int256/int256_test.gno b/contract/p/gnoswap/int256/int256_test.gno new file mode 100644 index 0000000..f8e3060 --- /dev/null +++ b/contract/p/gnoswap/int256/int256_test.gno @@ -0,0 +1,185 @@ +// ported from github.com/mempooler/int256 +package int256 + +import ( + "testing" +) + +func TestSign(t *testing.T) { + tests := []struct { + x string + want int + }{ + {"0", 0}, + {"1", 1}, + {"-1", -1}, + } + + for _, tt := range tests { + z := MustFromDecimal(tt.x) + got := z.Sign() + if got != tt.want { + t.Errorf("Sign(%s) = %d, want %d", tt.x, got, tt.want) + } + } +} + +func TestSetString(t *testing.T) { + tests := []struct { + input string + wantErr bool + wantVal string + wantSign bool + }{ + {"123", false, "123", false}, + {"+123", false, "123", false}, + {"-123", false, "123", true}, + {"9223372036854775807", false, "9223372036854775807", false}, + {"-9223372036854775808", false, "9223372036854775808", true}, + + {"++123", true, "", false}, + {"--123", true, "", false}, + {"+-123", true, "", false}, + {"-+123", true, "", false}, + {"+++123", true, "", false}, + {"---123", true, "", false}, + {"+-+-123", true, "", false}, + {"922337203-6854775807", true, "", false}, + + {"1+23", true, "", false}, + {"1-23", true, "", false}, + {"12+3", true, "", false}, + + // scientific notation not allowed + {"-1e23", true, "", false}, + {"1e-23", true, "", false}, + {"1e+23", true, "", false}, + + {"", true, "", false}, + {"+", true, "", false}, + {"-", true, "", false}, + {"+-", true, "", false}, + } + + for _, tt := range tests { + z, err := new(Int).SetString(tt.input) + + if tt.wantErr { + if err == nil { + t.Errorf("SetString(%q) = %v, want error", tt.input, z) + } + continue + } + + if err != nil { + t.Errorf("SetString(%q) returned unexpected error: %v", tt.input, err) + continue + } + + if got := z.abs.ToString(); got != tt.wantVal { + t.Errorf("SetString(%q).abs = %s, want %s", tt.input, got, tt.wantVal) + } + + if got := z.neg; got != tt.wantSign { + t.Errorf("SetString(%q).neg = %v, want %v", tt.input, got, tt.wantSign) + } + } +} + +func TestInitiateAbs(t *testing.T) { + tests := []struct { + name string + input *Int + wantNil bool + wantZero bool + }{ + { + name: "nil input returns new zero Int", + input: nil, + wantNil: false, + wantZero: true, + }, + { + name: "nil abs field gets initialized", + input: &Int{abs: nil, neg: false}, + wantNil: false, + wantZero: true, + }, + { + name: "existing abs field remains unchanged", + input: NewInt(123), + wantNil: false, + wantZero: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.input.initiateAbs() + + if result == nil { + t.Error("initiateAbs returned nil") + return + } + + if (result.abs == nil) != tc.wantNil { + t.Errorf("abs field nil status = %v, want %v", result.abs == nil, tc.wantNil) + } + + isZero := result.abs != nil && result.abs.IsZero() + if isZero != tc.wantZero { + t.Errorf("IsZero() = %v, want %v", isZero, tc.wantZero) + } + }) + } +} + +func TestInitiateAbsInOperation(t *testing.T) { + tests := []struct { + name string + setup func() *Int + op func(*Int) *Int + want string + }{ + { + name: "Add with nil receiver", + setup: func() *Int { return nil }, + op: func(z *Int) *Int { return z.Add(NewInt(10), NewInt(20)) }, + want: "30", + }, + { + name: "Add with nil abs field", + setup: func() *Int { return &Int{abs: nil} }, + op: func(z *Int) *Int { return z.Add(NewInt(10), NewInt(20)) }, + want: "30", + }, + { + name: "Sub with nil receiver", + setup: func() *Int { return nil }, + op: func(z *Int) *Int { return z.Sub(NewInt(30), NewInt(20)) }, + want: "10", + }, + { + name: "Sub with nil abs field", + setup: func() *Int { return &Int{abs: nil} }, + op: func(z *Int) *Int { return z.Sub(NewInt(30), NewInt(20)) }, + want: "10", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + z := tc.setup() + result := tc.op(z) + + if result == nil { + t.Error("operation returned nil") + return + } + + if got := result.ToString(); got != tc.want { + t.Errorf("got %v, want %v", got, tc.want) + } + }) + } +} diff --git a/contract/p/gnoswap/rbac/README.md b/contract/p/gnoswap/rbac/README.md new file mode 100644 index 0000000..b0f7860 --- /dev/null +++ b/contract/p/gnoswap/rbac/README.md @@ -0,0 +1,78 @@ +# RBAC + +Role-Based Access Control package for Gno smart contracts. + +## Overview + +Flexible RBAC system enabling dynamic role and permission management without contract redeployment. + +## Features + +- Dynamic role registration +- Multiple permissions per role +- Declarative role definition +- Custom permission logic +- Runtime updates + +## Core API + +```go +// Create RBAC manager +func New() *RBAC + +// Role management +func (rb *RBAC) RegisterRole(roleName string) error +func (rb *RBAC) DeclareRole(roleName string, opts ...RoleOption) error + +// Permission management +func (rb *RBAC) RegisterPermission(roleName, permissionName string, checker PermissionChecker) error +func (rb *RBAC) UpdatePermission(roleName, permissionName string, newChecker PermissionChecker) error +func (rb *RBAC) RemovePermission(roleName, permissionName string) error + +// Access control +func (rb *RBAC) CheckPermission(roleName, permissionName string, caller Address) error + +// Permission checker type +type PermissionChecker func(caller std.Address) error +``` + +## Usage + +```go +// Create manager +manager := rbac.New() + +// Register role +manager.RegisterRole("admin") + +// Add permission +adminChecker := func(caller std.Address) error { + if caller != adminAddr { + return errors.New("not admin") + } + return nil +} +manager.RegisterPermission("admin", "access", adminChecker) + +// Declarative role setup +manager.DeclareRole("editor", + rbac.WithPermission("edit", editorChecker)) + +// Check access +err := manager.CheckPermission("admin", "access", caller) +``` + +## Architecture + +``` +Client → RBAC → Role → PermissionChecker + ↓ ↓ ↓ + Manager Storage Validation +``` + +## Security + +- No direct address-to-role mapping +- Custom validation logic per permission +- Runtime permission updates +- Isolated permission checks \ No newline at end of file diff --git a/contract/p/gnoswap/rbac/doc.gno b/contract/p/gnoswap/rbac/doc.gno new file mode 100644 index 0000000..eafa943 --- /dev/null +++ b/contract/p/gnoswap/rbac/doc.gno @@ -0,0 +1,139 @@ +// Package rbac provides a flexible, upgradeable Role-Based Access Control (RBAC) +// system for Gno smart contracts and related applications. It decouples authorization +// logic from fixed addresses, enabling dynamic registration, update, and removal of roles +// and permissions. +// +// ## Overview +// +// The RBAC package encapsulates a manager that maintains an internal registry of roles. +// Each role is defined by a unique name and a set of permissions. A permission is +// represented by a `PermissionChecker` function that validates whether a given caller +// (`std.Address`) satisfies the required access conditions. +// +// Key components of this package include: +// +// 1. **Role**: Represents a role with a name and a collection of permission-checking functions. +// 2. **PermissionChecker**: A function type defined as `func(caller std.Address) error`, +// used to verify access for a given permission. +// 3. **RBAC Manager**: The core type (RBAC) that encapsulates role registration, permission +// assignment, verification, updating, and removal. +// +// ## Key Features +// +// - **Dynamic Role Management**: Roles can be registered, and permissions can be assigned +// or updated at runtime without requiring contract redeployment. +// - **Multiple Permissions per Role**: A single role can have multiple permissions, +// each with its own validation logic. +// - **Declarative Role Definition**: The package supports a Functional Option pattern, +// allowing roles and their permissions to be defined declaratively via functions like +// `DeclareRole` and `WithPermission`. +// - **Encapsulation**: Internal state (roles registry) is encapsulated within the RBAC +// manager, preventing unintended external modifications. +// - **Flexible Validation**: Permission checkers can implement custom logic, supporting +// arbitrary access control policies. +// +// ## Workflow +// +// Typical usage of the RBAC package includes the following steps: +// +// 1. **Initialization**: Create a new RBAC manager using `NewRBAC()`. +// 2. **Role Registration**: Register roles using `RegisterRole` or declaratively with +// `DeclareRole`. +// 3. **Permission Assignment**: Add permissions to roles using `RegisterPermission` or the +// `WithPermission` option during role declaration. +// 4. **Permission Verification**: Validate access by invoking `CheckPermission` with the +// role name, permission name, and the caller's address (std.Address). +// +// ## Example Usage +// +// The following example demonstrates how to use the RBAC package in both traditional and +// declarative styles: +// +// ```gno +// package main +// +// import ( +// +// "std" +// +// "gno.land/p/gnoswap/rbac" +// "gno.land/p/nt/ufmt" +// +// ) +// +// func main() { +// // Create a new RBAC manager +// manager := rbac.NewRBAC() +// +// // Define example addresses +// adminAddr := std.Address("admin") +// userAddr := std.Address("user") +// +// // --- Traditional Role Registration --- +// // Register an "admin" role +// if err := manager.RegisterRole("admin"); err != nil { +// panic(err) +// } +// +// // Register an "access" permission for the "admin" role. +// // The checker verifies that the caller matches adminAddr. +// adminChecker := func(caller std.Address) error { +// if caller != adminAddr { +// return ufmt.Errorf("caller %s is not admin", caller) +// } +// return nil +// } +// if err := manager.RegisterPermission("admin", "access", adminChecker); err != nil { +// panic(err) +// } +// +// // --- Declarative Role Registration --- +// // Register an "editor" role with a "modify" permission using the Functional Option pattern. +// editorChecker := func(caller std.Address) error { +// if caller != userAddr { +// return ufmt.Errorf("caller %s is not editor", caller) +// } +// return nil +// } +// if err := manager.DeclareRole("editor", rbac.WithPermission("modify", editorChecker)); err != nil { +// panic(err) +// } +// +// // --- Permission Check --- +// // Check if adminAddr has the "access" permission on the "admin" role. +// if err := manager.CheckPermission("admin", "access", adminAddr); err != nil { +// println("Access denied for admin:", err) +// } else { +// println("Admin access granted") +// } +// } +// +// ``` +// +// ## Error Handling +// +// The package reports errors using the ufmt.Errorf function. Typical errors include: +// +// - Registering a role that already exists. +// - Attempting to register a permission for a non-existent role. +// - Verifying a permission that does not exist on a role. +// - Failing a permission check due to a caller not meeting the required conditions. +// +// ## Limitations and Considerations +// +// - This RBAC implementation does not directly map addresses to roles; instead, it verifies +// the caller against permission-checking functions registered for a role. +// - Address validation relies on the logic provided within each PermissionChecker. Ensure that +// your checkers properly validate `std.Address` values (which follow the Bech32 format). +// - The encapsulated RBAC manager is designed to minimize external mutation, but integrating it +// with other modules may require additional mapping between addresses and roles. +// +// # Notes +// +// - The RBAC system is designed to be upgradeable, enabling contracts to modify permission +// logic without redeploying the entire contract. +// - Both imperative and declarative styles are supported, providing flexibility to developers. +// +// Package rbac is intended for use in Gno smart contracts and other systems requiring dynamic, +// upgradeable access control mechanisms. +package rbac diff --git a/contract/p/gnoswap/rbac/errors.gno b/contract/p/gnoswap/rbac/errors.gno new file mode 100644 index 0000000..990d654 --- /dev/null +++ b/contract/p/gnoswap/rbac/errors.gno @@ -0,0 +1,18 @@ +package rbac + +import ( + "errors" +) + +var ( + ErrNoPendingOwner = errors.New("no pending owner") + ErrUnauthorized = errors.New("caller is not owner") + ErrPendingUnauthorized = errors.New("caller is not pending owner") + ErrInvalidAddress = errors.New("invalid address") + + ErrInvalidRoleName = errors.New("invalid role name") + ErrRoleDoesNotExist = errors.New("role does not exist") + ErrRoleAlreadyExists = errors.New("role already exists") + ErrCannotRegisterSystemRole = errors.New("cannot register system role") + ErrCannotRemoveSystemRole = errors.New("cannot remove system role") +) diff --git a/contract/p/gnoswap/rbac/gnomod.toml b/contract/p/gnoswap/rbac/gnomod.toml new file mode 100644 index 0000000..ec2120e --- /dev/null +++ b/contract/p/gnoswap/rbac/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/p/gnoswap/rbac" +gno = "0.9" diff --git a/contract/p/gnoswap/rbac/ownable.gno b/contract/p/gnoswap/rbac/ownable.gno new file mode 100644 index 0000000..fa3be7f --- /dev/null +++ b/contract/p/gnoswap/rbac/ownable.gno @@ -0,0 +1,110 @@ +package rbac + +import ( + "std" +) + +const ( + OwnershipTransferEvent = "OwnershipTransfer" + OwnershipTransferStartedEvent = "OwnershipTransferStarted" +) + +// Ownable2Step implements a two-step ownership transfer mechanism. +// It requires the new owner to explicitly accept ownership before the transfer is completed, +// preventing accidental transfers to incorrect addresses. +type Ownable2Step struct { + owner std.Address + pendingOwner std.Address +} + +// newOwnable2StepWithAddress creates a new Ownable2Step instance with addr as owner. +func newOwnable2StepWithAddress(addr std.Address) *Ownable2Step { + return &Ownable2Step{ + owner: addr, + pendingOwner: "", + } +} + +// TransferOwnership initiates ownership transfer by setting newOwner as pending owner. +// The new owner must call AcceptOwnership to complete the transfer. +// Only the current owner can call this function. +func (o *Ownable2Step) TransferOwnership(newOwner std.Address) error { + if !o.OwnedByOriginCaller() { + return ErrUnauthorized + } + + if !newOwner.IsValid() { + return ErrInvalidAddress + } + + o.pendingOwner = newOwner + + std.Emit( + OwnershipTransferStartedEvent, + "from", o.owner.String(), + "to", newOwner.String(), + ) + + return nil +} + +// AcceptOwnership completes the ownership transfer. +// Must be called by the pending owner. Panics if no pending owner exists or caller is not the pending owner. +func (o *Ownable2Step) AcceptOwnership() error { + if o.pendingOwner.String() == "" { + return ErrNoPendingOwner + } + + if std.OriginCaller() != o.pendingOwner { + return ErrPendingUnauthorized + } + + prevOwner := o.owner + o.owner = o.pendingOwner + o.pendingOwner = "" + + std.Emit( + OwnershipTransferEvent, + "from", prevOwner.String(), + "to", o.owner.String(), + ) + + return nil +} + +// DropOwnership removes the owner, disabling all owner-only actions. +// When used at the top level, it disables all owner-only functions. +// When embedded, it acts like a burn function, removing ownership from the struct. +// Only the current owner can call this function. +func (o *Ownable2Step) DropOwnership() error { + if !o.OwnedByOriginCaller() { + return ErrUnauthorized + } + + prevOwner := o.owner + o.owner = "" + o.pendingOwner = "" + + std.Emit( + OwnershipTransferEvent, + "from", prevOwner.String(), + "to", "", + ) + + return nil +} + +// Owner returns the current owner address. Returns empty address if ownership has been dropped. +func (o *Ownable2Step) Owner() std.Address { + return o.owner +} + +// PendingOwner returns the pending owner address during ownership transfer. Returns empty address if no transfer is pending. +func (o *Ownable2Step) PendingOwner() std.Address { + return o.pendingOwner +} + +// OwnedByOriginCaller returns true if the origin caller is the current owner. +func (o *Ownable2Step) OwnedByOriginCaller() bool { + return std.OriginCaller() == o.owner +} diff --git a/contract/p/gnoswap/rbac/ownable_test.gno b/contract/p/gnoswap/rbac/ownable_test.gno new file mode 100644 index 0000000..6544148 --- /dev/null +++ b/contract/p/gnoswap/rbac/ownable_test.gno @@ -0,0 +1,175 @@ +package rbac + +import ( + "std" + "testing" + + "gno.land/p/nt/testutils" + "gno.land/p/nt/uassert" + "gno.land/p/nt/urequire" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") +) + +func TestNewWithAddress(t *testing.T) { + o := newOwnable2StepWithAddress(alice) + + got := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, got, alice) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestInitiateTransferOwnership(t *testing.T) { + testing.SetOriginCaller(alice) + + o := newOwnable2StepWithAddress(alice) + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, owner, alice) + uassert.Equal(t, pendingOwner, bob) +} + +func TestTransferOwnership(t *testing.T) { + testing.SetOriginCaller(alice) + + o := newOwnable2StepWithAddress(alice) + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + + uassert.Equal(t, owner, alice) + uassert.Equal(t, pendingOwner, bob) + + testing.SetOriginCaller(bob) + + err = o.AcceptOwnership() + urequire.NoError(t, err) + + owner = o.Owner() + pendingOwner = o.PendingOwner() + + uassert.Equal(t, owner, bob) + uassert.Equal(t, pendingOwner.String(), "") +} + +func TestOwnedByOriginCaller(t *testing.T) { + testing.SetOriginCaller(alice) + + o := newOwnable2StepWithAddress(alice) + unauthorizedCaller := bob + testing.SetOriginCaller(unauthorizedCaller) + uassert.False(t, o.OwnedByOriginCaller()) +} + +func TestOwnedByOriginCallerUnauthorized(t *testing.T) { + testing.SetOriginCaller(alice) + testing.SetRealm(std.NewUserRealm(alice)) + + var o *Ownable2Step + func() { + testing.SetRealm(std.NewCodeRealm("gno.land/r/test/test")) + o = newOwnable2StepWithAddress(alice) + }() + + uassert.True(t, o.OwnedByOriginCaller()) + + unauthorizedCaller := bob + testing.SetRealm(std.NewUserRealm(unauthorizedCaller)) + uassert.False(t, o.OwnedByOriginCaller()) +} + +func TestDropOwnership(t *testing.T) { + testing.SetOriginCaller(alice) + + o := New() + + err := o.DropOwnership() + urequire.NoError(t, err, "DropOwnership failed") + + owner := o.Owner() + uassert.Empty(t, owner, "owner should be empty") +} + +func TestDropOwnershipVulnerability(t *testing.T) { + testing.SetOriginCaller(alice) + o := New() + + // alice initiates ownership transfer to bob + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + // alice drops ownership while ownership transfer is pending + err = o.DropOwnership() + urequire.NoError(t, err) + + owner := o.Owner() + pendingOwner := o.PendingOwner() + uassert.Empty(t, owner, "owner should be empty") + uassert.Empty(t, pendingOwner.String(), "pending owner should be empty") + + // verify bob can no longer claim ownership + testing.SetOriginCaller(bob) + err = o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrNoPendingOwner.Error()) +} + +// Errors + +func TestErrUnauthorized(t *testing.T) { + testing.SetOriginCaller(alice) + + o := New() + + testing.SetOriginCaller(bob) + + uassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error()) + uassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error()) +} + +func TestErrInvalidAddress(t *testing.T) { + testing.SetOriginCaller(alice) + + o := New() + + err := o.TransferOwnership("") + uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) + + err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") + uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) +} + +func TestErrNoPendingOwner(t *testing.T) { + testing.SetOriginCaller(alice) + + o := New() + + err := o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrNoPendingOwner.Error()) +} + +func TestErrPendingUnauthorized(t *testing.T) { + testing.SetOriginCaller(alice) + + o := New() + + err := o.TransferOwnership(bob) + urequire.NoError(t, err) + + testing.SetOriginCaller(alice) + + err = o.AcceptOwnership() + uassert.ErrorContains(t, err, ErrPendingUnauthorized.Error()) +} diff --git a/contract/p/gnoswap/rbac/rbac.gno b/contract/p/gnoswap/rbac/rbac.gno new file mode 100644 index 0000000..03cf341 --- /dev/null +++ b/contract/p/gnoswap/rbac/rbac.gno @@ -0,0 +1,146 @@ +package rbac + +import ( + "std" +) + +// RBAC encapsulates and manages roles and their permissions. +// It combines role management with two-step ownership transfer functionality. +type RBAC struct { + ownable *Ownable2Step + // roles maps role names to their respective `Role` objects + roles map[string]*Role +} + +// New creates a new RBAC instance with the origin caller as owner. +func New() *RBAC { + return &RBAC{ + ownable: newOwnable2StepWithAddress(std.OriginCaller()), + roles: make(map[string]*Role), + } +} + +// NewRBACWithAddress creates a new RBAC instance with addr as owner. +func NewRBACWithAddress(addr std.Address) *RBAC { + return &RBAC{ + ownable: newOwnable2StepWithAddress(addr), + roles: make(map[string]*Role), + } +} + +// IsAuthorized checks if addr has the specified roleName. Returns false if the role does not exist. +func (rb *RBAC) IsAuthorized(roleName string, addr std.Address) bool { + role, exists := rb.roles[roleName] + if !exists { + return false + } + + return role.IsAuthorized(addr) +} + +// RegisterRole registers a new role with roleName. Returns ErrRoleAlreadyExists if the role already exists. +func (rb *RBAC) RegisterRole(roleName string) error { + if rb.existsRole(roleName) { + return ErrRoleAlreadyExists + } + + rb.roles[roleName] = NewRole(roleName) + + return nil +} + +// RemoveRole removes roleName from the RBAC system. Returns ErrRoleDoesNotExist if the role doesn't exist or ErrCannotRemoveSystemRole for system roles. +func (rb *RBAC) RemoveRole(roleName string) error { + if !rb.existsRole(roleName) { + return ErrRoleDoesNotExist + } + + // Check if it's a system role + if IsSystemRole(roleName) { + return ErrCannotRemoveSystemRole + } + + // Simply delete the role since permissions are no longer managed here + delete(rb.roles, roleName) + + return nil +} + +// GetRoleAddresses returns a map of all role names to their assigned addresses. +func (rb *RBAC) GetRoleAddresses() map[string]std.Address { + addresses := make(map[string]std.Address) + + for role := range rb.roles { + addresses[role] = rb.roles[role].Address() + } + + return addresses +} + +// GetRoleAddress returns the address assigned to roleName. Returns ErrRoleDoesNotExist if the role doesn't exist. +func (rb *RBAC) GetRoleAddress(roleName string) (std.Address, error) { + role, exists := rb.roles[roleName] + if !exists { + return "", ErrRoleDoesNotExist + } + + return role.Address(), nil +} + +// UpdateRoleAddress assigns addr to roleName. Returns ErrRoleDoesNotExist if the role doesn't exist or ErrInvalidAddress if addr is invalid. +func (rb *RBAC) UpdateRoleAddress(roleName string, addr std.Address) error { + role, exists := rb.roles[roleName] + if !exists { + return ErrRoleDoesNotExist + } + + if !addr.IsValid() { + return ErrInvalidAddress + } + + role.setAddress(addr) + + return nil +} + +// RemoveRoleAddress removes the address assignment from roleName. Only the owner can call this function. Returns ErrUnauthorized if caller is not owner or ErrRoleDoesNotExist if role doesn't exist. +func (rb *RBAC) RemoveRoleAddress(roleName string) error { + if !rb.ownable.OwnedByOriginCaller() { + return ErrUnauthorized + } + + role, exists := rb.roles[roleName] + if !exists { + return ErrRoleDoesNotExist + } + + role.setAddress("") + + return nil +} + +// Owner returns the current owner address. +func (rb *RBAC) Owner() std.Address { return rb.ownable.Owner() } + +// OwnedByOriginCaller returns true if the origin caller is the current owner. +func (rb *RBAC) OwnedByOriginCaller() bool { return rb.ownable.OwnedByOriginCaller() } + +// PendingOwner returns the pending owner address during ownership transfer. +func (rb *RBAC) PendingOwner() std.Address { return rb.ownable.PendingOwner() } + +// AcceptOwnership completes the ownership transfer process. Must be called by the pending owner. +func (rb *RBAC) AcceptOwnership() error { return rb.ownable.AcceptOwnership() } + +// DropOwnership removes the owner, effectively disabling owner-only actions. Only the current owner can call this function. +func (rb *RBAC) DropOwnership() error { return rb.ownable.DropOwnership() } + +// TransferOwnership initiates the two-step ownership transfer process to newOwner. Only the current owner can call this function. +func (rb *RBAC) TransferOwnership(newOwner std.Address) error { + return rb.ownable.TransferOwnership(newOwner) +} + +// existsRole checks if name exists in the RBAC system. +func (rb *RBAC) existsRole(name string) bool { + _, exists := rb.roles[name] + return exists +} diff --git a/contract/p/gnoswap/rbac/rbac_test.gno b/contract/p/gnoswap/rbac/rbac_test.gno new file mode 100644 index 0000000..4573caa --- /dev/null +++ b/contract/p/gnoswap/rbac/rbac_test.gno @@ -0,0 +1,72 @@ +package rbac + +import ( + "std" + "testing" + + "gno.land/p/nt/testutils" + "gno.land/p/nt/uassert" + "gno.land/p/nt/ufmt" +) + +var ( + adminRole = "admin" + editorRole = "editor" + + adminAddr = testutils.TestAddress(adminRole) + userAddr = testutils.TestAddress("user") + editorAddr = testutils.TestAddress(editorRole) +) + +func adminChecker(caller std.Address) error { + if caller != adminAddr { + return ufmt.Errorf("caller is not admin") + } + return nil +} + +func editorChecker(caller std.Address) error { + if caller != editorAddr { + return ufmt.Errorf("caller is not editor") + } + return nil +} + +func TestRegisterRole(t *testing.T) { + manager := New() + + err := manager.RegisterRole(adminRole) + uassert.NoError(t, err) + + err = manager.RegisterRole(adminRole) + uassert.Error(t, err) +} + +func TestRemoveRole(t *testing.T) { + manager := New() + + t.Run("success remove non-system role", func(t *testing.T) { + // First register role and permission + err := manager.RegisterRole(editorRole) + uassert.NoError(t, err) + + // Remove role + err = manager.RemoveRole(editorRole) + uassert.NoError(t, err) + }) + + t.Run("fail to remove non-existent role", func(t *testing.T) { + err := manager.RemoveRole("non_existent_role") + uassert.Error(t, err) + }) + + t.Run("fail to remove system role", func(t *testing.T) { + // Register system role + err := manager.RegisterRole(adminRole) + uassert.NoError(t, err) + + // Try to remove system role + err = manager.RemoveRole(adminRole) + uassert.Error(t, err) + }) +} diff --git a/contract/p/gnoswap/rbac/role.gno b/contract/p/gnoswap/rbac/role.gno new file mode 100644 index 0000000..46c82de --- /dev/null +++ b/contract/p/gnoswap/rbac/role.gno @@ -0,0 +1,35 @@ +package rbac + +import ( + "std" +) + +// Role represents a role with a name and an assigned address. +type Role struct { + // name represents the role's identifier + name string + address string +} + +// NewRole creates a new Role instance with roleName. +func NewRole(roleName string) *Role { + return &Role{ + name: roleName, + address: "", + } +} + +// Name returns the role's name. +func (r *Role) Name() string { return r.name } + +// Address returns the address assigned to this role. Returns empty address if no address is assigned. +func (r *Role) Address() std.Address { return std.Address(r.address) } + +// IsEmpty returns true if no address is assigned to this role. +func (r *Role) IsEmpty() bool { return r.address == "" } + +// IsAuthorized returns true if addr matches the role's assigned address. +func (r *Role) IsAuthorized(addr std.Address) bool { return r.address == addr.String() } + +// setAddress assigns addr to this role. +func (r *Role) setAddress(addr std.Address) { r.address = addr.String() } diff --git a/contract/p/gnoswap/rbac/types.gno b/contract/p/gnoswap/rbac/types.gno new file mode 100644 index 0000000..135b0ab --- /dev/null +++ b/contract/p/gnoswap/rbac/types.gno @@ -0,0 +1,54 @@ +package rbac + +// SystemRole represents a predefined system role that cannot be removed. +type SystemRole string + +var ( + ROLE_ADMIN SystemRole = "admin" + ROLE_DEVOPS SystemRole = "devops" + ROLE_COMMUNITY_POOL SystemRole = "community_pool" + ROLE_GOVERNANCE SystemRole = "governance" + ROLE_GOV_STAKER SystemRole = "gov_staker" + ROLE_XGNS SystemRole = "xgns" + ROLE_POOL SystemRole = "pool" + ROLE_POSITION SystemRole = "position" + ROLE_ROUTER SystemRole = "router" + ROLE_STAKER SystemRole = "staker" + ROLE_EMISSION SystemRole = "emission" + ROLE_LAUNCHPAD SystemRole = "launchpad" + ROLE_PROTOCOL_FEE SystemRole = "protocol_fee" +) + +var systemRoleNames = map[string]SystemRole{ + "admin": ROLE_ADMIN, + "devops": ROLE_DEVOPS, + "community_pool": ROLE_COMMUNITY_POOL, + "governance": ROLE_GOVERNANCE, + "gov_staker": ROLE_GOV_STAKER, + "xgns": ROLE_XGNS, + "pool": ROLE_POOL, + "position": ROLE_POSITION, + "router": ROLE_ROUTER, + "staker": ROLE_STAKER, + "emission": ROLE_EMISSION, + "launchpad": ROLE_LAUNCHPAD, + "protocol_fee": ROLE_PROTOCOL_FEE, +} + +// String returns the string representation of the SystemRole. +// Returns "Unknown" if the role is not a valid system role. +func (r SystemRole) String() string { + roleName := string(r) + if _, ok := systemRoleNames[roleName]; !ok { + return "Unknown" + } + + return roleName +} + +// IsSystemRole returns true if roleName is a system role. +func IsSystemRole(roleName string) bool { + _, ok := systemRoleNames[roleName] + + return ok +} diff --git a/contract/p/gnoswap/uint256/LICENSE b/contract/p/gnoswap/uint256/LICENSE new file mode 100644 index 0000000..505e432 --- /dev/null +++ b/contract/p/gnoswap/uint256/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright 2020 uint256 Authors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/contract/p/gnoswap/uint256/README.md b/contract/p/gnoswap/uint256/README.md new file mode 100644 index 0000000..e290ec8 --- /dev/null +++ b/contract/p/gnoswap/uint256/README.md @@ -0,0 +1,38 @@ +# uint256 + +256-bit unsigned integer arithmetic for GnoSwap. + +## Overview + +Fixed-size 256-bit unsigned integer library optimized for AMM calculations with overflow detection and precise MulDiv operations. + +## Features + +- Fixed 256-bit size (4 uint64 values) +- Overflow detection on all operations +- Optimized MulDiv for precise calculations +- String conversion (decimal, hex, binary) +- Range: 0 to 2^256-1 + +## Usage + +```go +import u256 "gno.land/p/gnoswap/uint256" + +// Create values +a := u256.NewUint(1000) +b := u256.MustFromDecimal("1000000000000000000") + +// Arithmetic with overflow detection +result, overflow := new(u256.Uint).AddOverflow(a, b) +if overflow { + // Handle overflow +} + +// Precise MulDiv (a * b / c) +result := u256.MulDiv(a, b, c) +``` + +## Credits + +Ported from [holiman/uint256](https://github.com/holiman/uint256) \ No newline at end of file diff --git a/contract/p/gnoswap/uint256/_helper_test.gno b/contract/p/gnoswap/uint256/_helper_test.gno new file mode 100644 index 0000000..59d90e7 --- /dev/null +++ b/contract/p/gnoswap/uint256/_helper_test.gno @@ -0,0 +1,52 @@ +package uint256 + +import ( + "testing" +) + +func shouldEQ(t *testing.T, got, expected any) { + if got != expected { + t.Errorf("got %v, expected %v", got, expected) + } +} + +func shouldNEQ(t *testing.T, got, expected any) { + if got == expected { + t.Errorf("got %v, didn't expected %v", got, expected) + } +} + +func shouldPanic(t *testing.T, f func()) { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic") + } + }() + f() +} + +func shouldPanicWithMsg(t *testing.T, f func(), msg string) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else { + if r != msg { + t.Errorf("excepted panic(%v), got(%v)", msg, r) + } + } + }() + f() +} + +// for original tests +func parseUint(s string) *Uint { + if len(s) >= 2 && s[:2] == "0x" { + return MustFromHex(s) + } + return MustFromDecimal(s) +} + +// for testing.T +func parseUintT(t *testing.T, s string) *Uint { + return parseUint(s) +} diff --git a/contract/p/gnoswap/uint256/arithmetic.gno b/contract/p/gnoswap/uint256/arithmetic.gno new file mode 100644 index 0000000..ca67a03 --- /dev/null +++ b/contract/p/gnoswap/uint256/arithmetic.gno @@ -0,0 +1,538 @@ +// arithmetic provides arithmetic operations for Uint objects. +// This includes basic binary operations such as addition, subtraction, multiplication, division, and modulo operations +// as well as overflow checks, and negation. These functions are essential for numeric +// calculations using 256-bit unsigned integers. +package uint256 + +import ( + "math/bits" +) + +// Add sets z to the sum x+y and returns z. +func (z *Uint) Add(x, y *Uint) *Uint { + var carry uint64 + z.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0) + z.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry) + z.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry) + z.arr[3], _ = bits.Add64(x.arr[3], y.arr[3], carry) + return z +} + +// AddOverflow sets z to the sum x+y and returns z and true if overflow occurred. +func (z *Uint) AddOverflow(x, y *Uint) (*Uint, bool) { + var carry uint64 + z.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0) + z.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry) + z.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry) + z.arr[3], carry = bits.Add64(x.arr[3], y.arr[3], carry) + return z, carry != 0 +} + +// Sub sets z to the difference x-y and returns z. +func (z *Uint) Sub(x, y *Uint) *Uint { + var carry uint64 + z.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0) + z.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry) + z.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry) + z.arr[3], _ = bits.Sub64(x.arr[3], y.arr[3], carry) + return z +} + +// SubOverflow sets z to the difference x-y and returns z and true if underflow occurred. +func (z *Uint) SubOverflow(x, y *Uint) (*Uint, bool) { + var carry uint64 + z.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0) + z.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry) + z.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry) + z.arr[3], carry = bits.Sub64(x.arr[3], y.arr[3], carry) + return z, carry != 0 +} + +// Neg returns -x mod 2^256. +func (z *Uint) Neg(x *Uint) *Uint { + return z.Sub(Zero(), x) +} + +// Mul sets z to the product x*y and returns z. +func (z *Uint) Mul(x, y *Uint) *Uint { + var ( + res Uint + carry uint64 + res1, res2, res3 uint64 + ) + + carry, res.arr[0] = bits.Mul64(x.arr[0], y.arr[0]) + carry, res1 = umulHop(carry, x.arr[1], y.arr[0]) + carry, res2 = umulHop(carry, x.arr[2], y.arr[0]) + res3 = x.arr[3]*y.arr[0] + carry + + carry, res.arr[1] = umulHop(res1, x.arr[0], y.arr[1]) + carry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry) + res3 = res3 + x.arr[2]*y.arr[1] + carry + + carry, res.arr[2] = umulHop(res2, x.arr[0], y.arr[2]) + res3 = res3 + x.arr[1]*y.arr[2] + carry + + res.arr[3] = res3 + x.arr[0]*y.arr[3] + + return z.Set(&res) +} + +// MulOverflow sets z to the product x*y and returns z and true if overflow occurred. +func (z *Uint) MulOverflow(x, y *Uint) (*Uint, bool) { + p := umul(x, y) + copy(z.arr[:], p[:4]) + return z, (p[4] | p[5] | p[6] | p[7]) != 0 +} + +// Div sets z to the quotient x/y and returns z. +// If y == 0, z is set to 0. +func (z *Uint) Div(x, y *Uint) *Uint { + if y.IsZero() || y.Gt(x) { + return z.Clear() + } + if x.Eq(y) { + return z.SetOne() + } + // Shortcut some cases + if x.IsUint64() { + return z.SetUint64(x.Uint64() / y.Uint64()) + } + + // At this point, we know + // x/y ; x > y > 0 + + var quot Uint + udivrem(quot.arr[:], x.arr[:], y) + return z.Set(") +} + +// Mod sets z to the modulus x%y for y != 0 and returns z. +// If y == 0, z is set to 0 (this differs from big.Int behavior). +func (z *Uint) Mod(x, y *Uint) *Uint { + if x.IsZero() || y.IsZero() { + return z.Clear() + } + switch x.Cmp(y) { + case -1: + // x < y + copy(z.arr[:], x.arr[:]) + return z + case 0: + // x == y + return z.Clear() // They are equal + } + + // At this point: + // x != 0 + // y != 0 + // x > y + + // Shortcut trivial case + if x.IsUint64() { + return z.SetUint64(x.Uint64() % y.Uint64()) + } + + var quot Uint + *z = udivrem(quot.arr[:], x.arr[:], y) + return z +} + +// MulMod sets z to (x * y) mod m and returns z. +// If m == 0, z is set to 0 (this differs from big.Int behavior). +func (z *Uint) MulMod(x, y, m *Uint) *Uint { + if x.IsZero() || y.IsZero() || m.IsZero() { + return z.Clear() + } + p := umul(x, y) + + if m.arr[3] != 0 { + mu := Reciprocal(m) + r := reduce4(p, m, mu) + return z.Set(&r) + } + + var ( + pl = Uint{arr: [4]uint64{p[0], p[1], p[2], p[3]}} + ph = Uint{arr: [4]uint64{p[4], p[5], p[6], p[7]}} + ) + + // If the multiplication is within 256 bits use Mod(). + if ph.IsZero() { + return z.Mod(&pl, m) + } + + var quot [8]uint64 + rem := udivrem(quot[:], p[:], m) + return z.Set(&rem) +} + +// DivMod sets z to the quotient x/y and m to the modulus x%y, returning the pair (z, m). +// If y == 0, both z and m are set to 0 (this differs from big.Int behavior). +func (z *Uint) DivMod(x, y, m *Uint) (*Uint, *Uint) { + if y.IsZero() { + return z.Clear(), m.Clear() + } + + switch x.Cmp(y) { + case -1: + // x < y + return z.Clear(), m.Set(x) + case 0: + // x == y + return z.SetOne(), m.Clear() + } + + // At this point: + // x != 0 + // y != 0 + // x > y + + // Shortcut trivial case + if x.IsUint64() { + x0, y0 := x.Uint64(), y.Uint64() + return z.SetUint64(x0 / y0), m.SetUint64(x0 % y0) + } + + var quot Uint + *m = udivrem(quot.arr[:], x.arr[:], y) + *z = quot + return z, m +} + +// Exp sets z to base**exponent mod 2**256 and returns z. +// The result is wrapped at 2^256 boundary. +func (z *Uint) Exp(base, exponent *Uint) *Uint { + res := Uint{arr: [4]uint64{1, 0, 0, 0}} + multiplier := *base + expBitLen := exponent.BitLen() + + curBit := 0 + word := exponent.arr[0] + for ; curBit < expBitLen && curBit < 64; curBit++ { + if word&1 == 1 { + res.Mul(&res, &multiplier) + } + multiplier.squared() + word >>= 1 + } + + word = exponent.arr[1] + for ; curBit < expBitLen && curBit < 128; curBit++ { + if word&1 == 1 { + res.Mul(&res, &multiplier) + } + multiplier.squared() + word >>= 1 + } + + word = exponent.arr[2] + for ; curBit < expBitLen && curBit < 192; curBit++ { + if word&1 == 1 { + res.Mul(&res, &multiplier) + } + multiplier.squared() + word >>= 1 + } + + word = exponent.arr[3] + for ; curBit < expBitLen && curBit < 256; curBit++ { + if word&1 == 1 { + res.Mul(&res, &multiplier) + } + multiplier.squared() + word >>= 1 + } + return z.Set(&res) +} + +func (z *Uint) squared() { + var ( + res Uint + carry0, carry1, carry2 uint64 + res1, res2 uint64 + ) + + carry0, res.arr[0] = bits.Mul64(z.arr[0], z.arr[0]) + carry0, res1 = umulHop(carry0, z.arr[0], z.arr[1]) + carry0, res2 = umulHop(carry0, z.arr[0], z.arr[2]) + + carry1, res.arr[1] = umulHop(res1, z.arr[0], z.arr[1]) + carry1, res2 = umulStep(res2, z.arr[1], z.arr[1], carry1) + + carry2, res.arr[2] = umulHop(res2, z.arr[0], z.arr[2]) + + res.arr[3] = 2*(z.arr[0]*z.arr[3]+z.arr[1]*z.arr[2]) + carry0 + carry1 + carry2 + + z.Set(&res) +} + +// udivrem divides u by d and produces both quotient and remainder. +// The quotient is stored in provided quot - len(u)-len(d)+1 words. +// It loosely follows the Knuth's division algorithm (sometimes referenced as "schoolbook" division) using 64-bit words. +// See Knuth, Volume 2, section 4.3.1, Algorithm D. +func udivrem(quot, u []uint64, d *Uint) (rem Uint) { + var dLen int + for i := len(d.arr) - 1; i >= 0; i-- { + if d.arr[i] != 0 { + dLen = i + 1 + break + } + } + + shift := uint(bits.LeadingZeros64(d.arr[dLen-1])) + + var dnStorage Uint + dn := dnStorage.arr[:dLen] + for i := dLen - 1; i > 0; i-- { + dn[i] = (d.arr[i] << shift) | (d.arr[i-1] >> (64 - shift)) + } + dn[0] = d.arr[0] << shift + + var uLen int + for i := len(u) - 1; i >= 0; i-- { + if u[i] != 0 { + uLen = i + 1 + break + } + } + + if uLen < dLen { + copy(rem.arr[:], u) + return rem + } + + var unStorage [9]uint64 + un := unStorage[:uLen+1] + un[uLen] = u[uLen-1] >> (64 - shift) + for i := uLen - 1; i > 0; i-- { + un[i] = (u[i] << shift) | (u[i-1] >> (64 - shift)) + } + un[0] = u[0] << shift + + // TODO: Skip the highest word of numerator if not significant. + + if dLen == 1 { + r := udivremBy1(quot, un, dn[0]) + rem.SetUint64(r >> shift) + return rem + } + + udivremKnuth(quot, un, dn) + + for i := 0; i < dLen-1; i++ { + rem.arr[i] = (un[i] >> shift) | (un[i+1] << (64 - shift)) + } + rem.arr[dLen-1] = un[dLen-1] >> shift + + return rem +} + +// umul computes full 256 x 256 -> 512 multiplication. +func umul(x, y *Uint) [8]uint64 { + var ( + res [8]uint64 + carry, carry4, carry5, carry6 uint64 + res1, res2, res3, res4, res5 uint64 + ) + + carry, res[0] = bits.Mul64(x.arr[0], y.arr[0]) + carry, res1 = umulHop(carry, x.arr[1], y.arr[0]) + carry, res2 = umulHop(carry, x.arr[2], y.arr[0]) + carry4, res3 = umulHop(carry, x.arr[3], y.arr[0]) + + carry, res[1] = umulHop(res1, x.arr[0], y.arr[1]) + carry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry) + carry, res3 = umulStep(res3, x.arr[2], y.arr[1], carry) + carry5, res4 = umulStep(carry4, x.arr[3], y.arr[1], carry) + + carry, res[2] = umulHop(res2, x.arr[0], y.arr[2]) + carry, res3 = umulStep(res3, x.arr[1], y.arr[2], carry) + carry, res4 = umulStep(res4, x.arr[2], y.arr[2], carry) + carry6, res5 = umulStep(carry5, x.arr[3], y.arr[2], carry) + + carry, res[3] = umulHop(res3, x.arr[0], y.arr[3]) + carry, res[4] = umulStep(res4, x.arr[1], y.arr[3], carry) + carry, res[5] = umulStep(res5, x.arr[2], y.arr[3], carry) + res[7], res[6] = umulStep(carry6, x.arr[3], y.arr[3], carry) + + return res +} + +// umulStep computes (hi * 2^64 + lo) = z + (x * y) + carry. +func umulStep(z, x, y, carry uint64) (hi, lo uint64) { + hi, lo = bits.Mul64(x, y) + lo, carry = bits.Add64(lo, carry, 0) + hi, _ = bits.Add64(hi, 0, carry) + lo, carry = bits.Add64(lo, z, 0) + hi, _ = bits.Add64(hi, 0, carry) + return hi, lo +} + +// umulHop computes (hi * 2^64 + lo) = z + (x * y) +func umulHop(z, x, y uint64) (hi, lo uint64) { + hi, lo = bits.Mul64(x, y) + lo, carry := bits.Add64(lo, z, 0) + hi, _ = bits.Add64(hi, 0, carry) + return hi, lo +} + +// udivremBy1 divides u by single normalized word d and produces both quotient and remainder. +// The quotient is stored in provided quot. +func udivremBy1(quot, u []uint64, d uint64) (rem uint64) { + reciprocal := reciprocal2by1(d) + rem = u[len(u)-1] // Set the top word as remainder. + for j := len(u) - 2; j >= 0; j-- { + quot[j], rem = udivrem2by1(rem, u[j], d, reciprocal) + } + return rem +} + +// udivremKnuth implements the division of u by normalized multiple word d from the Knuth's division algorithm. +// The quotient is stored in provided quot - len(u)-len(d) words. +// Updates u to contain the remainder - len(d) words. +func udivremKnuth(quot, u, d []uint64) { + dh := d[len(d)-1] + dl := d[len(d)-2] + reciprocal := reciprocal2by1(dh) + + for j := len(u) - len(d) - 1; j >= 0; j-- { + u2 := u[j+len(d)] + u1 := u[j+len(d)-1] + u0 := u[j+len(d)-2] + + var qhat, rhat uint64 + if u2 >= dh { // Division overflows. + qhat = ^uint64(0) + // TODO: Add "qhat one to big" adjustment (not needed for correctness, but helps avoiding "add back" case). + } else { + qhat, rhat = udivrem2by1(u2, u1, dh, reciprocal) + ph, pl := bits.Mul64(qhat, dl) + if ph > rhat || (ph == rhat && pl > u0) { + qhat-- + // TODO: Add "qhat one to big" adjustment (not needed for correctness, but helps avoiding "add back" case). + } + } + + // Multiply and subtract. + borrow := subMulTo(u[j:], d, qhat) + u[j+len(d)] = u2 - borrow + if u2 < borrow { // Too much subtracted, add back. + qhat-- + u[j+len(d)] += addTo(u[j:], d) + } + + quot[j] = qhat // Store quotient digit. + } +} + +// isBitSet returns true if bit n-th is set, where n = 0 is LSB. +// The n must be <= 255. +func (z *Uint) isBitSet(n uint) bool { + return (z.arr[n/64] & (1 << (n % 64))) != 0 +} + +func (z *Uint) IsOverflow() bool { + return z.isBitSet(255) +} + +// addTo computes x += y. +// Requires len(x) >= len(y). +func addTo(x, y []uint64) uint64 { + var carry uint64 + for i := 0; i < len(y); i++ { + x[i], carry = bits.Add64(x[i], y[i], carry) + } + return carry +} + +// subMulTo computes x -= y * multiplier. +// Requires len(x) >= len(y). +func subMulTo(x, y []uint64, multiplier uint64) uint64 { + var borrow uint64 + for i := 0; i < len(y); i++ { + s, carry1 := bits.Sub64(x[i], borrow, 0) + ph, pl := bits.Mul64(y[i], multiplier) + t, carry2 := bits.Sub64(s, pl, 0) + x[i] = t + borrow = ph + carry1 + carry2 + } + return borrow +} + +// reciprocal2by1 computes <^d, ^0> / d. +func reciprocal2by1(d uint64) uint64 { + reciprocal, _ := bits.Div64(^d, ^uint64(0), d) + return reciprocal +} + +// udivrem2by1 divides / d and produces both quotient and remainder. +// It uses the provided d's reciprocal. +// Implementation ported from https://github.com/chfast/intx and is based on +// "Improved division by invariant integers", Algorithm 4. +func udivrem2by1(uh, ul, d, reciprocal uint64) (quot, rem uint64) { + qh, ql := bits.Mul64(reciprocal, uh) + ql, carry := bits.Add64(ql, ul, 0) + qh, _ = bits.Add64(qh, uh, carry) + qh++ + + r := ul - qh*d + + if r > ql { + qh-- + r += d + } + + if r >= d { + qh++ + r -= d + } + + return qh, r +} + +// MustDiv sets z to the quotient x/y and returns z. +// It panics if y == 0. Used in critical AMM paths where division by zero represents a programming error. +func (z *Uint) MustDiv(x, y *Uint) *Uint { + if y.IsZero() { + panic("division by zero") + } + return z.Div(x, y) +} + +// MustMod sets z to the modulus x%y and returns z. +// It panics if y == 0. Used in critical AMM paths where modulo by zero represents a programming error. +func (z *Uint) MustMod(x, y *Uint) *Uint { + if y.IsZero() { + panic("modulo by zero") + } + return z.Mod(x, y) +} + +// MustMulMod sets z to (x * y) mod m and returns z. +// It panics if m == 0. Used in critical AMM paths where modulo by zero represents a programming error. +func (z *Uint) MustMulMod(x, y, m *Uint) *Uint { + if m.IsZero() { + panic("modulo by zero") + } + return z.MulMod(x, y, m) +} + +// MustDivMod sets z to the quotient x/y and m to the modulus x%y, returning the pair (z, m). +// It panics if y == 0. Used in critical AMM paths where division by zero represents a programming error. +func (z *Uint) MustDivMod(x, y, m *Uint) (*Uint, *Uint) { + if y.IsZero() { + panic("division by zero") + } + return z.DivMod(x, y, m) +} + +// MustMul sets z to the product x*y and returns z. +// It panics on overflow. Used in critical AMM calculations where overflow represents a programming error. +func (z *Uint) MustMul(x, y *Uint) *Uint { + result, overflow := z.MulOverflow(x, y) + if overflow { + panic("uint256: multiplication overflow") + } + return result +} diff --git a/contract/p/gnoswap/uint256/arithmetic_test.gno b/contract/p/gnoswap/uint256/arithmetic_test.gno new file mode 100644 index 0000000..3e0c7ed --- /dev/null +++ b/contract/p/gnoswap/uint256/arithmetic_test.gno @@ -0,0 +1,1883 @@ +package uint256 + +import ( + "testing" +) + +// TestAdd verifies addition operations +func TestAdd(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + }{ + // Basic addition + { + name: "zero_plus_zero", + x: "0", + y: "0", + want: "0", + }, + { + name: "zero_plus_one", + x: "0", + y: "1", + want: "1", + }, + { + name: "one_plus_one", + x: "1", + y: "1", + want: "2", + }, + { + name: "ten_plus_ten", + x: "10", + y: "10", + want: "20", + }, + + // uint64 boundary + { + name: "uint64_max_plus_one", + x: "18446744073709551615", + y: "1", + want: "18446744073709551616", + }, + { + name: "uint64_max_plus_uint64_max", + x: "18446744073709551615", + y: "18446744073709551615", + want: "36893488147419103230", + }, + + // Carry propagation + { + name: "carry_through_single_word", + x: "0xFFFFFFFFFFFFFFFF", + y: "1", + want: "0x10000000000000000", + }, + { + name: "carry_through_multiple_words", + x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + y: "1", + want: "0x100000000000000000000000000000000", + }, + + // Maximum value edge cases + { + name: "max_plus_zero", + x: MAX_UINT256, + y: "0", + want: MAX_UINT256, + }, + { + name: "max_plus_one_wraps_to_zero", + x: MAX_UINT256, + y: "1", + want: "0", + }, + { + name: "half_max_plus_half_max_plus_one", + x: "57896044618658097711785492504343953926634992332820282019728792003956564819967", + y: "57896044618658097711785492504343953926634992332820282019728792003956564819968", + want: MAX_UINT256, + }, + + // Additional boundary cases + { + name: "max_minus_one_plus_one", + x: "115792089237316195423570985008687907853269984665640564039457584007913129639934", + y: "1", + want: MAX_UINT256, + }, + { + name: "max_minus_one_plus_two", + x: "115792089237316195423570985008687907853269984665640564039457584007913129639934", + y: "2", + want: "0", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUintT(t, tc.x) + y := parseUintT(t, tc.y) + want := parseUintT(t, tc.want) + + got := new(Uint).Add(x, y) + if !got.Eq(want) { + t.Errorf("Add(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + }) + } +} + +// TestAddOverflow verifies addition with overflow detection +func TestAddOverflow(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + overflow bool + }{ + { + name: "no_overflow_small_numbers", + x: "100", + y: "200", + want: "300", + overflow: false, + }, + { + name: "no_overflow_at_boundary", + x: MAX_UINT256, + y: "0", + want: MAX_UINT256, + overflow: false, + }, + { + name: "overflow_max_plus_one", + x: MAX_UINT256, + y: "1", + want: "0", + overflow: true, + }, + { + name: "overflow_max_plus_max", + x: MAX_UINT256, + y: MAX_UINT256, + want: "115792089237316195423570985008687907853269984665640564039457584007913129639934", + overflow: true, + }, + { + name: "overflow_with_carry_propagation", + x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + y: "1", + want: "0", + overflow: true, + }, + { + name: "no_overflow_half_max_times_two", + x: "57896044618658097711785492504343953926634992332820282019728792003956564819967", + y: "57896044618658097711785492504343953926634992332820282019728792003956564819967", + want: "115792089237316195423570985008687907853269984665640564039457584007913129639934", + overflow: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + want := parseUint(tc.want) + + got, overflow := new(Uint).AddOverflow(x, y) + if !got.Eq(want) { + t.Errorf("AddOverflow(%s, %s) result = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + if overflow != tc.overflow { + t.Errorf("AddOverflow(%s, %s) overflow = %v, want %v", tc.x, tc.y, overflow, tc.overflow) + } + }) + } +} + +// TestSub verifies subtraction operations +func TestSub(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + }{ + // Basic subtraction + { + name: "one_minus_zero", + x: "1", + y: "0", + want: "1", + }, + { + name: "one_minus_one", + x: "1", + y: "1", + want: "0", + }, + { + name: "thousand_minus_hundred", + x: "31337", + y: "1337", + want: "30000", + }, + + // Underflow cases (wraps around) + { + name: "zero_minus_one_wraps_to_max", + x: "0", + y: "1", + want: MAX_UINT256, + }, + { + name: "small_minus_large", + x: "2", + y: "3", + want: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + + // Maximum value cases + { + name: "max_minus_zero", + x: MAX_UINT256, + y: "0", + want: MAX_UINT256, + }, + { + name: "max_minus_max", + x: MAX_UINT256, + y: MAX_UINT256, + want: "0", + }, + + // Borrow propagation + { + name: "borrow_propagation_single_word", + x: "0x10000000000000000", + y: "1", + want: "0xFFFFFFFFFFFFFFFF", + }, + { + name: "borrow_propagation_multiple_words", + x: "0x100000000000000000000000000000000", + y: "1", + want: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + }, + { + name: "borrow_propagation_all_words", + x: "0", // Start with 0 and add 1, then subtract 1 to test borrow + y: "1", + want: MAX_UINT256, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + want := parseUint(tc.want) + + got := new(Uint).Sub(x, y) + if !got.Eq(want) { + t.Errorf("Sub(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + }) + } +} + +// TestSubOverflow verifies subtraction with underflow detection +func TestSubOverflow(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + underflow bool + }{ + { + name: "no_underflow_normal_case", + x: "1000", + y: "100", + want: "900", + underflow: false, + }, + { + name: "no_underflow_equal_values", + x: "12345", + y: "12345", + want: "0", + underflow: false, + }, + { + name: "underflow_zero_minus_one", + x: "0", + y: "1", + want: MAX_UINT256, + underflow: true, + }, + { + name: "underflow_small_minus_large", + x: "100", + y: "1000", + want: "115792089237316195423570985008687907853269984665640564039457584007913129639036", + underflow: true, + }, + { + name: "underflow_one_minus_max", + x: "1", + y: MAX_UINT256, + want: "2", + underflow: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + want := parseUint(tc.want) + + got, underflow := new(Uint).SubOverflow(x, y) + if !got.Eq(want) { + t.Errorf("SubOverflow(%s, %s) result = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + if underflow != tc.underflow { + t.Errorf("SubOverflow(%s, %s) underflow = %v, want %v", tc.x, tc.y, underflow, tc.underflow) + } + }) + } +} + +// TestMul verifies multiplication operations +func TestMul(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + }{ + // Basic multiplication + { + name: "zero_times_zero", + x: "0", + y: "0", + want: "0", + }, + { + name: "one_times_one", + x: "1", + y: "1", + want: "1", + }, + { + name: "ten_times_ten", + x: "10", + y: "10", + want: "100", + }, + + // uint64 overflow but not uint256 + { + name: "uint64_max_times_two", + x: "18446744073709551615", + y: "2", + want: "36893488147419103230", + }, + { + name: "large_numbers_no_overflow", + x: "0xFFFFFFFFFFFFFFFF", + y: "0xFFFFFFFFFFFFFFFF", + want: "0xFFFFFFFFFFFFFFFE0000000000000001", + }, + + // Maximum value cases + { + name: "max_times_zero", + x: MAX_UINT256, + y: "0", + want: "0", + }, + { + name: "max_times_one", + x: MAX_UINT256, + y: "1", + want: MAX_UINT256, + }, + + // Q128 related (sqrt of max uint256) + { + name: "q128_minus_one_squared", + x: "340282366920938463463374607431768211455", + y: "340282366920938463463374607431768211455", + want: "115792089237316195423570985008687907852589419931798687112530834793049593217025", + }, + + // Q96 related (Uniswap V3 price) + { + name: "q96_squared", + x: "79228162514264337593543950336", + y: "79228162514264337593543950336", + want: "6277101735386680763835789423207666416102355444464034512896", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + want := parseUint(tc.want) + + got := new(Uint).Mul(x, y) + if !got.Eq(want) { + t.Errorf("Mul(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + }) + } +} + +// TestMulOverflow verifies multiplication with overflow detection +func TestMulOverflow(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + overflow bool + }{ + { + name: "no_overflow_small_numbers", + x: "1000", + y: "2000", + want: "2000000", + overflow: false, + }, + { + name: "no_overflow_at_boundary", + x: "340282366920938463463374607431768211455", + y: "340282366920938463463374607431768211455", + want: "115792089237316195423570985008687907852589419931798687112530834793049593217025", + overflow: false, + }, + { + name: "overflow_q128_squared", + x: "340282366920938463463374607431768211456", + y: "340282366920938463463374607431768211456", + want: "0", + overflow: true, + }, + { + name: "overflow_max_times_two", + x: MAX_UINT256, + y: "2", + want: "115792089237316195423570985008687907853269984665640564039457584007913129639934", + overflow: true, + }, + { + name: "overflow_max_times_max", + x: MAX_UINT256, + y: MAX_UINT256, + want: "1", + overflow: true, + }, + { + name: "no_overflow_large_numbers", + x: "340282366920938463463374607431768211455", // 2^128 - 1 + y: "340282366920938463463374607431768211456", // 2^128 + want: "115792089237316195423570985008687907852929702298719625575994209400481361428480", + overflow: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + want := parseUint(tc.want) + + got, overflow := new(Uint).MulOverflow(x, y) + if !got.Eq(want) { + t.Errorf("MulOverflow(%s, %s) result = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + if overflow != tc.overflow { + t.Errorf("MulOverflow(%s, %s) overflow = %v, want %v", tc.x, tc.y, overflow, tc.overflow) + } + }) + } +} + +// TestDiv verifies division operations +func TestDiv(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + wantErr bool + }{ + // Basic division + { + name: "ten_div_two", + x: "10", + y: "2", + want: "5", + wantErr: false, + }, + { + name: "exact_division", + x: "31337", + y: "3", + want: "10445", + wantErr: false, + }, + { + name: "division_with_remainder", + x: "10", + y: "3", + want: "3", + wantErr: false, + }, + + // Special cases + { + name: "zero_div_nonzero", + x: "0", + y: "31337", + want: "0", + wantErr: false, + }, + { + name: "smaller_div_larger", + x: "2", + y: "31337", + want: "0", + wantErr: false, + }, + { + name: "equal_div_equal", + x: "31337", + y: "31337", + want: "1", + wantErr: false, + }, + + // Division by zero + { + name: "div_by_zero", + x: "31337", + y: "0", + want: "", + wantErr: true, + }, + { + name: "zero_div_zero", + x: "0", + y: "0", + want: "", + wantErr: true, + }, + + // Large numbers + { + name: "max_div_one", + x: MAX_UINT256, + y: "1", + want: MAX_UINT256, + wantErr: false, + }, + { + name: "max_div_two", + x: MAX_UINT256, + y: "2", + want: "57896044618658097711785492504343953926634992332820282019728792003956564819967", + wantErr: false, + }, + { + name: "max_div_max", + x: MAX_UINT256, + y: MAX_UINT256, + want: "1", + wantErr: false, + }, + { + name: "large_div_large", + x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + y: "0xFFFFFFFFFFFFFFFF", + want: "0x10000000000000001", + wantErr: false, + }, + + // Verify truncation (rounds down) + { + name: "verify_truncation", + x: "1000000000000000001", + y: "1000000000000000000", + want: "1", + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + + got := new(Uint).Div(x, y) + + if tc.wantErr { + // Division by zero returns 0 + if !got.IsZero() { + t.Errorf("Div(%s, %s) expected zero for division by zero", tc.x, tc.y) + } + return + } + + want := parseUint(tc.want) + if !got.Eq(want) { + t.Errorf("Div(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + }) + } +} + +// TestMod verifies modulo operations +func TestMod(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + wantErr bool + }{ + // Basic modulo + { + name: "ten_mod_three", + x: "10", + y: "3", + want: "1", + wantErr: false, + }, + { + name: "exact_division_mod", + x: "100", + y: "10", + want: "0", + wantErr: false, + }, + { + name: "large_mod_calculation", + x: "31337", + y: "3", + want: "2", + wantErr: false, + }, + + // Special cases + { + name: "zero_mod_nonzero", + x: "0", + y: "31337", + want: "0", + wantErr: false, + }, + { + name: "smaller_mod_larger", + x: "2", + y: "31337", + want: "2", + wantErr: false, + }, + { + name: "equal_mod_equal", + x: "31337", + y: "31337", + want: "0", + wantErr: false, + }, + + // Modulo by zero + { + name: "mod_by_zero", + x: "31337", + y: "0", + want: "", + wantErr: true, + }, + { + name: "zero_mod_zero", + x: "0", + y: "0", + want: "", + wantErr: true, + }, + + // Large numbers + { + name: "max_mod_small", + x: MAX_UINT256, + y: "1000000", + want: "639935", + wantErr: false, + }, + { + name: "power_of_two_mod", + x: "0xFFFFFFFFFFFFFFFF", + y: "0x10000000000000000", + want: "0xFFFFFFFFFFFFFFFF", + wantErr: false, + }, + { + name: "max_mod_large_prime", + x: MAX_UINT256, + y: "115792089237316195423570985008687907853269984665640564039457584007913129639747", // Large prime + want: "188", + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + + got := new(Uint).Mod(x, y) + + if tc.wantErr { + // Modulo by zero returns 0 + if !got.IsZero() { + t.Errorf("Mod(%s, %s) expected zero for modulo by zero", tc.x, tc.y) + } + return + } + + want := parseUint(tc.want) + if !got.Eq(want) { + t.Errorf("Mod(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + }) + } +} + +// TestDivMod verifies combined division and modulo operations +func TestDivMod(t *testing.T) { + tests := []struct { + name string + x string + y string + wantDiv string + wantMod string + wantErr bool + }{ + // Basic cases + { + name: "simple_divmod", + x: "10", + y: "3", + wantDiv: "3", + wantMod: "1", + wantErr: false, + }, + { + name: "exact_divmod", + x: "100", + y: "10", + wantDiv: "10", + wantMod: "0", + wantErr: false, + }, + { + name: "large_divmod", + x: "31337", + y: "3", + wantDiv: "10445", + wantMod: "2", + wantErr: false, + }, + + // Special cases + { + name: "zero_divmod_nonzero", + x: "0", + y: "31337", + wantDiv: "0", + wantMod: "0", + wantErr: false, + }, + { + name: "smaller_divmod_larger", + x: "2", + y: "31337", + wantDiv: "0", + wantMod: "2", + wantErr: false, + }, + { + name: "equal_divmod_equal", + x: "31337", + y: "31337", + wantDiv: "1", + wantMod: "0", + wantErr: false, + }, + + // Division by zero + { + name: "divmod_by_zero", + x: "31337", + y: "0", + wantDiv: "", + wantMod: "", + wantErr: true, + }, + + // Large numbers + { + name: "max_divmod_small", + x: MAX_UINT256, + y: "1000000", + wantDiv: "115792089237316195423570985008687907853269984665640564039457584007913129", + wantMod: "639935", + wantErr: false, + }, + + // Power of two optimization cases + { + name: "divmod_by_2", + x: "31337", + y: "2", + wantDiv: "15668", + wantMod: "1", + wantErr: false, + }, + { + name: "divmod_by_4", + x: "31337", + y: "4", + wantDiv: "7834", + wantMod: "1", + wantErr: false, + }, + { + name: "divmod_by_256", + x: "0xABCDEF1234567890", + y: "0x100", + wantDiv: "0xABCDEF12345678", + wantMod: "0x90", + wantErr: false, + }, + { + name: "divmod_by_2_64", + x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + y: "0x10000000000000000", // 2^64 + wantDiv: "0xFFFFFFFFFFFFFFFF", + wantMod: "0xFFFFFFFFFFFFFFFF", + wantErr: false, + }, + { + name: "max_divmod_power_of_two", + x: MAX_UINT256, + y: "0x10000000000000000", // 2^64 + wantDiv: "6277101735386680763835789423207666416102355444464034512895", + wantMod: "0xFFFFFFFFFFFFFFFF", + wantErr: false, + }, + { + name: "divmod_by_2_128", + x: MAX_UINT256, + y: "340282366920938463463374607431768211456", // 2^128 + wantDiv: "340282366920938463463374607431768211455", + wantMod: "340282366920938463463374607431768211455", + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + + gotDiv := new(Uint) + gotMod := new(Uint) + gotDiv, gotMod = gotDiv.DivMod(x, y, gotMod) + + if tc.wantErr { + // Division by zero returns (0, 0) + if !gotDiv.IsZero() || !gotMod.IsZero() { + t.Errorf("DivMod(%s, %s) expected (0, 0) for division by zero", tc.x, tc.y) + } + return + } + + wantDiv := parseUint(tc.wantDiv) + wantMod := parseUint(tc.wantMod) + + if !gotDiv.Eq(wantDiv) { + t.Errorf("DivMod(%s, %s) div = %s, want %s", tc.x, tc.y, gotDiv.ToString(), tc.wantDiv) + } + if !gotMod.Eq(wantMod) { + t.Errorf("DivMod(%s, %s) mod = %s, want %s", tc.x, tc.y, gotMod.ToString(), tc.wantMod) + } + }) + } +} + +// TestMulMod verifies modular multiplication operations +func TestMulMod(t *testing.T) { + tests := []struct { + name string + x string + y string + m string + want string + wantErr bool + }{ + // Basic cases + { + name: "simple_mulmod", + x: "10", + y: "20", + m: "7", + want: "4", // (10 * 20) % 7 = 200 % 7 = 4 + wantErr: false, + }, + { + name: "exact_mulmod", + x: "10", + y: "20", + m: "200", + want: "0", + wantErr: false, + }, + + // Edge cases + { + name: "zero_times_any_mod", + x: "0", + y: "12345", + m: "100", + want: "0", + wantErr: false, + }, + { + name: "one_times_any_mod", + x: "1", + y: "12345", + m: "100", + want: "45", + wantErr: false, + }, + { + name: "mulmod_by_zero", + x: "10", + y: "20", + m: "0", + want: "", + wantErr: true, + }, + + // Fast path: small product + { + name: "fast_path_small_product", + x: "1000000", + y: "2000000", + m: "999999", + want: "2", // (1000000 * 2000000) % 999999 = 2 + wantErr: false, + }, + { + name: "fast_path_exact_256_bits", + x: "0x1FFFFFFFFFFFFFFFF", // Just over 2^64 + y: "2", + m: "0xFFFFFFFFFFFFFFFF", // 2^64 - 1 + want: "2", + wantErr: false, + }, + + // Large numbers that would overflow in regular multiplication + { + name: "large_mulmod_no_overflow", + x: "340282366920938463463374607431768211456", // 2^128 + y: "340282366920938463463374607431768211456", + m: MAX_UINT256, + want: "1", + wantErr: false, + }, + + // Reduce4 path: large modulus + { + name: "reduce4_path_large_modulus", + x: MAX_UINT256, + y: MAX_UINT256, + m: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 + want: "1", + wantErr: false, + }, + { + name: "reduce4_path_exact_division", + x: "340282366920938463463374607431768211456", + y: "340282366920938463463374607431768211456", + m: "115792089237316195423570985008687907852589419931798687112530834793049593217024", + want: "680564733841876926926749214863536422912", + wantErr: false, + }, + + // Q96 price calculation (corrected) + { + name: "q96_price_calculation", + x: "79228162514264337593543950336", // 2^96 + y: "1000000000000000000", // 1e18 + m: "79228162514264337593543950335", // 2^96 - 1 + want: "1000000000000000000", // Corrected from 1000000000000000001 + wantErr: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + m := parseUint(tc.m) + + got := new(Uint).MulMod(x, y, m) + + if tc.wantErr { + // MulMod by zero returns 0 + if !got.IsZero() { + t.Errorf("MulMod(%s, %s, %s) expected zero for modulo by zero", tc.x, tc.y, tc.m) + } + return + } + + want := parseUint(tc.want) + if !got.Eq(want) { + t.Errorf("MulMod(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.m, got.ToString(), tc.want) + } + }) + } +} + +// TestNeg verifies negation operations +func TestNeg(t *testing.T) { + tests := []struct { + name string + x string + want string + }{ + { + name: "neg_zero", + x: "0", + want: "0", + }, + { + name: "neg_one", + x: "1", + want: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + { + name: "neg_two", + x: "2", + want: "115792089237316195423570985008687907853269984665640564039457584007913129639934", + }, + { + name: "neg_large", + x: "31337", + want: "115792089237316195423570985008687907853269984665640564039457584007913129608599", + }, + { + name: "neg_neg_large", + x: "115792089237316195423570985008687907853269984665640564039457584007913129608599", + want: "31337", + }, + { + name: "neg_max", + x: MAX_UINT256, + want: "1", + }, + { + name: "neg_half", + x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 + want: "57896044618658097711785492504343953926634992332820282019728792003956564819968", + }, + { + name: "neg_max_minus_one", + x: "115792089237316195423570985008687907853269984665640564039457584007913129639934", + want: "2", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + want := parseUint(tc.want) + + got := new(Uint).Neg(x) + if !got.Eq(want) { + t.Errorf("Neg(%s) = %s, want %s", tc.x, got.ToString(), tc.want) + } + }) + } +} + +// TestExp verifies exponentiation operations +func TestExp(t *testing.T) { + tests := []struct { + name string + base string + exponent string + want string + }{ + // Basic cases + { + name: "zero_power_zero", + base: "0", + exponent: "0", + want: "1", // Mathematical convention + }, + { + name: "any_power_zero", + base: "31337", + exponent: "0", + want: "1", + }, + { + name: "zero_power_any", + base: "0", + exponent: "31337", + want: "0", + }, + { + name: "one_power_any", + base: "1", + exponent: "31337", + want: "1", + }, + { + name: "two_power_three", + base: "2", + exponent: "3", + want: "8", + }, + + // Powers of 2 + { + name: "two_power_64", + base: "2", + exponent: "64", + want: "18446744073709551616", + }, + { + name: "two_power_96", + base: "2", + exponent: "96", + want: "79228162514264337593543950336", + }, + { + name: "two_power_128", + base: "2", + exponent: "128", + want: "340282366920938463463374607431768211456", + }, + { + name: "two_power_255", + base: "2", + exponent: "255", + want: "57896044618658097711785492504343953926634992332820282019728792003956564819968", + }, + { + name: "two_power_256_wraps", + base: "2", + exponent: "256", + want: "0", + }, + + // Non-power-of-two cases + { + name: "three_power_small", + base: "3", + exponent: "5", + want: "243", + }, + { + name: "three_power_large", + base: "3", + exponent: "100", + want: "515377520732011331036461129765621272702107522001", + }, + { + name: "five_power_fifty", + base: "5", + exponent: "50", + want: "88817841970012523233890533447265625", + }, + { + name: "seven_power_thirty", + base: "7", + exponent: "30", + want: "22539340290692258087863249", + }, + + // Other cases + { + name: "large_base_small_exp", + base: "31337", + exponent: "3", + want: "30773171189753", + }, + { + name: "large_base_large_exp", + base: "12345678901234567890", + exponent: "3", + want: "1881676372353657772490265749424677022198701224860897069000", + }, + { + name: "max_base_exp_one", + base: MAX_UINT256, + exponent: "1", + want: MAX_UINT256, + }, + { + name: "max_base_exp_two", + base: MAX_UINT256, + exponent: "2", + want: "1", // Wraps around + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + base := parseUint(tc.base) + exponent := parseUint(tc.exponent) + want := parseUint(tc.want) + + got := new(Uint).Exp(base, exponent) + if !got.Eq(want) { + t.Errorf("Exp(%s, %s) = %s, want %s", tc.base, tc.exponent, got.ToString(), tc.want) + } + }) + } +} + +// TestMustMul verifies MustMul panic behavior +func TestMustMul(t *testing.T) { + t.Run("normal_multiplication", func(t *testing.T) { + x := NewUint(1000) + y := NewUint(2000) + result := new(Uint).MustMul(x, y) + if result.ToString() != "2000000" { + t.Errorf("MustMul(1000, 2000) = %s, want 2000000", result.ToString()) + } + }) + + t.Run("large_valid_multiplication", func(t *testing.T) { + x := parseUint("340282366920938463463374607431768211455") // 2^128 - 1 + y := NewUint(2) + result := new(Uint).MustMul(x, y) + if result.ToString() != "680564733841876926926749214863536422910" { + t.Errorf("MustMul large valid failed") + } + }) + + t.Run("overflow_panics", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("MustMul should panic on overflow") + } + }() + + max := MustFromDecimal(MAX_UINT256) + new(Uint).MustMul(max, NewUint(2)) + }) +} + +// TestIsOverflow verifies overflow bit detection +func TestIsOverflow(t *testing.T) { + tests := []struct { + name string + input *Uint + expected bool + }{ + { + name: "zero_not_overflow", + input: &Uint{arr: [4]uint64{0, 0, 0, 0}}, + expected: false, + }, + { + name: "max_value_not_overflow", + input: &Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0) >> 1}}, + expected: false, + }, + { + name: "bit_255_set_is_overflow", + input: &Uint{arr: [4]uint64{0, 0, 0, uint64(1) << 63}}, + expected: true, + }, + { + name: "all_bits_set_is_overflow", + input: &Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + expected: true, + }, + { + name: "max_plus_one_is_overflow", + input: &Uint{arr: [4]uint64{0, 0, 0, uint64(1) << 63}}, + expected: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := tc.input.IsOverflow(); got != tc.expected { + t.Errorf("IsOverflow() = %v, want %v", got, tc.expected) + } + }) + } +} + +// TestDivisionByZero verifies all division by zero cases return error +func TestDivisionByZero(t *testing.T) { + testCases := []struct { + name string + fn func() *Uint + }{ + { + name: "div_by_zero", + fn: func() *Uint { + x := MustFromDecimal("12345") + y := MustFromDecimal("0") + return new(Uint).Div(x, y) + }, + }, + { + name: "mod_by_zero", + fn: func() *Uint { + x := MustFromDecimal("12345") + y := MustFromDecimal("0") + return new(Uint).Mod(x, y) + }, + }, + { + name: "mulmod_by_zero", + fn: func() *Uint { + x := MustFromDecimal("12345") + y := MustFromDecimal("67890") + m := MustFromDecimal("0") + return new(Uint).MulMod(x, y, m) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.fn() + if !result.IsZero() { + t.Errorf("%s should return zero but returned %s", tc.name, result.ToString()) + } + }) + } +} + +// TestMustOperations verifies that Must* functions panic appropriately +func TestMustOperations(t *testing.T) { + t.Run("MustDiv_panics_on_zero", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("MustDiv should panic on division by zero") + } + }() + x := NewUint(100) + y := Zero() + new(Uint).MustDiv(x, y) + }) + + t.Run("MustMod_panics_on_zero", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("MustMod should panic on modulo by zero") + } + }() + x := NewUint(100) + y := Zero() + new(Uint).MustMod(x, y) + }) + + t.Run("MustMulMod_panics_on_zero", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("MustMulMod should panic on modulo by zero") + } + }() + x := NewUint(10) + y := NewUint(20) + m := Zero() + new(Uint).MustMulMod(x, y, m) + }) + + t.Run("MustDivMod_panics_on_zero", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("MustDivMod should panic on division by zero") + } + }() + x := NewUint(100) + y := Zero() + new(Uint).MustDivMod(x, y, new(Uint)) + }) + + t.Run("MustMul_panics_on_overflow", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("MustMul should panic on overflow") + } + }() + x := MustFromDecimal("340282366920938463463374607431768211456") // 2^128 + y := MustFromDecimal("340282366920938463463374607431768211456") + new(Uint).MustMul(x, y) + }) + + // Test successful Must operations + t.Run("MustDiv_success", func(t *testing.T) { + x := NewUint(100) + y := NewUint(10) + result := new(Uint).MustDiv(x, y) + if !result.Eq(NewUint(10)) { + t.Error("MustDiv failed") + } + }) + + t.Run("MustMul_success", func(t *testing.T) { + x := NewUint(100) + y := NewUint(200) + result := new(Uint).MustMul(x, y) + if !result.Eq(NewUint(20000)) { + t.Error("MustMul failed") + } + }) +} + +// TestMulModPaths verifies both fast and reduce4 paths in MulMod +func TestMulModPaths(t *testing.T) { + tests := []struct { + name string + x string + y string + m string + want string + path string // "fast" or "reduce4" + }{ + // Fast path: x*y fits in 256 bits, m has no high word + { + name: "fast_path_small_product", + x: "1000000", + y: "2000000", + m: "999999", + want: "2", + path: "fast", + }, + { + name: "fast_path_exact_256_bits", + x: "0x1FFFFFFFFFFFFFFFF", // Just over 2^64 + y: "2", + m: "0xFFFFFFFFFFFFFFFF", // 2^64 - 1 + want: "2", + path: "fast", + }, + { + name: "boundary_case_m_arr3_zero", + x: "340282366920938463463374607431768211456", // Q128 + y: "340282366920938463463374607431768211456", + m: "340282366920938463463374607431768211455", // m.arr[3] != 0 + want: "1", + path: "fast", // Takes fast path because m.arr[3] == 0 + }, + + // Reduce4 path: x*y > 2^256 and m.arr[3] != 0 + { + name: "reduce4_path_max_inputs", + x: MAX_UINT256, + y: MAX_UINT256, + m: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 + want: "1", + path: "reduce4", + }, + { + name: "reduce4_path_exact_division", + x: "340282366920938463463374607431768211456", + y: "340282366920938463463374607431768211456", + m: "115792089237316195423570985008687907852589419931798687112530834793049593217024", + want: "680564733841876926926749214863536422912", + path: "reduce4", + }, + { + name: "reduce4_path_q96_calculation", + x: "79228162514264337593543950336", // 2^96 + y: "79228162514264337593543950336", + m: "6277101735386680763835789423207666416102355444464034512895", // Just under result + want: "1", + path: "reduce4", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + m := parseUint(tc.m) + want := parseUint(tc.want) + + got := new(Uint).MulMod(x, y, m) + + if !got.Eq(want) { + t.Errorf("MulMod(%s, %s, %s) = %s, want %s", + tc.x, tc.y, tc.m, got.ToString(), tc.want) + } + }) + } +} + +// TestDivModPowerOfTwo verifies optimized division by powers of two +func TestDivModPowerOfTwo(t *testing.T) { + tests := []struct { + name string + x string + y string + wantDiv string + wantMod string + }{ + { + name: "divmod_by_2", + x: "31337", + y: "2", + wantDiv: "15668", + wantMod: "1", + }, + { + name: "divmod_by_4", + x: "31337", + y: "4", + wantDiv: "7834", + wantMod: "1", + }, + { + name: "divmod_by_8", + x: "31337", + y: "8", + wantDiv: "3917", + wantMod: "1", + }, + { + name: "divmod_by_256", + x: "0xABCDEF1234567890", + y: "0x100", + wantDiv: "0xABCDEF12345678", + wantMod: "0x90", + }, + { + name: "divmod_by_2_32", + x: "0xFFFFFFFFFFFFFFFF", + y: "0x100000000", // 2^32 + wantDiv: "0xFFFFFFFF", + wantMod: "0xFFFFFFFF", + }, + { + name: "divmod_by_2_64", + x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", + y: "0x10000000000000000", // 2^64 + wantDiv: "0xFFFFFFFFFFFFFFFF", + wantMod: "0xFFFFFFFFFFFFFFFF", + }, + { + name: "max_divmod_power_of_two", + x: MAX_UINT256, + y: "0x10000000000000000", // 2^64 + wantDiv: "6277101735386680763835789423207666416102355444464034512895", + wantMod: "0xFFFFFFFFFFFFFFFF", + }, + { + name: "divmod_by_2_96", + x: MAX_UINT256, + y: "79228162514264337593543950336", // 2^96 + wantDiv: "1461501637330902918203684832716283019655932542975", + wantMod: "79228162514264337593543950335", + }, + { + name: "divmod_by_2_128", + x: MAX_UINT256, + y: "340282366920938463463374607431768211456", // 2^128 + wantDiv: "340282366920938463463374607431768211455", + wantMod: "340282366920938463463374607431768211455", + }, + { + name: "divmod_by_2_192", + x: MAX_UINT256, + y: "6277101735386680763835789423207666416102355444464034512896", // 2^192 + wantDiv: "18446744073709551615", + wantMod: "6277101735386680763835789423207666416102355444464034512895", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := parseUint(tc.x) + y := parseUint(tc.y) + wantDiv := parseUint(tc.wantDiv) + wantMod := parseUint(tc.wantMod) + + gotDiv := new(Uint) + gotMod := new(Uint) + gotDiv, gotMod = gotDiv.DivMod(x, y, gotMod) + + if !gotDiv.Eq(wantDiv) { + t.Errorf("DivMod div = %s, want %s", gotDiv.ToString(), tc.wantDiv) + } + if !gotMod.Eq(wantMod) { + t.Errorf("DivMod mod = %s, want %s", gotMod.ToString(), tc.wantMod) + } + }) + } +} + +// TestChainedOperations verifies DEX-like operation chains +func TestChainedOperations(t *testing.T) { + tests := []struct { + name string + ops []struct { + op string + arg string + } + start string + want string + }{ + { + name: "swap_fee_calculation", + start: "1000000000000000000", // 1 token + ops: []struct { + op string + arg string + }{ + {op: "mul", arg: "997"}, // 0.3% fee + {op: "div", arg: "1000"}, // fee calculation + }, + want: "997000000000000000", // 0.997 tokens after fee + }, + { + name: "liquidity_calculation", + start: "1000000000000000000000", // 1000 tokens + ops: []struct { + op string + arg string + }{ + {op: "mul", arg: "79228162514264337593543950336"}, // Multiply by Q96 + {op: "div", arg: "1000000000000000000"}, // Normalize + }, + want: "79228162514264337593543950336000", // Result in Q96 format + }, + { + name: "complex_price_impact", + start: "1000000000000000000", // 1 token + ops: []struct { + op string + arg string + }{ + {op: "mul", arg: "997"}, // Fee + {op: "div", arg: "1000"}, + {op: "mul", arg: "79228162514264337593543950336"}, // Price in Q96 + {op: "div", arg: "79228162514264337593543950336"}, // Normalize back + }, + want: "997000000000000000", // Same as fee calculation + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := parseUint(tc.start) + + for _, op := range tc.ops { + arg := parseUint(op.arg) + switch op.op { + case "add": + result = new(Uint).Add(result, arg) + case "sub": + result = new(Uint).Sub(result, arg) + case "mul": + result = new(Uint).Mul(result, arg) + case "div": + result = new(Uint).Div(result, arg) + } + } + + want := parseUint(tc.want) + if !result.Eq(want) { + t.Errorf("Chained operations = %s, want %s", result.ToString(), tc.want) + } + }) + } +} + +// Division rounding consistency test +func TestDivisionRoundingConsistency(t *testing.T) { + tests := []struct { + name string + x string + y string + wantDiv string + wantMod string + }{ + { + name: "rounds_down_small", + x: "10", + y: "3", + wantDiv: "3", + wantMod: "1", + }, + { + name: "rounds_down_large", + x: "999999999999999999", + y: "1000000000000000000", + wantDiv: "0", + wantMod: "999999999999999999", + }, + { + name: "exact_division", + x: "1000000000000000000", + y: "1000000000000000", + wantDiv: "1000", + wantMod: "0", + }, + { + name: "almost_two", + x: "1999999999999999999", + y: "1000000000000000000", + wantDiv: "1", + wantMod: "999999999999999999", + }, + { + name: "large_division_rounds_down", + x: MAX_UINT256, + y: "1000000000000000001", + wantDiv: "115792089237316195307778895771371712545491088894268851493966", + wantMod: "495113644278145969", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + x := parseUint(tt.x) + y := parseUint(tt.y) + + gotDiv := new(Uint).Div(x, y) + gotMod := new(Uint).Mod(x, y) + + wantDiv := parseUint(tt.wantDiv) + wantMod := parseUint(tt.wantMod) + + if !gotDiv.Eq(wantDiv) { + t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, gotDiv.ToString(), tt.wantDiv) + } + if !gotMod.Eq(wantMod) { + t.Errorf("Mod(%s, %s) = %s, want %s", tt.x, tt.y, gotMod.ToString(), tt.wantMod) + } + + // Verify division always rounds down + // x = y * div + mod, and 0 <= mod < y + reconstructed := new(Uint).Mul(y, gotDiv) + reconstructed = new(Uint).Add(reconstructed, gotMod) + if !reconstructed.Eq(x) { + t.Errorf("Division property violated: y*div+mod != x") + } + if gotMod.Gte(y) { + t.Errorf("Modulo >= divisor: mod=%s, y=%s", gotMod.ToString(), tt.y) + } + }) + } +} + +// Test extreme values combinations +func TestExtremeValues(t *testing.T) { + extremeValues := []struct { + name string + value string + }{ + {"one", "1"}, + {"two", "2"}, + {"max_minus_two", "115792089237316195423570985008687907853269984665640564039457584007913129639933"}, + {"max_minus_one", "115792089237316195423570985008687907853269984665640564039457584007913129639934"}, + {"max", MAX_UINT256}, + } + + for _, x := range extremeValues { + for _, y := range extremeValues { + t.Run(x.name+"_with_"+y.name, func(t *testing.T) { + xu := parseUint(x.value) + yu := parseUint(y.value) + + // Test all operations don't panic unexpectedly + _ = new(Uint).Add(xu, yu) + _ = new(Uint).Sub(xu, yu) + _ = new(Uint).Mul(xu, yu) + + if !yu.IsZero() { + _ = new(Uint).Div(xu, yu) + _ = new(Uint).Mod(xu, yu) + + // DivMod + quotient := new(Uint) + remainder := new(Uint) + quotient, remainder = quotient.DivMod(xu, yu, remainder) + + // Verify DivMod consistency + divResult := new(Uint).Div(xu, yu) + modResult := new(Uint).Mod(xu, yu) + if !quotient.Eq(divResult) || !remainder.Eq(modResult) { + t.Errorf("DivMod inconsistent with Div/Mod") + } + } + }) + } + } +} + +// Benchmark functions +func BenchmarkAdd(b *testing.B) { + x := MustFromDecimal("123456789012345678901234567890123456789") + y := MustFromDecimal("987654321098765432109876543210987654321") + z := new(Uint) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + z.Add(x, y) + } +} + +func BenchmarkMul(b *testing.B) { + x := MustFromDecimal("123456789012345678901234567890") + y := MustFromDecimal("987654321098765432109876543210") + z := new(Uint) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + z.Mul(x, y) + } +} + +func BenchmarkDiv(b *testing.B) { + x := MustFromDecimal("123456789012345678901234567890123456789012345678901234567890") + y := MustFromDecimal("123456789012345678901234567890") + z := new(Uint) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + z.Div(x, y) + } +} + +func BenchmarkExp(b *testing.B) { + base := NewUint(2) + exp := NewUint(100) + z := new(Uint) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + z.Exp(base, exp) + } +} + +func BenchmarkMulMod(b *testing.B) { + x := MustFromDecimal("123456789012345678901234567890") + y := MustFromDecimal("987654321098765432109876543210") + m := MustFromDecimal("1000000000000000000000000000000") + z := new(Uint) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + z.MulMod(x, y, m) + } +} diff --git a/contract/p/gnoswap/uint256/bits_table.gno b/contract/p/gnoswap/uint256/bits_table.gno new file mode 100644 index 0000000..aaaef7e --- /dev/null +++ b/contract/p/gnoswap/uint256/bits_table.gno @@ -0,0 +1,115 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +package uint256 + +// ntz8tab: A lookup table for 8-bit values (0-255) that shows +// the number of trailing zeros (zeros from the rightmost/LSB position). +// +// Example) 0x28 (binary 00101000) +// +// Binary: [ 0 0 1 0 1 0 0 0 ] +// ^^^^^^^^ +// 3 consecutive zeros +// +// ntz8tab[0x28] = 3 +const ntz8tab = "" + + "\x08\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x05\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x06\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x05\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x07\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x05\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x06\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x05\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + + "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + +// pop8tab: A lookup table for 8-bit values (0-255) that allows +// quick lookup of the number of set bits (bits set to 1). +// +// Example) 0xB2 (binary 10110010) +// +// Binary: [ 1 0 1 1 0 0 1 0 ] +// Total of 4 set bits +// +// pop8tab[0xB2] = 4 +const pop8tab = "" + + "\x00\x01\x01\x02\x01\x02\x02\x03\x01\x02\x02\x03\x02\x03\x03\x04" + + "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + + "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + + "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + + "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + + "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + + "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + + "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + + "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + + "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + + "\x04\x05\x05\x06\x05\x06\x06\x07\x05\x06\x06\x07\x06\x07\x07\x08" + +// rev8tab: A lookup table that pre-calculates bit-reversed results +// for 8-bit values (0-255). +// +// Example) 0x16 (binary 00010110) +// +// Binary: [ 0 0 0 1 0 1 1 0 ] +// Reversed: [ 0 1 1 0 1 0 0 0 ] -> 0x68 (104 in decimal) +// +// rev8tab[0x16] = 0x68 +const rev8tab = "" + + "\x00\x80\x40\xc0\x20\xa0\x60\xe0\x10\x90\x50\xd0\x30\xb0\x70\xf0" + + "\x08\x88\x48\xc8\x28\xa8\x68\xe8\x18\x98\x58\xd8\x38\xb8\x78\xf8" + + "\x04\x84\x44\xc4\x24\xa4\x64\xe4\x14\x94\x54\xd4\x34\xb4\x74\xf4" + + "\x0c\x8c\x4c\xcc\x2c\xac\x6c\xec\x1c\x9c\x5c\xdc\x3c\xbc\x7c\xfc" + + "\x02\x82\x42\xc2\x22\xa2\x62\xe2\x12\x92\x52\xd2\x32\xb2\x72\xf2" + + "\x0a\x8a\x4a\xca\x2a\xaa\x6a\xea\x1a\x9a\x5a\xda\x3a\xba\x7a\xfa" + + "\x06\x86\x46\xc6\x26\xa6\x66\xe6\x16\x96\x56\xd6\x36\xb6\x76\xf6" + + "\x0e\x8e\x4e\xce\x2e\xae\x6e\xee\x1e\x9e\x5e\xde\x3e\xbe\x7e\xfe" + + "\x01\x81\x41\xc1\x21\xa1\x61\xe1\x11\x91\x51\xd1\x31\xb1\x71\xf1" + + "\x09\x89\x49\xc9\x29\xa9\x69\xe9\x19\x99\x59\xd9\x39\xb9\x79\xf9" + + "\x05\x85\x45\xc5\x25\xa5\x65\xe5\x15\x95\x55\xd5\x35\xb5\x75\xf5" + + "\x0d\x8d\x4d\xcd\x2d\xad\x6d\xed\x1d\x9d\x5d\xdd\x3d\xbd\x7d\xfd" + + "\x03\x83\x43\xc3\x23\xa3\x63\xe3\x13\x93\x53\xd3\x33\xb3\x73\xf3" + + "\x0b\x8b\x4b\xcb\x2b\xab\x6b\xeb\x1b\x9b\x5b\xdb\x3b\xbb\x7b\xfb" + + "\x07\x87\x47\xc7\x27\xa7\x67\xe7\x17\x97\x57\xd7\x37\xb7\x77\xf7" + + "\x0f\x8f\x4f\xcf\x2f\xaf\x6f\xef\x1f\x9f\x5f\xdf\x3f\xbf\x7f\xff" + +// len8tab: A lookup table that pre-calculates the "bit length" +// of 8-bit values (0-255). +// (Bit length: position of the most significant bit + 1) +// +// Examples) +// +// 0x00 (binary 00000000) → No MSB → length 0 +// 0x01 (binary 00000001) → MSB at rightmost position → length 1 +// 0x02 (binary 00000010) ~ 0x03 (00000011) → length 2 +// 0x04 (binary 00000100) ~ 0x07 (00000111) → length 3 +// ... +const len8tab = "" + + "\x00\x01\x02\x02\x03\x03\x03\x03\x04\x04\x04\x04\x04\x04\x04\x04" + + "\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05" + + "\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06" + + "\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06" + + "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + + "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + + "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + + "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + + "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" diff --git a/contract/p/gnoswap/uint256/bitwise.gno b/contract/p/gnoswap/uint256/bitwise.gno new file mode 100644 index 0000000..e231af1 --- /dev/null +++ b/contract/p/gnoswap/uint256/bitwise.gno @@ -0,0 +1,266 @@ +// bitwise contains bitwise operations for Uint instances. +// This file includes functions to perform bitwise AND, OR, XOR, and NOT operations, as well as bit shifting. +// These operations are crucial for manipulating individual bits within a 256-bit unsigned integer. +package uint256 + +// Or sets z to the bitwise OR of x and y and returns z. +func (z *Uint) Or(x, y *Uint) *Uint { + z.arr[0] = x.arr[0] | y.arr[0] + z.arr[1] = x.arr[1] | y.arr[1] + z.arr[2] = x.arr[2] | y.arr[2] + z.arr[3] = x.arr[3] | y.arr[3] + return z +} + +// And sets z to the bitwise AND of x and y and returns z. +func (z *Uint) And(x, y *Uint) *Uint { + z.arr[0] = x.arr[0] & y.arr[0] + z.arr[1] = x.arr[1] & y.arr[1] + z.arr[2] = x.arr[2] & y.arr[2] + z.arr[3] = x.arr[3] & y.arr[3] + return z +} + +// Not sets z to the bitwise NOT of x and returns z. +func (z *Uint) Not(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = ^x.arr[3], ^x.arr[2], ^x.arr[1], ^x.arr[0] + return z +} + +// AndNot sets z to x AND NOT y and returns z. +func (z *Uint) AndNot(x, y *Uint) *Uint { + z.arr[0] = x.arr[0] &^ y.arr[0] + z.arr[1] = x.arr[1] &^ y.arr[1] + z.arr[2] = x.arr[2] &^ y.arr[2] + z.arr[3] = x.arr[3] &^ y.arr[3] + return z +} + +// Xor sets z to the bitwise XOR of x and y and returns z. +func (z *Uint) Xor(x, y *Uint) *Uint { + z.arr[0] = x.arr[0] ^ y.arr[0] + z.arr[1] = x.arr[1] ^ y.arr[1] + z.arr[2] = x.arr[2] ^ y.arr[2] + z.arr[3] = x.arr[3] ^ y.arr[3] + return z +} + +// Lsh sets z to x left-shifted by n bits and returns z. +// If n >= 256, z is set to 0. +func (z *Uint) Lsh(x *Uint, n uint) *Uint { + // n % 64 == 0 + if n&0x3f == 0 { + switch n { + case 0: + return z.Set(x) + case 64: + return z.lsh64(x) + case 128: + return z.lsh128(x) + case 192: + return z.lsh192(x) + default: + return z.Clear() + } + } + var a, b uint64 + // Big swaps first + switch { + case n > 192: + if n > 256 { + return z.Clear() + } + z.lsh192(x) + n -= 192 + goto sh192 + case n > 128: + z.lsh128(x) + n -= 128 + goto sh128 + case n > 64: + z.lsh64(x) + n -= 64 + goto sh64 + default: + z.Set(x) + } + + // remaining shifts + a = z.arr[0] >> (64 - n) + z.arr[0] = z.arr[0] << n + +sh64: + b = z.arr[1] >> (64 - n) + z.arr[1] = (z.arr[1] << n) | a + +sh128: + a = z.arr[2] >> (64 - n) + z.arr[2] = (z.arr[2] << n) | b + +sh192: + z.arr[3] = (z.arr[3] << n) | a + + return z +} + +// Rsh sets z to x right-shifted by n bits and returns z. +// If n >= 256, z is set to 0. +func (z *Uint) Rsh(x *Uint, n uint) *Uint { + // n % 64 == 0 + if n&0x3f == 0 { + switch n { + case 0: + return z.Set(x) + case 64: + return z.rsh64(x) + case 128: + return z.rsh128(x) + case 192: + return z.rsh192(x) + default: + return z.Clear() + } + } + var a, b uint64 + // Big swaps first + switch { + case n > 192: + if n > 256 { + return z.Clear() + } + z.rsh192(x) + n -= 192 + goto sh192 + case n > 128: + z.rsh128(x) + n -= 128 + goto sh128 + case n > 64: + z.rsh64(x) + n -= 64 + goto sh64 + default: + z.Set(x) + } + + // remaining shifts + a = z.arr[3] << (64 - n) + z.arr[3] = z.arr[3] >> n + +sh64: + b = z.arr[2] << (64 - n) + z.arr[2] = (z.arr[2] >> n) | a + +sh128: + a = z.arr[1] << (64 - n) + z.arr[1] = (z.arr[1] >> n) | b + +sh192: + z.arr[0] = (z.arr[0] >> n) | a + + return z +} + +// SRsh sets z to x arithmetically right-shifted by n bits and returns z. +// It treats x as a signed two's complement integer. For negative values, +// high-order bits are filled with 1s instead of 0s. If n >= 256 and x is negative, z is set to all 1s. +func (z *Uint) SRsh(x *Uint, n uint) *Uint { + // If the MSB is 0, SRsh is same as Rsh. + if !x.isBitSet(255) { + return z.Rsh(x, n) + } + if n%64 == 0 { + switch n { + case 0: + return z.Set(x) + case 64: + return z.srsh64(x) + case 128: + return z.srsh128(x) + case 192: + return z.srsh192(x) + default: + return z.SetAllOne() + } + } + var a uint64 = MaxUint64 << (64 - n%64) + // Big swaps first + switch { + case n > 192: + if n > 256 { + return z.SetAllOne() + } + z.srsh192(x) + n -= 192 + goto sh192 + case n > 128: + z.srsh128(x) + n -= 128 + goto sh128 + case n > 64: + z.srsh64(x) + n -= 64 + goto sh64 + default: + z.Set(x) + } + + // remaining shifts + z.arr[3], a = (z.arr[3]>>n)|a, z.arr[3]<<(64-n) + +sh64: + z.arr[2], a = (z.arr[2]>>n)|a, z.arr[2]<<(64-n) + +sh128: + z.arr[1], a = (z.arr[1]>>n)|a, z.arr[1]<<(64-n) + +sh192: + z.arr[0] = (z.arr[0] >> n) | a + + return z +} + +func (z *Uint) lsh64(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[2], x.arr[1], x.arr[0], 0 + return z +} + +func (z *Uint) lsh128(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[1], x.arr[0], 0, 0 + return z +} + +func (z *Uint) lsh192(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[0], 0, 0, 0 + return z +} + +func (z *Uint) rsh64(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, x.arr[3], x.arr[2], x.arr[1] + return z +} + +func (z *Uint) rsh128(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, x.arr[3], x.arr[2] + return z +} + +func (z *Uint) rsh192(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x.arr[3] + return z +} + +func (z *Uint) srsh64(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, x.arr[3], x.arr[2], x.arr[1] + return z +} + +func (z *Uint) srsh128(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, x.arr[3], x.arr[2] + return z +} + +func (z *Uint) srsh192(x *Uint) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, x.arr[3] + return z +} diff --git a/contract/p/gnoswap/uint256/bitwise_test.gno b/contract/p/gnoswap/uint256/bitwise_test.gno new file mode 100644 index 0000000..4437e7d --- /dev/null +++ b/contract/p/gnoswap/uint256/bitwise_test.gno @@ -0,0 +1,346 @@ +package uint256 + +import ( + "testing" +) + +type logicOpTest struct { + name string + x Uint + y Uint + want Uint +} + +func TestOr(t *testing.T) { + tests := []logicOpTest{ + { + name: "all zeros", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + { + name: "mixed", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, + y: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + { + name: "one operand all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res := new(Uint).Or(&tc.x, &tc.y) + if *res != tc.want { + t.Errorf("Or(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) + } + }) + } +} + +func TestAnd(t *testing.T) { + tests := []logicOpTest{ + { + name: "all zeros", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + { + name: "mixed", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "mixed 2", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "mixed 3", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, + y: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "one operand zero", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "one operand all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, + want: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res := new(Uint).And(&tc.x, &tc.y) + if *res != tc.want { + t.Errorf("And(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) + } + }) + } +} + +func TestNot(t *testing.T) { + tests := []struct { + name string + x Uint + want Uint + }{ + { + name: "all zeros", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + { + name: "all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "mixed", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, + want: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res := new(Uint).Not(&tc.x) + if *res != tc.want { + t.Errorf("Not(%s) = %s, want %s", tc.x.ToString(), res.ToString(), (tc.want).ToString()) + } + }) + } +} + +func TestAndNot(t *testing.T) { + tests := []logicOpTest{ + { + name: "all zeros", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "mixed", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "mixed 2", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + { + name: "mixed 3", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, + y: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, + }, + { + name: "one operand zero", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "one operand all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, + want: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(0)}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res := new(Uint).AndNot(&tc.x, &tc.y) + if *res != tc.want { + t.Errorf("AndNot(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) + } + }) + } +} + +func TestXor(t *testing.T) { + tests := []logicOpTest{ + { + name: "all zeros", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{0, 0, 0, 0}}, + }, + { + name: "mixed", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + { + name: "mixed 2", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + { + name: "mixed 3", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, + y: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, + want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + }, + { + name: "one operand zero", + x: Uint{arr: [4]uint64{0, 0, 0, 0}}, + y: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}}, + want: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}}, + }, + { + name: "one operand all ones", + x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + y: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, + want: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(0)}}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res := new(Uint).Xor(&tc.x, &tc.y) + if *res != tc.want { + t.Errorf("Xor(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) + } + }) + } +} + +func TestLsh(t *testing.T) { + tests := []struct { + x string + y uint + want string + }{ + {"0", 0, "0"}, + {"0", 1, "0"}, + {"0", 64, "0"}, + {"1", 0, "1"}, + {"1", 1, "2"}, + {"1", 64, "18446744073709551616"}, + {"1", 128, "340282366920938463463374607431768211456"}, + {"1", 192, "6277101735386680763835789423207666416102355444464034512896"}, + {"1", 255, "57896044618658097711785492504343953926634992332820282019728792003956564819968"}, + {"1", 256, "0"}, + {"31337", 0, "31337"}, + {"31337", 1, "62674"}, + {"31337", 64, "578065619037836218990592"}, + {"31337", 128, "10663428532201448629551770073089320442396672"}, + {"31337", 192, "196705537081812415096322133155058642481399512563169449530621952"}, + {"31337", 193, "393411074163624830192644266310117284962799025126338899061243904"}, + {"31337", 255, "57896044618658097711785492504343953926634992332820282019728792003956564819968"}, + {"31337", 256, "0"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := &Uint{} + got.Lsh(x, tc.y) + + if got.Neq(want) { + t.Errorf("Lsh(%s, %d) = %s, want %s", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} + +func TestRsh(t *testing.T) { + tests := []struct { + x string + y uint + want string + }{ + {"0", 0, "0"}, + {"0", 1, "0"}, + {"0", 64, "0"}, + {"1", 0, "1"}, + {"1", 1, "0"}, + {"1", 64, "0"}, + {"1", 128, "0"}, + {"1", 192, "0"}, + {"1", 255, "0"}, + {"57896044618658097711785492504343953926634992332820282019728792003956564819968", 255, "1"}, + {"6277101735386680763835789423207666416102355444464034512896", 192, "1"}, + {"340282366920938463463374607431768211456", 128, "1"}, + {"18446744073709551616", 64, "1"}, + {"393411074163624830192644266310117284962799025126338899061243904", 193, "31337"}, + {"196705537081812415096322133155058642481399512563169449530621952", 192, "31337"}, + {"10663428532201448629551770073089320442396672", 128, "31337"}, + {"578065619037836218990592", 64, "31337"}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + want, err := FromDecimal(tc.want) + if err != nil { + t.Error(err) + continue + } + + got := &Uint{} + got.Rsh(x, tc.y) + + if got.Neq(want) { + t.Errorf("Rsh(%s, %d) = %s, want %s", tc.x, tc.y, got.ToString(), want.ToString()) + } + } +} diff --git a/contract/p/gnoswap/uint256/cmp.gno b/contract/p/gnoswap/uint256/cmp.gno new file mode 100644 index 0000000..961c598 --- /dev/null +++ b/contract/p/gnoswap/uint256/cmp.gno @@ -0,0 +1,103 @@ +// cmp (or, comparisons) includes methods for comparing Uint instances. +// These comparison functions cover a range of operations including equality checks, less than/greater than +// evaluations, and specialized comparisons such as signed greater than. These are fundamental for logical +// decision making based on Uint values. +package uint256 + +import ( + "math/bits" +) + +// Cmp compares z and x and returns -1 if z < x, 0 if z == x, or +1 if z > x. +func (z *Uint) Cmp(x *Uint) (r int) { + // z < x <=> z - x < 0 i.e. when subtraction overflows. + d0, carry := bits.Sub64(z.arr[0], x.arr[0], 0) + d1, carry := bits.Sub64(z.arr[1], x.arr[1], carry) + d2, carry := bits.Sub64(z.arr[2], x.arr[2], carry) + d3, carry := bits.Sub64(z.arr[3], x.arr[3], carry) + if carry == 1 { + return -1 + } + if d0|d1|d2|d3 == 0 { + return 0 + } + return 1 +} + +// IsZero returns true if z equals 0. +func (z *Uint) IsZero() bool { + return (z.arr[0] | z.arr[1] | z.arr[2] | z.arr[3]) == 0 +} + +// Sign returns the sign of z interpreted as a two's complement signed number. +// It returns -1 if z < 0, 0 if z == 0, or +1 if z > 0. +func (z *Uint) Sign() int { + if z.IsZero() { + return 0 + } + if z.arr[3] < 0x8000000000000000 { + return 1 + } + return -1 +} + +// LtUint64 returns true if z is less than the uint64 value n. +func (z *Uint) LtUint64(n uint64) bool { + return z.arr[0] < n && (z.arr[1]|z.arr[2]|z.arr[3]) == 0 +} + +// GtUint64 returns true if z is greater than the uint64 value n. +func (z *Uint) GtUint64(n uint64) bool { + return z.arr[0] > n || (z.arr[1]|z.arr[2]|z.arr[3]) != 0 +} + +// Lt returns true if z is less than x. +func (z *Uint) Lt(x *Uint) bool { + // z < x <=> z - x < 0 i.e. when subtraction overflows. + _, carry := bits.Sub64(z.arr[0], x.arr[0], 0) + _, carry = bits.Sub64(z.arr[1], x.arr[1], carry) + _, carry = bits.Sub64(z.arr[2], x.arr[2], carry) + _, carry = bits.Sub64(z.arr[3], x.arr[3], carry) + + return carry != 0 +} + +// Gt returns true if z is greater than x. +func (z *Uint) Gt(x *Uint) bool { + return x.Lt(z) +} + +// Lte returns true if z is less than or equal to x. +func (z *Uint) Lte(x *Uint) bool { + return !x.Lt(z) +} + +// Gte returns true if z is greater than or equal to x. +func (z *Uint) Gte(x *Uint) bool { + return !z.Lt(x) +} + +// Eq returns true if z equals x. +func (z *Uint) Eq(x *Uint) bool { + return (z.arr[0] == x.arr[0]) && (z.arr[1] == x.arr[1]) && (z.arr[2] == x.arr[2]) && (z.arr[3] == x.arr[3]) +} + +// Neq returns true if z does not equal x. +func (z *Uint) Neq(x *Uint) bool { + return !z.Eq(x) +} + +// Sgt returns true if z > x when both are interpreted as two's complement signed integers. +func (z *Uint) Sgt(x *Uint) bool { + zSign := z.Sign() + xSign := x.Sign() + + switch { + case zSign >= 0 && xSign < 0: + return true + case zSign < 0 && xSign >= 0: + return false + default: + return z.Gt(x) + } +} diff --git a/contract/p/gnoswap/uint256/cmp_test.gno b/contract/p/gnoswap/uint256/cmp_test.gno new file mode 100644 index 0000000..d536eb8 --- /dev/null +++ b/contract/p/gnoswap/uint256/cmp_test.gno @@ -0,0 +1,229 @@ +package uint256 + +import ( + "strings" + "testing" +) + +func TestCmp(t *testing.T) { + tests := []struct { + x, y string + want int + }{ + {"0", "0", 0}, + {"0", "1", -1}, + {"1", "0", 1}, + {"1", "1", 0}, + {"10", "10", 0}, + {"10", "11", -1}, + {"11", "10", 1}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Cmp(y) + if got != tc.want { + t.Errorf("Cmp(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestIsZero(t *testing.T) { + tests := []struct { + x string + want bool + }{ + {"0", true}, + {"1", false}, + {"10", false}, + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + got := x.IsZero() + if got != tc.want { + t.Errorf("IsZero(%s) = %v, want %v", tc.x, got, tc.want) + } + } +} + +func TestLtUint64(t *testing.T) { + tests := []struct { + x string + y uint64 + want bool + }{ + {"0", 1, true}, + {"1", 0, false}, + {"10", 10, false}, + {"0xffffffffffffffff", 0, false}, + {"0x10000000000000000", 10000000000000000, false}, + } + + for _, tc := range tests { + var x *Uint + var err error + + if strings.HasPrefix(tc.x, "0x") { + x, err = FromHex(tc.x) + if err != nil { + t.Error(err) + continue + } + } else { + x, err = FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + } + + got := x.LtUint64(tc.y) + + if got != tc.want { + t.Errorf("LtUint64(%s, %d) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestSGT(t *testing.T) { + x := MustFromHex("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe") + y := MustFromHex("0x0") + actual := x.Sgt(y) + if actual { + t.Fatalf("Expected %v false", actual) + } + + x = MustFromHex("0x0") + y = MustFromHex("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe") + actual = x.Sgt(y) + if !actual { + t.Fatalf("Expected %v true", actual) + } +} + +func TestLte(t *testing.T) { + tests := []struct { + x, y string + want bool + }{ + {"0", "0", true}, // equal + {"0", "1", true}, // less than + {"1", "0", false}, // greater than + {"10", "10", true}, // equal + {"10", "11", true}, // less than + {"11", "10", false}, // greater than + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Lte(y) + if got != tc.want { + t.Errorf("Lte(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestGte(t *testing.T) { + tests := []struct { + x, y string + want bool + }{ + {"0", "0", true}, // equal + {"0", "1", false}, // less than + {"1", "0", true}, // greater than + {"10", "10", true}, // equal + {"10", "11", false}, // less than + {"11", "10", true}, // greater than + } + + for _, tc := range tests { + x, err := FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Gte(y) + if got != tc.want { + t.Errorf("Gte(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} + +func TestEq(t *testing.T) { + tests := []struct { + x string + y string + want bool + }{ + {"0xffffffffffffffff", "18446744073709551615", true}, + {"0x10000000000000000", "18446744073709551616", true}, + {"0", "0", true}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, + } + + for _, tc := range tests { + var x *Uint + var err error + + if strings.HasPrefix(tc.x, "0x") { + x, err = FromHex(tc.x) + if err != nil { + t.Error(err) + continue + } + } else { + x, err = FromDecimal(tc.x) + if err != nil { + t.Error(err) + continue + } + } + + y, err := FromDecimal(tc.y) + if err != nil { + t.Error(err) + continue + } + + got := x.Eq(y) + + if got != tc.want { + t.Errorf("Eq(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) + } + } +} diff --git a/contract/p/gnoswap/uint256/conversion.gno b/contract/p/gnoswap/uint256/conversion.gno new file mode 100644 index 0000000..bfa8aa4 --- /dev/null +++ b/contract/p/gnoswap/uint256/conversion.gno @@ -0,0 +1,602 @@ +// conversions contains methods for converting Uint instances to other types and vice versa. +// This includes conversions to and from basic types such as uint64 and int32, as well as string representations +// and byte slices. Additionally, it covers marshaling and unmarshaling for JSON and other text formats. +package uint256 + +import ( + "encoding/binary" + "errors" + "strconv" + "strings" +) + +// Uint64 returns the lower 64 bits of z as a uint64. +func (z *Uint) Uint64() uint64 { + return z.arr[0] +} + +// Int64 returns the lower 64 bits of z as an int64. +func (z *Uint) Int64() int64 { + return int64(z.Uint64()) +} + +// Uint64WithOverflow returns the lower 64 bits of z and true if overflow occurred. +func (z *Uint) Uint64WithOverflow() (uint64, bool) { + return z.arr[0], (z.arr[1] | z.arr[2] | z.arr[3]) != 0 +} + +// SetUint64 sets z to the value of x and returns z. +func (z *Uint) SetUint64(x uint64) *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x + return z +} + +// IsUint64 reports whether z can be represented as a uint64. +func (z *Uint) IsUint64() bool { + return (z.arr[1] | z.arr[2] | z.arr[3]) == 0 +} + +// Dec returns the decimal representation of z. +func (z *Uint) Dec() string { + if z.IsZero() { + return "0" + } + if z.IsUint64() { + return strconv.FormatUint(z.Uint64(), 10) + } + + // The max uint64 value being 18446744073709551615, the largest + // power-of-ten below that is 10000000000000000000. + // When we do a DivMod using that number, the remainder that we + // get back is the lower part of the output. + // + // The ascii-output of remainder will never exceed 19 bytes (since it will be + // below 10000000000000000000). + // + // Algorithm example using 100 as divisor + // + // 12345 % 100 = 45 (rem) + // 12345 / 100 = 123 (quo) + // -> output '45', continue iterate on 123 + var ( + // out is 98 bytes long: 78 (max size of a string without leading zeroes, + // plus slack so we can copy 19 bytes every iteration). + // We init it with zeroes, because when strconv appends the ascii representations, + // it will omit leading zeroes. + out = []byte("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + divisor = NewUint(10000000000000000000) // 20 digits + y = new(Uint).Set(z) // copy to avoid modifying z + pos = len(out) // position to write to + buf = make([]byte, 0, 19) // buffer to write uint64:s to + ) + for { + // Obtain Q and R for divisor + var quot Uint + rem := udivrem(quot.arr[:], y.arr[:], divisor) + y.Set(") // Set Q for next loop + // Convert the R to ascii representation + buf = strconv.AppendUint(buf[:0], rem.Uint64(), 10) + // Copy in the ascii digits + copy(out[pos-len(buf):], buf) + if y.IsZero() { + break + } + // Move 19 digits left + pos -= 19 + } + // skip leading zeroes by only using the 'used size' of buf + return string(out[pos-len(buf):]) +} + +// Scan implements the database/sql Scanner interface. +// Supports scanning from strings, byte slices, and numeric types. Sets z to 0 if src is nil. +func (z *Uint) Scan(src any) error { + if src == nil { + z.Clear() + return nil + } + + switch src := src.(type) { + case string: + return z.scanScientificFromString(src) + case []byte: + return z.scanScientificFromString(string(src)) + } + return errors.New("default // unsupported type: can't convert to uint256.Uint") +} + +func (z *Uint) scanScientificFromString(src string) error { + if len(src) == 0 { + z.Clear() + return nil + } + + idx := strings.IndexByte(src, 'e') + if idx == -1 { + return z.SetFromDecimal(src) + } + if err := z.SetFromDecimal(src[:idx]); err != nil { + return err + } + if src[(idx+1):] == "0" { + return nil + } + exp := new(Uint) + if err := exp.SetFromDecimal(src[(idx + 1):]); err != nil { + return err + } + if exp.GtUint64(77) { // 10**78 is larger than 2**256 + return ErrBig256Range + } + exp.Exp(NewUint(10), exp) + if _, overflow := z.MulOverflow(z, exp); overflow { + return ErrBig256Range + } + return nil +} + +// ToString returns the decimal string representation of z. +// Returns an empty string if z is nil. This method doesn't exist in holiman's uint256. +func (z *Uint) ToString() string { + if z == nil { + return "" + } + + return z.Dec() +} + +// MarshalJSON implements json.Marshaler by marshaling z as a decimal string. +// This differs from big.Int which uses JSON's native numeric format. +// Uses string format to avoid JavaScript's 53-bit integer precision limitations. +func (z *Uint) MarshalJSON() ([]byte, error) { + return []byte(`"` + z.Dec() + `"`), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +// Accepts quoted strings (hexadecimal or decimal) or unquoted strings (decimal only). +func (z *Uint) UnmarshalJSON(input []byte) error { + if len(input) < 2 || input[0] != '"' || input[len(input)-1] != '"' { + // if not quoted, it must be decimal + return z.fromDecimal(string(input)) + } + return z.UnmarshalText(input[1 : len(input)-1]) +} + +// MarshalText implements encoding.TextMarshaler by marshaling z as a decimal string. +// Compatible with big.Int's text marshaling format. +func (z *Uint) MarshalText() ([]byte, error) { + return []byte(z.Dec()), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// Accepts decimal strings or hexadecimal strings prefixed with 0x or 0X. +func (z *Uint) UnmarshalText(input []byte) error { + if len(input) >= 2 && input[0] == '0' && (input[1] == 'x' || input[1] == 'X') { + return z.fromHex(string(input)) + } + return z.fromDecimal(string(input)) +} + +// SetBytes interprets buf as a big-endian unsigned integer and sets z to that value. +// If buf is larger than 32 bytes, uses only the last 32 bytes. Returns z. +func (z *Uint) SetBytes(buf []byte) *Uint { + switch l := len(buf); l { + case 0: + z.Clear() + case 1: + z.SetBytes1(buf) + case 2: + z.SetBytes2(buf) + case 3: + z.SetBytes3(buf) + case 4: + z.SetBytes4(buf) + case 5: + z.SetBytes5(buf) + case 6: + z.SetBytes6(buf) + case 7: + z.SetBytes7(buf) + case 8: + z.SetBytes8(buf) + case 9: + z.SetBytes9(buf) + case 10: + z.SetBytes10(buf) + case 11: + z.SetBytes11(buf) + case 12: + z.SetBytes12(buf) + case 13: + z.SetBytes13(buf) + case 14: + z.SetBytes14(buf) + case 15: + z.SetBytes15(buf) + case 16: + z.SetBytes16(buf) + case 17: + z.SetBytes17(buf) + case 18: + z.SetBytes18(buf) + case 19: + z.SetBytes19(buf) + case 20: + z.SetBytes20(buf) + case 21: + z.SetBytes21(buf) + case 22: + z.SetBytes22(buf) + case 23: + z.SetBytes23(buf) + case 24: + z.SetBytes24(buf) + case 25: + z.SetBytes25(buf) + case 26: + z.SetBytes26(buf) + case 27: + z.SetBytes27(buf) + case 28: + z.SetBytes28(buf) + case 29: + z.SetBytes29(buf) + case 30: + z.SetBytes30(buf) + case 31: + z.SetBytes31(buf) + default: + z.SetBytes32(buf[l-32:]) + } + return z +} + +// SetBytes1 sets z from a 1-byte big-endian slice and returns z. +// Panics if input is shorter than 1 byte. +func (z *Uint) SetBytes1(in []byte) *Uint { + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + z.arr[0] = uint64(in[0]) + return z +} + +// SetBytes2 sets z from a 2-byte big-endian slice and returns z. +// Panics if input is shorter than 2 bytes. +func (z *Uint) SetBytes2(in []byte) *Uint { + _ = in[1] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + z.arr[0] = uint64(binary.BigEndian.Uint16(in[0:2])) + return z +} + +// SetBytes3 sets z from a 3-byte big-endian slice and returns z. +// Panics if input is shorter than 3 bytes. +func (z *Uint) SetBytes3(in []byte) *Uint { + _ = in[2] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + z.arr[0] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])<<16 + return z +} + +// SetBytes4 sets z from a 4-byte big-endian slice and returns z. +// Panics if input is shorter than 4 bytes. +func (z *Uint) SetBytes4(in []byte) *Uint { + _ = in[3] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + z.arr[0] = uint64(binary.BigEndian.Uint32(in[0:4])) + return z +} + +// SetBytes5 sets z from a 5-byte big-endian slice and returns z. +// Panics if input is shorter than 5 bytes. +func (z *Uint) SetBytes5(in []byte) *Uint { + _ = in[4] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + z.arr[0] = bigEndianUint40(in[0:5]) + return z +} + +// SetBytes6 sets z from a 6-byte big-endian slice and returns z. +// Panics if input is shorter than 6 bytes. +func (z *Uint) SetBytes6(in []byte) *Uint { + _ = in[5] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + z.arr[0] = bigEndianUint48(in[0:6]) + return z +} + +// SetBytes7 sets z from a 7-byte big-endian slice and returns z. +// Panics if input is shorter than 7 bytes. +func (z *Uint) SetBytes7(in []byte) *Uint { + _ = in[6] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + z.arr[0] = bigEndianUint56(in[0:7]) + return z +} + +// SetBytes8 sets z from an 8-byte big-endian slice and returns z. +// Panics if input is shorter than 8 bytes. +func (z *Uint) SetBytes8(in []byte) *Uint { + _ = in[7] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + z.arr[0] = binary.BigEndian.Uint64(in[0:8]) + return z +} + +// SetBytes9 sets z from a 9-byte big-endian slice and returns z. +// Panics if input is shorter than 9 bytes. +func (z *Uint) SetBytes9(in []byte) *Uint { + _ = in[8] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2] = 0, 0 + z.arr[1] = uint64(in[0]) + z.arr[0] = binary.BigEndian.Uint64(in[1:9]) + return z +} + +// SetBytes10 sets z from a 10-byte big-endian slice and returns z. +// Panics if input is shorter than 10 bytes. +func (z *Uint) SetBytes10(in []byte) *Uint { + _ = in[9] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2] = 0, 0 + z.arr[1] = uint64(binary.BigEndian.Uint16(in[0:2])) + z.arr[0] = binary.BigEndian.Uint64(in[2:10]) + return z +} + +// SetBytes11 sets z from an 11-byte big-endian slice and returns z. +// Panics if input is shorter than 11 bytes. +func (z *Uint) SetBytes11(in []byte) *Uint { + _ = in[10] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2] = 0, 0 + z.arr[1] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])<<16 + z.arr[0] = binary.BigEndian.Uint64(in[3:11]) + return z +} + +// SetBytes12 sets z from a 12-byte big-endian slice and returns z. +// Panics if input is shorter than 12 bytes. +func (z *Uint) SetBytes12(in []byte) *Uint { + _ = in[11] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2] = 0, 0 + z.arr[1] = uint64(binary.BigEndian.Uint32(in[0:4])) + z.arr[0] = binary.BigEndian.Uint64(in[4:12]) + return z +} + +// SetBytes13 sets z from a 13-byte big-endian slice and returns z. +// Panics if input is shorter than 13 bytes. +func (z *Uint) SetBytes13(in []byte) *Uint { + _ = in[12] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2] = 0, 0 + z.arr[1] = bigEndianUint40(in[0:5]) + z.arr[0] = binary.BigEndian.Uint64(in[5:13]) + return z +} + +// SetBytes14 sets z from a 14-byte big-endian slice and returns z. +// Panics if input is shorter than 14 bytes. +func (z *Uint) SetBytes14(in []byte) *Uint { + _ = in[13] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2] = 0, 0 + z.arr[1] = bigEndianUint48(in[0:6]) + z.arr[0] = binary.BigEndian.Uint64(in[6:14]) + return z +} + +// SetBytes15 sets z from a 15-byte big-endian slice and returns z. +// Panics if input is shorter than 15 bytes. +func (z *Uint) SetBytes15(in []byte) *Uint { + _ = in[14] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2] = 0, 0 + z.arr[1] = bigEndianUint56(in[0:7]) + z.arr[0] = binary.BigEndian.Uint64(in[7:15]) + return z +} + +// SetBytes16 sets z from a 16-byte big-endian slice and returns z. +// Panics if input is shorter than 16 bytes. +func (z *Uint) SetBytes16(in []byte) *Uint { + _ = in[15] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3], z.arr[2] = 0, 0 + z.arr[1] = binary.BigEndian.Uint64(in[0:8]) + z.arr[0] = binary.BigEndian.Uint64(in[8:16]) + return z +} + +// SetBytes17 sets z from a 17-byte big-endian slice and returns z. +// Panics if input is shorter than 17 bytes. +func (z *Uint) SetBytes17(in []byte) *Uint { + _ = in[16] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = 0 + z.arr[2] = uint64(in[0]) + z.arr[1] = binary.BigEndian.Uint64(in[1:9]) + z.arr[0] = binary.BigEndian.Uint64(in[9:17]) + return z +} + +// SetBytes18 sets z from an 18-byte big-endian slice and returns z. +// Panics if input is shorter than 18 bytes. +func (z *Uint) SetBytes18(in []byte) *Uint { + _ = in[17] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = 0 + z.arr[2] = uint64(binary.BigEndian.Uint16(in[0:2])) + z.arr[1] = binary.BigEndian.Uint64(in[2:10]) + z.arr[0] = binary.BigEndian.Uint64(in[10:18]) + return z +} + +// SetBytes19 sets z from a 19-byte big-endian slice and returns z. +// Panics if input is shorter than 19 bytes. +func (z *Uint) SetBytes19(in []byte) *Uint { + _ = in[18] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = 0 + z.arr[2] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])<<16 + z.arr[1] = binary.BigEndian.Uint64(in[3:11]) + z.arr[0] = binary.BigEndian.Uint64(in[11:19]) + return z +} + +// SetBytes20 sets z from a 20-byte big-endian slice and returns z. +// Panics if input is shorter than 20 bytes. +func (z *Uint) SetBytes20(in []byte) *Uint { + _ = in[19] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = 0 + z.arr[2] = uint64(binary.BigEndian.Uint32(in[0:4])) + z.arr[1] = binary.BigEndian.Uint64(in[4:12]) + z.arr[0] = binary.BigEndian.Uint64(in[12:20]) + return z +} + +// SetBytes21 sets z from a 21-byte big-endian slice and returns z. +// Panics if input is shorter than 21 bytes. +func (z *Uint) SetBytes21(in []byte) *Uint { + _ = in[20] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = 0 + z.arr[2] = bigEndianUint40(in[0:5]) + z.arr[1] = binary.BigEndian.Uint64(in[5:13]) + z.arr[0] = binary.BigEndian.Uint64(in[13:21]) + return z +} + +// SetBytes22 sets z from a 22-byte big-endian slice and returns z. +// Panics if input is shorter than 22 bytes. +func (z *Uint) SetBytes22(in []byte) *Uint { + _ = in[21] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = 0 + z.arr[2] = bigEndianUint48(in[0:6]) + z.arr[1] = binary.BigEndian.Uint64(in[6:14]) + z.arr[0] = binary.BigEndian.Uint64(in[14:22]) + return z +} + +// SetBytes23 sets z from a 23-byte big-endian slice and returns z. +// Panics if input is shorter than 23 bytes. +func (z *Uint) SetBytes23(in []byte) *Uint { + _ = in[22] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = 0 + z.arr[2] = bigEndianUint56(in[0:7]) + z.arr[1] = binary.BigEndian.Uint64(in[7:15]) + z.arr[0] = binary.BigEndian.Uint64(in[15:23]) + return z +} + +// SetBytes24 sets z from a 24-byte big-endian slice and returns z. +// Panics if input is shorter than 24 bytes. +func (z *Uint) SetBytes24(in []byte) *Uint { + _ = in[23] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = 0 + z.arr[2] = binary.BigEndian.Uint64(in[0:8]) + z.arr[1] = binary.BigEndian.Uint64(in[8:16]) + z.arr[0] = binary.BigEndian.Uint64(in[16:24]) + return z +} + +// SetBytes25 sets z from a 25-byte big-endian slice and returns z. +// Panics if input is shorter than 25 bytes. +func (z *Uint) SetBytes25(in []byte) *Uint { + _ = in[24] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = uint64(in[0]) + z.arr[2] = binary.BigEndian.Uint64(in[1:9]) + z.arr[1] = binary.BigEndian.Uint64(in[9:17]) + z.arr[0] = binary.BigEndian.Uint64(in[17:25]) + return z +} + +// SetBytes26 sets z from a 26-byte big-endian slice and returns z. +// Panics if input is shorter than 26 bytes. +func (z *Uint) SetBytes26(in []byte) *Uint { + _ = in[25] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = uint64(binary.BigEndian.Uint16(in[0:2])) + z.arr[2] = binary.BigEndian.Uint64(in[2:10]) + z.arr[1] = binary.BigEndian.Uint64(in[10:18]) + z.arr[0] = binary.BigEndian.Uint64(in[18:26]) + return z +} + +// SetBytes27 sets z from a 27-byte big-endian slice and returns z. +// Panics if input is shorter than 27 bytes. +func (z *Uint) SetBytes27(in []byte) *Uint { + _ = in[26] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])<<16 + z.arr[2] = binary.BigEndian.Uint64(in[3:11]) + z.arr[1] = binary.BigEndian.Uint64(in[11:19]) + z.arr[0] = binary.BigEndian.Uint64(in[19:27]) + return z +} + +// SetBytes28 sets z from a 28-byte big-endian slice and returns z. +// Panics if input is shorter than 28 bytes. +func (z *Uint) SetBytes28(in []byte) *Uint { + _ = in[27] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = uint64(binary.BigEndian.Uint32(in[0:4])) + z.arr[2] = binary.BigEndian.Uint64(in[4:12]) + z.arr[1] = binary.BigEndian.Uint64(in[12:20]) + z.arr[0] = binary.BigEndian.Uint64(in[20:28]) + return z +} + +// SetBytes29 sets z from a 29-byte big-endian slice and returns z. +// Panics if input is shorter than 29 bytes. +func (z *Uint) SetBytes29(in []byte) *Uint { + _ = in[28] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = bigEndianUint40(in[0:5]) + z.arr[2] = binary.BigEndian.Uint64(in[5:13]) + z.arr[1] = binary.BigEndian.Uint64(in[13:21]) + z.arr[0] = binary.BigEndian.Uint64(in[21:29]) + return z +} + +// SetBytes30 sets z from a 30-byte big-endian slice and returns z. +// Panics if input is shorter than 30 bytes. +func (z *Uint) SetBytes30(in []byte) *Uint { + _ = in[29] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = bigEndianUint48(in[0:6]) + z.arr[2] = binary.BigEndian.Uint64(in[6:14]) + z.arr[1] = binary.BigEndian.Uint64(in[14:22]) + z.arr[0] = binary.BigEndian.Uint64(in[22:30]) + return z +} + +// SetBytes31 sets z from a 31-byte big-endian slice and returns z. +// Panics if input is shorter than 31 bytes. +func (z *Uint) SetBytes31(in []byte) *Uint { + _ = in[30] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = bigEndianUint56(in[0:7]) + z.arr[2] = binary.BigEndian.Uint64(in[7:15]) + z.arr[1] = binary.BigEndian.Uint64(in[15:23]) + z.arr[0] = binary.BigEndian.Uint64(in[23:31]) + return z +} + +// SetBytes32 sets z from a 32-byte big-endian slice and returns z. +// Panics if input is shorter than 32 bytes. +func (z *Uint) SetBytes32(in []byte) *Uint { + _ = in[31] // bounds check hint to compiler; see golang.org/issue/14808 + z.arr[3] = binary.BigEndian.Uint64(in[0:8]) + z.arr[2] = binary.BigEndian.Uint64(in[8:16]) + z.arr[1] = binary.BigEndian.Uint64(in[16:24]) + z.arr[0] = binary.BigEndian.Uint64(in[24:32]) + return z +} + +// Utility methods that are "missing" among the bigEndian.UintXX methods. + +// bigEndianUint40 returns the uint64 value represented by the 5 bytes in big-endian order. +func bigEndianUint40(b []byte) uint64 { + _ = b[4] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[4]) | uint64(b[3])<<8 | uint64(b[2])<<16 | uint64(b[1])<<24 | + uint64(b[0])<<32 +} + +// bigEndianUint56 returns the uint64 value represented by the 7 bytes in big-endian order. +func bigEndianUint56(b []byte) uint64 { + _ = b[6] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[6]) | uint64(b[5])<<8 | uint64(b[4])<<16 | uint64(b[3])<<24 | + uint64(b[2])<<32 | uint64(b[1])<<40 | uint64(b[0])<<48 +} + +// bigEndianUint48 returns the uint64 value represented by the 6 bytes in big-endian order. +func bigEndianUint48(b []byte) uint64 { + _ = b[5] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[5]) | uint64(b[4])<<8 | uint64(b[3])<<16 | uint64(b[2])<<24 | + uint64(b[1])<<32 | uint64(b[0])<<40 +} diff --git a/contract/p/gnoswap/uint256/conversion_test.gno b/contract/p/gnoswap/uint256/conversion_test.gno new file mode 100644 index 0000000..12ae99c --- /dev/null +++ b/contract/p/gnoswap/uint256/conversion_test.gno @@ -0,0 +1,60 @@ +package uint256 + +import ( + "testing" +) + +func TestIsUint64(t *testing.T) { + tests := []struct { + x string + want bool + }{ + {"0x0", true}, + {"0x1", true}, + {"0x10", true}, + {"0xffffffffffffffff", true}, + {"0x10000000000000000", false}, + } + + for _, tc := range tests { + x := MustFromHex(tc.x) + got := x.IsUint64() + + if got != tc.want { + t.Errorf("IsUint64(%s) = %v, want %v", tc.x, got, tc.want) + } + } +} + +func TestDec(t *testing.T) { + testCases := []struct { + name string + z Uint + want string + }{ + { + name: "zero", + z: Uint{arr: [4]uint64{0, 0, 0, 0}}, + want: "0", + }, + { + name: "less than 20 digits", + z: Uint{arr: [4]uint64{1234567890, 0, 0, 0}}, + want: "1234567890", + }, + { + name: "max possible value", + z: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, + want: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.z.Dec() + if result != tc.want { + t.Errorf("Dec(%v) = %s, want %s", tc.z, result, tc.want) + } + }) + } +} diff --git a/contract/p/gnoswap/uint256/doc.gno b/contract/p/gnoswap/uint256/doc.gno new file mode 100644 index 0000000..e2d721b --- /dev/null +++ b/contract/p/gnoswap/uint256/doc.gno @@ -0,0 +1,14 @@ +// Package uint256 implements 256-bit unsigned integer arithmetic for GnoSwap. +// +// This package provides a Uint type that represents a 256-bit unsigned integer, +// stored as four uint64 values in little-endian order. It includes arithmetic +// operations with overflow detection, which are essential for safe token +// calculations in DeFi protocols. +// +// The implementation is optimized for gas efficiency while maintaining +// compatibility with Ethereum's uint256 semantics, ensuring consistent +// behavior across different blockchain environments. +// +// All operations that may overflow return both the result and an overflow flag, +// allowing calling code to handle overflow conditions appropriately. +package uint256 diff --git a/contract/p/gnoswap/uint256/error.gno b/contract/p/gnoswap/uint256/error.gno new file mode 100644 index 0000000..d200bb9 --- /dev/null +++ b/contract/p/gnoswap/uint256/error.gno @@ -0,0 +1,73 @@ +package uint256 + +import ( + "errors" +) + +var ( + ErrEmptyString = errors.New("empty hex string") + ErrSyntax = errors.New("invalid hex string") + ErrRange = errors.New("number out of range") + ErrMissingPrefix = errors.New("hex string without 0x prefix") + ErrEmptyNumber = errors.New("hex string \"0x\"") + ErrLeadingZero = errors.New("hex number with leading zero digits") + ErrBig256Range = errors.New("hex number > 256 bits") + ErrBadBufferLength = errors.New("bad ssz buffer length") + ErrBadEncodedLength = errors.New("bad ssz encoded length") + ErrInvalidBase = errors.New("invalid base") + ErrInvalidBitSize = errors.New("invalid bit size") +) + +type u256Error struct { + fn string // function name + input string + err error +} + +func (e *u256Error) Error() string { + return e.fn + ": " + e.input + ": " + e.err.Error() +} + +func (e *u256Error) Unwrap() error { + return e.err +} + +func errEmptyString(fn, input string) error { + return &u256Error{fn: fn, input: input, err: ErrEmptyString} +} + +func errSyntax(fn, input string) error { + return &u256Error{fn: fn, input: input, err: ErrSyntax} +} + +func errMissingPrefix(fn, input string) error { + return &u256Error{fn: fn, input: input, err: ErrMissingPrefix} +} + +func errEmptyNumber(fn, input string) error { + return &u256Error{fn: fn, input: input, err: ErrEmptyNumber} +} + +func errLeadingZero(fn, input string) error { + return &u256Error{fn: fn, input: input, err: ErrLeadingZero} +} + +func errRange(fn, input string) error { + return &u256Error{fn: fn, input: input, err: ErrRange} +} + +func errBig256Range(fn, input string) error { + return &u256Error{fn: fn, input: input, err: ErrBig256Range} +} + +func errBadBufferLength(fn, input string) error { + return &u256Error{fn: fn, input: input, err: ErrBadBufferLength} +} + +func errInvalidBase(fn string, base int) error { + return &u256Error{fn: fn, input: string(base), err: ErrInvalidBase} +} + +func errInvalidBitSize(fn string, bitSize int) error { + return &u256Error{fn: fn, input: string(bitSize), err: ErrInvalidBitSize} +} diff --git a/contract/p/gnoswap/uint256/fullmath.gno b/contract/p/gnoswap/uint256/fullmath.gno new file mode 100644 index 0000000..fc86f95 --- /dev/null +++ b/contract/p/gnoswap/uint256/fullmath.gno @@ -0,0 +1,106 @@ +// REF: https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol + +// fullmath implements Uniswap V3's FullMath library. +// +// This library provides advanced fixed-point math operations that are essential +// for Uniswap V3's tick math and liquidity calculations. It enables precise +// calculations of (a * b / denominator) with full 512-bit intermediate precision. +// +// NOTE: Unlike other arithmetic functions in the uint256 package that return errors, +// functions in this file panic on invalid inputs to maintain behavioral compatibility +// with the original Solidity implementation which uses require() statements. +// +// This design choice is intentional because: +// 1. These functions are typically used in hot paths where error handling would add overhead +// 2. Invalid inputs (like zero denominator) represent programming errors, not runtime conditions +// 3. Staying close to the Solidity implementation makes protocol porting more reliable +// +// If you need error-returning versions, wrap these functions with appropriate error handling. +package uint256 + +import ( + "gno.land/p/nt/ufmt" +) + +// MulDiv calculates (a * b) / denominator with full 512-bit intermediate precision. +// Panics if denominator is zero or if the result overflows 256 bits. +func MulDiv(a, b, denominator *Uint) *Uint { + if denominator.IsZero() { + panic("denominator must be greater than 0") + } + + // 512-bit product (8 limbs of 64 bits) + p := umul(a, b) + + if (p[4] | p[5] | p[6] | p[7]) == 0 { + var lo Uint + lo.arr = [4]uint64{p[0], p[1], p[2], p[3]} + return new(Uint).Div(&lo, denominator) + } + + // optional early overflow check: + // If hi >= denominator then floor((hi*2^256 + lo) / denominator) >= 2^256, which is overflow. + { + var hi Uint + hi.arr = [4]uint64{p[4], p[5], p[6], p[7]} + if denominator.Lte(&hi) { + panic(ufmt.Sprintf("overflow: denominator(%s) must be greater than hi(%s)", denominator.ToString(), hi.ToString())) + } + } + + // perform 512 / 256 division + // udivrem stores quotient into `quot` (len(u) - len(d) + 1 words) + // we pass 8 words to be safe. + var quot [8]uint64 + udivrem(quot[:], p[:], denominator) // ignore remainder + + if (quot[4] | quot[5] | quot[6] | quot[7]) != 0 { + panic("uint256: MulDiv overflow (high quotient words non-zero)") + } + + // return lower 256 bits of quotient + var z Uint + copy(z.arr[:], quot[:4]) + return &z +} + +// MulDivRoundingUp calculates ceil((a * b) / denominator) with full 512-bit intermediate precision. +// Panics if denominator is zero or if the result overflows 256 bits. +func MulDivRoundingUp(a, b, denominator *Uint) *Uint { + result := MulDiv(a, b, denominator) + + // Check if there's a remainder + mulModResult := new(Uint).MulMod(a, b, denominator) + + // If there's no remainder, return the result as-is + if mulModResult.IsZero() { + return result + } + + // Add 1 to round up, but check for overflow + if result.Eq(MustFromDecimal(MAX_UINT256)) { + panic(ufmt.Sprintf("overflow: result(%s) + 1 would exceed MAX_UINT256", result.ToString())) + } + + return new(Uint).Add(result, one) +} + +// DivRoundingUp calculates ceil(x / y) and returns the result. +// Panics if y is zero. +func DivRoundingUp(x, y *Uint) *Uint { + if y.IsZero() { + panic("division by zero") + } + div := new(Uint).Div(x, y) + mod := new(Uint).Mod(x, y) + z := new(Uint).Add(div, gt(mod, Zero())) + return z +} + +// gt returns One() if x > y, otherwise returns Zero(). +func gt(x, y *Uint) *Uint { + if x.Gt(y) { + return one + } + return Zero() +} diff --git a/contract/p/gnoswap/uint256/fullmath_test.gno b/contract/p/gnoswap/uint256/fullmath_test.gno new file mode 100644 index 0000000..fe3b4a5 --- /dev/null +++ b/contract/p/gnoswap/uint256/fullmath_test.gno @@ -0,0 +1,844 @@ +package uint256 + +import ( + "testing" + + "gno.land/p/nt/ufmt" +) + +func TestFullMathMulDiv(t *testing.T) { + tests := []struct { + name string + x string + y string + denom string + want string + wantPanic bool + }{ + // Basic functionality + { + name: "simple_multiplication_division", + x: "100", + y: "200", + denom: "50", + want: "400", // (100 * 200) / 50 = 20000 / 50 = 400 + wantPanic: false, + }, + { + name: "exact_division", + x: "1000", + y: "3000", + denom: "100", + want: "30000", // (1000 * 3000) / 100 = 3000000 / 100 = 30000 + wantPanic: false, + }, + + // Zero inputs + { + name: "zero_first_operand", + x: "0", + y: "1000", + denom: "100", + want: "0", + wantPanic: false, + }, + { + name: "zero_second_operand", + x: "123456789", + y: "0", + denom: "100", + want: "0", + wantPanic: false, + }, + + // Identity operations (denom = 1) + { + name: "identity_small_numbers", + x: "123", + y: "456", + denom: "1", + want: "56088", // 123 * 456 = 56088 + wantPanic: false, + }, + { + name: "identity_max_value", + x: MAX_UINT256, + y: "1", + denom: "1", + want: MAX_UINT256, + wantPanic: false, + }, + + // Essential panic cases + { + name: "panic_denominator_zero", + x: "100", + y: "200", + denom: "0", + want: "", + wantPanic: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + denom := MustFromDecimal(tc.denom) + + if tc.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("MulDiv(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) + } + }() + MulDiv(x, y, denom) + return + } + + got := MulDiv(x, y, denom) + want := MustFromDecimal(tc.want) + + if !got.Eq(want) { + t.Errorf("MulDiv(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) + } + }) + } +} + +func TestMulDivBoundary(t *testing.T) { + tests := []struct { + name string + x string + y string + denom string + want string + wantPanic bool + }{ + // Core boundaries + { + name: "all_max_inputs", + x: MAX_UINT256, + y: MAX_UINT256, + denom: MAX_UINT256, + want: MAX_UINT256, + wantPanic: false, + }, + + // Overflow detection (hi >= denominator) + { + name: "overflow_hi_equals_denom", + x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 + y: "2", + denom: "1", // hiProduct = 1, denom = 1 + want: "", + wantPanic: true, + }, + { + name: "overflow_hi_exceeds_denom", + x: MAX_UINT256, + y: MAX_UINT256, + denom: "340282366920938463463374607431768211455", // 2^128 - 1 + want: "", + wantPanic: true, + }, + { + name: "overflow_q128_squared", + x: "340282366920938463463374607431768211456", // Q128 (2^128) + y: "340282366920938463463374607431768211456", // Q128 + denom: "1", + want: "", + wantPanic: true, + }, + + // Just below overflow boundary + { + name: "boundary_hi_lt_denom_no_overflow", + x: "340282366920938463463374607431768211456", // Q128 + y: "340282366920938463463374607431768211456", // Q128 + denom: "340282366920938463463374607431768211457", // Q128 + 1 + want: "340282366920938463463374607431768211455", // Q128 - 1 + wantPanic: false, + }, + + // Borrow conditions (remainder > lo) + { + name: "borrow_needed_remainder_gt_lo", + x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 + y: "2", + denom: "57896044618658097711785492504343953926634992332820282019728792003956564819967", // 2^255 - 1 + want: "2", + wantPanic: false, + }, + { + name: "no_borrow_remainder_lt_lo", + x: "100", + y: "100", + denom: "101", // remainder = 1, lo = 10000 + want: "99", // 10000 / 101 = 99 + wantPanic: false, + }, + + // Powers of 2 optimization path + { + name: "power_of_2_denominator", + x: "340282366920938463463374607431768211456", // Q128 + y: "1", + denom: "1024", // 2^10 + want: "332306998946228968225951765070086144", // Q128 / 1024 + wantPanic: false, + }, + { + name: "mixed_power_of_2_factorization", + x: "1000000000000000000", + y: "1000000000000000000", + denom: "9223372036854775808", // 2^63 (single high bit) + want: "108420217248550443", + wantPanic: false, + }, + { + name: "power_of_2_division_large", + x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 + y: "2", + denom: "4", + want: "28948022309329048855892746252171976963317496166410141009864396001978282409984", // 2^254 + wantPanic: false, + }, + + // Newton-Raphson precision (odd denominators) + { + name: "odd_denominator_prime", + x: "1000000000000000000", + y: "1000000000000000000", + denom: "999999999999999989", // Large prime + want: "1000000000000000011", + wantPanic: false, + }, + { + name: "odd_denominator_alternating_bits", + x: "1000000000000000000", + y: "1000000000000000000", + denom: "6148914691236517205", // 0x5555555555555555 (alternating bits) + want: "162630325872825665", + wantPanic: false, + }, + + // Q128 special cases + { + name: "q128_divided_by_small_odd", + x: "340282366920938463463374607431768211456", // Q128 + y: "1", + denom: "3", + want: "113427455640312821154458202477256070485", // Q128 / 3 + wantPanic: false, + }, + { + name: "q128_divided_by_q128_minus_1", + x: "340282366920938463463374607431768211456", // Q128 + y: "1", + denom: "340282366920938463463374607431768211455", // Q128 - 1 + want: "1", + wantPanic: false, + }, + + // Edge cases with zero + { + name: "zero_numerator", + x: "0", + y: "1000000000000000000", + denom: "999999999999999999", + want: "0", + wantPanic: false, + }, + + // Combined optimization paths + { + name: "combined_optimization_paths", + x: "1606938044258990275541962092341162602522202993782792835301376", // 2^200 + y: "36028797018963968", // 2^55 + denom: "1809251394333065553493296640760748560207343510400633813116524750123642650624", // 2^250 + want: "32", // 2^(200+55-250) = 2^5 = 32 + wantPanic: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + denom := MustFromDecimal(tc.denom) + + if tc.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("MulDiv(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) + } + }() + MulDiv(x, y, denom) + return + } + + got := MulDiv(x, y, denom) + want := MustFromDecimal(tc.want) + + if !got.Eq(want) { + t.Errorf("MulDiv(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) + } + }) + } +} + +// This test verifies that the MulDiv function does not modify its input parameters +func TestMulDivInputPreservation(t *testing.T) { + tests := []struct { + name string + x string + y string + denom string + }{ + { + name: "normal_inputs", + x: "12345678901234567890", + y: "98765432109876543210", + denom: "123456789", + }, + { + name: "power_of_2_boundary", + x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 + y: "2", + denom: "4", + }, + { + name: "phantom_overflow_case", + x: "340282366920938463463374607431768211456", // Q128 + y: "11930464781601263584560605149792510336", // 35 * Q128 / 1000 + denom: "2722258935367507707706996859454145691648", // 8 * Q128 + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + denom := MustFromDecimal(tc.denom) + + // Create copies of input values + xCopy := new(Uint).Set(x) + yCopy := new(Uint).Set(y) + denomCopy := new(Uint).Set(denom) + + // Call MulDiv + MulDiv(x, y, denom) + + // Verify that input values were not modified + if !x.Eq(xCopy) { + t.Errorf("Input 'x' was modified: original=%s, modified=%s", + xCopy.ToString(), x.ToString()) + } + + if !y.Eq(yCopy) { + t.Errorf("Input 'y' was modified: original=%s, modified=%s", + yCopy.ToString(), y.ToString()) + } + + if !denom.Eq(denomCopy) { + t.Errorf("Input 'denom' was modified: original=%s, modified=%s", + denomCopy.ToString(), denom.ToString()) + } + }) + } +} + +// Phantom overflow cases (product > 256 bits but result fits) +func TestMulDivPhantomOverflow(t *testing.T) { + tests := []struct { + name string + x string + y string + denom string + want string + wantPanic bool + }{ + { + name: "phantom_overflow_case_1", + x: "340282366920938463463374607431768211456", // Q128 + y: "11930464781601263584560605149792510336", // 35 * Q128 / 1000 + denom: "2722258935367507707706996859454145691648", // 8 * Q128 + want: "1491308097700157948070075643724063792", // (35/1000) / 8 * Q128 = 4.375/1000 * Q128 + wantPanic: false, + }, + { + name: "phantom_overflow_repeating_decimal", + x: "340282366920938463463374607431768211456", // Q128 + y: "340282366920938463463374607431768211456000", // 1000 * Q128 + denom: "1020847100762815390390123822295304634368000", // 3000 * Q128 + want: "113427455640312821154458202477256070485", // Q128 / 3 + wantPanic: false, + }, + { + name: "accurate_without_phantom_overflow", + x: "340282366920938463463374607431768211456", // Q128 + y: "170141183460469231731687303715884105728", // 0.5 * Q128 + denom: "510423550381407695195061911147652317184", // 1.5 * Q128 + want: "113427455640312821154458202477256070485", // Q128 / 3 + wantPanic: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + denom := MustFromDecimal(tc.denom) + + if tc.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("MulDiv(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) + } + }() + MulDiv(x, y, denom) + return + } + + got := MulDiv(x, y, denom) + want := MustFromDecimal(tc.want) + + if !got.Eq(want) { + t.Errorf("MulDiv(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) + } + }) + } +} + +func TestMulDivRoundingUp(t *testing.T) { + tests := []struct { + name string + x string + y string + denom string + want string + wantPanic bool + }{ + // Basic rounding functionality + { + name: "no_rounding_needed", + x: "100", + y: "200", + denom: "50", + want: "400", // 20000 / 50 = 400 (exact) + wantPanic: false, + }, + { + name: "rounding_up_needed", + x: "100", + y: "201", + denom: "50", + want: "402", // 20100 / 50 = 402 (exact) + wantPanic: false, + }, + { + name: "rounding_up_with_remainder", + x: "101", + y: "199", + denom: "50", + want: "402", // 1101 * 199 = 20099, 20099 / 50 = 401.98 → 402 + wantPanic: false, + }, + + // Identity operations + { + name: "identity_operation", + x: "789", + y: "123", + denom: "1", + want: "97047", // 789 * 123 = 97047 + wantPanic: false, + }, + + // Essential panic cases + { + name: "panic_denominator_zero", + x: "100", + y: "200", + denom: "0", + want: "", + wantPanic: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + denom := MustFromDecimal(tc.denom) + + if tc.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("MulDivRoundingUp(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) + } + }() + MulDivRoundingUp(x, y, denom) + return + } + + got := MulDivRoundingUp(x, y, denom) + want := MustFromDecimal(tc.want) + + if !got.Eq(want) { + t.Errorf("MulDivRoundingUp(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) + } + }) + } +} + +func TestMulDivRoundingUpBoundary(t *testing.T) { + tests := []struct { + name string + x string + y string + denom string + want string + wantPanic bool + }{ + // Boundary rounding cases + { + name: "max_result_no_overflow", + x: MAX_UINT256, + y: "1", + denom: "1", + want: MAX_UINT256, + wantPanic: false, + }, + { + name: "q128_divided_by_3_rounded_up", + x: "340282366920938463463374607431768211456", // Q128 + y: "1", + denom: "3", + want: "113427455640312821154458202477256070486", // (Q128 / 3) + 1 + wantPanic: false, + }, + { + name: "overflow_after_rounding", + x: "340282366920938463463374607431768211456", // Q128 + y: "340282366920938463463374607431768211456", // Q128 + denom: "1", + want: "", + wantPanic: true, + }, + { + name: "rounding_at_max_boundary", + x: MAX_UINT256, + y: "3", + denom: "3", + want: MAX_UINT256, // No rounding needed (exact division) + wantPanic: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + denom := MustFromDecimal(tc.denom) + + if tc.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("MulDivRoundingUp(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) + } + }() + MulDivRoundingUp(x, y, denom) + return + } + + got := MulDivRoundingUp(x, y, denom) + want := MustFromDecimal(tc.want) + + if !got.Eq(want) { + t.Errorf("MulDivRoundingUp(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) + } + }) + } +} + +func TestDivRoundingUp(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + wantPanic bool + }{ + // Basic functionality + { + name: "exact_division", + x: "100", + y: "10", + want: "10", + wantPanic: false, + }, + { + name: "division_with_remainder", + x: "101", + y: "10", + want: "11", // 10 + 1 + wantPanic: false, + }, + { + name: "zero_dividend", + x: "0", + y: "10", + want: "0", + wantPanic: false, + }, + { + name: "one_divided_by_two", + x: "1", + y: "2", + want: "1", // 0 + 1 (rounded up) + wantPanic: false, + }, + + // Identity operations + { + name: "identity_operation", + x: "12345", + y: "1", + want: "12345", + wantPanic: false, + }, + + // Essential panic cases + { + name: "panic_division_by_zero", + x: "100", + y: "0", + want: "", + wantPanic: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + + if tc.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("DivRoundingUp(%s, %s) expected panic but got none", tc.x, tc.y) + } + }() + DivRoundingUp(x, y) + return + } + + got := DivRoundingUp(x, y) + want := MustFromDecimal(tc.want) + + if !got.Eq(want) { + t.Errorf("DivRoundingUp(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + }) + } +} + +func TestDivRoundingUpBoundary(t *testing.T) { + tests := []struct { + name string + x string + y string + want string + wantPanic bool + }{ + // Boundary value cases + { + name: "max_divided_by_max", + x: MAX_UINT256, + y: MAX_UINT256, + want: "1", + wantPanic: false, + }, + { + name: "max_minus_1_divided_by_max_rounded_up", + x: "115792089237316195423570985008687907853269984665640564039457584007913129639934", // MAX - 1 + y: MAX_UINT256, + want: "1", // 0 + 1 (rounded up) + wantPanic: false, + }, + { + name: "large_number_with_remainder", + x: "1000000000000000000000000000000000000001", + y: "1000000000000000000", + want: "1000000000000000000001", // 1000000000000000000 + 1 + wantPanic: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + + if tc.wantPanic { + defer func() { + if r := recover(); r == nil { + t.Errorf("DivRoundingUp(%s, %s) expected panic but got none", tc.x, tc.y) + } + }() + DivRoundingUp(x, y) + return + } + + got := DivRoundingUp(x, y) + want := MustFromDecimal(tc.want) + + if !got.Eq(want) { + t.Errorf("DivRoundingUp(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) + } + }) + } +} + +// Floor vs Ceil comparison +func TestMulDivFloorVsCeil(t *testing.T) { + tests := []struct { + name string + x string + y string + denom string + expectSame bool // true if floor == ceil (exact division) + }{ + { + name: "exact_division", + x: "1000", + y: "2000", + denom: "100", + expectSame: true, // 2000000/100 = 20000 (exact) + }, + { + name: "inexact_division", + x: "1234", + y: "5678", + denom: "100", + expectSame: false, // 7006652/100 = 70066.52 (inexact) + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + x := MustFromDecimal(tc.x) + y := MustFromDecimal(tc.y) + denom := MustFromDecimal(tc.denom) + + floor := MulDiv(x, y, denom) + ceil := MulDivRoundingUp(x, y, denom) + + if tc.expectSame { + if !floor.Eq(ceil) { + t.Errorf("Expected floor == ceil for exact division, got floor=%s, ceil=%s", + floor.ToString(), ceil.ToString()) + } + } else { + expected := new(Uint).Add(floor, one) + if !ceil.Eq(expected) { + t.Errorf("Expected ceil == floor + 1 for inexact division, got floor=%s, ceil=%s", + floor.ToString(), ceil.ToString()) + } + } + }) + } +} + +// Property-based testing for DivRoundingUp +func TestDivRoundingUpProperty(t *testing.T) { + // Property: for any x,y>0: ceil = DivRoundingUp(x,y); floor = x.Div(y) + // assert ceil == floor || ceil == floor+1 + + testCases := []struct { + x, y uint64 + }{ + {100, 3}, // 33.33... -> floor=33, ceil=34 + {100, 10}, // 10.0 -> floor=10, ceil=10 (exact) + {1000, 7}, // 142.857... -> floor=142, ceil=143 + {999, 333}, // 3.0 -> floor=3, ceil=3 (exact) + {1, 2}, // 0.5 -> floor=0, ceil=1 + {0, 999}, // 0.0 -> floor=0, ceil=0 (exact) + } + + for i, tc := range testCases { + t.Run(ufmt.Sprintf("property_test_%d", i), func(t *testing.T) { + if tc.y == 0 { + return // Skip division by zero + } + + xUint := MustFromDecimal(ufmt.Sprintf("%d", tc.x)) + yUint := MustFromDecimal(ufmt.Sprintf("%d", tc.y)) + + ceil := DivRoundingUp(xUint, yUint) + floor := new(Uint).Div(xUint, yUint) + floorPlusOne := new(Uint).Add(floor, one) + + if !ceil.Eq(floor) && !ceil.Eq(floorPlusOne) { + t.Errorf("Property failed: ceil(%s) != floor(%s) && ceil != floor+1(%s)", + ceil.ToString(), floor.ToString(), floorPlusOne.ToString()) + } + }) + } +} + +// Mathematical property verification for MulDiv +func TestMulDivMathematicalProperty(t *testing.T) { + // Property: q = MulDiv(x, y, denom), r = (x * y) % denom + // Then: q * denom + r == x * y AND r < denom + + testCases := []struct { + x, y, denom uint64 + }{ + {123, 456, 789}, + {1000, 2000, 500}, + {999, 777, 333}, + {1, 1, 1}, + {0, 999, 123}, + {100, 7, 3}, + } + + for i, tc := range testCases { + t.Run(ufmt.Sprintf("math_property_%d", i), func(t *testing.T) { + if tc.denom == 0 { + return // Skip division by zero + } + + xUint := MustFromDecimal(ufmt.Sprintf("%d", tc.x)) + yUint := MustFromDecimal(ufmt.Sprintf("%d", tc.y)) + denomUint := MustFromDecimal(ufmt.Sprintf("%d", tc.denom)) + + // Skip if this would cause overflow + defer func() { + if r := recover(); r != nil { + // Overflow is expected for some cases + return + } + }() + + product := new(Uint).Mul(xUint, yUint) + quotient := MulDiv(xUint, yUint, denomUint) + remainder := new(Uint).MulMod(xUint, yUint, denomUint) + + // Property 1: remainder < denom + if remainder.Gte(denomUint) { + t.Errorf("Property 1 failed: remainder(%s) >= denom(%s)", + remainder.ToString(), denomUint.ToString()) + } + + // Property 2: quotient * denom + remainder == x * y + reconstructed := new(Uint).Add(new(Uint).Mul(quotient, denomUint), remainder) + if !reconstructed.Eq(product) { + t.Errorf("Property 2 failed: q*d+r(%s) != x*y(%s)", + reconstructed.ToString(), product.ToString()) + } + }) + } +} diff --git a/contract/p/gnoswap/uint256/gnomod.toml b/contract/p/gnoswap/uint256/gnomod.toml new file mode 100644 index 0000000..1267ec7 --- /dev/null +++ b/contract/p/gnoswap/uint256/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/p/gnoswap/uint256" +gno = "0.9" diff --git a/contract/p/gnoswap/uint256/gs_pointer.gno b/contract/p/gnoswap/uint256/gs_pointer.gno new file mode 100644 index 0000000..e24e6bf --- /dev/null +++ b/contract/p/gnoswap/uint256/gs_pointer.gno @@ -0,0 +1,8 @@ +package uint256 + +func (z *Uint) NilToZero() *Uint { + if z == nil { + z = NewUint(0) + } + return z +} diff --git a/contract/p/gnoswap/uint256/mod.gno b/contract/p/gnoswap/uint256/mod.gno new file mode 100644 index 0000000..f6ff096 --- /dev/null +++ b/contract/p/gnoswap/uint256/mod.gno @@ -0,0 +1,605 @@ +package uint256 + +import ( + "math/bits" +) + +// Some utility functions + +// Reciprocal computes a 320-bit value representing 1/m +// +// Notes: +// - specialized for m.arr[3] != 0, hence limited to 2^192 <= m < 2^256 +// - returns zero if m.arr[3] == 0 +// - starts with a 32-bit division, refines with newton-raphson iterations +func Reciprocal(m *Uint) (mu [5]uint64) { + if m.arr[3] == 0 { + return mu + } + + s := bits.LeadingZeros64(m.arr[3]) // Replace with leadingZeros(m) for general case + p := 255 - s // floor(log_2(m)), m>0 + + // 0 or a power of 2? + + // Check if at least one bit is set in m.arr[2], m.arr[1] or m.arr[0], + // or at least two bits in m.arr[3] + + if m.arr[0]|m.arr[1]|m.arr[2]|(m.arr[3]&(m.arr[3]-1)) == 0 { + + mu[4] = ^uint64(0) >> uint(p&63) + mu[3] = ^uint64(0) + mu[2] = ^uint64(0) + mu[1] = ^uint64(0) + mu[0] = ^uint64(0) + + return mu + } + + // Maximise division precision by left-aligning divisor + + var ( + y Uint // left-aligned copy of m + r0 uint32 // estimate of 2^31/y + ) + + y.Lsh(m, uint(s)) // 1/2 < y < 1 + + // Extract most significant 32 bits + + yh := uint32(y.arr[3] >> 32) + + if yh == 0x80000000 { // Avoid overflow in division + r0 = 0xffffffff + } else { + r0, _ = bits.Div32(0x80000000, 0, yh) + } + + // First iteration: 32 -> 64 + + t1 := uint64(r0) // 2^31/y + t1 *= t1 // 2^62/y^2 + t1, _ = bits.Mul64(t1, y.arr[3]) // 2^62/y^2 * 2^64/y / 2^64 = 2^62/y + + r1 := uint64(r0) << 32 // 2^63/y + r1 -= t1 // 2^63/y - 2^62/y = 2^62/y + r1 *= 2 // 2^63/y + + if (r1 | (y.arr[3] << 1)) == 0 { + r1 = ^uint64(0) + } + + // Second iteration: 64 -> 128 + + // square: 2^126/y^2 + a2h, a2l := bits.Mul64(r1, r1) + + // multiply by y: e2h:e2l:b2h = 2^126/y^2 * 2^128/y / 2^128 = 2^126/y + b2h, _ := bits.Mul64(a2l, y.arr[2]) + c2h, c2l := bits.Mul64(a2l, y.arr[3]) + d2h, d2l := bits.Mul64(a2h, y.arr[2]) + e2h, e2l := bits.Mul64(a2h, y.arr[3]) + + b2h, c := bits.Add64(b2h, c2l, 0) + e2l, c = bits.Add64(e2l, c2h, c) + e2h, _ = bits.Add64(e2h, 0, c) + + _, c = bits.Add64(b2h, d2l, 0) + e2l, c = bits.Add64(e2l, d2h, c) + e2h, _ = bits.Add64(e2h, 0, c) + + // subtract: t2h:t2l = 2^127/y - 2^126/y = 2^126/y + t2l, b := bits.Sub64(0, e2l, 0) + t2h, _ := bits.Sub64(r1, e2h, b) + + // double: r2h:r2l = 2^127/y + r2l, c := bits.Add64(t2l, t2l, 0) + r2h, _ := bits.Add64(t2h, t2h, c) + + if (r2h | r2l | (y.arr[3] << 1)) == 0 { + r2h = ^uint64(0) + r2l = ^uint64(0) + } + + // Third iteration: 128 -> 192 + + // square r2 (keep 256 bits): 2^190/y^2 + a3h, a3l := bits.Mul64(r2l, r2l) + b3h, b3l := bits.Mul64(r2l, r2h) + c3h, c3l := bits.Mul64(r2h, r2h) + + a3h, c = bits.Add64(a3h, b3l, 0) + c3l, c = bits.Add64(c3l, b3h, c) + c3h, _ = bits.Add64(c3h, 0, c) + + a3h, c = bits.Add64(a3h, b3l, 0) + c3l, c = bits.Add64(c3l, b3h, c) + c3h, _ = bits.Add64(c3h, 0, c) + + // multiply by y: q = 2^190/y^2 * 2^192/y / 2^192 = 2^190/y + + x0 := a3l + x1 := a3h + x2 := c3l + x3 := c3h + + var q0, q1, q2, q3, q4, t0 uint64 + + q0, _ = bits.Mul64(x2, y.arr[0]) + q1, t0 = bits.Mul64(x3, y.arr[0]) + q0, c = bits.Add64(q0, t0, 0) + q1, _ = bits.Add64(q1, 0, c) + + t1, _ = bits.Mul64(x1, y.arr[1]) + q0, c = bits.Add64(q0, t1, 0) + q2, t0 = bits.Mul64(x3, y.arr[1]) + q1, c = bits.Add64(q1, t0, c) + q2, _ = bits.Add64(q2, 0, c) + + t1, t0 = bits.Mul64(x2, y.arr[1]) + q0, c = bits.Add64(q0, t0, 0) + q1, c = bits.Add64(q1, t1, c) + q2, _ = bits.Add64(q2, 0, c) + + t1, t0 = bits.Mul64(x1, y.arr[2]) + q0, c = bits.Add64(q0, t0, 0) + q1, c = bits.Add64(q1, t1, c) + q3, t0 = bits.Mul64(x3, y.arr[2]) + q2, c = bits.Add64(q2, t0, c) + q3, _ = bits.Add64(q3, 0, c) + + t1, _ = bits.Mul64(x0, y.arr[2]) + q0, c = bits.Add64(q0, t1, 0) + t1, t0 = bits.Mul64(x2, y.arr[2]) + q1, c = bits.Add64(q1, t0, c) + q2, c = bits.Add64(q2, t1, c) + q3, _ = bits.Add64(q3, 0, c) + + t1, t0 = bits.Mul64(x1, y.arr[3]) + q1, c = bits.Add64(q1, t0, 0) + q2, c = bits.Add64(q2, t1, c) + q4, t0 = bits.Mul64(x3, y.arr[3]) + q3, c = bits.Add64(q3, t0, c) + q4, _ = bits.Add64(q4, 0, c) + + t1, t0 = bits.Mul64(x0, y.arr[3]) + q0, c = bits.Add64(q0, t0, 0) + q1, c = bits.Add64(q1, t1, c) + t1, t0 = bits.Mul64(x2, y.arr[3]) + q2, c = bits.Add64(q2, t0, c) + q3, c = bits.Add64(q3, t1, c) + q4, _ = bits.Add64(q4, 0, c) + + // subtract: t3 = 2^191/y - 2^190/y = 2^190/y + _, b = bits.Sub64(0, q0, 0) + _, b = bits.Sub64(0, q1, b) + t3l, b := bits.Sub64(0, q2, b) + t3m, b := bits.Sub64(r2l, q3, b) + t3h, _ := bits.Sub64(r2h, q4, b) + + // double: r3 = 2^191/y + r3l, c := bits.Add64(t3l, t3l, 0) + r3m, c := bits.Add64(t3m, t3m, c) + r3h, _ := bits.Add64(t3h, t3h, c) + + // Fourth iteration: 192 -> 320 + + // square r3 + + a4h, a4l := bits.Mul64(r3l, r3l) + b4h, b4l := bits.Mul64(r3l, r3m) + c4h, c4l := bits.Mul64(r3l, r3h) + d4h, d4l := bits.Mul64(r3m, r3m) + e4h, e4l := bits.Mul64(r3m, r3h) + f4h, f4l := bits.Mul64(r3h, r3h) + + b4h, c = bits.Add64(b4h, c4l, 0) + e4l, c = bits.Add64(e4l, c4h, c) + e4h, _ = bits.Add64(e4h, 0, c) + + a4h, c = bits.Add64(a4h, b4l, 0) + d4l, c = bits.Add64(d4l, b4h, c) + d4h, c = bits.Add64(d4h, e4l, c) + f4l, c = bits.Add64(f4l, e4h, c) + f4h, _ = bits.Add64(f4h, 0, c) + + a4h, c = bits.Add64(a4h, b4l, 0) + d4l, c = bits.Add64(d4l, b4h, c) + d4h, c = bits.Add64(d4h, e4l, c) + f4l, c = bits.Add64(f4l, e4h, c) + f4h, _ = bits.Add64(f4h, 0, c) + + // multiply by y + + x1, x0 = bits.Mul64(d4h, y.arr[0]) + x3, x2 = bits.Mul64(f4h, y.arr[0]) + t1, t0 = bits.Mul64(f4l, y.arr[0]) + x1, c = bits.Add64(x1, t0, 0) + x2, c = bits.Add64(x2, t1, c) + x3, _ = bits.Add64(x3, 0, c) + + t1, t0 = bits.Mul64(d4h, y.arr[1]) + x1, c = bits.Add64(x1, t0, 0) + x2, c = bits.Add64(x2, t1, c) + x4, t0 := bits.Mul64(f4h, y.arr[1]) + x3, c = bits.Add64(x3, t0, c) + x4, _ = bits.Add64(x4, 0, c) + t1, t0 = bits.Mul64(d4l, y.arr[1]) + x0, c = bits.Add64(x0, t0, 0) + x1, c = bits.Add64(x1, t1, c) + t1, t0 = bits.Mul64(f4l, y.arr[1]) + x2, c = bits.Add64(x2, t0, c) + x3, c = bits.Add64(x3, t1, c) + x4, _ = bits.Add64(x4, 0, c) + + t1, t0 = bits.Mul64(a4h, y.arr[2]) + x0, c = bits.Add64(x0, t0, 0) + x1, c = bits.Add64(x1, t1, c) + t1, t0 = bits.Mul64(d4h, y.arr[2]) + x2, c = bits.Add64(x2, t0, c) + x3, c = bits.Add64(x3, t1, c) + x5, t0 := bits.Mul64(f4h, y.arr[2]) + x4, c = bits.Add64(x4, t0, c) + x5, _ = bits.Add64(x5, 0, c) + t1, t0 = bits.Mul64(d4l, y.arr[2]) + x1, c = bits.Add64(x1, t0, 0) + x2, c = bits.Add64(x2, t1, c) + t1, t0 = bits.Mul64(f4l, y.arr[2]) + x3, c = bits.Add64(x3, t0, c) + x4, c = bits.Add64(x4, t1, c) + x5, _ = bits.Add64(x5, 0, c) + + t1, t0 = bits.Mul64(a4h, y.arr[3]) + x1, c = bits.Add64(x1, t0, 0) + x2, c = bits.Add64(x2, t1, c) + t1, t0 = bits.Mul64(d4h, y.arr[3]) + x3, c = bits.Add64(x3, t0, c) + x4, c = bits.Add64(x4, t1, c) + x6, t0 := bits.Mul64(f4h, y.arr[3]) + x5, c = bits.Add64(x5, t0, c) + x6, _ = bits.Add64(x6, 0, c) + t1, t0 = bits.Mul64(a4l, y.arr[3]) + x0, c = bits.Add64(x0, t0, 0) + x1, c = bits.Add64(x1, t1, c) + t1, t0 = bits.Mul64(d4l, y.arr[3]) + x2, c = bits.Add64(x2, t0, c) + x3, c = bits.Add64(x3, t1, c) + t1, t0 = bits.Mul64(f4l, y.arr[3]) + x4, c = bits.Add64(x4, t0, c) + x5, c = bits.Add64(x5, t1, c) + x6, _ = bits.Add64(x6, 0, c) + + // subtract + _, b = bits.Sub64(0, x0, 0) + _, b = bits.Sub64(0, x1, b) + r4l, b := bits.Sub64(0, x2, b) + r4k, b := bits.Sub64(0, x3, b) + r4j, b := bits.Sub64(r3l, x4, b) + r4i, b := bits.Sub64(r3m, x5, b) + r4h, _ := bits.Sub64(r3h, x6, b) + + // Multiply candidate for 1/4y by y, with full precision + + x0 = r4l + x1 = r4k + x2 = r4j + x3 = r4i + x4 = r4h + + q1, q0 = bits.Mul64(x0, y.arr[0]) + q3, q2 = bits.Mul64(x2, y.arr[0]) + q5, q4 := bits.Mul64(x4, y.arr[0]) + + t1, t0 = bits.Mul64(x1, y.arr[0]) + q1, c = bits.Add64(q1, t0, 0) + q2, c = bits.Add64(q2, t1, c) + t1, t0 = bits.Mul64(x3, y.arr[0]) + q3, c = bits.Add64(q3, t0, c) + q4, c = bits.Add64(q4, t1, c) + q5, _ = bits.Add64(q5, 0, c) + + t1, t0 = bits.Mul64(x0, y.arr[1]) + q1, c = bits.Add64(q1, t0, 0) + q2, c = bits.Add64(q2, t1, c) + t1, t0 = bits.Mul64(x2, y.arr[1]) + q3, c = bits.Add64(q3, t0, c) + q4, c = bits.Add64(q4, t1, c) + q6, t0 := bits.Mul64(x4, y.arr[1]) + q5, c = bits.Add64(q5, t0, c) + q6, _ = bits.Add64(q6, 0, c) + + t1, t0 = bits.Mul64(x1, y.arr[1]) + q2, c = bits.Add64(q2, t0, 0) + q3, c = bits.Add64(q3, t1, c) + t1, t0 = bits.Mul64(x3, y.arr[1]) + q4, c = bits.Add64(q4, t0, c) + q5, c = bits.Add64(q5, t1, c) + q6, _ = bits.Add64(q6, 0, c) + + t1, t0 = bits.Mul64(x0, y.arr[2]) + q2, c = bits.Add64(q2, t0, 0) + q3, c = bits.Add64(q3, t1, c) + t1, t0 = bits.Mul64(x2, y.arr[2]) + q4, c = bits.Add64(q4, t0, c) + q5, c = bits.Add64(q5, t1, c) + q7, t0 := bits.Mul64(x4, y.arr[2]) + q6, c = bits.Add64(q6, t0, c) + q7, _ = bits.Add64(q7, 0, c) + + t1, t0 = bits.Mul64(x1, y.arr[2]) + q3, c = bits.Add64(q3, t0, 0) + q4, c = bits.Add64(q4, t1, c) + t1, t0 = bits.Mul64(x3, y.arr[2]) + q5, c = bits.Add64(q5, t0, c) + q6, c = bits.Add64(q6, t1, c) + q7, _ = bits.Add64(q7, 0, c) + + t1, t0 = bits.Mul64(x0, y.arr[3]) + q3, c = bits.Add64(q3, t0, 0) + q4, c = bits.Add64(q4, t1, c) + t1, t0 = bits.Mul64(x2, y.arr[3]) + q5, c = bits.Add64(q5, t0, c) + q6, c = bits.Add64(q6, t1, c) + q8, t0 := bits.Mul64(x4, y.arr[3]) + q7, c = bits.Add64(q7, t0, c) + q8, _ = bits.Add64(q8, 0, c) + + t1, t0 = bits.Mul64(x1, y.arr[3]) + q4, c = bits.Add64(q4, t0, 0) + q5, c = bits.Add64(q5, t1, c) + t1, t0 = bits.Mul64(x3, y.arr[3]) + q6, c = bits.Add64(q6, t0, c) + q7, c = bits.Add64(q7, t1, c) + q8, _ = bits.Add64(q8, 0, c) + + // Final adjustment + + // subtract q from 1/4 + _, b = bits.Sub64(0, q0, 0) + _, b = bits.Sub64(0, q1, b) + _, b = bits.Sub64(0, q2, b) + _, b = bits.Sub64(0, q3, b) + _, b = bits.Sub64(0, q4, b) + _, b = bits.Sub64(0, q5, b) + _, b = bits.Sub64(0, q6, b) + _, b = bits.Sub64(0, q7, b) + _, b = bits.Sub64(uint64(1)<<62, q8, b) + + // decrement the result + x0, t := bits.Sub64(r4l, 1, 0) + x1, t = bits.Sub64(r4k, 0, t) + x2, t = bits.Sub64(r4j, 0, t) + x3, t = bits.Sub64(r4i, 0, t) + x4, _ = bits.Sub64(r4h, 0, t) + + // commit the decrement if the subtraction underflowed (reciprocal was too large) + if b != 0 { + r4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0 + } + + // Shift to correct bit alignment, truncating excess bits + + p = (p & 63) - 1 + + x0, c = bits.Add64(r4l, r4l, 0) + x1, c = bits.Add64(r4k, r4k, c) + x2, c = bits.Add64(r4j, r4j, c) + x3, c = bits.Add64(r4i, r4i, c) + x4, _ = bits.Add64(r4h, r4h, c) + + if p < 0 { + r4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0 + p = 0 // avoid negative shift below + } + + { + r := uint(p) // right shift + l := uint(64 - r) // left shift + + x0 = (r4l >> r) | (r4k << l) + x1 = (r4k >> r) | (r4j << l) + x2 = (r4j >> r) | (r4i << l) + x3 = (r4i >> r) | (r4h << l) + x4 = (r4h >> r) + } + + if p > 0 { + r4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0 + } + + mu[0] = r4l + mu[1] = r4k + mu[2] = r4j + mu[3] = r4i + mu[4] = r4h + + return mu +} + +// reduce4 computes the least non-negative residue of x modulo m +// +// requires a four-word modulus (m.arr[3] > 1) and its inverse (mu) +func reduce4(x [8]uint64, m *Uint, mu [5]uint64) (z Uint) { + // NB: Most variable names in the comments match the pseudocode for + // Barrett reduction in the Handbook of Applied Cryptography. + + // q1 = x/2^192 + + x0 := x[3] + x1 := x[4] + x2 := x[5] + x3 := x[6] + x4 := x[7] + + // q2 = q1 * mu; q3 = q2 / 2^320 + + var q0, q1, q2, q3, q4, q5, t0, t1, c uint64 + + q0, _ = bits.Mul64(x3, mu[0]) + q1, t0 = bits.Mul64(x4, mu[0]) + q0, c = bits.Add64(q0, t0, 0) + q1, _ = bits.Add64(q1, 0, c) + + t1, _ = bits.Mul64(x2, mu[1]) + q0, c = bits.Add64(q0, t1, 0) + q2, t0 = bits.Mul64(x4, mu[1]) + q1, c = bits.Add64(q1, t0, c) + q2, _ = bits.Add64(q2, 0, c) + + t1, t0 = bits.Mul64(x3, mu[1]) + q0, c = bits.Add64(q0, t0, 0) + q1, c = bits.Add64(q1, t1, c) + q2, _ = bits.Add64(q2, 0, c) + + t1, t0 = bits.Mul64(x2, mu[2]) + q0, c = bits.Add64(q0, t0, 0) + q1, c = bits.Add64(q1, t1, c) + q3, t0 = bits.Mul64(x4, mu[2]) + q2, c = bits.Add64(q2, t0, c) + q3, _ = bits.Add64(q3, 0, c) + + t1, _ = bits.Mul64(x1, mu[2]) + q0, c = bits.Add64(q0, t1, 0) + t1, t0 = bits.Mul64(x3, mu[2]) + q1, c = bits.Add64(q1, t0, c) + q2, c = bits.Add64(q2, t1, c) + q3, _ = bits.Add64(q3, 0, c) + + t1, _ = bits.Mul64(x0, mu[3]) + q0, c = bits.Add64(q0, t1, 0) + t1, t0 = bits.Mul64(x2, mu[3]) + q1, c = bits.Add64(q1, t0, c) + q2, c = bits.Add64(q2, t1, c) + q4, t0 = bits.Mul64(x4, mu[3]) + q3, c = bits.Add64(q3, t0, c) + q4, _ = bits.Add64(q4, 0, c) + + t1, t0 = bits.Mul64(x1, mu[3]) + q0, c = bits.Add64(q0, t0, 0) + q1, c = bits.Add64(q1, t1, c) + t1, t0 = bits.Mul64(x3, mu[3]) + q2, c = bits.Add64(q2, t0, c) + q3, c = bits.Add64(q3, t1, c) + q4, _ = bits.Add64(q4, 0, c) + + t1, t0 = bits.Mul64(x0, mu[4]) + _, c = bits.Add64(q0, t0, 0) + q1, c = bits.Add64(q1, t1, c) + t1, t0 = bits.Mul64(x2, mu[4]) + q2, c = bits.Add64(q2, t0, c) + q3, c = bits.Add64(q3, t1, c) + q5, t0 = bits.Mul64(x4, mu[4]) + q4, c = bits.Add64(q4, t0, c) + q5, _ = bits.Add64(q5, 0, c) + + t1, t0 = bits.Mul64(x1, mu[4]) + q1, c = bits.Add64(q1, t0, 0) + q2, c = bits.Add64(q2, t1, c) + t1, t0 = bits.Mul64(x3, mu[4]) + q3, c = bits.Add64(q3, t0, c) + q4, c = bits.Add64(q4, t1, c) + q5, _ = bits.Add64(q5, 0, c) + + // Drop the fractional part of q3 + + q0 = q1 + q1 = q2 + q2 = q3 + q3 = q4 + q4 = q5 + + // r1 = x mod 2^320 + + x0 = x[0] + x1 = x[1] + x2 = x[2] + x3 = x[3] + x4 = x[4] + + // r2 = q3 * m mod 2^320 + + var r0, r1, r2, r3, r4 uint64 + + r4, r3 = bits.Mul64(q0, m.arr[3]) + _, t0 = bits.Mul64(q1, m.arr[3]) + r4, _ = bits.Add64(r4, t0, 0) + + t1, r2 = bits.Mul64(q0, m.arr[2]) + r3, c = bits.Add64(r3, t1, 0) + _, t0 = bits.Mul64(q2, m.arr[2]) + r4, _ = bits.Add64(r4, t0, c) + + t1, t0 = bits.Mul64(q1, m.arr[2]) + r3, c = bits.Add64(r3, t0, 0) + r4, _ = bits.Add64(r4, t1, c) + + t1, r1 = bits.Mul64(q0, m.arr[1]) + r2, c = bits.Add64(r2, t1, 0) + t1, t0 = bits.Mul64(q2, m.arr[1]) + r3, c = bits.Add64(r3, t0, c) + r4, _ = bits.Add64(r4, t1, c) + + t1, t0 = bits.Mul64(q1, m.arr[1]) + r2, c = bits.Add64(r2, t0, 0) + r3, c = bits.Add64(r3, t1, c) + _, t0 = bits.Mul64(q3, m.arr[1]) + r4, _ = bits.Add64(r4, t0, c) + + t1, r0 = bits.Mul64(q0, m.arr[0]) + r1, c = bits.Add64(r1, t1, 0) + t1, t0 = bits.Mul64(q2, m.arr[0]) + r2, c = bits.Add64(r2, t0, c) + r3, c = bits.Add64(r3, t1, c) + _, t0 = bits.Mul64(q4, m.arr[0]) + r4, _ = bits.Add64(r4, t0, c) + + t1, t0 = bits.Mul64(q1, m.arr[0]) + r1, c = bits.Add64(r1, t0, 0) + r2, c = bits.Add64(r2, t1, c) + t1, t0 = bits.Mul64(q3, m.arr[0]) + r3, c = bits.Add64(r3, t0, c) + r4, _ = bits.Add64(r4, t1, c) + + // r = r1 - r2 + + var b uint64 + + r0, b = bits.Sub64(x0, r0, 0) + r1, b = bits.Sub64(x1, r1, b) + r2, b = bits.Sub64(x2, r2, b) + r3, b = bits.Sub64(x3, r3, b) + r4, b = bits.Sub64(x4, r4, b) + + // if r<0 then r+=m + + if b != 0 { + r0, c = bits.Add64(r0, m.arr[0], 0) + r1, c = bits.Add64(r1, m.arr[1], c) + r2, c = bits.Add64(r2, m.arr[2], c) + r3, c = bits.Add64(r3, m.arr[3], c) + r4, _ = bits.Add64(r4, 0, c) + } + + // while (r>=m) r-=m + + for { + // q = r - m + q0, b = bits.Sub64(r0, m.arr[0], 0) + q1, b = bits.Sub64(r1, m.arr[1], b) + q2, b = bits.Sub64(r2, m.arr[2], b) + q3, b = bits.Sub64(r3, m.arr[3], b) + q4, b = bits.Sub64(r4, 0, b) + + // if borrow break + if b != 0 { + break + } + + // r = q + r4, r3, r2, r1, r0 = q4, q3, q2, q1, q0 + } + + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = r3, r2, r1, r0 + + return z +} diff --git a/contract/p/gnoswap/uint256/uint256.gno b/contract/p/gnoswap/uint256/uint256.gno new file mode 100644 index 0000000..b2f72b1 --- /dev/null +++ b/contract/p/gnoswap/uint256/uint256.gno @@ -0,0 +1,303 @@ +package uint256 + +import ( + "errors" + "math/bits" + "strconv" +) + +const ( + MaxUint64 = 1<<64 - 1 + MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935" +) + +var ( + zero = Zero() + one = One() + two = NewUint(2) + three = NewUint(3) +) + +// Uint represents a 256-bit unsigned integer. +// It is stored as an array of 4 uint64 in little-endian order, +// where arr[0] is the least significant and arr[3] is the most significant. +type Uint struct { + arr [4]uint64 +} + +// NewUint returns a new Uint initialized with the given uint64 value. +func NewUint(val uint64) *Uint { + return &Uint{arr: [4]uint64{val, 0, 0, 0}} +} + +// NewUintFromInt64 returns a new Uint initialized with the given int64 value. +// Panics if val is negative. +func NewUintFromInt64(val int64) *Uint { + if val < 0 { + panic("val is negative") + } + return NewUint(uint64(val)) +} + +// Zero returns a new Uint with value 0. +func Zero() *Uint { + return NewUint(0) +} + +// One returns a new Uint with value 1. +func One() *Uint { + return NewUint(1) +} + +// SetAllOne sets z to the maximum 256-bit value (all bits set to 1) and returns z. +func (z *Uint) SetAllOne() *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, MaxUint64 + return z +} + +// Set sets z to x and returns z. +func (z *Uint) Set(x *Uint) *Uint { + *z = *x + return z +} + +// SetOne sets z to 1 and returns z. +func (z *Uint) SetOne() *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 1 + return z +} + +// SetFromDecimal sets z from a decimal string and returns an error if invalid. +// Accepts an optional leading "+" sign but rejects underscores and negative values. +// Returns ErrBig256Range if the number exceeds 256 bits. +func (z *Uint) SetFromDecimal(s string) (err error) { + // Remove max one leading + + if len(s) > 0 && s[0] == '+' { + s = s[1:] + } + // Remove any number of leading zeroes + if len(s) > 0 && s[0] == '0' { + var i int + var c rune + for i, c = range s { + if c != '0' { + break + } + } + s = s[i:] + } + if len(s) < len(MAX_UINT256) { + return z.fromDecimal(s) + } + if len(s) == len(MAX_UINT256) { + if s > MAX_UINT256 { + return ErrBig256Range + } + return z.fromDecimal(s) + } + return ErrBig256Range +} + +// FromDecimal creates a new Uint from a decimal string. +// Returns an error if the number exceeds 256 bits or is invalid. +func FromDecimal(decimal string) (*Uint, error) { + var z Uint + if err := z.SetFromDecimal(decimal); err != nil { + return nil, err + } + return &z, nil +} + +// MustFromDecimal creates a new Uint from a decimal string. +// Panics if the string is invalid or the number exceeds 256 bits. +func MustFromDecimal(decimal string) *Uint { + var z Uint + if err := z.SetFromDecimal(decimal); err != nil { + panic(err) + } + return &z +} + +// multipliers holds the values that are needed for fromDecimal +var multipliers = [5]*Uint{ + nil, // represents first round, no multiplication needed + {[4]uint64{10000000000000000000, 0, 0, 0}}, // 10 ^ 19 + {[4]uint64{687399551400673280, 5421010862427522170, 0, 0}}, // 10 ^ 38 + {[4]uint64{5332261958806667264, 17004971331911604867, 2938735877055718769, 0}}, // 10 ^ 57 + {[4]uint64{0, 8607968719199866880, 532749306367912313, 1593091911132452277}}, // 10 ^ 76 +} + +// fromDecimal parses a decimal string by processing it in 19-character chunks. +// Each chunk is multiplied by the appropriate power of 10 and accumulated. +func (z *Uint) fromDecimal(bs string) error { + // first clear the input + z.Clear() + // the maximum value of uint64 is 18446744073709551615, which is 20 characters + // one less means that a string of 19 9's is always within the uint64 limit + var ( + num uint64 + err error + remaining = len(bs) + ) + if remaining == 0 { + return errors.New("EOF") + } + // We proceed in steps of 19 characters (nibbles), from least significant to most significant. + // This means that the first (up to) 19 characters do not need to be multiplied. + // In the second iteration, our slice of 19 characters needs to be multipleied + // by a factor of 10^19. Et cetera. + for i, mult := range multipliers { + if remaining <= 0 { + return nil // Done + } + if remaining > 19 { + num, err = strconv.ParseUint(bs[remaining-19:remaining], 10, 64) + } else { + // Final round + num, err = strconv.ParseUint(bs, 10, 64) + } + if err != nil { + return err + } + // add that number to our running total + if i == 0 { + z.SetUint64(num) + } else { + base := NewUint(num) + // Check for overflow in multiplication + mulResult, overflow := new(Uint).MulOverflow(base, mult) + if overflow { + return ErrBig256Range + } + // Check for overflow in addition + addResult, overflow := new(Uint).AddOverflow(z, mulResult) + if overflow { + return ErrBig256Range + } + z.Set(addResult) + } + // Chop off another 19 characters + if remaining > 19 { + bs = bs[0 : remaining-19] + } + remaining -= 19 + } + return nil +} + +// Byte returns the value of the byte at position n as a Uint. +// Position n is counted from the right (0 = least significant byte). +// Returns 0 if n >= 32. +func (z *Uint) Byte(n *Uint) *Uint { + // in z, z.arr[0] is the least significant + if number, overflow := n.Uint64WithOverflow(); !overflow { + if number < 32 { + number := z.arr[4-1-number/8] + offset := (n.arr[0] & 0x7) << 3 // 8*(n.d % 8) + z.arr[0] = (number & (0xff00000000000000 >> offset)) >> (56 - offset) + z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 + return z + } + } + + return z.Clear() +} + +// BitLen returns the number of bits required to represent z. +// BitLen(0) returns 0. +func (z *Uint) BitLen() int { + switch { + case z.arr[3] != 0: + return 192 + bits.Len64(z.arr[3]) + case z.arr[2] != 0: + return 128 + bits.Len64(z.arr[2]) + case z.arr[1] != 0: + return 64 + bits.Len64(z.arr[1]) + default: + return bits.Len64(z.arr[0]) + } +} + +// ByteLen returns the number of bytes required to represent z. +// ByteLen(0) returns 0. +func (z *Uint) ByteLen() int { + return (z.BitLen() + 7) / 8 +} + +// Clear sets z to 0 and returns z. +func (z *Uint) Clear() *Uint { + z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 0 + return z +} + +const ( + // hextable = "0123456789abcdef" + bintable = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x01\x02\x03\x04\x05\x06\a\b\t\xff\xff\xff\xff\xff\xff\xff\n\v\f\r\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\n\v\f\r\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" + badNibble = 0xff +) + +// SetFromHex sets z from a hexadecimal string and returns an error if invalid. +// Requires "0x" or "0X" prefix and rejects leading zeros after prefix, underscores, and negative values. +// Returns ErrBig256Range if the number exceeds 256 bits. +func (z *Uint) SetFromHex(hex string) error { + return z.fromHex(hex) +} + +// fromHex parses a hex-string into z. +func (z *Uint) fromHex(hex string) error { + if err := checkNumberS(hex); err != nil { + return err + } + if len(hex) > 66 { + return ErrBig256Range + } + z.Clear() + end := len(hex) + for i := 0; i < 4; i++ { + start := end - 16 + if start < 2 { + start = 2 + } + for ri := start; ri < end; ri++ { + nib := bintable[hex[ri]] + if nib == badNibble { + return ErrSyntax + } + z.arr[i] = z.arr[i] << 4 + z.arr[i] += uint64(nib) + } + end = start + } + return nil +} + +// FromHex creates a new Uint from a hexadecimal string. +// The string must be 0x-prefixed and represent a value within 256 bits. +func FromHex(hex string) (*Uint, error) { + var z Uint + if err := z.fromHex(hex); err != nil { + return nil, err + } + return &z, nil +} + +// MustFromHex creates a new Uint from a hexadecimal string. +// Panics if the string is invalid or the number exceeds 256 bits. +func MustFromHex(hex string) *Uint { + var z Uint + if err := z.fromHex(hex); err != nil { + panic(err) + } + return &z +} + +// Clone returns a new Uint with the same value as z. +func (z *Uint) Clone() *Uint { + var x Uint + x.arr[0] = z.arr[0] + x.arr[1] = z.arr[1] + x.arr[2] = z.arr[2] + x.arr[3] = z.arr[3] + + return &x +} diff --git a/contract/p/gnoswap/uint256/uint256_test.gno b/contract/p/gnoswap/uint256/uint256_test.gno new file mode 100644 index 0000000..2468b0a --- /dev/null +++ b/contract/p/gnoswap/uint256/uint256_test.gno @@ -0,0 +1,825 @@ +package uint256 + +import ( + "testing" + + "gno.land/p/nt/uassert" +) + +func TestFromDecimal(t *testing.T) { + tests := []struct { + name string + input string + expected string + shouldPanic bool + panicMsg string + }{ + // Basic cases + { + name: "zero", + input: "0", + expected: "0", + }, + { + name: "one", + input: "1", + expected: "1", + }, + { + name: "max_uint64", + input: "18446744073709551615", + expected: "18446744073709551615", + }, + { + name: "max_uint128", + input: "340282366920938463463374607431768211455", + expected: "340282366920938463463374607431768211455", + }, + { + name: "max_uint256", + input: MAX_UINT256, + expected: MAX_UINT256, + }, + + // Format cases + { + name: "leading_zeros", + input: "00000000000000000000000001234567890", + expected: "1234567890", + }, + { + name: "plus_sign", + input: "+12345", + expected: "12345", + }, + + // Error cases + { + name: "max_uint256_plus_one", + input: "115792089237316195423570985008687907853269984665640564039457584007913129639936", + shouldPanic: true, + panicMsg: "hex number > 256 bits", + }, + { + name: "multiple_plus_signs", + input: "++12345", + shouldPanic: true, + }, + { + name: "negative_number", + input: "-12345", + shouldPanic: true, + }, + { + name: "empty_string", + input: "", + shouldPanic: true, + panicMsg: "EOF", + }, + { + name: "invalid_characters", + input: "123abc456", + shouldPanic: true, + }, + { + name: "spaces_in_number", + input: "123 456", + shouldPanic: true, + }, + { + name: "decimal_point", + input: "123.456", + shouldPanic: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + if tt.panicMsg != "" { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + MustFromDecimal(tt.input) + }) + } else { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic but got none") + } + }() + MustFromDecimal(tt.input) + } + } else { + result := MustFromDecimal(tt.input) + uassert.Equal(t, tt.expected, result.ToString()) + } + }) + } +} + +func TestFromHex(t *testing.T) { + tests := []struct { + name string + input string + expected string + shouldPanic bool + panicMsg string + }{ + // Basic cases + { + name: "zero", + input: "0x0", + expected: "0", + }, + { + name: "one", + input: "0x1", + expected: "1", + }, + { + name: "max_uint256", + input: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + expected: MAX_UINT256, + }, + + // Format cases + { + name: "uppercase_0X", + input: "0Xff", + expected: "255", + }, + { + name: "mixed_case", + input: "0xAbCdEf", + expected: "11259375", + }, + + // Error cases + { + name: "hex_overflow_67_chars", + input: "0x10000000000000000000000000000000000000000000000000000000000000000", + shouldPanic: true, + panicMsg: "hex number > 256 bits", + }, + { + name: "no_0x_prefix", + input: "ffffffff", + shouldPanic: true, + panicMsg: "UnmarshalText: ffffffff: hex string without 0x prefix", + }, + { + name: "empty_string", + input: "", + shouldPanic: true, + panicMsg: "UnmarshalText: : empty hex string", + }, + { + name: "only_0x", + input: "0x", + shouldPanic: true, + panicMsg: "UnmarshalText: 0x: hex string \"0x\"", + }, + { + name: "invalid_hex_chars", + input: "0xgg", + shouldPanic: true, + panicMsg: "invalid hex string", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.shouldPanic { + if tt.panicMsg != "" { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + MustFromHex(tt.input) + }) + } else { + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected panic but got none") + } + }() + MustFromHex(tt.input) + } + } else { + result := MustFromHex(tt.input) + uassert.Equal(t, tt.expected, result.ToString()) + } + }) + } +} + +func TestComparisonOperations(t *testing.T) { + tests := []struct { + name string + operation string + x string + y string + expected bool + }{ + // Equality tests + { + name: "eq_true", + operation: "eq", + x: "12345", + y: "12345", + expected: true, + }, + { + name: "eq_false", + operation: "eq", + x: "12345", + y: "12346", + expected: false, + }, + + // Less than tests + { + name: "lt_true", + operation: "lt", + x: "12345", + y: "12346", + expected: true, + }, + { + name: "lt_false_greater", + operation: "lt", + x: "12346", + y: "12345", + expected: false, + }, + { + name: "lt_false_equal", + operation: "lt", + x: "12345", + y: "12345", + expected: false, + }, + + // Greater than tests + { + name: "gt_true", + operation: "gt", + x: "12346", + y: "12345", + expected: true, + }, + { + name: "gt_false_smaller", + operation: "gt", + x: "12345", + y: "12346", + expected: false, + }, + { + name: "gt_false_equal", + operation: "gt", + x: "12345", + y: "12345", + expected: false, + }, + + // Less than or equal tests + { + name: "lte_true_less", + operation: "lte", + x: "12345", + y: "12346", + expected: true, + }, + { + name: "lte_true_equal", + operation: "lte", + x: "12345", + y: "12345", + expected: true, + }, + { + name: "lte_false", + operation: "lte", + x: "12346", + y: "12345", + expected: false, + }, + + // Greater than or equal tests + { + name: "gte_true_greater", + operation: "gte", + x: "12346", + y: "12345", + expected: true, + }, + { + name: "gte_true_equal", + operation: "gte", + x: "12345", + y: "12345", + expected: true, + }, + { + name: "gte_false", + operation: "gte", + x: "12345", + y: "12346", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + + var result bool + switch tt.operation { + case "eq": + result = x.Eq(y) + case "lt": + result = x.Lt(y) + case "gt": + result = x.Gt(y) + case "lte": + result = x.Lte(y) + case "gte": + result = x.Gte(y) + } + + uassert.Equal(t, tt.expected, result) + }) + } +} + +func TestBitOperations(t *testing.T) { + tests := []struct { + name string + operation string + x string + n uint + expected string + }{ + // Left shift tests + { + name: "lsh_zero", + operation: "lsh", + x: "0", + n: 100, + expected: "0", + }, + { + name: "lsh_one_by_zero", + operation: "lsh", + x: "1", + n: 0, + expected: "1", + }, + { + name: "lsh_one_by_one", + operation: "lsh", + x: "1", + n: 1, + expected: "2", + }, + { + name: "lsh_one_by_64", + operation: "lsh", + x: "1", + n: 64, + expected: "18446744073709551616", + }, + { + name: "lsh_one_by_255", + operation: "lsh", + x: "1", + n: 255, + expected: "57896044618658097711785492504343953926634992332820282019728792003956564819968", + }, + { + name: "lsh_one_by_256", + operation: "lsh", + x: "1", + n: 256, + expected: "0", // Shifts out of range + }, + + // Right shift tests + { + name: "rsh_zero", + operation: "rsh", + x: "0", + n: 100, + expected: "0", + }, + { + name: "rsh_by_zero", + operation: "rsh", + x: "123456", + n: 0, + expected: "123456", + }, + { + name: "rsh_by_one", + operation: "rsh", + x: "123456", + n: 1, + expected: "61728", + }, + { + name: "rsh_by_64", + operation: "rsh", + x: "340282366920938463463374607431768211456", // 2^128 + n: 64, + expected: "18446744073709551616", // 2^64 + }, + { + name: "rsh_max_by_255", + operation: "rsh", + x: MAX_UINT256, + n: 255, + expected: "1", + }, + { + name: "rsh_max_by_256", + operation: "rsh", + x: MAX_UINT256, + n: 256, + expected: "0", // Shifts out of range + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + x := MustFromDecimal(tt.x) + var result *Uint + + switch tt.operation { + case "lsh": + result = new(Uint).Lsh(x, tt.n) + case "rsh": + result = new(Uint).Rsh(x, tt.n) + } + + uassert.Equal(t, tt.expected, result.ToString()) + }) + } +} + +func TestByteOperation(t *testing.T) { + tests := []struct { + name string + x string + n uint64 + expected string + }{ + { + name: "byte_0_of_max", + x: MAX_UINT256, + n: 0, + expected: "255", + }, + { + name: "byte_31_of_max", + x: MAX_UINT256, + n: 31, + expected: "255", + }, + { + name: "byte_32_out_of_range", + x: MAX_UINT256, + n: 32, + expected: "0", + }, + { + name: "byte_0_of_256", + x: "256", + n: 30, + expected: "1", + }, + { + name: "byte_31_of_single_byte", + x: "255", + n: 31, + expected: "255", + }, + { + name: "byte_at_boundary", + x: "18446744073709551616", // 2^64 + n: 23, + expected: "1", + }, + { + name: "byte_in_middle", + x: "0xff00ff00ff00ff00ff00", // Hexadecimal format without leading zeros + n: 15, + expected: "0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var x *Uint + if len(tt.x) >= 2 && tt.x[:2] == "0x" { + x = MustFromHex(tt.x) + } else { + x = MustFromDecimal(tt.x) + } + + n := NewUint(tt.n) + result := x.Byte(n) + + uassert.Equal(t, tt.expected, result.ToString()) + }) + } +} + +func TestMulDiv(t *testing.T) { + tests := []struct { + name string + x string + y string + denominator string + expected string + shouldPanic bool + panicMsg string + }{ + { + name: "reverts_if_denominator_is_0", + x: "340282366920938463463374607431768211456", // Q128 + y: "5", + denominator: "0", + shouldPanic: true, + panicMsg: "denominator must be greater than 0", + }, + { + name: "reverts_if_output_overflows_uint256", + x: "340282366920938463463374607431768211456", // Q128 + y: "340282366920938463463374607431768211456", // Q128 + denominator: "1", + shouldPanic: true, + panicMsg: "overflow: denominator(1) must be greater than hi(1)", + }, + { + name: "all_max_inputs", + x: MAX_UINT256, + y: MAX_UINT256, + denominator: MAX_UINT256, + expected: MAX_UINT256, + }, + { + name: "simple_case_no_remainder", + x: "1000000", + y: "1000000", + denominator: "1000", + expected: "1000000000", + }, + { + name: "accurate_without_phantom_overflow", + x: "340282366920938463463374607431768211456", // Q128 + y: "170141183460469231731687303715884105728", // 50 * Q128 / 100 + denominator: "510423550381407695195061911147652317184", // 150 * Q128 / 100 + expected: "113427455640312821154458202477256070485", // Q128 / 3 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + denominator := MustFromDecimal(tt.denominator) + + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + MulDiv(x, y, denominator) + }) + } else { + result := MulDiv(x, y, denominator) + uassert.Equal(t, tt.expected, result.ToString()) + } + }) + } +} + +func TestMulDivRoundingUpSimple(t *testing.T) { + tests := []struct { + name string + x string + y string + denominator string + expected string + shouldPanic bool + panicMsg string + }{ + { + name: "reverts_if_denominator_is_0", + x: "340282366920938463463374607431768211456", // Q128 + y: "5", + denominator: "0", + shouldPanic: true, + panicMsg: "denominator must be greater than 0", + }, + { + name: "rounds_up_when_remainder", + x: "5", + y: "2", + denominator: "3", + expected: "4", // (5*2)/3 = 3.33, rounded up to 4 + }, + { + name: "no_rounding_when_exact", + x: "6", + y: "3", + denominator: "2", + expected: "9", // (6*3)/2 = 9, no rounding needed + }, + { + name: "accurate_with_rounding", + x: "340282366920938463463374607431768211456", // Q128 + y: "17014118346046923173168730371588410572800", // 50 * Q128 + denominator: "51042355038140769519506191114765231718400", // 150 * Q128 + expected: "113427455640312821154458202477256070486", // Q128/3 + 1 + }, + { + name: "max_result_no_remainder", + x: MAX_UINT256, + y: "1", + denominator: "1", + expected: MAX_UINT256, + }, + { + name: "rounding_overflow", + x: MAX_UINT256, + y: "2", + denominator: "1", + shouldPanic: true, + panicMsg: "overflow: denominator(1) must be greater than hi(1)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + x := MustFromDecimal(tt.x) + y := MustFromDecimal(tt.y) + denominator := MustFromDecimal(tt.denominator) + + if tt.shouldPanic { + uassert.PanicsWithMessage(t, tt.panicMsg, func() { + MulDivRoundingUp(x, y, denominator) + }) + } else { + result := MulDivRoundingUp(x, y, denominator) + uassert.Equal(t, tt.expected, result.ToString()) + } + }) + } +} + +func TestBitLenAndByteLen(t *testing.T) { + tests := []struct { + name string + input string + expectedBit int + expectedByte int + }{ + { + name: "zero", + input: "0", + expectedBit: 0, + expectedByte: 0, + }, + { + name: "one", + input: "1", + expectedBit: 1, + expectedByte: 1, + }, + { + name: "byte_boundary", + input: "255", // 2^8 - 1 + expectedBit: 8, + expectedByte: 1, + }, + { + name: "word_boundary", + input: "18446744073709551615", // 2^64 - 1 + expectedBit: 64, + expectedByte: 8, + }, + { + name: "max_uint256", + input: MAX_UINT256, + expectedBit: 256, + expectedByte: 32, + }, + { + name: "half_max", + input: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 + expectedBit: 256, + expectedByte: 32, + }, + { + name: "uneven_bits", + input: "123456789", + expectedBit: 27, + expectedByte: 4, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + x := MustFromDecimal(tt.input) + bitLen := x.BitLen() + byteLen := x.ByteLen() + + uassert.Equal(t, tt.expectedBit, bitLen) + uassert.Equal(t, tt.expectedByte, byteLen) + }) + } +} + +func TestInPlaceSafety(t *testing.T) { + // Testing that operations don't mutate their inputs + t.Run("original_values_preserved", func(t *testing.T) { + a := MustFromDecimal("1234567890") + b := MustFromDecimal("9876543210") + + original_a := a.Clone() + original_b := b.Clone() + + _ = new(Uint).Add(a, b) + + uassert.True(t, a.Eq(original_a)) + uassert.True(t, b.Eq(original_b)) + }) + + // Testing that chained operations work correctly + t.Run("chained_operations", func(t *testing.T) { + a := MustFromDecimal("1000000") + b := MustFromDecimal("2000000") + c := MustFromDecimal("3000000") + + // (a + b) * c + temp := new(Uint).Add(a, b) + result := new(Uint).Mul(temp, c) + + expected := MustFromDecimal("9000000000000") + uassert.True(t, result.Eq(expected)) + }) +} + +func TestOperationConsistency(t *testing.T) { + // Testing mathematical properties + t.Run("addition_associativity", func(t *testing.T) { + a := NewUint(12345) + b := NewUint(67890) + c := NewUint(11111) + + // (a + b) + c + path1 := new(Uint).Add(a, b) + path1 = new(Uint).Add(path1, c) + + // a + (b + c) + path2 := new(Uint).Add(b, c) + path2 = new(Uint).Add(a, path2) + + uassert.True(t, path1.Eq(path2)) + }) + + t.Run("distributive_property", func(t *testing.T) { + a := NewUint(12345) + b := NewUint(67) + c := NewUint(89) + + // a * (b + c) + sum := new(Uint).Add(b, c) + dist1 := new(Uint).Mul(a, sum) + + // (a * b) + (a * c) + prod1 := new(Uint).Mul(a, b) + prod2 := new(Uint).Mul(a, c) + dist2 := new(Uint).Add(prod1, prod2) + + uassert.True(t, dist1.Eq(dist2)) + }) + + t.Run("inverse_operations", func(t *testing.T) { + x := MustFromDecimal("123456789012345678901234567890") + + // x + y - y = x + y := MustFromDecimal("999999999999999999999999999999") + result := new(Uint).Add(x, y) + result = new(Uint).Sub(result, y) + uassert.True(t, result.Eq(x)) + + // x * y / y = x (when no remainder) + y = NewUint(12345) + result = new(Uint).Mul(x, y) + result = new(Uint).Div(result, y) + uassert.True(t, result.Eq(x)) + + // x << n >> n = x (when n < bitlen(x)) + n := uint(10) + result = new(Uint).Lsh(x, n) + result = new(Uint).Rsh(result, n) + uassert.True(t, result.Eq(x)) + }) +} diff --git a/contract/p/gnoswap/uint256/utils.gno b/contract/p/gnoswap/uint256/utils.gno new file mode 100644 index 0000000..284e301 --- /dev/null +++ b/contract/p/gnoswap/uint256/utils.gno @@ -0,0 +1,22 @@ +package uint256 + +// checkNumberS validates that input represents a valid hexadecimal number. +// Requires "0x" or "0X" prefix and disallows leading zeros after the prefix. +func checkNumberS(input string) error { + const fn = "UnmarshalText" + l := len(input) + if l == 0 { + return errEmptyString(fn, input) + } + if l < 2 || input[0] != '0' || + (input[1] != 'x' && input[1] != 'X') { + return errMissingPrefix(fn, input) + } + if l == 2 { + return errEmptyNumber(fn, input) + } + if len(input) > 3 && input[2] == '0' { + return errLeadingZero(fn, input) + } + return nil +} diff --git a/contract/p/volos/math/shares_math.gno b/contract/p/volos/math/shares_math.gno index c2820a3..b5002da 100644 --- a/contract/p/volos/math/shares_math.gno +++ b/contract/p/volos/math/shares_math.gno @@ -13,8 +13,7 @@ var ( VIRTUAL_SHARES = u256.NewUint(1000000000) // 1 billion shares // VIRTUAL_ASSETS represents the minimum number of assets that exist in a pool - // This matches VIRTUAL_SHARES to maintain 1:1 initial share ratio - VIRTUAL_ASSETS = u256.NewUint(1000000000) // 1 billion base units + VIRTUAL_ASSETS = u256.NewUint(1) // 1 base units ) // ToSharesDown converts assets to shares, rounding down (used for supply) diff --git a/contract/r/gnoswap/access/README.md b/contract/r/gnoswap/access/README.md new file mode 100644 index 0000000..a79b7a4 --- /dev/null +++ b/contract/r/gnoswap/access/README.md @@ -0,0 +1,68 @@ +# Access + +Role-based access control for GnoSwap contracts. + +## Overview + +Access control system manages permissions across all protocol contracts using role-based authorization. + +## Roles + +- **admin**: Protocol administrator +- **governance**: Governance contract +- **router**: Swap router +- **pool**: Pool management +- **position**: Position NFT +- **staker**: Liquidity staking +- **emission**: GNS emission +- **protocol_fee**: Fee collector +- **launchpad**: Token launchpad +- **gov_staker**: Governance staking +- **gov_xgns**: xGNS token + +## Key Functions + +### `GetAddress` +Returns address for a role. + +### `SetRoleAddresses` +Updates all role addresses (RBAC only). + +### `IsAuthorized` +Checks if address has role. + +### Assert Functions +- `AssertIsAdmin` - Require admin role +- `AssertIsGovernance` - Require governance +- `AssertIsAdminOrGovernance` - Admin or governance +- `AssertIsRouter`, `AssertIsPool`, etc. + +### Swap Whitelist +- `UpdateSwapWhiteList` - Add to whitelist +- `RemoveFromSwapWhiteList` - Remove from whitelist +- `IsSwapWhitelisted` - Check whitelist status + +## Usage + +```go +// Check permission +if !access.IsAuthorized("admin", caller) { + panic("unauthorized") +} + +// Assert permission (panics if unauthorized) +access.AssertIsAdminOrGovernance(caller) + +// Get role address +addr, exists := access.GetAddress("router") + +// Manage whitelist +access.UpdateSwapWhiteList(routerAddr) +``` + +## Security + +- Centralized permission management +- Role-based authorization +- Swap whitelist for approved routers +- RBAC-only role updates \ No newline at end of file diff --git a/contract/r/gnoswap/access/access.gno b/contract/r/gnoswap/access/access.gno new file mode 100644 index 0000000..044a855 --- /dev/null +++ b/contract/r/gnoswap/access/access.gno @@ -0,0 +1,67 @@ +package access + +import ( + "std" + + "gno.land/p/nt/ufmt" +) + +var roleAddresses map[string]std.Address + +func init() { + roleAddresses = make(map[string]std.Address) +} + +// GetAddress returns the address for a role and whether it exists. +func GetAddress(role string) (std.Address, bool) { + addr, ok := roleAddresses[role] + + return addr, ok +} + +// GetRoleAddresses returns a copy of all role addresses. +func GetRoleAddresses() map[string]std.Address { + addresses := make(map[string]std.Address) + + for role, addr := range roleAddresses { + addresses[role] = addr + } + + return addresses +} + +// SetRoleAddresses updates all role addresses. +// +// Parameters: +// - newRoleAddresses: map of role names to addresses +// +// Only callable by RBAC contract. +func SetRoleAddresses(cur realm, newRoleAddresses map[string]std.Address) { + caller := std.PreviousRealm().Address() + assertIsRBAC(caller) + + // Validate all addresses before applying updates + for role, addr := range newRoleAddresses { + if !addr.IsValid() || addr == std.Address("") { + panic(ufmt.Errorf("invalid address for role %s: %s", role, addr)) + } + } + + roleAddresses = newRoleAddresses +} + +// IsAuthorized checks if caller has the specified role. +// +// Parameters: +// - role: role name to check +// - caller: address to verify +// +// Returns true if authorized, false otherwise. +func IsAuthorized(role string, caller std.Address) bool { + roleAddr, ok := roleAddresses[role] + if !ok { + return false + } + + return caller == roleAddr +} diff --git a/contract/r/gnoswap/access/assert.gno b/contract/r/gnoswap/access/assert.gno new file mode 100644 index 0000000..1ad15bf --- /dev/null +++ b/contract/r/gnoswap/access/assert.gno @@ -0,0 +1,145 @@ +package access + +import ( + "std" + + "gno.land/p/nt/ufmt" + prbac "gno.land/p/gnoswap/rbac" +) + +// AssertIsAdminOrGovernance panics if the caller is not admin or governance. +// Used for functions that require elevated privileges. +func AssertIsAdminOrGovernance(caller std.Address) { + if !IsAuthorized(prbac.ROLE_ADMIN.String(), caller) && !IsAuthorized(prbac.ROLE_GOVERNANCE.String(), caller) { + panic(ufmt.Errorf("unauthorized: caller %s is not admin or governance", caller)) + } +} + +// AssertIsAdmin panics if the caller is not admin. +// Used for admin-only functions. +func AssertIsAdmin(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_ADMIN.String(), caller) +} + +// AssertIsGovernance panics if the caller is not governance. +// Used for governance-only functions. +func AssertIsGovernance(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_GOVERNANCE.String(), caller) +} + +// AssertIsGovStaker panics if the caller is not governance staker. +// Used for governance staking functions. +func AssertIsGovStaker(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_GOV_STAKER.String(), caller) +} + +// AssertIsRouter panics if the caller is not router. +// Used for router-only functions. +func AssertIsRouter(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_ROUTER.String(), caller) +} + +// AssertIsPool panics if the caller is not pool. +// Used for pool-only functions. +func AssertIsPool(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_POOL.String(), caller) +} + +// AssertIsPosition panics if the caller is not position. +// Used for position-only functions. +func AssertIsPosition(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_POSITION.String(), caller) +} + +// AssertIsStaker panics if the caller is not staker. +// Used for staker-only functions. +func AssertIsStaker(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_STAKER.String(), caller) +} + +// AssertIsLaunchpad panics if the caller is not launchpad. +// Used for launchpad-only functions. +func AssertIsLaunchpad(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_LAUNCHPAD.String(), caller) +} + +// AssertIsEmission panics if the caller is not emission. +// Used for emission-only functions. +func AssertIsEmission(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_EMISSION.String(), caller) +} + +// AssertIsProtocolFee panics if the caller is not protocol fee. +// Used for protocol fee management functions. +func AssertIsProtocolFee(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_PROTOCOL_FEE.String(), caller) +} + +// AssertIsGovXGNS panics if the caller is not xGNS governance. +// Used for xGNS governance functions. +func AssertIsGovXGNS(caller std.Address) { + AssertIsAuthorized(prbac.ROLE_XGNS.String(), caller) +} + +// AssertIsAuthorized panics if the caller does not have the specified role. +// Also panics if the role does not exist. +func AssertIsAuthorized(roleName string, caller std.Address) { + roleAddr, ok := GetAddress(roleName) + if !ok { + panic(ufmt.Errorf("role %s does not exist", roleName)) + } + + if caller != roleAddr { + panic(ufmt.Errorf("unauthorized: caller %s is not %s", caller, roleName)) + } +} + +// AssertHasAnyRole panics if the caller does not have any of the specified roles. +// Also panics if any of the roles do not exist. +func AssertHasAnyRole(caller std.Address, roleNames ...string) { + for _, roleName := range roleNames { + roleAddr, ok := GetAddress(roleName) + if !ok { + panic(ufmt.Errorf("role %s does not exist", roleName)) + } + + if caller == roleAddr { + return + } + } + + panic(ufmt.Errorf("unauthorized: caller %s is not any of the roles %v", caller, roleNames)) +} + +// AssertIsValidAddress panics if the provided address is invalid. +func AssertIsValidAddress(addr std.Address) { + if !addr.IsValid() { + panic(ufmt.Errorf("invalid address: %s", addr)) + } +} + +// AssertIsUser panics if the caller is not a user realm. +// Used to ensure calls come from user accounts, not other contracts. +func AssertIsUser(r std.Realm) { + if !r.IsUser() { + panic(ufmt.Errorf("caller is not user")) + } +} + +// AssertIsSwapWhitelisted panics if the caller is not on the swap whitelist. +// Used to restrict swap operations to authorized routers only. +func AssertIsSwapWhitelisted(caller std.Address) { + if !IsSwapWhitelisted(caller) { + panic(ufmt.Errorf("unauthorized: caller %s is not a whitelisted router", caller)) + } +} + +// assertIsRBAC panics if the caller is not the RBAC contract. +// Used internally to protect role management functions. +func assertIsRBAC(caller std.Address) { + rbacAddress := std.DerivePkgAddr(rbacPackagePath) + + if caller != rbacAddress { + panic(ufmt.Errorf("unauthorized: caller %s is not rbac", caller)) + } +} diff --git a/contract/r/gnoswap/access/consts.gno b/contract/r/gnoswap/access/consts.gno new file mode 100644 index 0000000..036c6b0 --- /dev/null +++ b/contract/r/gnoswap/access/consts.gno @@ -0,0 +1,5 @@ +package access + +const ( + rbacPackagePath = "gno.land/r/gnoswap/rbac" +) diff --git a/contract/r/gnoswap/access/errors.gno b/contract/r/gnoswap/access/errors.gno new file mode 100644 index 0000000..07a6dba --- /dev/null +++ b/contract/r/gnoswap/access/errors.gno @@ -0,0 +1,10 @@ +package access + +const ( + errConfigNil = "config cannot be nil" + errNotInitialized = "access control not initialized" + errEmptyRole = "role name cannot be empty" + errRoleExists = "role %s already exists" + errDeclareRole = "failed to declare role %s: %v" + errUnauthorized = "caller(%s) is not authorized for role %s" +) diff --git a/contract/r/gnoswap/access/gnomod.toml b/contract/r/gnoswap/access/gnomod.toml new file mode 100644 index 0000000..38289c9 --- /dev/null +++ b/contract/r/gnoswap/access/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/access" +gno = "0.9" diff --git a/contract/r/gnoswap/access/swap_whitelist.gno b/contract/r/gnoswap/access/swap_whitelist.gno new file mode 100644 index 0000000..0c5f864 --- /dev/null +++ b/contract/r/gnoswap/access/swap_whitelist.gno @@ -0,0 +1,77 @@ +package access + +import ( + "std" + + "gno.land/p/nt/ufmt" + prbac "gno.land/p/gnoswap/rbac" +) + +// Router whitelist storage +var swapWhitelist map[std.Address]bool + +func init() { + swapWhitelist = make(map[std.Address]bool) +} + +// UpdateSwapWhiteList adds a router address to the swap whitelist. +// Panics if router address is invalid. +// Only admin or governance can call this function. +func UpdateSwapWhiteList(cur realm, router std.Address) { + caller := std.PreviousRealm().Address() + AssertIsAdminOrGovernance(caller) + + if !router.IsValid() { + panic(ufmt.Errorf("invalid router address: %s", router)) + } + + // Add or update the router in the whitelist + swapWhitelist[router] = true +} + +// RemoveFromSwapWhiteList removes a router address from the swap whitelist. +// Does nothing if the router is not in the whitelist. +// Only admin or governance can call this function. +func RemoveFromSwapWhiteList(cur realm, router std.Address) { + caller := std.PreviousRealm().Address() + AssertIsAdminOrGovernance(caller) + + delete(swapWhitelist, router) +} + +// IsSwapWhitelisted returns true if the address is either the official router +// or is in the swap whitelist. Returns false otherwise. +func IsSwapWhitelisted(addr std.Address) bool { + // Check if it's the official router first + // + // Note: While it's a common pattern to store the router's address + // in a global variable to prevent unnecessary function calls, + // this function is called infrequently and retrieves the address internally + // to respond to address changes. + officialRouter, ok := GetAddress(prbac.ROLE_ROUTER.String()) + if ok && addr == officialRouter { + return true + } + + // Then check whitelist + return swapWhitelist[addr] +} + +// GetWhitelistedSwaps returns all whitelisted router addresses including +// the official router address if it exists. +func GetWhitelistedSwaps() []std.Address { + routers := make([]std.Address, 0, len(swapWhitelist)+1) + + // Include official router + officialRouter, ok := GetAddress(prbac.ROLE_ROUTER.String()) + if ok { + routers = append(routers, officialRouter) + } + + // Add whitelisted routers + for router := range swapWhitelist { + routers = append(routers, router) + } + + return routers +} diff --git a/contract/r/gnoswap/emission/README.md b/contract/r/gnoswap/emission/README.md new file mode 100644 index 0000000..2142078 --- /dev/null +++ b/contract/r/gnoswap/emission/README.md @@ -0,0 +1,107 @@ +# Emission + +GNS token emission and distribution system. + +## Overview + +The emission system controls creation and distribution of new GNS tokens with a deflationary model featuring periodic halvings, ensuring predictable and decreasing supply growth over 12 years. For more details, check out [docs](https://docs.gnoswap.io/gnoswap-token/emission). + +## Token Economics + +- **Total Supply Cap**: 1,000,000,000 GNS +- **Initial Minted**: 100,000,000 GNS +- **To Be Minted**: 900,000,000 GNS over 12 years +- **Halving Period**: Every 2 years (63,072,000 seconds) +- **Halving Reduction**: 50% decrease in emission rate +- **Distribution**: Automatic during protocol activity + +## Configuration + +- **Distribution Ratios** (modifiable by governance): + - Liquidity Staker: 75% (default) + - DevOps: 20% (default) + - Community Pool: 5% (default) + - Governance Staker: 0% (default) +- **Start Time**: Unix timestamp (immutable once set) + +## Core Features + +### Emission Schedule +Implements Bitcoin-style halving model: +- Year 0-2: 100% emission rate +- Year 2-4: 50% emission rate +- Year 4-6: 25% emission rate +- Year 6-8: 12.5% emission rate +- Year 8-10: 6.25% emission rate +- Year 10-12: 3.125% emission rate + +### Distribution Mechanism +When triggered by protocol activity: +1. Calculates elapsed time since last distribution +2. Mints GNS based on current emission rate +3. Distributes to targets per configured ratios +4. Carries forward any undistributed amounts + +## Key Functions + +### `MintAndDistributeGns` +Mints and distributes GNS tokens automatically. + +### `SetDistributionStartTime` +One-time setup of emission start timestamp. + +### `SetDistributionRatio` +Updates distribution percentages (governance only). + +### `GetDistributionRatio` +Returns current distribution ratios. + +## Technical Details + +### Timestamp-Based Emission +``` +emissionPerSecond = baseEmission / (2^halvingCount) +amountToMint = emissionPerSecond * timeSinceLastMint +``` + +### Halving Calculation +``` +halvingCount = floor(timeSinceStart / halvingPeriod) +``` + +### Distribution Targets +1. **Liquidity Staker**: Rewards for LP providers +2. **DevOps**: Development and operations fund +3. **Community Pool**: Community-governed treasury +4. **Governance Staker**: GNS staking rewards (currently 0%) + +## Usage + +```go +// Set emission start (one-time by admin) +SetDistributionStartTime(1704067200) // Jan 1, 2024 + +// Trigger emission (called automatically) +amount := MintAndDistributeGns() + +// Update distribution ratios +ChangeDistributionPct( + 1, 7000, // 70% to liquidity stakers + 2, 2000, // 20% to devops + 3, 1000, // 10% to community pool + 4, 0 // 0% to governance stakers +) + +// Query distribution info +stakerPct := GetDistributionBpsPct(LIQUIDITY_STAKER) +accumulated := GetAccuDistributedToStaker() +rate := GetStakerEmissionAmountPerSecond() +``` + +## Security + +- Start time immutable once set and passed +- Distribution percentages must sum to 10000 (100%) +- Automatic triggers prevent manipulation +- Leftover tracking ensures no token loss +- Halving enforced at protocol level \ No newline at end of file diff --git a/contract/r/gnoswap/emission/assert.gno b/contract/r/gnoswap/emission/assert.gno new file mode 100644 index 0000000..490e9bf --- /dev/null +++ b/contract/r/gnoswap/emission/assert.gno @@ -0,0 +1,82 @@ +package emission + +import ( + "gno.land/p/nt/ufmt" +) + +// assertValidDistributionTargets panics if any of the four distribution targets is invalid +// or if there are duplicate targets. All four distribution targets must be unique and valid. +func assertValidDistributionTargets(target01, target02, target03, target04 int) { + validTargets := map[int]bool{ + LIQUIDITY_STAKER: false, + DEVOPS: false, + COMMUNITY_POOL: false, + GOV_STAKER: false, + } + + currentTargets := []int{target01, target02, target03, target04} + + for _, target := range currentTargets { + if _, ok := validTargets[target]; !ok { + panic(makeErrorWithDetails( + errInvalidEmissionTarget, + ufmt.Sprintf("invalid target(%d)", target), + )) + } + + validTargets[target] = true + } + + for _, valid := range validTargets { + if !valid { + panic(errDuplicateTarget) + } + } +} + +// assertValidDistributionTarget panics if the given distribution target is invalid. +func assertValidDistributionTarget(target int) { + validTargets := map[int]bool{ + LIQUIDITY_STAKER: false, + DEVOPS: false, + COMMUNITY_POOL: false, + GOV_STAKER: false, + } + + if _, ok := validTargets[target]; !ok { + panic(makeErrorWithDetails( + errInvalidEmissionTarget, + ufmt.Sprintf("invalid target(%d)", target), + )) + } +} + +// assertValidDistributionPct ensures the sum of all distribution percentages equals 10000 (100%). +// Panics if the sum does not equal exactly 10000 basis points. +func assertValidDistributionPct(pct01, pct02, pct03, pct04 int64) { + // Validate individual percentages are non-negative and reasonable + percentages := []int64{pct01, pct02, pct03, pct04} + for i, pct := range percentages { + if pct < 0 { + panic(makeErrorWithDetails( + errInvalidEmissionPct, + ufmt.Sprintf("percentage %d cannot be negative: %d", i+1, pct), + )) + } + + if pct > 10000 { + panic(makeErrorWithDetails( + errInvalidEmissionPct, + ufmt.Sprintf("percentage %d cannot exceed 100%%: %d", i+1, pct), + )) + } + } + + sum := pct01 + pct02 + pct03 + pct04 + if sum != 10000 { + panic(makeErrorWithDetails( + errInvalidEmissionPct, + ufmt.Sprintf("sum of percentages must be 10000, got %d", sum), + )) + } +} diff --git a/contract/r/gnoswap/emission/distribution.gno b/contract/r/gnoswap/emission/distribution.gno new file mode 100644 index 0000000..f091066 --- /dev/null +++ b/contract/r/gnoswap/emission/distribution.gno @@ -0,0 +1,389 @@ +package emission + +import ( + "std" + "strconv" + "time" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + prbac "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/gns" + "gno.land/r/gnoswap/halt" +) + +const ( + LIQUIDITY_STAKER int = iota + 1 + DEVOPS + COMMUNITY_POOL + GOV_STAKER +) + +var ( + // Stores the percentage (in basis points) for each distribution target + // 1 basis point = 0.01% + // These percentages can be modified through governance. + distributionBpsPct *avl.Tree + + distributedToStaker int64 // can be cleared by staker contract + distributedToDevOps int64 + distributedToCommunityPool int64 + distributedToGovStaker int64 // can be cleared by governance staker + + // Historical total distributions (never reset) + accuDistributedToStaker int64 + accuDistributedToDevOps int64 + accuDistributedToCommunityPool int64 + accuDistributedToGovStaker int64 +) + +// Initialize default distribution percentages: +// - Liquidity Stakers: 75% +// - DevOps: 20% +// - Community Pool: 5% +// - Governance Stakers: 0% +// +// ref: https://docs.gnoswap.io/gnoswap-token/emission +func init() { + distributionBpsPct = avl.NewTree() + distributionBpsPct.Set(strconv.Itoa(LIQUIDITY_STAKER), int64(7500)) + distributionBpsPct.Set(strconv.Itoa(DEVOPS), int64(2000)) + distributionBpsPct.Set(strconv.Itoa(COMMUNITY_POOL), int64(500)) + distributionBpsPct.Set(strconv.Itoa(GOV_STAKER), int64(0)) +} + +// ChangeDistributionPct changes distribution percentages for emission targets. +// +// This function redistributes how newly minted GNS tokens are allocated across +// protocol components. Changes take effect immediately for future emissions. +// Historical distributions are not affected. +// +// Parameters: +// - target01-04: Target identifiers (1=LIQUIDITY_STAKER, 2=DEVOPS, 3=COMMUNITY_POOL, 4=GOV_STAKER) +// - pct01-04: Percentage in basis points (100 = 1%, 10000 = 100%) +// +// Requirements: +// - All four targets must be specified (use current values if unchanged) +// - Percentages must sum to exactly 10000 (100%) +// - Each percentage must be 0-10000 +// - Targets must be unique (no duplicates) +// +// Example: +// +// ChangeDistributionPct( +// 1, 7000, // 70% to liquidity stakers +// 2, 2000, // 20% to devops +// 3, 1000, // 10% to community pool +// 4, 0 // 0% to governance stakers +// ) +// +// Only callable by admin or governance. +func ChangeDistributionPct( + cur realm, + target01 int, pct01 int64, + target02 int, pct02 int64, + target03 int, pct03 int64, + target04 int, pct04 int64, +) { + halt.AssertIsNotHaltedEmission() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertValidDistributionTargets(target01, target02, target03, target04) + assertValidDistributionPct(pct01, pct02, pct03, pct04) + + changeDistributionPcts( + target01, pct01, + target02, pct02, + target03, pct03, + target04, pct04, + ) + + previousRealm := std.PreviousRealm() + std.Emit( + "ChangeDistributionPct", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "target01", targetToStr(target01), + "pct01", formatInt(pct01), + "target02", targetToStr(target02), + "pct02", formatInt(pct02), + "target03", targetToStr(target03), + "pct03", formatInt(pct03), + "target04", targetToStr(target04), + "pct04", formatInt(pct04), + ) +} + +// changeDistributionPcts updates the distribution percentages in the AVL tree. +func changeDistributionPcts( + target01 int, pct01 int64, + target02 int, pct02 int64, + target03 int, pct03 int64, + target04 int, pct04 int64, +) { + // First, cache the percentage of the staker just before it changes Callback if needed + // (check if the LIQUIDITY_STAKER was located between target01 and 04) + setDistributionBpsPct(target01, pct01) + setDistributionBpsPct(target02, pct02) + setDistributionBpsPct(target03, pct03) + setDistributionBpsPct(target04, pct04) +} + +// distributeToTarget distributes tokens according to configured percentages. +// +// Returns total amount distributed and any error. +func distributeToTarget(amount int64) (int64, error) { + totalSent := int64(0) + + var err error + + distributionBpsPct.Iterate("", "", func(targetStr string, iPct any) bool { + targetInt, distErr := strconv.Atoi(targetStr) + if distErr != nil { + err = distErr + return true + } + + pct, ok := iPct.(int64) + if !ok { + panic("failed to cast distributionBpsPct's element to int64") + } + + distAmount := calculateAmount(amount, pct) + + // Skip zero amounts to avoid unnecessary transfers + if distAmount == 0 { + return false + } + + totalSent += distAmount + + distErr = transferToTarget(targetInt, distAmount) + if distErr != nil { + err = distErr + + return true + } + + return false + }) + + return totalSent, err +} + +// calculateAmount converts basis points to actual token amount. +func calculateAmount(amount, bptPct int64) int64 { + if amount < 0 || bptPct < 0 || bptPct > 10000 { + panic("invalid amount or bptPct") + } + + // More precise overflow prevention + const maxInt64 = 9223372036854775807 + if amount > maxInt64/10000 { + panic("amount too large, would cause overflow") + } + + // Additional safety check for zero division + if bptPct == 0 { + return 0 + } + + return amount * bptPct / 10000 +} + +// transferToTarget sends tokens to the appropriate target address. +// +// Returns error if target address not found. +func transferToTarget(target int, amount int64) error { + switch target { + case LIQUIDITY_STAKER: + stakerAddr, ok := access.GetAddress(prbac.ROLE_STAKER.String()) + if !ok { + return makeErrorWithDetails( + errDistributionAddressNotFound, + ufmt.Sprintf("%s not found", prbac.ROLE_STAKER.String()), + ) + } + + gns.Transfer(cross, stakerAddr, amount) + distributedToStaker += amount + accuDistributedToStaker += amount + + case DEVOPS: + devOpsAddr, ok := access.GetAddress(prbac.ROLE_DEVOPS.String()) + if !ok { + return makeErrorWithDetails( + errDistributionAddressNotFound, + ufmt.Sprintf("%s not found", prbac.ROLE_DEVOPS.String()), + ) + } + + gns.Transfer(cross, devOpsAddr, amount) + distributedToDevOps += amount + accuDistributedToDevOps += amount + + case COMMUNITY_POOL: + communityPoolAddr, ok := access.GetAddress(prbac.ROLE_COMMUNITY_POOL.String()) + if !ok { + return makeErrorWithDetails( + errDistributionAddressNotFound, + ufmt.Sprintf("%s not found", prbac.ROLE_COMMUNITY_POOL.String()), + ) + } + + gns.Transfer(cross, communityPoolAddr, amount) + distributedToCommunityPool += amount + accuDistributedToCommunityPool += amount + + case GOV_STAKER: + govStakerAddr, ok := access.GetAddress(prbac.ROLE_GOV_STAKER.String()) + if !ok { + return makeErrorWithDetails( + errDistributionAddressNotFound, + ufmt.Sprintf("%s not found", prbac.ROLE_GOV_STAKER.String()), + ) + } + + gns.Transfer(cross, govStakerAddr, amount) + distributedToGovStaker += amount + accuDistributedToGovStaker += amount + + default: + return makeErrorWithDetails( + errInvalidEmissionTarget, + ufmt.Sprintf("invalid target(%d)", target), + ) + } + + return nil +} + +// GetDistributionBpsPct returns the distribution percentage in basis points for a specific target. +func GetDistributionBpsPct(target int) int64 { + assertValidDistributionTarget(target) + if distributionBpsPct == nil { + panic("distributionBpsPct is nil") + } + + iInt64, exist := distributionBpsPct.Get(strconv.Itoa(target)) + if !exist { + panic(makeErrorWithDetails( + errInvalidEmissionTarget, + ufmt.Sprintf("invalid target(%d)", target), + )) + } + + pct, ok := iInt64.(int64) + if !ok { + panic("failed to cast distributionBpsPct's element to int64") + } + + return pct +} + +// GetDistributedToStaker returns pending GNS for liquidity stakers. +func GetDistributedToStaker() int64 { + return distributedToStaker +} + +// GetDistributedToDevOps returns accumulated GNS for DevOps. +func GetDistributedToDevOps() int64 { + return distributedToDevOps +} + +// GetDistributedToCommunityPool returns the amount of GNS distributed to Community Pool. +func GetDistributedToCommunityPool() int64 { + return distributedToCommunityPool +} + +// GetDistributedToGovStaker returns the amount of GNS distributed to governance stakers since last clear. +func GetDistributedToGovStaker() int64 { + return distributedToGovStaker +} + +// GetAccuDistributedToStaker returns the total historical GNS distributed to liquidity stakers. +func GetAccuDistributedToStaker() int64 { + return accuDistributedToStaker +} + +// GetAccuDistributedToDevOps returns the total historical GNS distributed to DevOps. +func GetAccuDistributedToDevOps() int64 { + return accuDistributedToDevOps +} + +// GetAccuDistributedToCommunityPool returns the total historical GNS distributed to Community Pool. +func GetAccuDistributedToCommunityPool() int64 { + return accuDistributedToCommunityPool +} + +// GetAccuDistributedToGovStaker returns the total historical GNS distributed to governance stakers. +func GetAccuDistributedToGovStaker() int64 { + return accuDistributedToGovStaker +} + +// GetStakerEmissionAmountPerSecond returns the current per-second emission amount allocated to liquidity stakers. +func GetStakerEmissionAmountPerSecond() int64 { + currentTimestamp := time.Now().Unix() + return calculateAmount(gns.GetEmissionAmountPerSecondByTimestamp(currentTimestamp), GetDistributionBpsPct(LIQUIDITY_STAKER)) +} + +// GetStakerEmissionAmountPerSecondInRange returns emission amounts allocated to liquidity stakers for a time range. +func GetStakerEmissionAmountPerSecondInRange(start, end int64) ([]int64, []int64) { + halvingBlocks, halvingEmissions := gns.GetEmissionAmountPerSecondInRange(start, end) + for i := range halvingBlocks { + // Applying staker ratio for past halving blocks + halvingEmissions[i] = calculateAmount(halvingEmissions[i], GetDistributionBpsPct(LIQUIDITY_STAKER)) + } + + return halvingBlocks, halvingEmissions +} + +// ClearDistributedToStaker resets the pending distribution amount for liquidity stakers. +// +// Only callable by staker contract. +func ClearDistributedToStaker(cur realm) { + caller := std.PreviousRealm().Address() + access.AssertIsStaker(caller) + + distributedToStaker = 0 +} + +// ClearDistributedToGovStaker resets the pending distribution amount for governance stakers. +// +// Only callable by governance staker contract. +func ClearDistributedToGovStaker(cur realm) { + caller := std.PreviousRealm().Address() + access.AssertIsGovStaker(caller) + distributedToGovStaker = 0 +} + +// setDistributionBpsPct changes percentage of each target for how much GNS it will get by emission. +// Creates new AVL tree if nil. +func setDistributionBpsPct(target int, pct int64) { + if distributionBpsPct == nil { + distributionBpsPct = avl.NewTree() + } + + distributionBpsPct.Set(strconv.Itoa(target), pct) +} + +// targetToStr converts target constant to string representation. +func targetToStr(target int) string { + switch target { + case LIQUIDITY_STAKER: + return "LIQUIDITY_STAKER" + case DEVOPS: + return "DEVOPS" + case COMMUNITY_POOL: + return "COMMUNITY_POOL" + case GOV_STAKER: + return "GOV_STAKER" + default: + return "UNKNOWN" + } +} diff --git a/contract/r/gnoswap/emission/emission.gno b/contract/r/gnoswap/emission/emission.gno new file mode 100644 index 0000000..244fcab --- /dev/null +++ b/contract/r/gnoswap/emission/emission.gno @@ -0,0 +1,216 @@ +package emission + +import ( + "math" + "std" + "time" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/gns" + "gno.land/r/gnoswap/halt" +) + +var ( + // leftGNSAmount tracks undistributed GNS tokens from previous distributions + leftGNSAmount int64 + // lastExecutedTimestamp stores the last timestamp when distribution was executed + lastExecutedTimestamp int64 + + // emissionAddr is the address of the emission realm + emissionAddr = std.CurrentRealm().Address() + + // distributionStartTimestamp is the timestamp from which emission distribution starts + // Default is 0, meaning distribution is not started until explicitly set + distributionStartTimestamp int64 +) + +// GetLeftGNSAmount returns the amount of undistributed GNS tokens from previous distributions. +func GetLeftGNSAmount() int64 { return leftGNSAmount } + +// GetDistributionStartTimestamp returns the timestamp when emission distribution started. +// Returns 0 if distribution has not been started yet. +func GetDistributionStartTimestamp() int64 { return distributionStartTimestamp } + +// setLeftGNSAmount updates the undistributed GNS token amount +func setLeftGNSAmount(amount int64) { + if amount < 0 { + panic("left GNS amount cannot be negative") + } + + leftGNSAmount = amount +} + +// GetLastExecutedTimestamp returns the timestamp of the last emission distribution execution. +func GetLastExecutedTimestamp() int64 { return lastExecutedTimestamp } + +// setLastExecutedTimestamp updates the timestamp of the last emission distribution execution. +func setLastExecutedTimestamp(timestamp int64) { + if timestamp < 0 { + panic("last executed timestamp cannot be negative") + } + + lastExecutedTimestamp = timestamp +} + +// MintAndDistributeGns mints and distributes GNS tokens according to the emission schedule. +// +// This function is called automatically by protocol contracts during user interactions +// to trigger periodic GNS emission. It mints new tokens based on elapsed time since +// last distribution and distributes them to predefined targets (staker, devops, etc.). +// +// Returns: +// - int64: Total amount of GNS distributed in this call +// +// Note: Distribution only occurs if start timestamp is set and reached. +// Any undistributed tokens from previous calls are carried forward. +func MintAndDistributeGns(cur realm) int64 { + halt.AssertIsNotHaltedEmission() + + currentHeight := std.ChainHeight() + currentTimestamp := time.Now().Unix() + + // Check if distribution start timestamp is set and if current timestamp has reached it + // If distributionStartTimestamp is 0 (default), skip distribution to prevent immediate start + // If current timestamp is below start timestamp, skip distribution + if distributionStartTimestamp == 0 || currentTimestamp < distributionStartTimestamp { + return 0 + } + + // Skip if we've already minted tokens at this timestamp + lastMintedTimestamp := gns.LastMintedTimestamp() + if currentTimestamp <= lastMintedTimestamp { + return 0 + } + + // Additional check to prevent re-entrancy + if lastExecutedTimestamp >= currentTimestamp { + // Skip if we've already processed this height in emission + return 0 + } + + // Mint new tokens and add any leftover amounts from previous distribution + mintedEmissionRewardAmount := gns.MintGns(cross, emissionAddr) + + // Validate minted amount + if mintedEmissionRewardAmount < 0 { + panic("minted emission reward amount cannot be negative") + } + + distributableAmount := mintedEmissionRewardAmount + prevLeftAmount := GetLeftGNSAmount() + + if leftGNSAmount > 0 { + // Check for overflow before addition + if distributableAmount > math.MaxInt64-prevLeftAmount { + panic("distributable amount would overflow") + } + + distributableAmount += prevLeftAmount + setLeftGNSAmount(0) + } + + // Distribute tokens and track any undistributed amount + distributedGNSAmount, err := distributeToTarget(distributableAmount) + if err != nil { + panic(err) + } + + // Validate distribution arithmetic + if distributedGNSAmount < 0 { + panic("distributed amount cannot be negative") + } + + if distributedGNSAmount > distributableAmount { + panic("distributed amount cannot exceed distributable amount") + } + + if distributableAmount != distributedGNSAmount { + remainder := distributableAmount - distributedGNSAmount + if remainder < 0 { + panic("remainder calculation error") + } + setLeftGNSAmount(remainder) + } + + setLastExecutedTimestamp(currentTimestamp) + + previousRealm := std.PreviousRealm() + std.Emit( + "MintAndDistributeGns", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "lastTimestamp", formatInt(lastExecutedTimestamp), + "currentTimestamp", formatInt(currentTimestamp), + "currentHeight", formatInt(currentHeight), + "mintedAmount", formatInt(mintedEmissionRewardAmount), + "prevLeftAmount", formatInt(prevLeftAmount), + "distributedAmount", formatInt(distributedGNSAmount), + "currentLeftAmount", formatInt(GetLeftGNSAmount()), + "gnsTotalSupply", formatInt(gns.TotalSupply()), + ) + + return distributedGNSAmount +} + +// SetDistributionStartTime sets the timestamp when emission distribution starts. +// +// This function controls when GNS emission begins. Once set and reached, the protocol +// starts minting GNS tokens according to the emission schedule. The timestamp can only +// be set before distribution starts - it becomes immutable once active. +// +// Parameters: +// - startTimestamp: Unix timestamp when emission should begin +// +// Requirements: +// - Must be called before distribution starts (one-time setup) +// - Timestamp must be in the future +// - Cannot be negative +// +// Effects: +// - Sets global distribution start time +// - Initializes GNS emission state if not already started +// - Emission begins automatically when timestamp is reached +// +// Only callable by admin or governance. +func SetDistributionStartTime(cur realm, startTimestamp int64) { + halt.AssertIsNotHaltedEmission() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + // Prevent negative timestamps. + if startTimestamp < 0 { + panic("distribution start timestamp cannot be negative") + } + + currentTimestamp := time.Now().Unix() + + // Cannot change after distribution started. + if distributionStartTimestamp != 0 && distributionStartTimestamp <= currentTimestamp { + panic("distribution has already started, cannot change start timestamp") + } + + // Must be in the future. + if startTimestamp > 0 && startTimestamp <= currentTimestamp { + panic("distribution start timestamp must be greater than current timestamp") + } + + prevStartTimestamp := distributionStartTimestamp + + // Reinitialize emission state if not started yet. + if startTimestamp > currentTimestamp && gns.MintedEmissionAmount() == 0 { + gns.InitEmissionState(cross, startTimestamp, startTimestamp) + } + + distributionStartTimestamp = startTimestamp + + std.Emit( + "SetDistributionStartTime", + "caller", caller.String(), + "prevStartTimestamp", formatInt(prevStartTimestamp), + "newStartTimestamp", formatInt(startTimestamp), + "height", formatInt(std.ChainHeight()), + "timestamp", formatInt(time.Now().Unix()), + ) +} diff --git a/contract/r/gnoswap/emission/errors.gno b/contract/r/gnoswap/emission/errors.gno new file mode 100644 index 0000000..a20da21 --- /dev/null +++ b/contract/r/gnoswap/emission/errors.gno @@ -0,0 +1,21 @@ +package emission + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errCallbackIsNil = errors.New("[GNOSWAP-EMISSION-001] callback func is nil") + errInvalidEmissionTarget = errors.New("[GNOSWAP-EMISSION-002] invalid emission target") + errInvalidEmissionPct = errors.New("[GNOSWAP-EMISSION-003] invalid emission percentage") + errDuplicateTarget = errors.New("[GNOSWAP-EMISSION-004] duplicate emission target") + errEmissionAddressNotFound = errors.New("[GNOSWAP-EMISSION-005] emission address not found") + errDistributionAddressNotFound = errors.New("[GNOSWAP-EMISSION-006] distribution address not found") +) + +// makeErrorWithDetails creates a new error by combining a base error with additional details. +func makeErrorWithDetails(err error, detail string) error { + return ufmt.Errorf("%s || %s", err.Error(), detail) +} diff --git a/contract/r/gnoswap/emission/gnomod.toml b/contract/r/gnoswap/emission/gnomod.toml new file mode 100644 index 0000000..e719176 --- /dev/null +++ b/contract/r/gnoswap/emission/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/emission" +gno = "0.9" diff --git a/contract/r/gnoswap/emission/utils.gno b/contract/r/gnoswap/emission/utils.gno new file mode 100644 index 0000000..e415b9b --- /dev/null +++ b/contract/r/gnoswap/emission/utils.gno @@ -0,0 +1,37 @@ +package emission + +import ( + "strconv" + + "gno.land/p/nt/ufmt" +) + +// formatUint converts various unsigned integer types to string representation. +// Panics if the type is not supported. +func formatUint(v any) string { + switch v := v.(type) { + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} + +// formatInt converts various signed integer types to string representation. +// Panics if the type is not supported. +func formatInt(v any) string { + switch v := v.(type) { + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case int: + return strconv.Itoa(v) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} diff --git a/contract/r/gnoswap/gns/README.md b/contract/r/gnoswap/gns/README.md new file mode 100644 index 0000000..7ffc1e9 --- /dev/null +++ b/contract/r/gnoswap/gns/README.md @@ -0,0 +1,60 @@ +# GNS + +GnoSwap governance and utility token. + +## Overview + +GNS is the native governance token of GnoSwap, featuring a deflationary emission schedule with halvings every 2 years over 12 years total. + +## Token Economics + +- **Symbol**: GNS +- **Decimals**: 6 +- **Max Supply**: 1,000,000,000 GNS +- **Initial Mint**: 100,000,000 GNS +- **Total Emission**: 900,000,000 GNS over 12 years + +## Emission Schedule + +| Years | Annual Emission | Rate | +|-------|----------------|------| +| 1-2 | 225,000,000 | 100% | +| 3-4 | 112,500,000 | 50% | +| 5-6 | 56,250,000 | 25% | +| 7-8 | 28,125,000 | 12.5%| +| 9-12 | 14,062,500 | 6.25%| + +## Core Functions + +### `Transfer` +Transfers tokens between addresses. + +### `TransferFrom` +Transfers with allowance. + +### `Approve` +Approves spending allowance. + +### `MintGns` +Mints new tokens per emission schedule. + +### `Burn` +Burns tokens from supply. + +## Usage + +```go +// Transfer tokens +Transfer(to, amount) + +// Approve and transfer +Approve(spender, amount) +TransferFrom(from, to, amount) + +// Mint per emission schedule +MintGns() +``` + +## Distribution + +See [emission contract](../emission) for distribution details. \ No newline at end of file diff --git a/contract/r/gnoswap/gns/assert.gno b/contract/r/gnoswap/gns/assert.gno new file mode 100644 index 0000000..f07c8b0 --- /dev/null +++ b/contract/r/gnoswap/gns/assert.gno @@ -0,0 +1,14 @@ +package gns + +import ( + "std" + + "gno.land/p/nt/ufmt" +) + +func assertAddressIsPreviousRealm(addr std.Address) { + previousRealm := std.PreviousRealm() + if addr != previousRealm.Address() { + panic(ufmt.Errorf("address(%s) is not previous realm", addr.String())) + } +} diff --git a/contract/r/gnoswap/gns/consts.gno b/contract/r/gnoswap/gns/consts.gno new file mode 100644 index 0000000..9db42d0 --- /dev/null +++ b/contract/r/gnoswap/gns/consts.gno @@ -0,0 +1,29 @@ +package gns + +const ( + DAY_PER_YEAR = 365 + SECONDS_PER_DAY = 86400 + SECONDS_IN_YEAR = 31536000 + + HALVING_START_YEAR = int64(1) + HALVING_END_YEAR = int64(12) + + // Maximum allowed block time in milliseconds (1 second) + MAX_BLOCK_TIME_MS = 1e9 +) + +// Annual halving amount - maximum issuance per year +var halvingAmountsPerYear = [HALVING_END_YEAR]int64{ + 18_750_000_000_000 * 12, // Year 1: 225000000000000 + 18_750_000_000_000 * 12, // Year 2: 225000000000000 + 9_375_000_000_000 * 12, // Year 3: 112500000000000 + 9_375_000_000_000 * 12, // Year 4: 112500000000000 + 4_687_500_000_000 * 12, // Year 5: 56250000000000 + 4_687_500_000_000 * 12, // Year 6: 56250000000000 + 2_343_750_000_000 * 12, // Year 7: 28125000000000 + 2_343_750_000_000 * 12, // Year 8: 28125000000000 + 1_171_875_000_000 * 12, // Year 9: 14062500000000 + 1_171_875_000_000 * 12, // Year 10: 14062500000000 + 1_171_875_000_000 * 12, // Year 11: 14062500000000 + 1_171_875_000_000 * 12, // Year 12: 14062500000000 +} diff --git a/contract/r/gnoswap/gns/emission_state.gno b/contract/r/gnoswap/gns/emission_state.gno new file mode 100644 index 0000000..4029a4e --- /dev/null +++ b/contract/r/gnoswap/gns/emission_state.gno @@ -0,0 +1,166 @@ +package gns + +import ( + "std" + "time" + + "gno.land/p/nt/ufmt" +) + +var emissionState *EmissionState + +func init() { + emissionState = NewEmissionState(0, 0) +} + +// EmissionState manages emission state and halving data. +// Tracks emission timing, status, and halving year information for 12-year schedule. +type EmissionState struct { + startHeight int64 + startTimestamp int64 + endTimestamp int64 + halvingData *HalvingData +} + +// isInitialized returns true if emission state has been initialized with valid height and timestamp. +func (e *EmissionState) isInitialized() bool { + return e.startHeight != 0 && e.startTimestamp != 0 +} + +// isActive returns true if emission is currently active at the given timestamp. +// Returns false if not initialized or timestamp is outside emission period. +func (e *EmissionState) isActive(timestamp int64) bool { + if !e.isInitialized() { + return false + } + + if e.startTimestamp > timestamp { + return false + } + + if e.endTimestamp < timestamp { + return false + } + + return true +} + +// isEnded returns true if emission has ended at the given timestamp. +func (e *EmissionState) isEnded(timestamp int64) bool { + return e.endTimestamp < timestamp +} + +// getCurrentYear returns the halving year (1-12) for the given timestamp, or 0 if outside emission period. +func (e *EmissionState) getCurrentYear(timestamp int64) int64 { + if timestamp < e.startTimestamp { + return 0 + } + + if timestamp > e.endTimestamp { + return 0 + } + + year := (timestamp - e.startTimestamp) / SECONDS_IN_YEAR + return year + 1 +} + +// getStartHeight returns the blockchain height when emission started. +func (e *EmissionState) getStartHeight() int64 { + return e.startHeight +} + +// getStartTimestamp returns the timestamp when emission started. +func (e *EmissionState) getStartTimestamp() int64 { + return e.startTimestamp +} + +// getEndTimestamp returns the timestamp when emission ends. +func (e *EmissionState) getEndTimestamp() int64 { + return e.endTimestamp +} + +// getHalvingData returns the halving data containing emission schedule details. +func (e *EmissionState) getHalvingData() *HalvingData { + return e.halvingData +} + +// getHalvingYearStartTimestamp returns the start timestamp for the specified halving year. +func (e *EmissionState) getHalvingYearStartTimestamp(year int64) int64 { + return e.halvingData.getStartTimestamp(year) +} + +// getHalvingYearEndTimestamp returns the end timestamp for the specified halving year. +func (e *EmissionState) getHalvingYearEndTimestamp(year int64) int64 { + return e.halvingData.getEndTimestamp(year) +} + +// getHalvingYearAmountPerSecond returns the emission rate per second for the specified halving year. +func (e *EmissionState) getHalvingYearAmountPerSecond(year int64) int64 { + return e.halvingData.getAmountPerSecond(year) +} + +// getHalvingYearAccumulatedAmount returns the accumulated emission amount for the specified halving year. +func (e *EmissionState) getHalvingYearAccumulatedAmount(year int64) int64 { + return e.halvingData.getAccumAmount(year) +} + +// getHalvingYearLeftAmount returns the remaining emission amount for the specified halving year. +func (e *EmissionState) getHalvingYearLeftAmount(year int64) int64 { + return e.halvingData.getLeftAmount(year) +} + +// addHalvingYearAccumulatedAmount adds to the accumulated emission amount for the specified halving year. +// Returns error if year is invalid (0 or outside 1-12 range). +func (e *EmissionState) addHalvingYearAccumulatedAmount(year int64, amount int64) error { + if year == 0 { + return makeErrorWithDetails(errInvalidYear, ufmt.Sprintf("year: %d", year)) + } + + accumulatedAmount := e.halvingData.getAccumAmount(year) + accumulatedAmount += amount + + e.halvingData.setAccumAmount(year, accumulatedAmount) + return nil +} + +// subHalvingYearLeftAmount subtracts from the remaining emission amount for the specified halving year. +// Returns error if year is invalid (0 or outside 1-12 range). +func (e *EmissionState) subHalvingYearLeftAmount(year int64, amount int64) error { + if year == 0 { + return makeErrorWithDetails(errInvalidYear, ufmt.Sprintf("year: %d", year)) + } + + leftAmount := e.halvingData.getLeftAmount(year) + leftAmount -= amount + + e.halvingData.setLeftAmount(year, leftAmount) + return nil +} + +// updateHalvingData initializes halving data with the given start timestamp. +func (e *EmissionState) updateHalvingData(startTimestamp int64) { + e.halvingData = NewHalvingData(startTimestamp) +} + +// NewEmissionState creates a new EmissionState with specified start height and timestamp. +// Calculates emission end time based on 12-year schedule and initializes halving data. +func NewEmissionState(startHeight int64, startTimestamp int64) *EmissionState { + emissionEndTime := startTimestamp + SECONDS_IN_YEAR*HALVING_END_YEAR - 1 + + return &EmissionState{ + startHeight: startHeight, + startTimestamp: startTimestamp, + endTimestamp: emissionEndTime, + halvingData: NewHalvingData(startTimestamp), + } +} + +// getEmissionState returns the singleton emission state instance. +// Creates a new instance with current height and timestamp if one doesn't exist. +func getEmissionState() *EmissionState { + if emissionState == nil { + emissionState = NewEmissionState(std.ChainHeight(), time.Now().Unix()) + } + + return emissionState +} diff --git a/contract/r/gnoswap/gns/errors.gno b/contract/r/gnoswap/gns/errors.gno new file mode 100644 index 0000000..4ebb218 --- /dev/null +++ b/contract/r/gnoswap/gns/errors.gno @@ -0,0 +1,18 @@ +package gns + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errInvalidYear = errors.New("[GNOSWAP-GNS-001] invalid year") + errTooManyEmission = errors.New("[GNOSWAP-GNS-002] too many emission reward") + errEmissionChangeIsNilCallback = errors.New("[GNOSWAP-GNS-003] callback emission change is nil") + errInvalidAvgBlockTimeInMs = errors.New("[GNOSWAP-GNS-004] invalid avg block time in ms") +) + +func makeErrorWithDetails(err error, details string) error { + return ufmt.Errorf("%s || %s", err.Error(), details) +} diff --git a/contract/r/gnoswap/gns/getter.gno b/contract/r/gnoswap/gns/getter.gno new file mode 100644 index 0000000..5b7aae8 --- /dev/null +++ b/contract/r/gnoswap/gns/getter.gno @@ -0,0 +1,185 @@ +package gns + +import ( + "std" + "strconv" + "time" + + "gno.land/p/onbloc/json" +) + +// IsEmissionInitialized returns true if emission schedule has been initialized. +func IsEmissionInitialized() bool { + return getEmissionState().isInitialized() +} + +// IsEmissionActive returns true if emission is currently active based on current time. +func IsEmissionActive() bool { + return getEmissionState().isActive(time.Now().Unix()) +} + +// IsEmissionEnded returns true if emission schedule has completed. +func IsEmissionEnded() bool { + return getEmissionState().isEnded(time.Now().Unix()) +} + +// GetHalvingYear returns the halving year (1-12) for a given timestamp. +func GetHalvingYear(timestamp int64) int64 { + return getEmissionState().getCurrentYear(timestamp) +} + +// GetCurrentYear returns the current halving year (1-12) or 0 if emission is not active. +func GetCurrentYear() int64 { + return getEmissionState().getCurrentYear(time.Now().Unix()) +} + +// GetEmissionAmountPerSecondInRange returns halving timestamps and emission rates for the given time range. +// Returns two slices: timestamps when halving periods start and corresponding emission rates per second. +func GetEmissionAmountPerSecondInRange(fromTime, toTime int64) ([]int64, []int64) { + halvingData := getEmissionState().getHalvingData() + halvingTimes := make([]int64, 0) + halvingEmissions := make([]int64, 0) + + for year := HALVING_START_YEAR; year <= HALVING_END_YEAR; year++ { + startTimestamp := halvingData.getStartTimestamp(year) + if startTimestamp < fromTime { + continue + } + + if toTime < startTimestamp { + break + } + + halvingTimes = append(halvingTimes, startTimestamp) + halvingEmissions = append(halvingEmissions, halvingData.getAmountPerSecond(year)) + } + + return halvingTimes, halvingEmissions +} + +// GetEmissionAmountPerSecondByTimestamp returns the emission rate per second for a given timestamp. +// Returns 0 if timestamp is outside emission period. +func GetEmissionAmountPerSecondByTimestamp(timestamp int64) int64 { + year := getEmissionState().getCurrentYear(timestamp) + return getEmissionState().getHalvingYearAmountPerSecond(year) +} + +// GetEmissionLeftAmountByTimestamp returns the remaining emission amount for the halving year at given timestamp. +// Returns 0 if timestamp is outside emission period. +func GetEmissionLeftAmountByTimestamp(timestamp int64) int64 { + year := getEmissionState().getCurrentYear(timestamp) + return getEmissionState().getHalvingYearLeftAmount(year) +} + +// GetEmissionAccumulatedAmountByTimestamp returns the accumulated emission amount for the halving year at given timestamp. +// Returns 0 if timestamp is outside emission period. +func GetEmissionAccumulatedAmountByTimestamp(timestamp int64) int64 { + year := getEmissionState().getCurrentYear(timestamp) + return getEmissionState().getHalvingYearAccumulatedAmount(year) +} + +// GetHalvingYearStartTimestamp returns the start timestamp for the specified halving year. +func GetHalvingYearStartTimestamp(year int64) int64 { + halvingData := getEmissionState().getHalvingData() + return halvingData.getStartTimestamp(year) +} + +// GetHalvingYearEndTimestamp returns the end timestamp for the specified halving year. +func GetHalvingYearEndTimestamp(year int64) int64 { + halvingData := getEmissionState().getHalvingData() + return halvingData.getEndTimestamp(year) +} + +// GetHalvingYearTimestamp returns the start timestamp for the specified halving year. +func GetHalvingYearTimestamp(year int64) int64 { + halvingData := getEmissionState().getHalvingData() + return halvingData.getStartTimestamp(year) +} + +// GetHalvingYearMaxAmount returns the maximum token issuance for the specified halving year. +func GetHalvingYearMaxAmount(year int64) int64 { + halvingData := getEmissionState().getHalvingData() + return halvingData.getMaxAmount(year) +} + +// GetHalvingYearMintAmount returns the amount of tokens minted for the specified halving year. +func GetHalvingYearMintAmount(year int64) int64 { + halvingData := getEmissionState().getHalvingData() + return halvingData.getMintedAmount(year) +} + +// GetHalvingYearLeftAmount returns the remaining token issuance for the specified halving year. +func GetHalvingYearLeftAmount(year int64) int64 { + halvingData := getEmissionState().getHalvingData() + return halvingData.getLeftAmount(year) +} + +// GetHalvingYearAccuAmount returns the accumulated token issuance for the specified halving year. +func GetHalvingYearAccuAmount(year int64) int64 { + halvingData := getEmissionState().getHalvingData() + return halvingData.getAccumAmount(year) +} + +// GetAmountPerSecondPerHalvingYear returns the emission rate per second for the specified halving year. +func GetAmountPerSecondPerHalvingYear(year int64) int64 { + halvingData := getEmissionState().getHalvingData() + return halvingData.getAmountPerSecond(year) +} + +// GetHalvingAmountsPerYear returns the total emission amount allocated for the specified year. +func GetHalvingAmountsPerYear(year int64) int64 { + return halvingAmountsPerYear[year-1] +} + +// GetEmissionStartTimestamp returns the timestamp when emission schedule begins. +func GetEmissionStartTimestamp() int64 { + return getEmissionState().getStartTimestamp() +} + +// GetEmissionEndTimestamp returns the timestamp when emission schedule ends. +func GetEmissionEndTimestamp() int64 { + return getEmissionState().getEndTimestamp() +} + +// GetHalvingYearInfo returns the halving year, start timestamp, and end timestamp for a given timestamp. +// Returns (year, startTimestamp, endTimestamp). Year is 0 if outside emission period. +func GetHalvingYearInfo(timestamp int64) (int64, int64, int64) { + state := getEmissionState() + + endTimestamp := state.getEndTimestamp() + startTimestamp := state.getStartTimestamp() + + year := getEmissionState().getCurrentYear(timestamp) + + return year, startTimestamp + (SECONDS_IN_YEAR * year), endTimestamp +} + +// GetHalvingInfo returns comprehensive halving information as JSON string. +// Includes current height, timestamp, and details for all halving years (1-12). Panics on JSON marshal error. +func GetHalvingInfo() string { + currentTime := time.Now().Unix() + currentHeight := std.ChainHeight() + + halvings := make([]*json.Node, 0) + + for year := HALVING_START_YEAR; year <= HALVING_END_YEAR; year++ { + halvings = append(halvings, json.ObjectNode("", map[string]*json.Node{ + "year": json.StringNode("year", strconv.FormatInt(year, 10)), + "timestamp": json.NumberNode("timestamp", float64(GetHalvingYearTimestamp(year))), + "amount": json.NumberNode("amount", float64(GetAmountPerSecondPerHalvingYear(year))), + })) + } + + node := json.ObjectNode("", map[string]*json.Node{ + "height": json.NumberNode("height", float64(currentHeight)), + "currentTime": json.NumberNode("timestamp", float64(currentTime)), + "halvings": json.ArrayNode("", halvings), + }) + + b, err := json.Marshal(node) + if err != nil { + panic(err.Error()) + } + + return string(b) +} diff --git a/contract/r/gnoswap/gns/gnomod.toml b/contract/r/gnoswap/gns/gnomod.toml new file mode 100644 index 0000000..c2d2ffa --- /dev/null +++ b/contract/r/gnoswap/gns/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/gns" +gno = "0.9" diff --git a/contract/r/gnoswap/gns/gns.gno b/contract/r/gnoswap/gns/gns.gno new file mode 100644 index 0000000..f72f870 --- /dev/null +++ b/contract/r/gnoswap/gns/gns.gno @@ -0,0 +1,309 @@ +package gns + +import ( + "std" + "strings" + "time" + + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" + + "gno.land/r/demo/defi/grc20reg" + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" +) + +const ( + MAXIMUM_SUPPLY = int64(1_000_000_000_000_000) + INITIAL_MINT_AMOUNT = int64(100_000_000_000_000) + MAX_EMISSION_AMOUNT = int64(900_000_000_000_000) // MAXIMUM_SUPPLY - INITIAL_MINT_AMOUNT +) + +var ( + adminAddr = getAdminAddress() + token, privateLedger = grc20.NewToken("Gnoswap", "GNS", 6) + UserTeller = token.CallerTeller() + owner = ownable.NewWithAddress(adminAddr) + + leftEmissionAmount int64 // amount of GNS can be minted for emission + mintedEmissionAmount int64 // amount of GNS that has been minted for emission + lastMintedTimestamp int64 // last block time that gns was minted for emission + + burnAmount int64 // amount of GNS that has been burned +) + +func init() { + privateLedger.Mint(owner.Owner(), INITIAL_MINT_AMOUNT) + grc20reg.Register(cross, token, "") + + // Initial amount set to 900_000_000_000_000 (MAXIMUM_SUPPLY - INITIAL_MINT_AMOUNT). + // leftEmissionAmount will decrease as tokens are minted. + setLeftEmissionAmount(MAX_EMISSION_AMOUNT) + setMintedEmissionAmount(0) + setLastMintedTimestamp(std.ChainHeight()) + burnAmount = 0 +} + +// Name returns the name of the GNS token. +func Name() string { return token.GetName() } + +// Symbol returns the symbol of the GNS token. +func Symbol() string { return token.GetSymbol() } + +// Decimals returns the number of decimal places for GNS token. +func Decimals() int { return token.GetDecimals() } + +// TotalSupply returns the total supply of GNS tokens in circulation. +func TotalSupply() int64 { return token.TotalSupply() } + +// KnownAccounts returns the number of addresses that have held GNS. +func KnownAccounts() int { return token.KnownAccounts() } + +// BalanceOf returns the GNS balance of a specific address. +func BalanceOf(owner std.Address) int64 { return token.BalanceOf(owner) } + +// Allowance returns the amount of GNS that a spender is allowed to transfer from an owner. +func Allowance(owner, spender std.Address) int64 { return token.Allowance(owner, spender) } + +// MintGns mints new GNS tokens according to the emission schedule. +// +// Parameters: +// - address: recipient address for minted tokens +// +// Returns amount minted. +// Only callable by emission contract. +func MintGns(cur realm, address std.Address) int64 { + halt.AssertIsNotHaltedEmission() + + caller := std.PreviousRealm().Address() + access.AssertIsEmission(caller) + + lastGNSMintedTimestamp := LastMintedTimestamp() + currentTime := time.Now().Unix() + + // Skip if already minted this timestamp or emission ended. + if lastGNSMintedTimestamp == currentTime || lastGNSMintedTimestamp >= GetEmissionEndTimestamp() { + return 0 + } + + amountToMint := calculateAmountToMint(lastGNSMintedTimestamp+1, currentTime) + err := validEmissionAmount(amountToMint) + if err != nil { + panic(err) + } + + setLastMintedTimestamp(currentTime) + setMintedEmissionAmount(MintedEmissionAmount() + amountToMint) + setLeftEmissionAmount(LeftEmissionAmount() - amountToMint) + + err = privateLedger.Mint(address, amountToMint) + if err != nil { + panic(err.Error()) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "MintGNS", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "mintedBlockTime", formatInt(currentTime), + "mintedGNSAmount", formatInt(amountToMint), + "accumMintedGNSAmount", formatInt(MintedEmissionAmount()), + "accumLeftMintGNSAmount", formatInt(LeftEmissionAmount()), + ) + + return amountToMint +} + +// Burn permanently removes GNS tokens from circulation. +// +// Parameters: +// - from: address to burn tokens from +// - amount: amount to burn +// +// Only callable by contract owner. +func Burn(cur realm, from std.Address, amount int64) { + assertAddressIsPreviousRealm(from) + + checkErr(privateLedger.Burn(from, amount)) + + burnAmount += amount + + previousRealm := std.PreviousRealm() + std.Emit( + "Burn", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "burnedBlockHeight", formatInt(std.ChainHeight()), + "burnFrom", from.String(), + "burnedGNSAmount", formatInt(amount), + "accumBurnedGNSAmount", formatInt(BurnAmount()), + ) +} + +// Transfer transfers GNS tokens from caller to recipient. +// +// Parameters: +// - to: recipient address +// - amount: amount to transfer +func Transfer(cur realm, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Transfer(to, amount)) +} + +// Approve allows spender to transfer GNS tokens from caller's account. +// +// Parameters: +// - spender: address authorized to spend +// - amount: maximum amount spender can transfer +func Approve(cur realm, spender std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Approve(spender, amount)) +} + +// TransferFrom transfers GNS tokens on behalf of owner. +// +// Parameters: +// - from: token owner address +// - to: recipient address +// - amount: amount to transfer +func TransferFrom(cur realm, from, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.TransferFrom(from, to, amount)) +} + +// Render returns token information for web interface. +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return token.RenderHome() + case c == 2 && parts[0] == "balance": + owner := std.Address(parts[1]) + balance := token.BalanceOf(owner) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +// checkErr panics if error is not nil. +func checkErr(err error) { + if err != nil { + panic(err.Error()) + } +} + +// calculateAmountToMint calculates GNS tokens to mint for given timestamp range. +func calculateAmountToMint(fromTimestamp, toTimestamp int64) int64 { + endTimestamp := GetEmissionEndTimestamp() + if toTimestamp > endTimestamp { + toTimestamp = endTimestamp + } + + if fromTimestamp > toTimestamp { + return 0 + } + + startTimestamp := getEmissionState().getStartTimestamp() + if fromTimestamp < startTimestamp { + fromTimestamp = startTimestamp + } + + if toTimestamp < startTimestamp { + return 0 + } + + fromYear := getEmissionState().getCurrentYear(fromTimestamp) + toYear := getEmissionState().getCurrentYear(toTimestamp) + + if fromYear == 0 || toYear == 0 { + return 0 + } + + totalAmountToMint := int64(0) + + for year := fromYear; year <= toYear; year++ { + yearEndTimestamp := GetHalvingYearEndTimestamp(year) + currentToTimestamp := i64Min(toTimestamp, yearEndTimestamp) + + seconds := currentToTimestamp - fromTimestamp + 1 + if seconds <= 0 { + break + } + + amountPerSecond := GetAmountPerSecondPerHalvingYear(year) + + yearAmountToMint := amountPerSecond * seconds + + if currentToTimestamp >= yearEndTimestamp { + leftover := handleLeftEmissionAmount(year, yearAmountToMint) + yearAmountToMint += leftover + } + + totalAmountToMint += yearAmountToMint + + getEmissionState().addHalvingYearAccumulatedAmount(year, yearAmountToMint) + getEmissionState().subHalvingYearLeftAmount(year, yearAmountToMint) + + std.Emit( + "CalculateAmountToMint", + "fromTimestamp", formatInt(fromTimestamp), + "toTimestamp", formatInt(currentToTimestamp), + "year", formatInt(year), + "amountPerSecond", formatInt(amountPerSecond), + ) + + fromTimestamp = currentToTimestamp + 1 + + if fromTimestamp > toTimestamp { + break + } + } + + return totalAmountToMint +} + +// isLastSecondOfHalvingYear returns true if timestamp is the last second of a halving year. +func isLastSecondOfHalvingYear(timestamp int64) bool { + year := getEmissionState().getCurrentYear(timestamp) + lastSecond := GetHalvingYearEndTimestamp(year) + + return timestamp == lastSecond +} + +// handleLeftEmissionAmount returns the remaining emission amount for a halving year. +func handleLeftEmissionAmount(year, amount int64) int64 { + return GetHalvingYearLeftAmount(year) - amount +} + +// skipIfSameHeight returns true if last minted height equals current height, +// preventing multiple mints in the same block. +func skipIfSameHeight(lastMintedHeight, currentHeight int64) bool { + return lastMintedHeight == currentHeight +} + +// BurnAmount returns the total amount of GNS tokens burned. +func BurnAmount() int64 { return burnAmount } + +// LastMintedTimestamp returns the timestamp of the last GNS emission mint. +func LastMintedTimestamp() int64 { return lastMintedTimestamp } + +// LeftEmissionAmount returns the remaining GNS tokens available for emission. +func LeftEmissionAmount() int64 { return leftEmissionAmount } + +// MintedEmissionAmount returns the total GNS tokens minted through emission, +// excluding the initial mint amount. +func MintedEmissionAmount() int64 { return mintedEmissionAmount } + +// setLastMintedTimestamp sets the timestamp of the last emission mint. +func setLastMintedTimestamp(timestamp int64) { lastMintedTimestamp = timestamp } + +// setLeftEmissionAmount sets the remaining emission amount. +func setLeftEmissionAmount(amount int64) { leftEmissionAmount = amount } + +// setMintedEmissionAmount sets the total minted emission amount. +func setMintedEmissionAmount(amount int64) { mintedEmissionAmount = amount } diff --git a/contract/r/gnoswap/gns/gns_emission.gno b/contract/r/gnoswap/gns/gns_emission.gno new file mode 100644 index 0000000..3e41c4c --- /dev/null +++ b/contract/r/gnoswap/gns/gns_emission.gno @@ -0,0 +1,28 @@ +package gns + +import ( + "std" + + "gno.land/r/gnoswap/access" +) + +// InitEmissionState initializes emission schedule with start timestamp. +// Only callable by emission contract. Sets up 12-year emission schedule +// with halving every 2 years. Panics if caller is not emission contract. +func InitEmissionState(cur realm, height int64, timestamp int64) { + caller := std.PreviousRealm().Address() + access.AssertIsEmission(caller) + + emissionState = NewEmissionState(height, timestamp) + + previousRealm := std.PreviousRealm() + std.Emit( + "InitEmissionState", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "height", formatInt(height), + "timestamp", formatInt(timestamp), + "startTimestamp", formatInt(emissionState.getStartTimestamp()), + "endTimestamp", formatInt(emissionState.getEndTimestamp()), + ) +} diff --git a/contract/r/gnoswap/gns/halving.gno b/contract/r/gnoswap/gns/halving.gno new file mode 100644 index 0000000..593a464 --- /dev/null +++ b/contract/r/gnoswap/gns/halving.gno @@ -0,0 +1,205 @@ +package gns + +// HalvingData stores emission data for each halving period. +// Contains timestamps, amounts, and rates for the 12-year emission schedule. +type HalvingData struct { + startTimestamps []int64 + endTimestamps []int64 + maxAmount []int64 + mintedAmount []int64 + leftAmount []int64 + accumAmount []int64 + amountPerSecond []int64 +} + +// getStartTimestamp returns the start timestamp for the specified halving year. +// Returns 0 if year is invalid. +func (h *HalvingData) getStartTimestamp(year int64) int64 { + if validYear(year) != nil { + return 0 + } + + return h.startTimestamps[year-1] +} + +// getEndTimestamp returns the end timestamp for the specified halving year. +// Returns 0 if year is invalid. +func (h *HalvingData) getEndTimestamp(year int64) int64 { + if validYear(year) != nil { + return 0 + } + + return h.endTimestamps[year-1] +} + +// getMaxAmount returns the maximum emission amount for the specified halving year. +// Returns 0 if year is invalid. +func (h *HalvingData) getMaxAmount(year int64) int64 { + if validYear(year) != nil { + return 0 + } + + return h.maxAmount[year-1] +} + +// getMintedAmount returns the amount already minted for the specified halving year. +func (h *HalvingData) getMintedAmount(year int64) int64 { + if validYear(year) != nil { + return 0 + } + return h.mintedAmount[year-1] +} + +// getAccumAmount returns the accumulated emission amount for the specified halving year. +func (h *HalvingData) getAccumAmount(year int64) int64 { + if validYear(year) != nil { + return 0 + } + return h.accumAmount[year-1] +} + +// getLeftAmount returns the remaining emission amount for the specified halving year. +func (h *HalvingData) getLeftAmount(year int64) int64 { + if validYear(year) != nil { + return 0 + } + return h.leftAmount[year-1] +} + +// getAmountPerSecond returns the emission rate per second for the specified halving year. +// Returns 0 if year is invalid. +func (h *HalvingData) getAmountPerSecond(year int64) int64 { + if validYear(year) != nil { + return 0 + } + return h.amountPerSecond[year-1] +} + +// setStartTimestamp sets the start timestamp for the specified halving year. +// Returns error if year is invalid. +func (h *HalvingData) setStartTimestamp(year int64, timestamp int64) error { + err := validYear(year) + if err != nil { + return err + } + + h.startTimestamps[year-1] = timestamp + + return nil +} + +// setEndTimestamp sets the end timestamp for the specified halving year. +func (h *HalvingData) setEndTimestamp(year int64, timestamp int64) error { + err := validYear(year) + if err != nil { + return err + } + + h.endTimestamps[year-1] = timestamp + + return nil +} + +// setMaxAmount sets the maximum emission amount for the specified halving year. +func (h *HalvingData) setMaxAmount(year, amount int64) error { + err := validYear(year) + if err != nil { + return err + } + + h.maxAmount[year-1] = amount + + return nil +} + +// setMintedAmount sets the minted amount for the specified halving year. +func (h *HalvingData) setMintedAmount(year, amount int64) error { + err := validYear(year) + if err != nil { + return err + } + + h.mintedAmount[year-1] = amount + + return nil +} + +// setAccumAmount sets the accumulated amount for the specified halving year. +func (h *HalvingData) setAccumAmount(year, amount int64) error { + err := validYear(year) + if err != nil { + return err + } + + h.accumAmount[year-1] = amount + + return nil +} + +// setLeftAmount sets the remaining amount for the specified halving year. +func (h *HalvingData) setLeftAmount(year, amount int64) error { + err := validYear(year) + if err != nil { + return err + } + + h.leftAmount[year-1] = amount + + return nil +} + +// addAccumAmount adds to the accumulated amount for the specified halving year. +func (h *HalvingData) addAccumAmount(year, amount int64) error { + err := validYear(year) + if err != nil { + return err + } + + h.accumAmount[year-1] += amount + + return nil +} + +// setAmountPerSecond sets the emission rate per second for the specified halving year. +// Returns error if year is invalid. +func (h *HalvingData) setAmountPerSecond(year, amount int64) error { + err := validYear(year) + if err != nil { + return err + } + + h.amountPerSecond[year-1] = amount + + return nil +} + +// NewHalvingData creates a new HalvingData instance with emission schedule. +// Initializes 12 years of halving periods with timestamps, amounts, and rates based on startTimestamp. +func NewHalvingData(startTimestamp int64) *HalvingData { + halvingData := &HalvingData{ + startTimestamps: make([]int64, HALVING_END_YEAR), + endTimestamps: make([]int64, HALVING_END_YEAR), + maxAmount: make([]int64, HALVING_END_YEAR), + mintedAmount: make([]int64, HALVING_END_YEAR), + leftAmount: make([]int64, HALVING_END_YEAR), + accumAmount: make([]int64, HALVING_END_YEAR), + amountPerSecond: make([]int64, HALVING_END_YEAR), + } + + for year := HALVING_START_YEAR; year <= HALVING_END_YEAR; year++ { + yearStartTimestamp := startTimestamp + (SECONDS_IN_YEAR * (year - 1)) + yearEndTimestamp := yearStartTimestamp + SECONDS_IN_YEAR - 1 + yearDistributionAmount := GetHalvingAmountsPerYear(year) + yearAmountPerSecond := yearDistributionAmount / SECONDS_IN_YEAR + + halvingData.setStartTimestamp(year, yearStartTimestamp) + halvingData.setEndTimestamp(year, yearEndTimestamp) + halvingData.setMaxAmount(year, yearDistributionAmount) + halvingData.setMintedAmount(year, 0) + halvingData.setAccumAmount(year, 0) + halvingData.setAmountPerSecond(year, yearAmountPerSecond) + halvingData.setLeftAmount(year, yearDistributionAmount) + } + + return halvingData +} diff --git a/contract/r/gnoswap/gns/utils.gno b/contract/r/gnoswap/gns/utils.gno new file mode 100644 index 0000000..5557175 --- /dev/null +++ b/contract/r/gnoswap/gns/utils.gno @@ -0,0 +1,91 @@ +package gns + +import ( + "std" + "strconv" + + "gno.land/p/nt/ufmt" + + prabc "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/rbac" +) + +// validBlockTime validates that block time is within acceptable range. +// Returns error if block time is <= 0 or >= 1e9. +func validBlockTime(blockTime int64) error { + if blockTime <= 0 || blockTime >= 1e9 { + return errInvalidAvgBlockTimeInMs + } + + return nil +} + +// validYear validates that year is within halving period range (1-12). +// Returns error if year is outside the valid range. +func validYear(year int64) error { + if year < HALVING_START_YEAR || year > HALVING_END_YEAR { + return makeErrorWithDetails(errInvalidYear, ufmt.Sprintf("year: %d", year)) + } + + return nil +} + +// validEmissionAmount validates that the emission amount does not exceed maximum. +// Returns error if minting the amount would exceed MAX_EMISSION_AMOUNT. +func validEmissionAmount(amount int64) error { + if (amount + MintedEmissionAmount()) > MAX_EMISSION_AMOUNT { + return ufmt.Errorf("too many emission amount: %d", amount) + } + + return nil +} + +// getAdminAddress returns the admin address from access control or default role address. +func getAdminAddress() std.Address { + addr, exists := access.GetAddress(prabc.ROLE_ADMIN.String()) + if !exists { + return rbac.DefaultRoleAddresses[prabc.ROLE_ADMIN] + } + + return addr +} + +// i64Min returns the smaller of two int64 values. +func i64Min(x, y int64) int64 { + if x < y { + return x + } + return y +} + +// formatUint formats unsigned integer types to string. +// Supports uint8, uint32, and uint64. Panics for unsupported types. +func formatUint(v any) string { + switch v := v.(type) { + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} + +// formatInt formats signed integer types to string. +// Supports int32, int64, and int. Panics for unsupported types. +func formatInt(v any) string { + switch v := v.(type) { + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case int: + return strconv.Itoa(v) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} diff --git a/contract/r/gnoswap/halt/README.md b/contract/r/gnoswap/halt/README.md new file mode 100644 index 0000000..20e425b --- /dev/null +++ b/contract/r/gnoswap/halt/README.md @@ -0,0 +1,92 @@ +# Halt + +Emergency pause mechanism for protocol safety. + +## Overview + +Halt system provides granular control over protocol operations for emergency response and beta safety mode. + +## Configuration + +### Halt Levels + +- **NONE**: All operations enabled (normal operation) +- **SAFE_MODE**: All operations enabled except withdrawals (beta mainnet default) +- **EMERGENCY**: Only governance and withdrawal operations enabled (crisis response) +- **COMPLETE**: All operations disabled (full system halt) + +### Controllable Operations (OpTypes) + +- **pool**: Pool creation and liquidity operations +- **position**: Position NFT minting and management +- **protocol_fee**: Fee collection and distribution +- **router**: Swap routing and execution +- **staker**: Liquidity staking and rewards +- **launchpad**: Token distribution projects +- **governance**: Proposal creation and voting +- **gov_staker**: GNS staking for xGNS +- **xgns**: xGNS token operations +- **community_pool**: Treasury management +- **emission**: GNS emission and distribution +- **withdraw**: Withdrawal operations (LP, rewards, etc.) + +## Key Functions + +### `SetHaltLevel` +Sets system-wide halt level. + +### `SetOperationStatus` +Controls individual operation types. + +### `IsHalted` +Checks if operation is halted. + +## Usage + +```go +// Set system to safe mode (beta mainnet) +SetHaltLevel(HaltLevelSafeMode) + +// Enable emergency mode +SetHaltLevel(HaltLevelEmergency) + +// Halt specific operation +SetOperationStatus(OpTypeRouter, true) + +// Resume specific operation +SetOperationStatus(OpTypeRouter, false) + +// Check before operation +if IsHalted(OpTypeWithdraw) { + panic("withdrawals halted") +} +``` + +## Halt Level Behaviors + +### NONE (Normal Operation) +- All contracts fully operational +- No restrictions applied + +### SAFE_MODE (Beta Mainnet) +- All operations enabled except withdrawals +- Used during initial mainnet launch +- Allows trading but prevents fund extraction + +### EMERGENCY (Crisis Response) +- Only governance and withdrawals enabled +- Allows users to exit positions +- Governance can still execute proposals + +### COMPLETE (Full Halt) +- All operations disabled +- Complete system freeze +- Recovery requires admin/governance action + +## Security + +- Admin/governance control only +- Beta mainnet starts in SAFE_MODE +- Granular operation control +- Event emission for transparency +- Emergency response capability \ No newline at end of file diff --git a/contract/r/gnoswap/halt/assert.gno b/contract/r/gnoswap/halt/assert.gno new file mode 100644 index 0000000..da83f44 --- /dev/null +++ b/contract/r/gnoswap/halt/assert.gno @@ -0,0 +1,82 @@ +package halt + +// AssertIsNotHalted panics if any of the specified operation types are halted. +// Panics with error if operation type is invalid or any operation is halted. +func AssertIsNotHalted(opTypes ...OpType) { + halted, err := IsHalted(opTypes...) + if err != nil { + panic(err) + } + + if halted { + panic(halted) + } +} + +// AssertIsNotHaltedPool panics if pool operations are halted. +func AssertIsNotHaltedPool() { + AssertIsNotHaltedOperation(OpTypePool) +} + +// AssertIsNotHaltedPosition panics if position operations are halted. +func AssertIsNotHaltedPosition() { + AssertIsNotHaltedOperation(OpTypePosition) +} + +// AssertIsNotHaltedProtocolFee panics if protocol fee operations are halted. +func AssertIsNotHaltedProtocolFee() { + AssertIsNotHaltedOperation(OpTypeProtocolFee) +} + +// AssertIsNotHaltedRouter panics if router operations are halted. +func AssertIsNotHaltedRouter() { + AssertIsNotHaltedOperation(OpTypeRouter) +} + +// AssertIsNotHaltedStaker panics if staker operations are halted. +func AssertIsNotHaltedStaker() { + AssertIsNotHaltedOperation(OpTypeStaker) +} + +// AssertIsNotHaltedLaunchpad panics if launchpad operations are halted. +func AssertIsNotHaltedLaunchpad() { + AssertIsNotHaltedOperation(OpTypeLaunchpad) +} + +// AssertIsNotHaltedGovernance panics if governance operations are halted. +func AssertIsNotHaltedGovernance() { + AssertIsNotHaltedOperation(OpTypeGovernance) +} + +// AssertIsNotHaltedGovStaker panics if governance staker operations are halted. +func AssertIsNotHaltedGovStaker() { + AssertIsNotHaltedOperation(OpTypeGovStaker) +} + +// AssertIsNotHaltedXGns panics if xGNS operations are halted. +func AssertIsNotHaltedXGns() { + AssertIsNotHaltedOperation(OpTypeXGns) +} + +// AssertIsNotHaltedCommunityPool panics if community pool operations are halted. +func AssertIsNotHaltedCommunityPool() { + AssertIsNotHaltedOperation(OpTypeCommunityPool) +} + +// AssertIsNotHaltedEmission panics if emission operations are halted. +func AssertIsNotHaltedEmission() { + AssertIsNotHaltedOperation(OpTypeEmission) +} + +// AssertIsNotHaltedWithdraw panics if withdraw operations are halted. +func AssertIsNotHaltedWithdraw() { + AssertIsNotHaltedOperation(OpTypeWithdraw) +} + +// AssertIsNotHaltedOperation panics if the specified operation type is halted. +// Panics with error details including operation type name. +func AssertIsNotHaltedOperation(op OpType) { + if halted := isHaltedOperation(op); halted { + panic(makeErrorWithDetails(errHalted, op.String())) + } +} diff --git a/contract/r/gnoswap/halt/config.gno b/contract/r/gnoswap/halt/config.gno new file mode 100644 index 0000000..9b52fce --- /dev/null +++ b/contract/r/gnoswap/halt/config.gno @@ -0,0 +1,122 @@ +package halt + +// HaltConfig stores halt state for each operation type. +type HaltConfig map[OpType]bool + +// IsHalted returns true if the specified operation is halted, false otherwise. +// Returns false if operation type is not found in configuration. +func (c HaltConfig) IsHalted(op OpType) bool { + halted, exists := c[op] + if !exists { + return false + } + + return halted +} + +// Clone creates a deep copy of the halt configuration and returns it. +func (c HaltConfig) Clone() HaltConfig { + clone := make(HaltConfig) + + for op, option := range c { + clone[op] = option + } + + return clone +} + +// get retrieves halt state for the specified operation type. +// Returns error if operation type is invalid. +func (c HaltConfig) get(op OpType) (bool, error) { + enabled, exists := c[op] + if !exists { + return false, makeErrorWithDetails(errInvalidOpType, op.String()) + } + + return enabled, nil +} + +// set updates halt state for the specified operation type. +// Returns error if operation type is invalid. +func (c HaltConfig) set(op OpType, enabled bool) error { + if !op.IsValid() { + return makeErrorWithDetails(errInvalidOpType, op.String()) + } + + c[op] = enabled + + return nil +} + +// newNoneConfig creates configuration with all operations enabled (no halts). +func newNoneConfig() HaltConfig { + return HaltConfig{ + OpTypePool: false, + OpTypePosition: false, + OpTypeProtocolFee: false, + OpTypeRouter: false, + OpTypeStaker: false, + OpTypeLaunchpad: false, + OpTypeGovernance: false, + OpTypeGovStaker: false, + OpTypeXGns: false, + OpTypeCommunityPool: false, + OpTypeEmission: false, + OpTypeWithdraw: false, + } +} + +// newSafeModeConfig creates configuration for safe mode with only withdrawals halted. +func newSafeModeConfig() HaltConfig { + return HaltConfig{ + OpTypePool: false, + OpTypePosition: false, + OpTypeProtocolFee: false, + OpTypeRouter: false, + OpTypeStaker: false, + OpTypeLaunchpad: false, + OpTypeGovernance: false, + OpTypeGovStaker: false, + OpTypeXGns: false, + OpTypeCommunityPool: false, + OpTypeEmission: false, + OpTypeWithdraw: true, + } +} + +// newEmergencyConfig creates configuration for emergency mode with most operations halted. +// Only governance and withdraw operations remain enabled for emergency recovery. +func newEmergencyConfig() HaltConfig { + return HaltConfig{ + OpTypePool: true, + OpTypePosition: true, + OpTypeProtocolFee: true, + OpTypeRouter: true, + OpTypeStaker: true, + OpTypeLaunchpad: true, + OpTypeGovernance: false, + OpTypeGovStaker: true, + OpTypeXGns: true, + OpTypeCommunityPool: true, + OpTypeEmission: true, + OpTypeWithdraw: false, + } +} + +// newCompleteConfig creates configuration with all operations halted for complete lockdown. +func newCompleteConfig() HaltConfig { + return HaltConfig{ + OpTypePool: true, + OpTypePosition: true, + OpTypeProtocolFee: true, + OpTypeRouter: true, + OpTypeStaker: true, + OpTypeLaunchpad: true, + OpTypeGovernance: true, + OpTypeGovStaker: true, + OpTypeXGns: true, + OpTypeCommunityPool: true, + OpTypeEmission: true, + OpTypeWithdraw: true, + } +} diff --git a/contract/r/gnoswap/halt/doc.gno b/contract/r/gnoswap/halt/doc.gno new file mode 100644 index 0000000..a3a9f4f --- /dev/null +++ b/contract/r/gnoswap/halt/doc.gno @@ -0,0 +1,2 @@ +// Package halt provides functionality for managing protocol halt levels and operations. +package halt diff --git a/contract/r/gnoswap/halt/errors.gno b/contract/r/gnoswap/halt/errors.gno new file mode 100644 index 0000000..83449dc --- /dev/null +++ b/contract/r/gnoswap/halt/errors.gno @@ -0,0 +1,18 @@ +package halt + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errHalted = errors.New("halted") + errInvalidOpType = errors.New("invalid operation type") + errInvalidHaltLevel = errors.New("invalid halt level") +) + +// makeErrorWithDetails creates an error with additional details appended to the base error message. +func makeErrorWithDetails(err error, details string) error { + return ufmt.Errorf("%s: %s", err.Error(), details) +} diff --git a/contract/r/gnoswap/halt/getters.gno b/contract/r/gnoswap/halt/getters.gno new file mode 100644 index 0000000..f68ecec --- /dev/null +++ b/contract/r/gnoswap/halt/getters.gno @@ -0,0 +1,117 @@ +package halt + +import ( + "gno.land/p/onbloc/json" +) + +// IsHalted returns true if any of the specified operation types are halted. +// Returns error if any operation type is invalid. +func IsHalted(opTypes ...OpType) (bool, error) { + for _, op := range opTypes { + if !op.IsValid() { + return true, makeErrorWithDetails(errInvalidOpType, op.String()) + } + + halted, err := haltConfig.get(op) + if err != nil { + return true, err + } + + if halted { + return true, nil + } + } + + return false, nil +} + +// GetHaltConfig returns a copy of the current halt configuration. +func GetHaltConfig() HaltConfig { + return haltConfig.Clone() +} + +// GetHaltConfigJson returns the halt configuration as a JSON string. +func GetHaltConfigJson() string { + haltConfig := GetHaltConfig() + + statusNodes := make(map[string]*json.Node) + + for op, halted := range haltConfig { + statusNodes[op.String()] = json.BoolNode(op.String(), halted) + } + + objectNode := json.ObjectNode("status", statusNodes) + + return objectNode.String() +} + +// IsHaltedPool returns true if pool operations are halted. +func IsHaltedPool() bool { + return isHaltedOperation(OpTypePool) +} + +// IsHaltedPosition returns true if position operations are halted. +func IsHaltedPosition() bool { + return isHaltedOperation(OpTypePosition) +} + +// IsHaltedProtocolFee returns true if protocol fee operations are halted. +func IsHaltedProtocolFee() bool { + return isHaltedOperation(OpTypeProtocolFee) +} + +// IsHaltedRouter returns true if router operations are halted. +func IsHaltedRouter() bool { + return isHaltedOperation(OpTypeRouter) +} + +// IsHaltedStaker returns true if staker operations are halted. +func IsHaltedStaker() bool { + return isHaltedOperation(OpTypeStaker) +} + +// IsHaltedLaunchpad returns true if launchpad operations are halted. +func IsHaltedLaunchpad() bool { + return isHaltedOperation(OpTypeLaunchpad) +} + +// IsHaltedGovernance returns true if governance operations are halted. +func IsHaltedGovernance() bool { + return isHaltedOperation(OpTypeGovernance) +} + +// IsHaltedGovStaker returns true if governance staker operations are halted. +func IsHaltedGovStaker() bool { + return isHaltedOperation(OpTypeGovStaker) +} + +// IsHaltedXGns returns true if xGNS operations are halted. +func IsHaltedXGns() bool { + return isHaltedOperation(OpTypeXGns) +} + +// IsHaltedCommunityPool returns true if community pool operations are halted. +func IsHaltedCommunityPool() bool { + return isHaltedOperation(OpTypeCommunityPool) +} + +// IsHaltedEmission returns true if emission operations are halted. +func IsHaltedEmission() bool { + return isHaltedOperation(OpTypeEmission) +} + +// IsHaltedWithdraw returns true if withdraw operations are halted. +func IsHaltedWithdraw() bool { + return isHaltedOperation(OpTypeWithdraw) +} + +// isHaltedOperation returns halt status for the specified operation type. +// Panics if operation type is invalid. +func isHaltedOperation(op OpType) bool { + halted, err := haltConfig.get(op) + if err != nil { + panic(err) + } + + return halted +} diff --git a/contract/r/gnoswap/halt/gnomod.toml b/contract/r/gnoswap/halt/gnomod.toml new file mode 100644 index 0000000..176cb8a --- /dev/null +++ b/contract/r/gnoswap/halt/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/halt" +gno = "0.9" diff --git a/contract/r/gnoswap/halt/halt.gno b/contract/r/gnoswap/halt/halt.gno new file mode 100644 index 0000000..9ef6675 --- /dev/null +++ b/contract/r/gnoswap/halt/halt.gno @@ -0,0 +1,84 @@ +package halt + +import ( + "std" + "strconv" + + "gno.land/r/gnoswap/access" +) + +var haltConfig HaltConfig + +func init() { + haltConfig = newNoneConfig() +} + +// SetHaltLevel sets the global halt level. +// +// Parameters: +// - level: halt level to apply (None, SafeMode, Emergency, Complete) +// +// Only callable by admin or governance. +func SetHaltLevel(cur realm, level HaltLevel) { + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + err := setHaltLevel(level) + if err != nil { + panic(err) + } + + std.Emit( + "SetHaltLevel", + "level", level.String(), + "description", level.Description(), + "caller", caller.String(), + ) +} + +// SetOperationStatus sets halt status for a specific operation. +// +// Parameters: +// - op: operation type +// - halted: true to halt, false to resume +// +// Only callable by admin or governance. +func SetOperationStatus(cur realm, op OpType, halted bool) { + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + if !op.IsValid() { + panic(makeErrorWithDetails(errInvalidOpType, op.String())) + } + + haltConfig.set(op, halted) + + std.Emit( + "SetOperationStatus", + "operation", string(op), + "halted", strconv.FormatBool(halted), + "caller", caller.String(), + ) +} + +// setHaltLevel applies predefined halt level configuration. +func setHaltLevel(level HaltLevel) error { + var config HaltConfig + + switch level { + case HaltLevelNone: + config = newNoneConfig() + case HaltLevelSafeMode: + config = newSafeModeConfig() + case HaltLevelEmergency: + config = newEmergencyConfig() + case HaltLevelComplete: + config = newCompleteConfig() + default: + return makeErrorWithDetails(errInvalidHaltLevel, level.String()) + } + + haltConfig = config + + return nil +} diff --git a/contract/r/gnoswap/halt/types.gno b/contract/r/gnoswap/halt/types.gno new file mode 100644 index 0000000..3a1a6b6 --- /dev/null +++ b/contract/r/gnoswap/halt/types.gno @@ -0,0 +1,89 @@ +package halt + +// Halt levels define different states of system operation restriction. +const ( + HaltLevelNone HaltLevel = "NONE" // All operations enabled. + HaltLevelSafeMode HaltLevel = "SAFE_MODE" // All operations enabled except withdrawals. + HaltLevelEmergency HaltLevel = "EMERGENCY" // Only governance and withdrawal operations enabled. + HaltLevelComplete HaltLevel = "COMPLETE" // All operations disabled. +) + +// Operation types representing individual contracts. +const ( + OpTypePool OpType = "pool" + OpTypePosition OpType = "position" + OpTypeProtocolFee OpType = "protocol_fee" + OpTypeRouter OpType = "router" + OpTypeStaker OpType = "staker" + OpTypeLaunchpad OpType = "launchpad" + OpTypeGovernance OpType = "governance" + OpTypeGovStaker OpType = "gov_staker" + OpTypeXGns OpType = "xgns" + OpTypeCommunityPool OpType = "community_pool" + OpTypeEmission OpType = "emission" + OpTypeWithdraw OpType = "withdraw" +) + +var haltLevelDescriptions = map[HaltLevel]string{ + HaltLevelNone: "All operations enabled", + HaltLevelSafeMode: "All operations enabled except withdrawals", + HaltLevelEmergency: "Only governance and withdrawal operations enabled", + HaltLevelComplete: "All operations disabled", +} + +// HaltLevel represents current system halt state. +type HaltLevel string + +// String returns the string representation of the halt level. +func (h HaltLevel) String() string { + return string(h) +} + +// Description returns a human-readable description of the halt level. +func (h HaltLevel) Description() string { + desc, ok := haltLevelDescriptions[h] + if !ok { + return "Unknown halt level" + } + + return desc +} + +// IsValid returns true if the halt level is valid. +func (h HaltLevel) IsValid() bool { + switch h { + case HaltLevelNone, HaltLevelSafeMode, HaltLevelEmergency, HaltLevelComplete: + return true + default: + return false + } +} + +// OpType represents operation types that can be controlled independently. +type OpType string + +// String returns the string representation of the operation type. +func (o OpType) String() string { + return string(o) +} + +// IsValid returns true if the operation type is valid. +func (o OpType) IsValid() bool { + switch o { + case OpTypePool, + OpTypePosition, + OpTypeProtocolFee, + OpTypeRouter, + OpTypeStaker, + OpTypeLaunchpad, + OpTypeGovernance, + OpTypeGovStaker, + OpTypeXGns, + OpTypeCommunityPool, + OpTypeEmission, + OpTypeWithdraw: + return true + default: + return false + } +} diff --git a/contract/r/gnoswap/rbac/README.md b/contract/r/gnoswap/rbac/README.md new file mode 100644 index 0000000..aa52748 --- /dev/null +++ b/contract/r/gnoswap/rbac/README.md @@ -0,0 +1,120 @@ +# RBAC + +Role-based access control management realm. + +## Overview + +RBAC realm manages role addresses and permissions for the GnoSwap protocol, integrating with the access package. + +## Configuration + +- **Admin Control**: Full role management +- **Dynamic Roles**: Add/remove at runtime +- **Access Integration**: Syncs with access package + +## Key Functions + +### `RegisterRole` + +Registers new role in system. + +### `RemoveRole` + +Removes existing role. + +### `UpdateRoleAddress` + +Updates address for role. + +### `GetRoleAddress` + +Returns address for role. + +### `TransferOwnership` + +Transfers admin role to new address. + +## Usage + +```go +// Register new role +RegisterRole("new_role") + +// Update role address +UpdateRoleAddress("staker", newAddress) + +// Get role address +addr, err := GetRoleAddress("router") + +// Transfer admin ownership +TransferOwnership(newAdmin) +``` + +## Contract Upgrade + +RBAC enables seamless contract upgrades through role address updates. Versioned contracts (with paths like `v1`) can be upgraded by deploying new versions and updating role addresses. + +### Upgrade Process + +1. **Deploy new contract version** (e.g., `v2` contracts) +2. **Update role addresses** to point to new contracts +3. **Verify distribution** flows to new contract addresses + +### Upgradeable Components + +All versioned contracts under `gno.land/r/gnoswap/{version}/` are upgradeable: + +- `pool` - Liquidity pool management +- `position` - Position management +- `router` - Swap routing engine +- `staker` - Staking and rewards +- `governance` - Governance system (governance, staker, xgns) +- `launchpad` - Token launch platform +- `protocol_fee` - Fee collection +- `community_pool` - Community treasury + +### Example: GNS Distribution Upgrade + +```go +// Before upgrade - GNS distributed to v1 contracts +mintAndDistribute() // → v1 staker, devops, community_pool + +// Upgrade process - update role addresses +rbac.UpdateRoleAddress("staker", newV2StakerAddr) +rbac.UpdateRoleAddress("devops", newV2DevOpsAddr) +rbac.UpdateRoleAddress("community_pool", newV2CommunityPoolAddr) + +// After upgrade - GNS distributed to v2 contracts +mintAndDistribute() // → v2 staker, devops, community_pool +``` + +This approach ensures zero-downtime upgrades with atomic role address switches, maintaining protocol continuity while enabling feature updates and bug fixes. + +### Test Example + +The upgrade mechanism is demonstrated in the test file: +[upgrade scenario test](./../../../../tests/scenario/upgrade/change_gns_distribution_target_filetest.gno) + +```go +// Test scenario steps: +// 1. Initialize emission and mint GNS to v1 contracts +// 2. Update role addresses to point to v2 contracts +// 3. Verify GNS now flows to v2 contracts + +func changeDistributionTarget() { + // Update all role addresses atomically + rbac.UpdateRoleAddress("staker", newStakerAddr) + rbac.UpdateRoleAddress("gov_staker", newGovStakerAddr) + rbac.UpdateRoleAddress("devops", newDevOpsAddr) + rbac.UpdateRoleAddress("community_pool", newCommunityPoolAddr) +} +``` + +The test validates that after role updates, GNS distribution switches from v1 to v2 contracts without any protocol downtime or loss of funds. + +## Security + +- Admin-only role management +- Synchronized with access package +- Ownership transfer capability +- Role validation before updates diff --git a/contract/r/gnoswap/rbac/assert.gno b/contract/r/gnoswap/rbac/assert.gno new file mode 100644 index 0000000..7d8c8a1 --- /dev/null +++ b/contract/r/gnoswap/rbac/assert.gno @@ -0,0 +1,51 @@ +package rbac + +import ( + "std" + + "gno.land/p/nt/ufmt" + + prbac "gno.land/p/gnoswap/rbac" +) + +// assertIsOwner panics if addr is not the current owner. +func assertIsOwner(addr std.Address) { + if manager.Owner() != addr { + panic(makeErrorWithDetails( + errCallerIsNotOwner, + ufmt.Sprintf("caller: %s", addr.String()), + )) + } +} + +// assertIsPendingOwner panics if addr is not the pending owner. +func assertIsPendingOwner(addr std.Address) { + if manager.PendingOwner() != addr { + panic(makeErrorWithDetails( + errCallerIsNotPendingOwner, + ufmt.Sprintf("caller: %s", addr.String()), + )) + } +} + +// assertIsAdmin panics if addr is not authorized for admin role. +func assertIsAdmin(addr std.Address) { + if !manager.IsAuthorized(prbac.ROLE_ADMIN.String(), addr) { + panic( + makeErrorWithDetails( + errCallerIsNotAdmin, + ufmt.Sprintf("caller: %s", addr.String()), + ), + ) + } +} + +// assertIsValidRoleName panics if roleName is invalid (empty). +func assertIsValidRoleName(roleName string) { + if roleName == "" { + panic(makeErrorWithDetails( + errInvalidRoleName, + ufmt.Sprintf("role name: %s", roleName), + )) + } +} diff --git a/contract/r/gnoswap/rbac/consts.gno b/contract/r/gnoswap/rbac/consts.gno new file mode 100644 index 0000000..2f4f54e --- /dev/null +++ b/contract/r/gnoswap/rbac/consts.gno @@ -0,0 +1,40 @@ +package rbac + +import "std" + +// Initial addresses for protocol roles. +const ( + // ADMIN is the initial admin address for RBAC management. + ADMIN std.Address = "g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42" + // DEV_OPS is the initial DevOps address for operational tasks. + DEV_OPS std.Address = "g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42" +) + +// Derived addresses for GnoSwap protocol packages. +var ( + // GNS_ADDR is the derived address for the GNS token package. + GNS_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/gns") + // EMISSION_ADDR is the derived address for the emission package. + EMISSION_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/emission") + + // POOL_ADDR is the derived address for the pool package. + POOL_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/pool") + // POSITION_ADDR is the derived address for the position package. + POSITION_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/position") + // ROUTER_ADDR is the derived address for the router package. + ROUTER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/router") + // STAKER_ADDR is the derived address for the staker package. + STAKER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/staker") + // PROTOCOL_FEE_ADDR is the derived address for the protocol fee package. + PROTOCOL_FEE_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/protocol_fee") + // COMMUNITY_POOL_ADDR is the derived address for the community pool package. + COMMUNITY_POOL_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/community_pool") + // GOV_GOVERNANCE_ADDR is the derived address for the governance package. + GOV_GOVERNANCE_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/governance") + // GOV_STAKER_ADDR is the derived address for the governance staker package. + GOV_STAKER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/staker") + // GOV_XGNS_ADDR is the derived address for the xGNS governance package. + GOV_XGNS_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/xgns") + // LAUNCHPAD_ADDR is the derived address for the launchpad package. + LAUNCHPAD_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/launchpad") +) diff --git a/contract/r/gnoswap/rbac/emit.gno b/contract/r/gnoswap/rbac/emit.gno new file mode 100644 index 0000000..889f24e --- /dev/null +++ b/contract/r/gnoswap/rbac/emit.gno @@ -0,0 +1,39 @@ +package rbac + +import "std" + +// emitRegisterRoleEvent emits a RegisterRole event with roleName information. +func emitRegisterRoleEvent(roleName string) { + prevRealm := std.PreviousRealm() + std.Emit( + "RegisterRole", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "roleName", roleName, + "roleAddress", "", + ) +} + +// emitRemoveRoleEvent emits a RemoveRole event with roleName information. +func emitRemoveRoleEvent(roleName string) { + prevRealm := std.PreviousRealm() + std.Emit( + "RemoveRole", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "roleName", roleName, + "roleAddress", "", + ) +} + +// emitUpdateRoleAddressEvent emits an UpdateRoleAddress event with roleName and address information. +func emitUpdateRoleAddressEvent(roleName string, address std.Address) { + prevRealm := std.PreviousRealm() + std.Emit( + "UpdateRoleAddress", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "roleName", roleName, + "roleAddress", address.String(), + ) +} diff --git a/contract/r/gnoswap/rbac/errors.gno b/contract/r/gnoswap/rbac/errors.gno new file mode 100644 index 0000000..6e9f5bb --- /dev/null +++ b/contract/r/gnoswap/rbac/errors.gno @@ -0,0 +1,20 @@ +package rbac + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errCallerIsNotOwner = errors.New("caller is not owner") + errCallerIsNotAdmin = errors.New("caller is not admin") + errCallerIsNotPendingOwner = errors.New("caller is not pending owner") + errInvalidAddress = errors.New("invalid address") + errInvalidRoleName = errors.New("invalid role name") +) + +// makeErrorWithDetails combines an error with additional details. +func makeErrorWithDetails(err error, details string) error { + return ufmt.Errorf("%s || %s", err.Error(), details) +} diff --git a/contract/r/gnoswap/rbac/gnomod.toml b/contract/r/gnoswap/rbac/gnomod.toml new file mode 100644 index 0000000..cf30f83 --- /dev/null +++ b/contract/r/gnoswap/rbac/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/rbac" +gno = "0.9" diff --git a/contract/r/gnoswap/rbac/ownership.gno b/contract/r/gnoswap/rbac/ownership.gno new file mode 100644 index 0000000..8239720 --- /dev/null +++ b/contract/r/gnoswap/rbac/ownership.gno @@ -0,0 +1,45 @@ +package rbac + +import "std" + +// IsOwner returns true if addr is the current owner. +func IsOwner(addr std.Address) bool { + return manager.Owner() == addr +} + +// IsPendingOwner returns true if addr is the pending owner. +func IsPendingOwner(addr std.Address) bool { + return manager.PendingOwner() == addr +} + +// GetOwner returns the current owner address. +func GetOwner() std.Address { + return manager.Owner() +} + +// GetPendingOwner returns the pending owner address. +func GetPendingOwner() std.Address { + return manager.PendingOwner() +} + +// AcceptOwnership completes the ownership transfer process. +// Only callable by pending owner. +func AcceptOwnership(cur realm) { + err := manager.AcceptOwnership() + if err != nil { + panic(err) + } +} + +// TransferOwnership initiates the ownership transfer process. +// +// Parameters: +// - addr: address to transfer ownership to +// +// Only callable by current owner. +func TransferOwnership(cur realm, addr std.Address) { + err := manager.TransferOwnership(addr) + if err != nil { + panic(err) + } +} diff --git a/contract/r/gnoswap/rbac/rbac.gno b/contract/r/gnoswap/rbac/rbac.gno new file mode 100644 index 0000000..b6b846f --- /dev/null +++ b/contract/r/gnoswap/rbac/rbac.gno @@ -0,0 +1,112 @@ +package rbac + +import ( + "std" + + "gno.land/r/gnoswap/access" + + "gno.land/p/nt/ufmt" + prbac "gno.land/p/gnoswap/rbac" +) + +var manager *prbac.RBAC + +func init() { + initRbac() +} + +// initRbac initializes RBAC manager with default admin and role mappings. +func initRbac() { + manager = prbac.NewRBACWithAddress(ADMIN) + + for role, addr := range DefaultRoleAddresses { + manager.RegisterRole(role.String()) + manager.UpdateRoleAddress(role.String(), addr) + } + + updateAccessRoleAddresses(manager.GetRoleAddresses()) +} + +// RegisterRole registers a new role in the RBAC system. +// +// Parameters: +// - roleName: name of the role to register +// +// Only callable by admin or governance. +func RegisterRole(cur realm, roleName string) { + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsValidRoleName(roleName) + + err := manager.RegisterRole(roleName) + if err != nil { + panic(makeErrorWithDetails( + err, + ufmt.Sprintf("role name: %s", roleName), + )) + } + + updateAccessRoleAddresses(manager.GetRoleAddresses()) + + emitRegisterRoleEvent(roleName) +} + +// RemoveRole removes a role from the RBAC system. +// +// Parameters: +// - roleName: name of the role to remove +// +// Only callable by admin or governance. +func RemoveRole(cur realm, roleName string) { + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsValidRoleName(roleName) + + err := manager.RemoveRole(roleName) + if err != nil { + panic(makeErrorWithDetails( + err, + ufmt.Sprintf("role name: %s", roleName), + )) + } + + updateAccessRoleAddresses(manager.GetRoleAddresses()) + + emitRemoveRoleEvent(roleName) +} + +// GetRoleAddress returns the address assigned to roleName. +func GetRoleAddress(roleName string) (std.Address, error) { + return manager.GetRoleAddress(roleName) +} + +// UpdateRoleAddress updates the address assigned to a role. +// +// Parameters: +// - roleName: name of the role +// - addr: new address for the role +// +// Only callable by admin or governance. +func UpdateRoleAddress(cur realm, roleName string, addr std.Address) { + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + err := manager.UpdateRoleAddress(roleName, addr) + if err != nil { + panic(makeErrorWithDetails( + err, + ufmt.Sprintf("role name: %s, address: %s", roleName, addr.String()), + )) + } + + updateAccessRoleAddresses(manager.GetRoleAddresses()) + + emitUpdateRoleAddressEvent(roleName, addr) +} + +// updateAccessRoleAddresses synchronizes role addresses with the access package. +func updateAccessRoleAddresses(newRoleAddresses map[string]std.Address) { + access.SetRoleAddresses(cross, newRoleAddresses) +} diff --git a/contract/r/gnoswap/rbac/role.gno b/contract/r/gnoswap/rbac/role.gno new file mode 100644 index 0000000..a7586d2 --- /dev/null +++ b/contract/r/gnoswap/rbac/role.gno @@ -0,0 +1,25 @@ +package rbac + +import ( + "std" + + prbac "gno.land/p/gnoswap/rbac" +) + +// DefaultRoleAddresses maps system roles to their default addresses. +// Used during RBAC initialization to set up the protocol role structure. +var DefaultRoleAddresses = map[prbac.SystemRole]std.Address{ + prbac.ROLE_ADMIN: ADMIN, + prbac.ROLE_DEVOPS: DEV_OPS, + prbac.ROLE_COMMUNITY_POOL: COMMUNITY_POOL_ADDR, + prbac.ROLE_GOVERNANCE: GOV_GOVERNANCE_ADDR, + prbac.ROLE_GOV_STAKER: GOV_STAKER_ADDR, + prbac.ROLE_XGNS: GOV_XGNS_ADDR, + prbac.ROLE_POOL: POOL_ADDR, + prbac.ROLE_POSITION: POSITION_ADDR, + prbac.ROLE_ROUTER: ROUTER_ADDR, + prbac.ROLE_STAKER: STAKER_ADDR, + prbac.ROLE_EMISSION: EMISSION_ADDR, + prbac.ROLE_LAUNCHPAD: LAUNCHPAD_ADDR, + prbac.ROLE_PROTOCOL_FEE: PROTOCOL_FEE_ADDR, +} diff --git a/contract/r/gnoswap/referral/README.md b/contract/r/gnoswap/referral/README.md new file mode 100644 index 0000000..124415a --- /dev/null +++ b/contract/r/gnoswap/referral/README.md @@ -0,0 +1,47 @@ +# Referral + +Referral system for tracking user relationships. + +## Overview + +Manages referral relationships between users with cooldown periods to prevent gaming. + +## Key Functions + +### `TryRegister` +Attempts to register referral relationship. + +### `Register` +Registers new referral (panics if exists). + +### `UpdateReferral` +Changes referral address after cooldown. + +### `DeleteReferral` +Removes referral relationship. + +### `GetReferral` +Returns referral for address. + +## Usage + +```go +// Register referral +success := TryRegister(user, referrer) + +// Update after cooldown +UpdateReferral(newReferrer) + +// Query referral +referrer := GetReferral(userAddress) + +// Remove referral +DeleteReferral() +``` + +## Security + +- One referral per address +- 24-hour change cooldown +- No self-referrals +- Immutable during cooldown \ No newline at end of file diff --git a/contract/r/gnoswap/referral/doc.gno b/contract/r/gnoswap/referral/doc.gno new file mode 100644 index 0000000..3f526af --- /dev/null +++ b/contract/r/gnoswap/referral/doc.gno @@ -0,0 +1,109 @@ +// Package referral implements a referral system on Gno. It allows +// any authorized caller to register, update, or remove referral +// information. A referral link is defined as a mapping from one +// address (the "user") to another address (the "referrer"). +// +// ## Overview +// +// The referral package is composed of the following components: +// +// 1. **errors.gno**: Provides custom error types (ReferralError) with +// specific error codes and messages. +// 2. **utils.gno**: Contains utility functions for permission checks, +// especially isValidCaller, which ensures only specific, pre-authorized +// callers (e.g., governance or router addresses) can invoke the core +// functions. +// 3. **types.gno**: Defines core constants for event types, attributes, +// and the ReferralKeeper interface, which outlines the fundamental +// methods of the referral system (Register, Update, Remove, etc.). +// 4. **keeper.gno**: Implements the actual business logic behind the +// ReferralKeeper interface. It uses an AVL Tree (avl.Tree) to store +// referral data (address -> referrer). The keeper methods emit events +// when a new referral is registered, updated, or removed. +// 5. **referral.gno**: Exposes a public API (the Referral struct) +// that delegates to the keeper, providing external contracts or +// applications a straightforward way to interact with the system. +// +// ## Workflow +// +// Typical usage of this contract follows these steps: +// +// 1. A caller with valid permissions invokes Register, Update, or Remove +// through the Referral struct. +// 2. The Referral struct forwards the request to the internal keeper +// methods. +// 3. The keeper checks caller permission (via isValidCaller), validates +// addresses, and stores or removes data in the AVL Tree. +// 4. An event is emitted for off-chain or cross-module notifications. +// +// ## Integration with Other Contracts +// +// Other contracts can leverage the referral system in two major ways: +// +// 1. **Direct Calls**: If you wish to directly call this contract, +// instantiate the Referral object (via NewReferral) and invoke its +// methods, assuming you meet the authorized-caller criteria. +// +// 2. **Embedded or Extended**: If you have a complex module that includes +// referral features, import this package and embed a Referral instance +// in your own keeper. This way, you can handle additional validations +// or custom logic before delegating to the existing referral functions. +// +// ## Error Handling +// +// The package defines several error types through ReferralError: +// - `ErrInvalidAddress`: Returned when an address format is invalid +// - `ErrUnauthorized`: Returned when the caller lacks permission +// - `ErrNotFound`: Returned when attempting to get a non-existent referral +// - `ErrZeroAddress`: Returned when attempting operations with zero address +// +// ## Example: Integration with a Staking Contract +// +// Suppose you have a staking contract that wants to reward referrers +// when a new user stakes tokens: +// +// ```go +// +// import ( +// "std" +// "gno.land/r/gnoswap/referral" +// "gno.land/p/demo/mystaking" // example staking contract +// ) +// +// func rewardReferrerOnStake(user std.Address, amount int) { +// // 1) Access the referral system +// r := referral.NewReferral() +// +// // 2) Get the user's referrer +// refAddr, err := r.GetReferral(user) +// if err != nil { +// // handle error or skip if not found +// return +// } +// +// // 3) Reward the referrer +// mystaking.AddReward(refAddr, calculateReward(amount)) +// } +// +// ``` +// +// In this simple example, the staking contract checks if the user has +// a referrer by calling `GetReferral`. If a referrer is found, it then +// calculates a reward based on the staked amount. +// +// ## Limitations and Constraints +// +// - A user can have only one referrer at a time +// - Once a referral is removed, it cannot be automatically restored +// - Only authorized contracts can modify referral relationships +// - Address validation is strict and requires proper Bech32 format +// +// # Notes +// +// - The contract strictly enforces caller restrictions via isValidCaller. +// Make sure to configure it to permit only the addresses or roles that +// should be able to register or update referrals. +// - Zero addresses are treated as a trigger for removing a referral record. +// - The system emits events (register_referral, update_referral, remove_referral) +// which can be consumed by other on-chain or off-chain services. +package referral diff --git a/contract/r/gnoswap/referral/errors.gno b/contract/r/gnoswap/referral/errors.gno new file mode 100644 index 0000000..480886c --- /dev/null +++ b/contract/r/gnoswap/referral/errors.gno @@ -0,0 +1,16 @@ +package referral + +import ( + "errors" +) + +var ( + ErrInvalidAddress = errors.New("invalid address format") + ErrZeroAddress = errors.New("zero address is not allowed") + ErrSelfReferral = errors.New("self referral is not allowed") + ErrUnauthorized = errors.New("unauthorized caller") + ErrInvalidCaller = errors.New("invalid caller") + ErrCyclicReference = errors.New("cyclic reference is not allowed") + ErrTooManyRequests = errors.New("too many requests: operations allowed once per 24 hours for each address") + ErrNotFound = errors.New("referral not found") +) diff --git a/contract/r/gnoswap/referral/global_keeper.gno b/contract/r/gnoswap/referral/global_keeper.gno new file mode 100644 index 0000000..73a27f9 --- /dev/null +++ b/contract/r/gnoswap/referral/global_keeper.gno @@ -0,0 +1,72 @@ +package referral + +import "std" + +var gReferralKeeper ReferralKeeper + +const ( + EventReferralInvalid = "ReferralInvalid" + EventRegisterFailed = "ReferralRegistrationFailed" + EventRegisterSuccess = "ReferralRegistrationSuccess" +) + +func init() { + if gReferralKeeper == nil { + gReferralKeeper = NewKeeper() + } +} + +// getKeeper returns the global referral keeper instance. +func getKeeper() ReferralKeeper { + return gReferralKeeper +} + +// GetReferral returns the referral address for the given address. +func GetReferral(addr string) string { + referral, err := gReferralKeeper.get(std.Address(addr)) + if err != nil { + return "" + } + return referral.String() +} + +// HasReferral returns true if the given address has a referral. +func HasReferral(addr string) bool { + referral, err := gReferralKeeper.get(std.Address(addr)) + if err != nil { + return false + } + return referral != zeroAddress +} + +// IsEmpty returns true if no referrals exist in the system. +func IsEmpty() bool { + return gReferralKeeper.isEmpty() +} + +// TryRegister attempts to register a new referral. +// +// Parameters: +// - addr: address to register +// - referral: referral address string +// +// Returns true on success, false on failure. +func TryRegister(cur realm, addr std.Address, referral string) bool { + refAddr := std.Address(referral) + err := gReferralKeeper.register(addr, refAddr) + if err != nil { + std.Emit( + EventRegisterFailed, + "address", addr.String(), + "error", err.Error(), + ) + return false + } + + std.Emit( + EventRegisterSuccess, + "address", addr.String(), + "referral", referral, + ) + return true +} diff --git a/contract/r/gnoswap/referral/gnomod.toml b/contract/r/gnoswap/referral/gnomod.toml new file mode 100644 index 0000000..93123cf --- /dev/null +++ b/contract/r/gnoswap/referral/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/referral" +gno = "0.9" diff --git a/contract/r/gnoswap/referral/keeper.gno b/contract/r/gnoswap/referral/keeper.gno new file mode 100644 index 0000000..14ab417 --- /dev/null +++ b/contract/r/gnoswap/referral/keeper.gno @@ -0,0 +1,176 @@ +package referral + +import ( + "std" + "time" + + "gno.land/p/nt/avl" +) + +const ( + // MinTimeBetweenUpdates is minimum duration between operations (24 hours). + MinTimeBetweenUpdates int64 = 24 * 60 * 60 +) + +// keeper implements ReferralKeeper using AVL tree storage. +// It includes rate limiting to prevent abuse. +type keeper struct { + store *avl.Tree + lastOps map[string]int64 +} + +var _ ReferralKeeper = &keeper{} + +// NewKeeper creates a new ReferralKeeper instance. +func NewKeeper() ReferralKeeper { + return &keeper{ + store: avl.NewTree(), + lastOps: make(map[string]int64), + } +} + +// register creates a new referral relationship between addresses. +// Caller must have valid permissions. +func (k *keeper) register(addr, refAddr std.Address) error { + return k.setReferral(addr, refAddr, EventTypeRegister) +} + +// update modifies an existing referral address. +// Caller must have valid permissions. +func (k *keeper) update(addr, newRefAddr std.Address) error { + return k.setReferral(addr, newRefAddr, EventTypeUpdate) +} + +// setReferral handles common logic for registering and updating referrals. +// Setting refAddr to zero address removes the referral. +// Rate limiting applies to prevent abuse. +func (k *keeper) setReferral(addr, refAddr std.Address, eventType string) error { + if err := isValidCaller(std.PreviousRealm().Address()); err != nil { + return err + } + + if err := k.validateAddresses(addr, refAddr); err != nil { + return err + } + + addrStr := addr.String() + refAddrStr := refAddr.String() + + if refAddr == zeroAddress { + if k.has(addr) { + _, ok := k.store.Remove(addrStr) + if !ok { + return ErrNotFound + } + } + return nil + } + + if err := k.checkRateLimit(addrStr); err != nil { + return err + } + + k.store.Set(addrStr, refAddrStr) + k.lastOps[addrStr] = time.Now().Unix() + + std.Emit( + eventType, + "myAddress", addrStr, + "refAddress", refAddrStr, + ) + + return nil +} + +// validateAddresses validates that addresses are properly formatted and not self-referencing. +func (k *keeper) validateAddresses(addr, refAddr std.Address) error { + if !addr.IsValid() || (refAddr != zeroAddress && !refAddr.IsValid()) { + return ErrInvalidAddress + } + if addr == refAddr { + return ErrSelfReferral + } + return nil +} + +// remove deletes a referral relationship. +// Caller must have valid permissions and rate limiting applies. +func (k *keeper) remove(target std.Address) error { + if err := isValidCaller(std.PreviousRealm().Address()); err != nil { + return err + } + + if err := k.validateAddresses(target, zeroAddress); err != nil { + return err + } + + tgt := target.String() + + if err := k.checkRateLimit(tgt); err != nil { + return err + } + + _, ok := k.store.Remove(tgt) + if !ok { + return ErrNotFound + } + + std.Emit( + EventTypeRemove, + "removedAddress", tgt, + ) + + return nil +} + +// has returns true if a referral exists for the given address. +func (k *keeper) has(addr std.Address) bool { + _, exists := k.store.Get(addr.String()) + return exists +} + +// get retrieves the referral address for a given address. +// Returns ErrNotFound if no referral exists. +func (k *keeper) get(addr std.Address) (std.Address, error) { + if !addr.IsValid() { + return zeroAddress, ErrInvalidAddress + } + + val, ok := k.store.Get(addr.String()) + if !ok { + return zeroAddress, ErrNotFound + } + + refAddr, ok := val.(string) + if !ok { + return zeroAddress, ErrInvalidAddress + } + + return std.Address(refAddr), nil +} + +// isEmpty returns true if no referrals exist in the store. +func (k *keeper) isEmpty() bool { + empty := true + k.store.Iterate("", "", func(key string, value any) bool { + empty = false + return true // stop iteration on first item + }) + return empty +} + +// checkRateLimit verifies if enough time has passed since the last operation. +// Returns ErrTooManyRequests if rate limit is exceeded. +func (k *keeper) checkRateLimit(addr string) error { + now := time.Now().Unix() + + if lastOpTime, exists := k.lastOps[addr]; exists { + timeSinceLastOp := now - lastOpTime + + if timeSinceLastOp < MinTimeBetweenUpdates { + return ErrTooManyRequests + } + } + + return nil +} diff --git a/contract/r/gnoswap/referral/referral.gno b/contract/r/gnoswap/referral/referral.gno new file mode 100644 index 0000000..a7f9763 --- /dev/null +++ b/contract/r/gnoswap/referral/referral.gno @@ -0,0 +1,56 @@ +package referral + +import ( + "std" +) + +// Referral manages referral relationships between addresses. +type Referral struct { + keeper ReferralKeeper +} + +// NewReferral creates a new Referral instance. +func NewReferral() *Referral { + if gReferralKeeper == nil { + gReferralKeeper = NewKeeper() + } + return &Referral{ + keeper: gReferralKeeper, + } +} + +// Register creates a new referral relationship. +// +// Parameters: +// - addr: address to register +// - refAddr: referral address +func (r *Referral) Register(addr, refAddr std.Address) error { + return r.keeper.register(addr, refAddr) +} + +// Update modifies an existing referral relationship. +// +// Parameters: +// - addr: address to update +// - newAddr: new referral address +func (r *Referral) Update(addr, newAddr std.Address) error { + return r.keeper.update(addr, newAddr) +} + +// Remove deletes a referral relationship. +// +// Parameters: +// - addr: address to remove +func (r *Referral) Remove(addr std.Address) error { + return r.keeper.remove(addr) +} + +// Has returns true if a referral exists for the given address. +func (r *Referral) Has(addr std.Address) bool { + return r.keeper.has(addr) +} + +// Get retrieves the referral address for the given address. +func (r *Referral) Get(addr std.Address) (std.Address, error) { + return r.keeper.get(addr) +} diff --git a/contract/r/gnoswap/referral/type.gno b/contract/r/gnoswap/referral/type.gno new file mode 100644 index 0000000..38b4db4 --- /dev/null +++ b/contract/r/gnoswap/referral/type.gno @@ -0,0 +1,33 @@ +package referral + +import "std" + +var zeroAddress = std.Address("") + +// Event types for referral actions. +const ( + EventTypeRegister = "RegisterReferral" + EventTypeUpdate = "UpdateReferral" + EventTypeRemove = "RemoveReferral" +) + +// ReferralKeeper defines the interface for managing referral relationships. +type ReferralKeeper interface { + // register creates a new referral relationship between addresses. + register(addr, refAddr std.Address) error + + // update modifies an existing referral address. + update(addr, newRefAddr std.Address) error + + // remove deletes a referral relationship. + remove(addr std.Address) error + + // has returns true if a referral exists for the given address. + has(addr std.Address) bool + + // get retrieves the referral address for a given address. + get(addr std.Address) (std.Address, error) + + // isEmpty returns true if no referrals exist in the system. + isEmpty() bool +} diff --git a/contract/r/gnoswap/referral/utils.gno b/contract/r/gnoswap/referral/utils.gno new file mode 100644 index 0000000..336ca28 --- /dev/null +++ b/contract/r/gnoswap/referral/utils.gno @@ -0,0 +1,38 @@ +package referral + +import ( + "std" + + "gno.land/p/nt/ufmt" + + prabc "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoswap/access" + _ "gno.land/r/gnoswap/rbac" +) + +// validCallerRoles is a list of roles that are authorized to modify referral data. +// This includes governance contracts, router, position manager, and staker contracts. +var validCallerRoles = []string{ + prabc.ROLE_GOVERNANCE.String(), + prabc.ROLE_GOV_STAKER.String(), + prabc.ROLE_ROUTER.String(), + prabc.ROLE_POSITION.String(), + prabc.ROLE_STAKER.String(), + prabc.ROLE_LAUNCHPAD.String(), +} + +// isValidCaller checks if the caller address has permission to modify referral data. +// Only addresses with specific roles defined in validCallerRoles are authorized. +// Returns an error if the caller is not authorized. +func isValidCaller(caller std.Address) error { + roleAddresses := access.GetRoleAddresses() + + for _, role := range validCallerRoles { + if roleAddresses[role] == caller { + return nil + } + } + + return ufmt.Errorf("unauthorized caller: %s", caller) +} diff --git a/contract/r/gnoswap/v1/test_token/bar/bar.gno b/contract/r/gnoswap/test_token/bar/bar.gno similarity index 93% rename from contract/r/gnoswap/v1/test_token/bar/bar.gno rename to contract/r/gnoswap/test_token/bar/bar.gno index e187e7a..e81cbd2 100644 --- a/contract/r/gnoswap/v1/test_token/bar/bar.gno +++ b/contract/r/gnoswap/test_token/bar/bar.gno @@ -4,11 +4,11 @@ import ( "std" "strings" - "gno.land/p/demo/grc/grc20" - "gno.land/p/demo/ownable" - "gno.land/p/demo/ufmt" + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" - "gno.land/r/demo/grc20reg" + "gno.land/r/demo/defi/grc20reg" ) var ( diff --git a/contract/r/gnoswap/test_token/bar/gnomod.toml b/contract/r/gnoswap/test_token/bar/gnomod.toml new file mode 100644 index 0000000..b7362c0 --- /dev/null +++ b/contract/r/gnoswap/test_token/bar/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/onbloc/bar" +gno = "0.9" diff --git a/contract/r/gnoswap/test_token/baz/baz.gno b/contract/r/gnoswap/test_token/baz/baz.gno new file mode 100644 index 0000000..d2f332d --- /dev/null +++ b/contract/r/gnoswap/test_token/baz/baz.gno @@ -0,0 +1,80 @@ +package baz + +import ( + "std" + "strings" + + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" + + "gno.land/r/demo/defi/grc20reg" +) + +var ( + token, privateLedger = grc20.NewToken("Baz", "BAZ", 6) + owner = ownable.NewWithAddress("g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42") // ADMIN +) + +func init() { + privateLedger.Mint(owner.Owner(), 100_000_000_000_000) + grc20reg.Register(cross, token, "") +} + +func TotalSupply() int64 { + userTeller := token.CallerTeller() + return userTeller.TotalSupply() +} + +func BalanceOf(owner std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.BalanceOf(owner) +} + +func Allowance(owner, spender std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.Allowance(owner, spender) +} + +func Transfer(cur realm, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Transfer(to, amount)) +} + +func Approve(cur realm, spender std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Approve(spender, amount)) +} + +func TransferFrom(cur realm, from, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.TransferFrom(from, to, amount)) +} + +func Burn(cur realm, from std.Address, amount int64) { + owner.AssertOwnedByPrevious() + checkErr(privateLedger.Burn(from, amount)) +} + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return token.RenderHome() + case c == 2 && parts[0] == "balance": + owner := std.Address(parts[1]) + userTeller := token.CallerTeller() + balance := userTeller.BalanceOf(owner) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/contract/r/gnoswap/test_token/baz/gnomod.toml b/contract/r/gnoswap/test_token/baz/gnomod.toml new file mode 100644 index 0000000..1eda940 --- /dev/null +++ b/contract/r/gnoswap/test_token/baz/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/onbloc/baz" +gno = "0.9" diff --git a/contract/r/gnoswap/test_token/foo/foo.gno b/contract/r/gnoswap/test_token/foo/foo.gno new file mode 100644 index 0000000..07b9758 --- /dev/null +++ b/contract/r/gnoswap/test_token/foo/foo.gno @@ -0,0 +1,80 @@ +package foo + +import ( + "std" + "strings" + + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" + + "gno.land/r/demo/defi/grc20reg" +) + +var ( + token, privateLedger = grc20.NewToken("Foo", "FOO", 6) + owner = ownable.NewWithAddress("g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42") // ADMIN +) + +func init() { + privateLedger.Mint(owner.Owner(), 100_000_000_000_000) + grc20reg.Register(cross, token, "") +} + +func TotalSupply() int64 { + userTeller := token.CallerTeller() + return userTeller.TotalSupply() +} + +func BalanceOf(owner std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.BalanceOf(owner) +} + +func Allowance(owner, spender std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.Allowance(owner, spender) +} + +func Transfer(cur realm, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Transfer(to, amount)) +} + +func Approve(cur realm, spender std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Approve(spender, amount)) +} + +func TransferFrom(cur realm, from, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.TransferFrom(from, to, amount)) +} + +func Burn(cur realm, from std.Address, amount int64) { + owner.AssertOwnedByPrevious() + checkErr(privateLedger.Burn(from, amount)) +} + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return token.RenderHome() + case c == 2 && parts[0] == "balance": + owner := std.Address(parts[1]) + userTeller := token.CallerTeller() + balance := userTeller.BalanceOf(owner) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/contract/r/gnoswap/test_token/foo/gnomod.toml b/contract/r/gnoswap/test_token/foo/gnomod.toml new file mode 100644 index 0000000..2bf457d --- /dev/null +++ b/contract/r/gnoswap/test_token/foo/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/onbloc/foo" +gno = "0.9" diff --git a/contract/r/gnoswap/test_token/obl/gnomod.toml b/contract/r/gnoswap/test_token/obl/gnomod.toml new file mode 100644 index 0000000..7eeb174 --- /dev/null +++ b/contract/r/gnoswap/test_token/obl/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/onbloc/obl" +gno = "0.9" diff --git a/contract/r/gnoswap/test_token/obl/obl.gno b/contract/r/gnoswap/test_token/obl/obl.gno new file mode 100644 index 0000000..0935598 --- /dev/null +++ b/contract/r/gnoswap/test_token/obl/obl.gno @@ -0,0 +1,80 @@ +package obl + +import ( + "std" + "strings" + + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" + + "gno.land/r/demo/defi/grc20reg" +) + +var ( + token, privateLedger = grc20.NewToken("Obl", "OBL", 6) + owner = ownable.NewWithAddress("g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42") // ADMIN +) + +func init() { + privateLedger.Mint(owner.Owner(), 100_000_000_000_000) + grc20reg.Register(cross, token, "") +} + +func TotalSupply() int64 { + userTeller := token.CallerTeller() + return userTeller.TotalSupply() +} + +func BalanceOf(owner std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.BalanceOf(owner) +} + +func Allowance(owner, spender std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.Allowance(owner, spender) +} + +func Transfer(cur realm, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Transfer(to, amount)) +} + +func Approve(cur realm, spender std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Approve(spender, amount)) +} + +func TransferFrom(cur realm, from, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.TransferFrom(from, to, amount)) +} + +func Burn(cur realm, from std.Address, amount int64) { + owner.AssertOwnedByPrevious() + checkErr(privateLedger.Burn(from, amount)) +} + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return token.RenderHome() + case c == 2 && parts[0] == "balance": + owner := std.Address(parts[1]) + userTeller := token.CallerTeller() + balance := userTeller.BalanceOf(owner) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/contract/r/gnoswap/test_token/qux/gnomod.toml b/contract/r/gnoswap/test_token/qux/gnomod.toml new file mode 100644 index 0000000..ffae17f --- /dev/null +++ b/contract/r/gnoswap/test_token/qux/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/onbloc/qux" +gno = "0.9" diff --git a/contract/r/gnoswap/test_token/qux/qux.gno b/contract/r/gnoswap/test_token/qux/qux.gno new file mode 100644 index 0000000..d1c908c --- /dev/null +++ b/contract/r/gnoswap/test_token/qux/qux.gno @@ -0,0 +1,80 @@ +package qux + +import ( + "std" + "strings" + + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" + + "gno.land/r/demo/defi/grc20reg" +) + +var ( + token, privateLedger = grc20.NewToken("Qux", "QUX", 6) + owner = ownable.NewWithAddress("g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42") // ADMIN +) + +func init() { + privateLedger.Mint(owner.Owner(), 100_000_000_000_000) + grc20reg.Register(cross, token, "") +} + +func TotalSupply() int64 { + userTeller := token.CallerTeller() + return userTeller.TotalSupply() +} + +func BalanceOf(owner std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.BalanceOf(owner) +} + +func Allowance(owner, spender std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.Allowance(owner, spender) +} + +func Transfer(cur realm, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Transfer(to, amount)) +} + +func Approve(cur realm, spender std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Approve(spender, amount)) +} + +func TransferFrom(cur realm, from, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.TransferFrom(from, to, amount)) +} + +func Burn(cur realm, from std.Address, amount int64) { + owner.AssertOwnedByPrevious() + checkErr(privateLedger.Burn(from, amount)) +} + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return token.RenderHome() + case c == 2 && parts[0] == "balance": + owner := std.Address(parts[1]) + userTeller := token.CallerTeller() + balance := userTeller.BalanceOf(owner) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/contract/r/gnoswap/test_token/usdc/gnomod.toml b/contract/r/gnoswap/test_token/usdc/gnomod.toml new file mode 100644 index 0000000..10e2525 --- /dev/null +++ b/contract/r/gnoswap/test_token/usdc/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/onbloc/usdc" +gno = "0.9" diff --git a/contract/r/gnoswap/test_token/usdc/usdc.gno b/contract/r/gnoswap/test_token/usdc/usdc.gno new file mode 100644 index 0000000..2964f2c --- /dev/null +++ b/contract/r/gnoswap/test_token/usdc/usdc.gno @@ -0,0 +1,80 @@ +package usdc + +import ( + "std" + "strings" + + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" + + "gno.land/r/demo/defi/grc20reg" +) + +var ( + token, privateLedger = grc20.NewToken("Usd Coin", "USDC", 6) + owner = ownable.NewWithAddress("g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42") // ADMIN +) + +func init() { + privateLedger.Mint(owner.Owner(), 100_000_000_000_000) + grc20reg.Register(cross, token, "") +} + +func TotalSupply() int64 { + userTeller := token.CallerTeller() + return userTeller.TotalSupply() +} + +func BalanceOf(owner std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.BalanceOf(owner) +} + +func Allowance(owner, spender std.Address) int64 { + userTeller := token.CallerTeller() + return userTeller.Allowance(owner, spender) +} + +func Transfer(cur realm, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Transfer(to, amount)) +} + +func Approve(cur realm, spender std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.Approve(spender, amount)) +} + +func TransferFrom(cur realm, from, to std.Address, amount int64) { + userTeller := token.CallerTeller() + checkErr(userTeller.TransferFrom(from, to, amount)) +} + +func Burn(cur realm, from std.Address, amount int64) { + owner.AssertOwnedByPrevious() + checkErr(privateLedger.Burn(from, amount)) +} + +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return token.RenderHome() + case c == 2 && parts[0] == "balance": + owner := std.Address(parts[1]) + userTeller := token.CallerTeller() + balance := userTeller.BalanceOf(owner) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +func checkErr(err error) { + if err != nil { + panic(err) + } +} diff --git a/contract/r/gnoswap/v1/common/consts.gno b/contract/r/gnoswap/v1/common/consts.gno new file mode 100644 index 0000000..55a64ee --- /dev/null +++ b/contract/r/gnoswap/v1/common/consts.gno @@ -0,0 +1,7 @@ +package common + +// Tick bounds. +const ( + minTick = -887272 + maxTick = 887272 +) diff --git a/contract/r/gnoswap/v1/common/doc.gno b/contract/r/gnoswap/v1/common/doc.gno new file mode 100644 index 0000000..52e8c26 --- /dev/null +++ b/contract/r/gnoswap/v1/common/doc.gno @@ -0,0 +1,6 @@ +// Package common provides shared utilities for GnoSwap v1 contracts. +// +// This package contains core mathematical functions and helpers used across +// the protocol, including tick math for price calculations, liquidity math +// for position management, and GRC20 registry integration. +package common diff --git a/contract/r/gnoswap/v1/common/errors.gno b/contract/r/gnoswap/v1/common/errors.gno new file mode 100644 index 0000000..8c8783f --- /dev/null +++ b/contract/r/gnoswap/v1/common/errors.gno @@ -0,0 +1,25 @@ +package common + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errNoPermission = errors.New("[GNOSWAP-COMMON-001] caller has no permission") + errHalted = errors.New("[GNOSWAP-COMMON-002] halted") + errOutOfRange = errors.New("[GNOSWAP-COMMON-003] value out of range") + errNotRegistered = errors.New("[GNOSWAP-COMMON-004] token is not registered") + errInvalidAddr = errors.New("[GNOSWAP-COMMON-005] invalid address") + errOverflow = errors.New("[GNOSWAP-COMMON-006] overflow") + errInvalidPositionId = errors.New("[GNOSWAP-COMMON-007] invalid positionId") + errInvalidInput = errors.New("[GNOSWAP-COMMON-008] invalid input data") + errOverFlow = errors.New("[GNOSWAP-COMMON-009] overflow") + errIdenticalTicks = errors.New("[GNOSWAP-COMMON-010] identical ticks") +) + +// newErrorWithDetail creates an error message with additional context in format " || ". +func newErrorWithDetail(err error, detail string) string { + return ufmt.Errorf("%s || %s", err.Error(), detail).Error() +} diff --git a/contract/r/gnoswap/v1/common/gnomod.toml b/contract/r/gnoswap/v1/common/gnomod.toml new file mode 100644 index 0000000..91da871 --- /dev/null +++ b/contract/r/gnoswap/v1/common/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/common" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/common/grc20reg_helper.gno b/contract/r/gnoswap/v1/common/grc20reg_helper.gno new file mode 100644 index 0000000..3235641 --- /dev/null +++ b/contract/r/gnoswap/v1/common/grc20reg_helper.gno @@ -0,0 +1,86 @@ +package common + +import ( + "regexp" + "std" + "strings" + + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ufmt" + "gno.land/r/demo/defi/grc20reg" +) + +var re = regexp.MustCompile(`\[gno\.land/r/[^\]]+\]`) + +// GetToken returns a grc20.Token instance for the specified path, panicking if not registered. +func GetToken(path string) *grc20.Token { + return grc20reg.MustGet(path) +} + +// GetTokenTeller returns a grc20.Teller instance for the specified path, panicking if not registered. +func GetTokenTeller(path string) grc20.Teller { + return GetToken(path).CallerTeller() +} + +// IsRegistered checks if a token is registered in grc20reg, returning nil if registered or error if not. +func IsRegistered(path string) error { + getter := grc20reg.Get(path) + if getter == nil { + return ufmt.Errorf("token(%s) is not registered to grc20reg", path) + } + return nil +} + +// MustRegistered checks if all provided tokens are registered, panicking if any is not registered. +func MustRegistered(paths ...string) { + for _, path := range paths { + if err := IsRegistered(path); err != nil { + panic(newErrorWithDetail( + errNotRegistered, + ufmt.Sprintf("token(%s)", path), + )) + } + } +} + +// extractTokenPathsFromRender extracts token paths from rendered grc20reg output. +func extractTokenPathsFromRender(render string) []string { + matches := re.FindAllString(render, -1) + + tokenPaths := make([]string, 0, len(matches)) + for _, match := range matches { + tokenPath := strings.Trim(match, "[]") // Remove the brackets + tokenPaths = append(tokenPaths, tokenPath) + } + return tokenPaths +} + +// TotalSupply returns the total supply of the specified token. +func TotalSupply(path string) int64 { + return GetToken(path).TotalSupply() +} + +// BalanceOf returns the token balance for the specified address. +func BalanceOf(path string, addr std.Address) int64 { + return GetToken(path).BalanceOf(addr) +} + +// Allowance returns the token allowance from owner to spender. +func Allowance(path string, owner, spender std.Address) int64 { + return GetToken(path).Allowance(owner, spender) +} + +// Transfer transfers tokens to the specified address using grc20.Teller.Transfer. +func Transfer(cur realm, path string, to std.Address, amount int64) error { + return GetTokenTeller(path).Transfer(to, amount) +} + +// TransferFrom transfers tokens from one address to another using grc20.Teller.TransferFrom. +func TransferFrom(cur realm, path string, from, to std.Address, amount int64) error { + return GetTokenTeller(path).TransferFrom(from, to, amount) +} + +// Approve approves tokens for the specified spender using grc20.Teller.Approve. +func Approve(cur realm, path string, spender std.Address, amount int64) error { + return GetTokenTeller(path).Approve(spender, amount) +} diff --git a/contract/r/gnoswap/v1/common/liquidity_amounts.gno b/contract/r/gnoswap/v1/common/liquidity_amounts.gno new file mode 100644 index 0000000..cce5e6d --- /dev/null +++ b/contract/r/gnoswap/v1/common/liquidity_amounts.gno @@ -0,0 +1,332 @@ +package common + +import ( + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" +) + +const ( + Q96_RESOLUTION = 96 + Q128_RESOLUTION = 128 + MAX_UINT128 = "340282366920938463463374607431768211455" // 2^128 - 1 + Q96 = "79228162514264337593543950336" // 2^96 +) + +var ( + maxUint128 = u256.MustFromDecimal(MAX_UINT128) + q96Uint = u256.MustFromDecimal(Q96) + q128Mask = func() *u256.Uint { + mask := u256.Zero().Lsh(u256.One(), Q128_RESOLUTION) + mask = u256.Zero().Sub(mask, u256.One()) + return mask + }() + // only used for return value + zero = u256.Zero() +) + +// toAscendingOrder returns the two values in ascending order. +func toAscendingOrder(a, b *u256.Uint) (*u256.Uint, *u256.Uint) { + if a.Gt(b) { + return b, a + } + + return a, b +} + +// toUint128 ensures the value fits within uint128 range. +// +// Validates and constrains a 256-bit unsigned integer to 128-bit range. +// Used for liquidity calculations where amounts must fit in compact storage. +// +// Parameters: +// - value: 256-bit unsigned integer to constrain +// +// Returns: +// - Masked value if exceeds MAX_UINT128 (2^128 - 1) +// - Original value if within range +// +// Panics if value is nil. +// Critical for preventing overflow in liquidity math. +func toUint128(value *u256.Uint) *u256.Uint { + if value == nil { + panic(newErrorWithDetail( + errInvalidInput, + "value is nil", + )) + } + + if value.Gt(maxUint128) { + return u256.Zero().And(value, q128Mask) + } + return value +} + +// safeConvertToUint128 safely ensures a *u256.Uint value fits within the uint128 range. +// +// This function verifies that the provided unsigned 256-bit integer does not exceed the maximum value for uint128 (`2^128 - 1`). +// If the value is within the uint128 range, it is returned as is; otherwise, the function triggers a panic. +// +// Parameters: +// - value (*u256.Uint): The unsigned 256-bit integer to be checked. +// +// Returns: +// - *u256.Uint: The same value if it is within the uint128 range. +// +// Panics: +// - If the value exceeds the maximum uint128 value (`2^128 - 1`), the function will panic with a descriptive error +// indicating the overflow and the original value. +// +// Notes: +// - The constant `MAX_UINT128` is defined as `340282366920938463463374607431768211455` (the largest uint128 value). +// - No actual conversion occurs since the function works directly with *u256.Uint types. +// +// Example: +// validUint128 := safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211455")) // Valid +// safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211456")) // Panics due to overflow +func safeConvertToUint128(value *u256.Uint) *u256.Uint { + if value.Gt(maxUint128) { + panic(ufmt.Sprintf( + "%v: amount(%s) overflows uint128 range", + errOverFlow, value.ToString())) + } + return value +} + +// computeLiquidityForAmount0 calculates the liquidity for a given amount of token0. +// +// This function computes the maximum possible liquidity that can be provided for `token0` +// based on the provided price boundaries (sqrtRatioAX96 and sqrtRatioBX96) in Q64.96 format. +// +// Parameters: +// - sqrtRatioAX96: *u256.Uint - The square root price at the lower tick boundary (Q64.96). +// - sqrtRatioBX96: *u256.Uint - The square root price at the upper tick boundary (Q64.96). +// - amount0: *u256.Uint - The amount of token0 to be converted to liquidity. +// +// Returns: +// - *u256.Uint: The calculated liquidity, represented as an unsigned 128-bit integer (uint128). +// +// Panics: +// - If the resulting liquidity exceeds the uint128 range, `safeConvertToUint128` will trigger a panic. +func computeLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0 *u256.Uint) *u256.Uint { + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) + intermediate := u256.MulDiv(sqrtRatioAX96, sqrtRatioBX96, q96Uint) + + diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) + if diff.IsZero() { + panic(newErrorWithDetail( + errIdenticalTicks, + ufmt.Sprintf("sqrtRatioAX96 (%s) and sqrtRatioBX96 (%s) are identical", sqrtRatioAX96.ToString(), sqrtRatioBX96.ToString()), + )) + } + res := u256.MulDiv(amount0, intermediate, diff) + return safeConvertToUint128(res) +} + +// computeLiquidityForAmount1 calculates liquidity based on the provided token1 amount and price range. +// +// This function computes the liquidity for a given amount of token1 by using the difference +// between the upper and lower square root price ratios. The calculation uses Q96 fixed-point +// arithmetic to maintain precision. +// +// Parameters: +// - sqrtRatioAX96: *u256.Uint - The square root ratio of price at the lower tick, represented in Q96 format. +// - sqrtRatioBX96: *u256.Uint - The square root ratio of price at the upper tick, represented in Q96 format. +// - amount1: *u256.Uint - The amount of token1 to calculate liquidity for. +// +// Returns: +// - *u256.Uint: The calculated liquidity based on the provided amount of token1 and price range. +// +// Notes: +// - The result is not directly limited to uint128, as liquidity values can exceed uint128 bounds. +// - If `sqrtRatioAX96 == sqrtRatioBX96`, the function will panic due to division by zero. +// - Q96 is a constant representing `2^96`, ensuring that precision is maintained during division. +// +// Panics: +// - If the resulting liquidity exceeds the uint128 range, `safeConvertToUint128` will trigger a panic. +func computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1 *u256.Uint) *u256.Uint { + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) + + diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) + if diff.IsZero() { + panic(newErrorWithDetail( + errIdenticalTicks, + ufmt.Sprintf("sqrtRatioAX96 (%s) and sqrtRatioBX96 (%s) are identical", sqrtRatioAX96.ToString(), sqrtRatioBX96.ToString()), + )) + } + res := u256.MulDiv(amount1, q96Uint, diff) + return safeConvertToUint128(res) +} + +// GetLiquidityForAmounts calculates the maximum liquidity given the current price (sqrtRatioX96), +// upper and lower price bounds (sqrtRatioAX96 and sqrtRatioBX96), and token amounts (amount0, amount1). +// +// This function evaluates how much liquidity can be obtained for specified amounts of token0 and token1 +// within the provided price range. It returns the lesser liquidity based on available token0 or token1 +// to ensure the pool remains balanced. +// +// Parameters: +// - sqrtRatioX96: The current price as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - amount0: The amount of token0 available to provide liquidity (*u256.Uint). +// - amount1: The amount of token1 available to provide liquidity (*u256.Uint). +// +// Returns: +// - *u256.Uint: The maximum possible liquidity that can be minted. +// +// Notes: +// - The `Clone` method is used to prevent modification of the original values during computation. +// - The function ensures that liquidity calculations handle edge cases when the current price +// is outside the specified range by returning liquidity based on the dominant token. +// +// TODO: consider to reduce the number of clones (after confirmed this logic is correct) +func GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1 *u256.Uint) (liquidity *u256.Uint) { + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone()) + + if sqrtRatioX96.Lte(sqrtRatioAX96) { + liquidity = computeLiquidityForAmount0(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone(), amount0.Clone()) + } else if sqrtRatioX96.Lt(sqrtRatioBX96) { + liquidity0 := computeLiquidityForAmount0(sqrtRatioX96.Clone(), sqrtRatioBX96.Clone(), amount0.Clone()) + liquidity1 := computeLiquidityForAmount1(sqrtRatioAX96.Clone(), sqrtRatioX96.Clone(), amount1.Clone()) + + if liquidity0.Lt(liquidity1) { + liquidity = liquidity0 + } else { + liquidity = liquidity1 + } + } else { + liquidity = computeLiquidityForAmount1(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone(), amount1.Clone()) + } + return liquidity +} + +// computeAmount0ForLiquidity calculates the required amount of token0 for a given liquidity level +// within a specified price range (represented by sqrt ratios). +// +// This function determines the amount of token0 needed to provide a specified amount of liquidity +// within a price range defined by sqrtRatioAX96 (lower bound) and sqrtRatioBX96 (upper bound). +// +// Parameters: +// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - liquidity: The liquidity to be provided (*u256.Uint). +// +// Returns: +// - *u256.Uint: The amount of token0 required to achieve the specified liquidity level. +// +// Notes: +// - This function assumes the price bounds are expressed in Q64.96 fixed-point format. +// - The function returns 0 if the liquidity is 0 or the price bounds are invalid. +// - Handles edge cases where sqrtRatioAX96 equals sqrtRatioBX96 by returning 0 (to prevent division by zero). +func computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) + if sqrtRatioAX96.IsZero() || sqrtRatioBX96.IsZero() || liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { + return zero + } + + val1 := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) + val2 := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) + + res := u256.MulDiv(val1, val2, sqrtRatioBX96) + res = res.Div(res, sqrtRatioAX96) + + return res +} + +// computeAmount1ForLiquidity calculates the required amount of token1 for a given liquidity level +// within a specified price range (represented by sqrt ratios). +// +// This function determines the amount of token1 needed to provide liquidity between the +// lower (sqrtRatioAX96) and upper (sqrtRatioBX96) price bounds. The calculation is performed +// in Q64.96 fixed-point format, which is standard for many liquidity calculations. +// +// Parameters: +// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - liquidity: The liquidity amount to be used in the calculation (*u256.Uint). +// +// Returns: +// - *u256.Uint: The amount of token1 required to achieve the specified liquidity level. +// +// Notes: +// - This function handles edge cases where the liquidity is zero or when sqrtRatioAX96 equals sqrtRatioBX96 +// to prevent division by zero. +// - The calculation assumes sqrtRatioAX96 is always less than or equal to sqrtRatioBX96 after the initial +// ascending order sorting. +func computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) + if liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { + return zero + } + + diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) + res := u256.MulDiv(liquidity, diff, q96Uint) + + return res +} + +// GetAmountsForLiquidity calculates the amounts of token0 and token1 required +// to provide a specified liquidity within a price range. +// +// This function determines the quantities of token0 and token1 necessary to achieve +// a given liquidity level, depending on the current price (sqrtRatioX96) and the +// bounds of the price range (sqrtRatioAX96 and sqrtRatioBX96). The function returns +// the calculated amounts of token0 and token1 as strings. +// +// If the current price is below the lower bound of the price range, only token0 is required. +// If the current price is above the upper bound, only token1 is required. When the +// price is within the range, both token0 and token1 are calculated. +// +// Parameters: +// - sqrtRatioX96: The current price represented as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). +// - liquidity: The amount of liquidity to be provided (*u256.Uint). +// +// Returns: +// - string: The calculated amount of token0 required to achieve the specified liquidity. +// - string: The calculated amount of token1 required to achieve the specified liquidity. +// +// Notes: +// - If liquidity is zero, the function returns "0" for both token0 and token1. +// - The function guarantees that sqrtRatioAX96 is always the lower bound and +// sqrtRatioBX96 is the upper bound by calling toAscendingOrder(). +// - Edge cases where the current price is exactly on the bounds are handled without division by zero. +// +// Example: +// ``` +// amount0, amount1 := GetAmountsForLiquidity( +// +// u256.MustFromDecimal("79228162514264337593543950336"), // sqrtRatioX96 (1.0 in Q64.96) +// u256.MustFromDecimal("39614081257132168796771975168"), // sqrtRatioAX96 (0.5 in Q64.96) +// u256.MustFromDecimal("158456325028528675187087900672"), // sqrtRatioBX96 (2.0 in Q64.96) +// u256.MustFromDecimal("1000000"), // Liquidity +// +// ) +// +// println("Token0:", amount0, "Token1:", amount1) +// +// // Output: +// Token0: 500000, Token1: 250000 +// ``` +func GetAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) (string, string) { + if liquidity.IsZero() { + return "0", "0" + } + + sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) + + amount0 := u256.Zero() + amount1 := u256.Zero() + + if sqrtRatioX96.Lte(sqrtRatioAX96) { + amount0 = computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) + } else if sqrtRatioX96.Lt(sqrtRatioBX96) { + amount0 = computeAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity) + amount1 = computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity) + } else { + amount1 = computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) + } + + return amount0.ToString(), amount1.ToString() +} diff --git a/contract/r/gnoswap/v1/common/tick_math.gno b/contract/r/gnoswap/v1/common/tick_math.gno new file mode 100644 index 0000000..02bb47d --- /dev/null +++ b/contract/r/gnoswap/v1/common/tick_math.gno @@ -0,0 +1,263 @@ +package common + +import ( + "gno.land/p/nt/ufmt" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +// Pre-calculated ratio constants for performance optimization. +// These values are pre-computed to avoid runtime decimal parsing. +var ( + // Initial ratio constants - exactly matching Uniswap V3 + ratio0 = u256.MustFromDecimal("340265354078544963557816517032075149313") // 0xfffcb933bd6fad37aa2d162d1a594001 + ratio1 = u256.MustFromDecimal("340282366920938463463374607431768211456") // 0x100000000000000000000000000000000 (2^128) + + // Bit mask ratio constants in order (bit 1 to bit 19) + ratioConstants = []*u256.Uint{ + u256.MustFromDecimal("340248342086729790484326174814286782778"), // 0xfff97272373d413259a46990580e213a (bit 1) + u256.MustFromDecimal("340214320654664324051920982716015181260"), // 0xfff2e50f5f656932ef12357cf3c7fdcc (bit 2) + u256.MustFromDecimal("340146287995602323631171512101879684304"), // 0xffe5caca7e10e4e61c3624eaa0941cd0 (bit 3) + u256.MustFromDecimal("340010263488231146823593991679159461444"), // 0xffcb9843d60f6159c9db58835c926644 (bit 4) + u256.MustFromDecimal("339738377640345403697157401104375502016"), // 0xff973b41fa98c081472e6896dfb254c0 (bit 5) + u256.MustFromDecimal("339195258003219555707034227454543997025"), // 0xff2ea16466c96a3843ec78b326b52861 (bit 6) + u256.MustFromDecimal("338111622100601834656805679988414885971"), // 0xfe5dee046a99a2a811c461f1969c3053 (bit 7) + u256.MustFromDecimal("335954724994790223023589805789778977700"), // 0xfcbe86c7900a88aedcffc83b479aa3a4 (bit 8) + u256.MustFromDecimal("331682121138379247127172139078559817300"), // 0xf987a7253ac413176f2b074cf7815e54 (bit 9) + u256.MustFromDecimal("323299236684853023288211250268160618739"), // 0xf3392b0822b70005940c7a398e4b70f3 (bit 10) + u256.MustFromDecimal("307163716377032989948697243942600083929"), // 0xe7159475a2c29b7443b29c7fa6e889d9 (bit 11) + u256.MustFromDecimal("277268403626896220162999269216087595045"), // 0xd097f3bdfd2022b8845ad8f792aa5825 (bit 12) + u256.MustFromDecimal("225923453940442621947126027127485391333"), // 0xa9f746462d870fdf8a65dc1f90e061e5 (bit 13) + u256.MustFromDecimal("149997214084966997727330242082538205943"), // 0x70d869a156d2a1b890bb3df62baf32f7 (bit 14) + u256.MustFromDecimal("66119101136024775622716233608466517926"), // 0x31be135f97d08fd981231505542fcfa6 (bit 15) + u256.MustFromDecimal("12847376061809297530290974190478138313"), // 0x9aa508b5b7a84e1c677de54f3e99bc9 (bit 16) + u256.MustFromDecimal("485053260817066172746253684029974020"), // 0x5d6af8dedb81196699c329225ee604 (bit 17) + u256.MustFromDecimal("691415978906521570653435304214168"), // 0x2216e584f5fa1ea926041bedfe98 (bit 18) + u256.MustFromDecimal("1404880482679654955896180642"), // 0x48a170391f7dc42444e8fa2 (bit 19) + } + + // Pre-computed constants for optimization + maxUint256 = u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") // 2^256 - 1 + minSqrtRatio = u256.MustFromDecimal("4295128739") // same as TickMathGetSqrtRatioAtTick(minTick) + maxSqrtRatio = u256.MustFromDecimal("1461446703485210103287273052203988822378723970342") // same as TickMathGetSqrtRatioAtTick(maxTick) + + // MSB calculation thresholds - pre-computed for performance + msb128Threshold = u256.MustFromDecimal("340282366920938463463374607431768211455") // 2^128 - 1 + msb64Threshold = u256.MustFromDecimal("18446744073709551615") // 2^64 - 1 + msb32Threshold = u256.MustFromDecimal("4294967295") // 2^32 - 1 + msb16Threshold = u256.NewUint(65535) // 2^16 - 1 + msb8Threshold = u256.NewUint(255) // 2^8 - 1 + msb4Threshold = u256.NewUint(15) // 2^4 - 1 + msb2Threshold = u256.NewUint(3) // 2^2 - 1 + msb1Threshold = u256.One() // 1 + + // Pre-computed constants for tick calculation + log2Multiplier = i256.MustFromDecimal("255738958999603826347141") + tickLowOffset = i256.MustFromDecimal("3402992956809132418596140100660247210") + tickHiOffset = i256.MustFromDecimal("291339464771989622907027621153398088495") + + oneLsh32 = u256.One().Lsh(u256.One(), 32) // 1 << 32 +) + +// TickMathGetSqrtRatioAtTick calculates sqrt price ratio for given tick. +// +// Converts tick index to square root price in Q64.96 fixed-point format. +// Based on Uniswap V3's mathematical formula: price = 1.0001^tick. +// Uses bit manipulation for gas-efficient calculation. +// +// Parameters: +// - tick: Tick index in range [-887272, 887272] +// +// Returns: +// - Square root of price ratio as Q64.96 fixed-point +// - Result represents sqrt(token1/token0) price +// +// Mathematical formula: +// +// sqrtPriceX96 = sqrt(1.0001^tick) * 2^96 +// +// Panics if tick outside valid range. +// Critical for all price calculations in concentrated liquidity. +func TickMathGetSqrtRatioAtTick(tick int32) *u256.Uint { + assertValidTickRange(tick) + absTick := abs(tick) + + // Initialize ratio based on LSB - exactly like Uniswap V3 + var ratio *u256.Uint + + if absTick&0x1 != 0 { + ratio = ratio0.Clone() + } else { + ratio = ratio1.Clone() + } + + temp := u256.Zero() + + // Apply bit masks using optimized loop - maintains exact same logic + for i := 1; i < 20; i++ { + if absTick&(1< 0 { + ratio = temp.Div(maxUint256, ratio) + } + + // Convert from Q128.128 to Q128.96 with rounding up. + // This divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96 + upper := u256.Zero().Rsh(ratio, 32) // ratio >> 32 + remainder := u256.Zero().Mod(ratio, oneLsh32) // ratio % (1 << 32) + + // Round up: add 1 if remainder != 0 + if !remainder.IsZero() { + upper = u256.Zero().Add(upper, u256.One()) + } + + return upper +} + +// getMostSignificantBit returns the position of the most significant bit. +func getMostSignificantBit(r *u256.Uint) (msb uint64) { + temp := r.Clone() + + // Optimized MSB calculation using pre-computed thresholds + if temp.Gt(msb128Threshold) { + msb |= 128 + temp = temp.Rsh(temp, 128) + } + + if temp.Gt(msb64Threshold) { + msb |= 64 + temp = temp.Rsh(temp, 64) + } + + if temp.Gt(msb32Threshold) { + msb |= 32 + temp = temp.Rsh(temp, 32) + } + + if temp.Gt(msb16Threshold) { + msb |= 16 + temp = temp.Rsh(temp, 16) + } + + if temp.Gt(msb8Threshold) { + msb |= 8 + temp = temp.Rsh(temp, 8) + } + + if temp.Gt(msb4Threshold) { + msb |= 4 + temp = temp.Rsh(temp, 4) + } + + if temp.Gt(msb2Threshold) { + msb |= 2 + temp = temp.Rsh(temp, 2) + } + + if temp.Gt(msb1Threshold) { + msb |= 1 + } + + return +} + +// TickMathGetTickAtSqrtRatio calculates tick value for given sqrt price ratio, returning greatest tick where getSqrtRatioAtTick(tick) <= ratio, matching Uniswap V3 exactly. +func TickMathGetTickAtSqrtRatio(sqrtPriceX96 *u256.Uint) int32 { + if sqrtPriceX96.Lt(minSqrtRatio) || sqrtPriceX96.Gte(maxSqrtRatio) { + panic(newErrorWithDetail( + errOutOfRange, + ufmt.Sprintf("sqrtPriceX96(%s) is out of range", sqrtPriceX96.ToString()), + )) + } + + // Scale ratio by 32 bits to convert from Q64.96 to Q96.128 + ratio := u256.Zero().Lsh(sqrtPriceX96, 32) + + // Find MSB using optimized calculation + msb := getMostSignificantBit(ratio) + + // Adjust ratio based on MSB + var r *u256.Uint + + if msb >= 128 { + r = u256.Zero().Rsh(ratio, uint(msb-127)) + } else { + r = u256.Zero().Lsh(ratio, uint(127-msb)) + } + + // Calculate log_2 using fixed-point arithmetic + log2 := i256.NewInt(int64(msb) - 128) + log2 = i256.Zero().Lsh(log2, 64) + + // Define temporary variables for optimization + tempR := u256.Zero() + tempF := u256.Zero() + tempI256 := i256.Zero() + + // Optimized iterative calculation using loop - maintains exact same logic + for i := 0; i < 14; i++ { + tempR = tempR.Mul(r, r) + r = u256.Zero().Rsh(tempR, 127) + + tempF = tempF.Rsh(r, 128) + tempI256 = i256.FromUint256(tempF) + f := tempF + + tempI256 = tempI256.Lsh(tempI256, uint(63-i)) + log2 = log2.Or(log2, tempI256) + r = u256.Zero().Rsh(r, uint(f.Uint64())) + } + + // Calculate tick from log_sqrt10001 + logSqrt10001 := i256.Zero().Mul(log2, log2Multiplier) + + // Calculate tick bounds + tickLow := i256.Zero().Sub(logSqrt10001, tickLowOffset) + tickLow = tickLow.Rsh(tickLow, 128) + tickLowInt32 := int32(tickLow.Int64()) + + tickHi := i256.Zero().Add(logSqrt10001, tickHiOffset) + tickHi = tickHi.Rsh(tickHi, 128) + tickHiInt32 := int32(tickHi.Int64()) + + // Select the appropriate tick + if tickLowInt32 == tickHiInt32 { + return tickLowInt32 + } else if TickMathGetSqrtRatioAtTick(tickHiInt32).Lte(sqrtPriceX96) { + return tickHiInt32 + } + + return tickLowInt32 +} + +// abs returns the absolute value of x. +func abs(x int32) int32 { + if x < 0 { + return -x + } + + return x +} + +// assertValidTickRange panics if tick is outside valid range [-887272, 887272]. +func assertValidTickRange(tick int32) { + if tick > maxTick { + panic(newErrorWithDetail( + errOutOfRange, + ufmt.Sprintf("tick is out of range (larger than 887272), tick: %d", tick), + )) + } + if tick < minTick { + panic(newErrorWithDetail( + errOutOfRange, + ufmt.Sprintf("tick is out of range (smaller than -887272), tick: %d", tick), + )) + } +} diff --git a/contract/r/gnoswap/v1/community_pool/README.md b/contract/r/gnoswap/v1/community_pool/README.md new file mode 100644 index 0000000..170c362 --- /dev/null +++ b/contract/r/gnoswap/v1/community_pool/README.md @@ -0,0 +1,43 @@ +# Community Pool + +GnoSwap community treasury for ecosystem development. + +## Overview + +Community-governed treasury that receives protocol emissions and fees for ecosystem growth initiatives. Also collects unclaimed internal staking rewards from warmup periods. + +## Configuration + +- **Emission Allocation**: 5% of GNS emissions (default) +- **Governance Control**: All disbursements require proposal +- **Fund Sources**: GNS emissions, unclaimed rewards (internal reward only), protocol fees + +## Governance Process + +- **Proposal Creation**: Submit funding request with justification +- **Voting Period**: Token holders vote on proposal +- **Execution**: Approved transfers execute automatically +- **Transparency**: All operations emit events + +## Key Functions + +### `TransferToken` +Transfers tokens to specified address (governance only). + +## Usage + +```go +// Transfer via governance proposal +TransferToken( + "gno.land/r/demo/usdc", + recipientAddr, + 1000000, +) +``` + +## Security + +- Governance-only transfers +- No emergency withdrawals +- Event emission for transparency +- Multi-token support \ No newline at end of file diff --git a/contract/r/gnoswap/v1/community_pool/community_pool.gno b/contract/r/gnoswap/v1/community_pool/community_pool.gno new file mode 100644 index 0000000..d5dd6f4 --- /dev/null +++ b/contract/r/gnoswap/v1/community_pool/community_pool.gno @@ -0,0 +1,51 @@ +package community_pool + +import ( + "std" + "strconv" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" +) + +// TransferToken transfers tokens from the community pool. +// +// Parameters: +// - tokenPath: token contract path +// - to: recipient address +// - amount: transfer amount +// +// Only callable by admin or governance. +func TransferToken(cur realm, tokenPath string, to std.Address, amount int64) { + halt.AssertIsNotHaltedCommunityPool() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + err := transferToken(tokenPath, to, amount) + if err != nil { + panic(err) + } +} + +// transferToken performs actual token transfer. +func transferToken(tokenPath string, to std.Address, amount int64) error { + err := common.Transfer(cross, tokenPath, to, amount) + if err != nil { + return err + } + + prevRealm := std.PreviousRealm() + std.Emit( + "TransferToken", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "tokenPath", tokenPath, + "to", to.String(), + "amount", strconv.FormatInt(amount, 10), + ) + + return nil +} diff --git a/contract/r/gnoswap/v1/community_pool/doc.gno b/contract/r/gnoswap/v1/community_pool/doc.gno new file mode 100644 index 0000000..019f9f2 --- /dev/null +++ b/contract/r/gnoswap/v1/community_pool/doc.gno @@ -0,0 +1,6 @@ +// Package community_pool manages the GnoSwap community treasury. +// +// This contract holds protocol-owned assets that can be allocated through +// governance proposals. It receives a portion of GNS emissions and can be +// used for ecosystem development, grants, and protocol improvements. +package community_pool diff --git a/contract/r/gnoswap/v1/community_pool/errors.gno b/contract/r/gnoswap/v1/community_pool/errors.gno new file mode 100644 index 0000000..e060c33 --- /dev/null +++ b/contract/r/gnoswap/v1/community_pool/errors.gno @@ -0,0 +1,13 @@ +package community_pool + +import ( + "errors" +) + +var ( + errNoPermission = errors.New("[GNOSWAP-COMMUNITY_POOL-001] caller has no permission") + errNotRegistered = errors.New("[GNOSWAP-COMMUNITY_POOL-002] not registered") + errAlreadyRegistered = errors.New("[GNOSWAP-COMMUNITY_POOL-003] already registered") + errLocked = errors.New("[GNOSWAP-COMMUNITY_POOL-004] can't transfer token while locked") + errHalted = errors.New("[GNOSWAP-COMMUNITY_POOL-005] halted") +) diff --git a/contract/r/gnoswap/v1/community_pool/gnomod.toml b/contract/r/gnoswap/v1/community_pool/gnomod.toml new file mode 100644 index 0000000..d121d3d --- /dev/null +++ b/contract/r/gnoswap/v1/community_pool/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/community_pool" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/gnft/assert.gno b/contract/r/gnoswap/v1/gnft/assert.gno new file mode 100644 index 0000000..252ef71 --- /dev/null +++ b/contract/r/gnoswap/v1/gnft/assert.gno @@ -0,0 +1,37 @@ +package gnft + +import ( + "std" + + "gno.land/p/demo/tokens/grc721" + "gno.land/p/nt/ufmt" +) + +// assertIsValidTokenURI panics if the token already has a URI set. +func assertIsValidTokenURI(tid grc721.TokenID) { + uri, _ := nft.TokenURI(tid) + if string(uri) != "" { + panic(makeErrorWithDetails(errCannotSetURI, ufmt.Sprintf("token id (%s) has already set URI", string(tid)))) + } +} + +// assertIsValidAddress panics if the address is invalid. +func assertIsValidAddress(addr std.Address) { + if !addr.IsValid() { + panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("address (%s)", addr.String()))) + } +} + +// assertFromIsValidAddress panics if the from address is invalid. +func assertFromIsValidAddress(from std.Address) { + if !from.IsValid() { + panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("from address (%s)", from.String()))) + } +} + +// assertToIsValidAddress panics if the to address is invalid. +func assertToIsValidAddress(to std.Address) { + if !to.IsValid() { + panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("to address (%s)", to.String()))) + } +} diff --git a/contract/r/gnoswap/v1/gnft/errors.gno b/contract/r/gnoswap/v1/gnft/errors.gno new file mode 100644 index 0000000..bb1bc0a --- /dev/null +++ b/contract/r/gnoswap/v1/gnft/errors.gno @@ -0,0 +1,31 @@ +package gnft + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errNoPermission = errors.New("[GNOSWAP-GNFT-001] caller has no permission") + errNotTokenOwner = errors.New("[GNOSWAP-GNFT-001] caller is not token owner") + + errCannotSetURI = errors.New("[GNOSWAP-GNFT-002] cannot set URI") + errTokenDoesNotExist = errors.New("[GNOSWAP-GNFT-002] cannot set URI || token does not exist") + errTokenBurned = errors.New("[GNOSWAP-GNFT-002] cannot set URI || token has been burned") + + errNoTokenForCaller = errors.New("[GNOSWAP-GNFT-003] no token for caller") + errInvalidAddress = errors.New("[GNOSWAP-GNFT-004] invalid addresss") + errInvalidTokenID = errors.New("[GNOSWAP-GNFT-005] invalid token ID") + + // Transfer errors + errNotOwnerOrApproved = errors.New("[GNOSWAP-GNFT-006] caller is not token owner or approved") + errTokenNotExists = errors.New("[GNOSWAP-GNFT-007] token does not exist") + errTransferToSelf = errors.New("[GNOSWAP-GNFT-008] cannot transfer to self") + errNotApprovedForAll = errors.New("[GNOSWAP-GNFT-009] not approved for all tokens") +) + +// makeErrorWithDetails creates an error with additional context. +func makeErrorWithDetails(err error, details string) error { + return ufmt.Errorf("%s || %s", err.Error(), details) +} diff --git a/contract/r/gnoswap/v1/gnft/gnft.gno b/contract/r/gnoswap/v1/gnft/gnft.gno new file mode 100644 index 0000000..0fd86a4 --- /dev/null +++ b/contract/r/gnoswap/v1/gnft/gnft.gno @@ -0,0 +1,272 @@ +package gnft + +import ( + "std" + + "gno.land/p/demo/tokens/grc721" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" +) + +var ( + nft = grc721.NewBasicNFT("GNOSWAP NFT", "GNFT") + owner = ownable.NewWithAddress(getPositionAddress()) +) + +// Name returns the NFT collection name. +func Name() string { + return nft.Name() +} + +// Symbol returns the NFT symbol. +func Symbol() string { + return nft.Symbol() +} + +// TotalSupply returns the total number of NFTs minted. +func TotalSupply() int64 { + return nft.TokenCount() +} + +// TokenURI returns the metadata URI for the specified token ID. +func TokenURI(tid grc721.TokenID) (string, error) { + uri, err := nft.TokenURI(tid) + if err != nil { + return "", err + } + + return string(uri), nil +} + +// BalanceOf returns the number of NFTs owned by the specified address. +func BalanceOf(owner std.Address) (int64, error) { + assertIsValidAddress(owner) + + balance, err := nft.BalanceOf(owner) + if err != nil { + return 0, err + } + return balance, nil +} + +// OwnerOf returns the owner address for the specified token ID. +func OwnerOf(tid grc721.TokenID) (std.Address, error) { + ownerAddr, err := nft.OwnerOf(tid) + if err != nil { + return "", err + } + + return ownerAddr, nil +} + +// MustOwnerOf returns the owner address for the specified token ID. +// It panics if the token ID is invalid. +func MustOwnerOf(tid grc721.TokenID) std.Address { + ownerAddr, err := OwnerOf(tid) + if err != nil { + panic(err.Error()) + } + + return ownerAddr +} + +// SetTokenURI sets the metadata URI for the specified token. +// +// Parameters: +// - tid: token ID +// - tURI: token URI +// +// Only callable by position contract. +func SetTokenURI(cur realm, tid grc721.TokenID, tURI grc721.TokenURI) (bool, error) { + halt.AssertIsNotHaltedPosition() + + assertIsValidTokenURI(tid) + + err := setTokenURI(tid, tURI) + if err != nil { + panic(err) + } + + return true, nil +} + +// SafeTransferFrom transfers token ownership with receiver validation. +// +// Parameters: +// - from: current owner address +// - to: recipient address +// - tid: token ID to transfer +// +// Returns error if transfer fails. +// Only callable by staker contract. +func SafeTransferFrom(cur realm, from, to std.Address, tid grc721.TokenID) error { + halt.AssertIsNotHaltedPosition() + + caller := std.PreviousRealm().Address() + access.AssertIsStaker(caller) + + assertFromIsValidAddress(from) + assertToIsValidAddress(to) + + err := nft.SafeTransferFrom(from, to, tid) + checkTransferErr(err, from, to, tid) + return nil +} + +// TransferFrom transfers a token from one address to another. +// +// Parameters: +// - from: current owner address +// - to: recipient address +// - tid: token ID +// +// Returns error if transfer fails. +// Only callable by staker contract. +func TransferFrom(cur realm, from, to std.Address, tid grc721.TokenID) error { + halt.AssertIsNotHaltedPosition() + + caller := std.PreviousRealm().Address() + access.AssertIsStaker(caller) + + assertFromIsValidAddress(from) + assertToIsValidAddress(to) + + err := nft.TransferFrom(from, to, tid) + checkTransferErr(err, from, to, tid) + return nil +} + +// Approve grants permission to transfer a specific token ID to another address. +// +// Parameters: +// - approved: address to approve +// - tid: token ID to approve for transfer +// +// Returns error if approval fails. +// Only callable when not halted. +func Approve(cur realm, approved std.Address, tid grc721.TokenID) error { + halt.AssertIsNotHaltedPosition() + assertIsValidAddress(approved) + + err := nft.Approve(approved, tid) + checkApproveErr(err, approved, tid) + return nil +} + +// SetApprovalForAll enables/disables operator approval for all tokens. +// +// Parameters: +// - operator: address to set approval for +// - approved: true to approve, false to revoke +// +// Returns error if operation fails. +// Only callable when not halted. +func SetApprovalForAll(cur realm, operator std.Address, approved bool) error { + halt.AssertIsNotHaltedPosition() + assertIsValidAddress(operator) + + checkErr(nft.SetApprovalForAll(operator, approved)) + return nil +} + +// GetApproved returns approved address for token ID. +// +// Parameters: +// - tid: token ID to check +// +// Returns approved address and error if token doesn't exist. +func GetApproved(tid grc721.TokenID) (std.Address, error) { + return nft.GetApproved(tid) +} + +// IsApprovedForAll checks if operator can manage all owner's tokens. +// +// Parameters: +// - owner: token owner address +// - operator: operator address to check +// +// Returns true if operator is approved for all owner's tokens. +func IsApprovedForAll(owner, operator std.Address) bool { + return nft.IsApprovedForAll(owner, operator) +} + +// Mint creates new NFT and transfers to address. +// +// Parameters: +// - to: recipient address +// - tid: token ID +// +// Returns minted token ID. +// Only callable by position contract. +func Mint(cur realm, to std.Address, tid grc721.TokenID) grc721.TokenID { + halt.AssertIsNotHaltedPosition() + owner.AssertOwnedByPrevious() + + ownerAddress := owner.Owner() + + checkErr(nft.Mint(ownerAddress, tid)) + + tokenURI := genImageURI(generateRandInstance()) + err := setTokenURI(tid, grc721.TokenURI(tokenURI)) + if err != nil { + panic(err) + } + + checkErr(nft.TransferFrom(ownerAddress, to, tid)) + + return tid +} + +// Exists checks if token ID exists. +func Exists(tid grc721.TokenID) bool { + _, err := nft.OwnerOf(tid) + if err != nil { + return false + } + + return true +} + +// Burn removes a specific token ID. +// +// Parameters: +// - tid: token ID to burn +// +// Only callable by owner. +func Burn(cur realm, tid grc721.TokenID) { + halt.AssertIsNotHaltedPosition() + owner.AssertOwnedByPrevious() + + checkErr(nft.Burn(tid)) +} + +// Render returns the HTML representation of the NFT. +func Render(path string) string { + switch { + case path == "": + return nft.RenderHome() + default: + return "404\n" + } +} + +// setTokenURI sets the metadata URI for a specific token ID. +func setTokenURI(tid grc721.TokenID, tURI grc721.TokenURI) error { + _, err := nft.SetTokenURI(tid, tURI) + if err != nil { + return makeErrorWithDetails(err, ufmt.Sprintf("token id (%s)", tid)) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "SetTokenURI", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "tokenId", string(tid), + "tokenURI", string(tURI), + ) + + return nil +} diff --git a/contract/r/gnoswap/v1/gnft/gnomod.toml b/contract/r/gnoswap/v1/gnft/gnomod.toml new file mode 100644 index 0000000..4291218 --- /dev/null +++ b/contract/r/gnoswap/v1/gnft/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/gnft" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/gnft/svg_generator.gno b/contract/r/gnoswap/v1/gnft/svg_generator.gno new file mode 100644 index 0000000..e0c8936 --- /dev/null +++ b/contract/r/gnoswap/v1/gnft/svg_generator.gno @@ -0,0 +1,69 @@ +package gnft + +import ( + b64 "encoding/base64" + "math/rand" + "strings" + + "gno.land/p/nt/ufmt" +) + +var baseTempalte = ` + + + + + + + + + + + + + + + + + + + + + + + +` + +// charset contains valid hex digits for color generation. +const charset = "0123456789ABCDEF" + +// genImageURI generates a base64-encoded SVG image URI with random gradient colors. +func genImageURI(r *rand.Rand) string { + imageRaw := genImageRaw(r) + sEnc := b64.StdEncoding.EncodeToString([]byte(imageRaw)) + + return "data:image/svg+xml;base64," + sEnc +} + +// genImageRaw generates an SVG image with random gradient parameters. +func genImageRaw(r *rand.Rand) string { + x1 := 7 + r.Uint64N(7) + y1 := 7 + r.Uint64N(7) + + x2 := 121 + r.Uint64N(6) + y2 := 121 + r.Uint64N(6) + + var color1, color2 strings.Builder + color1.Grow(7) + color2.Grow(7) + color1.WriteByte('#') + color2.WriteByte('#') + + for i := 0; i < 6; i++ { + color1.WriteByte(charset[r.IntN(16)]) + color2.WriteByte(charset[r.IntN(16)]) + } + + randImage := ufmt.Sprintf(baseTempalte, x1, y1, x2, y2, color1.String(), color2.String()) + return randImage +} diff --git a/contract/r/gnoswap/v1/gnft/utils.gno b/contract/r/gnoswap/v1/gnft/utils.gno new file mode 100644 index 0000000..366c002 --- /dev/null +++ b/contract/r/gnoswap/v1/gnft/utils.gno @@ -0,0 +1,114 @@ +package gnft + +import ( + "math/rand" + "std" + "time" + + "gno.land/p/demo/tokens/grc721" + "gno.land/p/nt/ufmt" + prabc "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/rbac" +) + +func getPositionAddress() std.Address { + addr, exists := access.GetAddress(prabc.ROLE_POSITION.String()) + if !exists { + return rbac.DefaultRoleAddresses[prabc.ROLE_POSITION] + } + + return addr +} + +// tid converts uint64 to grc721.TokenID. +func tid(id uint64) grc721.TokenID { + return grc721.TokenID(ufmt.Sprintf("%d", id)) +} + +// generateRandInstance generates a new random instance. +func generateRandInstance() *rand.Rand { + seed1 := time.Now().Unix() + TotalSupply() + seed2 := time.Now().UnixNano() + TotalSupply() + pcg := rand.NewPCG(uint64(seed1), uint64(seed2)) + return rand.New(pcg) +} + +// checkErr panics if an error occurs. +func checkErr(err error) { + if err != nil { + panic(err.Error()) + } +} + +// checkTransferErr wraps transfer errors with more specific context. +func checkTransferErr(err error, from, to std.Address, tid grc721.TokenID) { + if err == nil { + return + } + + caller := std.PreviousRealm().Address() + + // Check if token exists + owner, ownerErr := nft.OwnerOf(tid) + if ownerErr != nil { + panic(ownerErr) + } + + switch err { + case grc721.ErrCallerIsNotOwnerOrApproved: + // Check if caller is the owner + if caller == owner { + panic(makeErrorWithDetails(grc721.ErrTransferFromIncorrectOwner, ufmt.Sprintf("owner mismatch - from: %s, actual owner: %s, token: %s", from, owner, string(tid)))) + } + + // Check if caller is approved for this specific token + approved, _ := nft.GetApproved(tid) + if approved != caller { + // Check if caller is approved for all tokens + if !nft.IsApprovedForAll(owner, caller) { + panic(makeErrorWithDetails(grc721.ErrCallerIsNotOwnerOrApproved, ufmt.Sprintf("caller %s is not owner %s or approved for token %s", caller, owner, string(tid)))) + } + } + + case grc721.ErrInvalidAddress: + panic(makeErrorWithDetails(grc721.ErrInvalidAddress, ufmt.Sprintf("to address (%s)", to))) + + case grc721.ErrTransferFromIncorrectOwner: + panic(makeErrorWithDetails(grc721.ErrTransferFromIncorrectOwner, ufmt.Sprintf("from %s is not the owner %s of token %s", from, owner, string(tid)))) + + case grc721.ErrInvalidTokenId: + panic(makeErrorWithDetails(grc721.ErrInvalidTokenId, ufmt.Sprintf("token %s", string(tid)))) + + default: + panic(err.Error()) + } +} + +// checkApproveErr wraps approve errors with more specific context. +func checkApproveErr(err error, approved std.Address, tid grc721.TokenID) { + if err == nil { + return + } + + errMsg := err.Error() + caller := std.PreviousRealm().Address() + + // Check if token exists + owner, ownerErr := nft.OwnerOf(tid) + if ownerErr != nil { + panic(makeErrorWithDetails(errTokenNotExists, ufmt.Sprintf("token %s", string(tid)))) + } + + switch { + case errMsg == "caller is not token owner or approved": + panic(makeErrorWithDetails(errNotOwnerOrApproved, ufmt.Sprintf("caller %s cannot approve for token %s owned by %s", caller, string(tid), owner))) + + case errMsg == "approval to current owner": + panic(makeErrorWithDetails(errTransferToSelf, ufmt.Sprintf("cannot approve to current owner %s for token %s", approved, string(tid)))) + + default: + panic(err.Error()) + } +} diff --git a/contract/r/gnoswap/v1/gov/README.md b/contract/r/gnoswap/v1/gov/README.md new file mode 100644 index 0000000..2c5deb5 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/README.md @@ -0,0 +1,103 @@ +# Governance + +Decentralized protocol governance via GNS staking and voting. + +## Overview + +Governance system enables GNS holders to stake for xGNS voting power, create proposals, and vote on protocol changes. For more details, check out [docs](https://docs.gnoswap.io/core-concepts/governance). + +## Configuration + +- **Voting Period**: 7 days +- **Quorum**: 50% of xGNS supply +- **Proposal Threshold**: 1,000 GNS +- **Execution Delay**: 1 day timelock +- **Execution Window**: 30 days +- **Undelegation Lockup**: 7 days +- **Vote Weight Smoothing**: 24 hours + +## Core Mechanics + +### Staking Flow +``` +GNS → Stake → xGNS (voting power) → Delegate → Vote +``` +1. Stake GNS to receive equal xGNS +2. Delegate voting power (can be self) +3. Vote on proposals with delegated power +4. 7-day lockup for undelegation + +### Proposal Types +- **Text**: Signal proposals without execution +- **CommunityPoolSpend**: Treasury disbursements +- **ParameterChange**: Protocol parameter updates + +## Proposal Lifecycle + +### Creation +- Requires 1,000 GNS balance +- One active proposal per address +- Valid type and parameters required + +### Voting +- 1 day delay before voting starts +- 7 days voting period +- Weight = 24hr average delegation (prevents flash loans) + +### Execution +- Requires quorum (50%) and majority (>50%) +- 1 day timelock after voting +- 30 day execution window +- Anyone can trigger execution + +## Technical Details + +### Vote Weight Calculation +```go +// 24-hour average prevents manipulation +snapshot1 = getDelegationAt(proposalTime - 24hr) +snapshot2 = getDelegationAt(proposalTime) +voteWeight = (snapshot1 + snapshot2) / 2 +``` + +### Dynamic Quorum +```go +activeXGNS = totalXGNS - launchpadXGNS +requiredVotes = activeXGNS * 0.5 +``` + +### Rewards Distribution +xGNS holders earn protocol fees: +``` +userShare = (userXGNS / totalXGNS) * protocolFees +``` + +## Usage + +```go +// Stake GNS for xGNS +Delegate(amount, delegateTo) + +// Create proposal +ProposeText(title, description, body) +ProposeCommunityPoolSpend(recipient, amount) +ProposeParameterChange(params) + +// Vote on proposal +Vote(proposalId, true) // YES +Vote(proposalId, false) // NO + +// Execute after timelock +Execute(proposalId) + +// Undelegate (7-day lockup) +Undelegate() +``` + +## Security + +- Flash loan protection via vote smoothing +- Sybil resistance through stake weighting +- Timelock prevents rushed execution +- Single proposal limit per address +- Dynamic quorum excludes inactive xGNS \ No newline at end of file diff --git a/contract/r/gnoswap/v1/gov/doc.gno b/contract/r/gnoswap/v1/gov/doc.gno new file mode 100644 index 0000000..94cf398 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/doc.gno @@ -0,0 +1,5 @@ +// Package gov provides Gnoswap's governance system through three packages: +// 1. governance: Handles proposal creation, voting, and execution +// 2. staker: Manages GNS staking, delegation, and reward distribution +// 3. xgns: Implements the xGNS token representing staked GNS +package gov diff --git a/contract/r/gnoswap/v1/gov/gnomod.toml b/contract/r/gnoswap/v1/gov/gnomod.toml new file mode 100644 index 0000000..15b44f5 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/gov" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/gov/governance/api.gno b/contract/r/gnoswap/v1/gov/governance/api.gno new file mode 100644 index 0000000..e19f41f --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/api.gno @@ -0,0 +1,195 @@ +package governance + +import ( + "std" + "strconv" + "strings" + "time" + + "gno.land/p/onbloc/json" +) + +func createProposalJsonNode(id int64, proposal *Proposal) *json.Node { + return json.Builder(). + WriteString("id", formatInt(id)). + WriteString("configVersion", formatInt(proposal.ConfigVersion)). + WriteString("proposer", proposal.Proposer().String()). + WriteString("status", b64Encode(getProposalStatus(id))). + WriteString("type", proposal.Type().String()). + WriteString("title", proposal.Title()). + WriteString("description", proposal.Description()). + WriteString("vote", b64Encode(getProposalVotes(id))). + WriteString("extra", b64Encode(getProposalExtraData(id))). + Node() +} + +// GetProposalById returns a single proposal with necessary information. +func GetProposalById(id int64) string { + _, exists := getProposal(id) + if !exists { + return "" + } + + proposalsObj := metaNode() + proposalArr := json.ArrayNode("", nil) + proposalObj := getProposalById(id) + proposalArr.AppendArray(proposalObj) + proposalsObj.AppendObject("proposals", proposalArr) + + return marshal(proposalsObj) +} + +// GetVoteStatusFromProposalById returns the vote status(max, yes, no) of a proposal. +func GetVoteStatusFromProposalById(id int64) string { + _, exists := getProposal(id) + if !exists { + return "" + } + + votesObj := metaNode() + votesObj.AppendObject("proposalId", json.StringNode("proposalId", formatInt(id))) + votesObj.AppendObject("votes", json.StringNode("votes", b64Encode(getProposalVotes(id)))) // max, yes, no + + return marshal(votesObj) +} + +// GetVoteByAddressFromProposalById returns the vote of an address from a certain proposal. +func GetVoteByAddressFromProposalById(addr std.Address, id int64) string { + vote, exists := getProposalUserVotingInfo(id, addr) + if !exists { + return "" + } + + votesObj := metaNode() + voteArr := json.ArrayNode("", nil) + voteObj := createVoteJsonNode(addr, id, vote) + voteArr.AppendArray(voteObj) + votesObj.AppendObject("votes", voteArr) + + return marshal(votesObj) +} + +// getProposalById is a helper function for GetProposals and GetProposalById. +func getProposalById(id int64) *json.Node { + proposal := mustGetProposal(id) + return createProposalJsonNode(id, proposal) +} + +func createVoteJsonNode(addr std.Address, id int64, vote *VotingInfo) *json.Node { + return json.Builder(). + WriteString("proposalId", formatInt(id)). + WriteString("voteYes", formatBool(vote.votedYes)). + WriteString("voteWeight", formatInt(vote.votedWeight)). + WriteString("voteHeight", formatInt(vote.votedHeight)). + WriteString("voteTimestamp", formatInt(vote.votedAt)). + Node() +} + +// getProposalExtraData returns extra data of a proposal based on its type. +func getProposalExtraData(proposalId int64) string { + proposal, exist := getProposal(proposalId) + if !exist { + return "" + } + + switch proposal.Type() { + case Text: + return "" + case CommunityPoolSpend: + return getCommunityPoolSpendProposalData(proposalId) + case ParameterChange: + return getParameterChangeProposalData(proposalId) + } + + return "" +} + +// getCommunityPoolSpendProposalData returns community pool spending proposal data including recipient address, token path, and amount. +func getCommunityPoolSpendProposalData(proposalId int64) string { + proposal := mustGetProposal(proposalId) + spend := proposal.data.CommunityPoolSpend() + + proposalObj := json.Builder(). + WriteString("to", spend.to.String()). + WriteString("tokenPath", spend.tokenPath). + WriteString("amount", formatInt(spend.amount)). + Node() + + return marshal(proposalObj) +} + +// getParameterChangeProposalData returns parameter change proposal data as a joined string of messages. +func getParameterChangeProposalData(proposalId int64) string { + proposal := mustGetProposal(proposalId) + + msgs := proposal.data.Execution().msgs + msgsStr := strings.Join(msgs, "*GOV*") + + return msgsStr +} + +// getProposalStatus returns status of a proposal. +func getProposalStatus(id int64) string { + proposal, exist := getProposal(id) + if !exist { + return "" + } + + // Get current status dynamically + status := proposal.Status(time.Now().Unix()) + + schedule := proposal.status.schedule + // Create status node with schedule and current status + node := json.Builder(). + WriteString("status", status). + WriteString("createTime", formatInt(schedule.createTime)). + WriteString("activeTime", formatInt(schedule.activeTime)). + WriteString("votingEndTime", formatInt(schedule.votingEndTime)). + WriteString("executableTime", formatInt(schedule.executableTime)). + WriteString("expiredTime", formatInt(schedule.expiredTime)) + + // Add action state if applicable + if proposal.status.IsCanceled(time.Now().Unix()) { + node. + WriteString("canceled", formatBool(true)). + WriteString("canceledAt", formatInt(proposal.status.actionStatus.canceledAt)). + WriteString("canceledBy", proposal.status.actionStatus.canceledBy.String()) + } + if proposal.status.IsExecuted(time.Now().Unix()) { + node. + WriteString("executed", formatBool(true)). + WriteString("executedAt", formatInt(proposal.status.actionStatus.executedAt)). + WriteString("executedBy", proposal.status.actionStatus.executedBy.String()) + } + + return marshal(node.Node()) +} + +// getProposalVotes returns votes of a proposal. +func getProposalVotes(id int64) string { + proposal, exist := getProposal(id) + if !exist { + return "" + } + voting := proposal.status.voteStatus + maxVoting := formatInt(voting.maxVotingWeight) + + proposalObj := json.Builder(). + WriteString("quorum", formatInt(voting.quorumAmount)). + WriteString("max", maxVoting). + WriteString("yes", formatInt(voting.yea)). + WriteString("no", formatInt(voting.nay)). + Node() + + return marshal(proposalObj) +} + +func metaNode() *json.Node { + height := std.ChainHeight() + now := time.Now().Unix() + + return json.Builder(). + WriteString("height", strconv.FormatInt(height, 10)). + WriteString("now", strconv.FormatInt(now, 10)). + Node() +} diff --git a/contract/r/gnoswap/v1/gov/governance/assert.gno b/contract/r/gnoswap/v1/gov/governance/assert.gno new file mode 100644 index 0000000..920780e --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/assert.gno @@ -0,0 +1,15 @@ +package governance + +import "std" + +// assertCallerIsProposer panics if the caller is not the proposer of the given proposal. +func assertCallerIsProposer(proposalID int64, caller std.Address) { + proposal, exists := getProposal(proposalID) + if !exists { + panic(errProposalNotFound) + } + + if !proposal.IsProposer(caller) { + panic(errNotProposer) + } +} diff --git a/contract/r/gnoswap/v1/gov/governance/config.gno b/contract/r/gnoswap/v1/gov/governance/config.gno new file mode 100644 index 0000000..6527982 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/config.gno @@ -0,0 +1,116 @@ +package governance + +import ( + "std" + + "gno.land/r/gnoswap/access" + en "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" +) + +// Config represents the configuration of the governor contract +// All parameters in this struct can be modified through governance. +type Config struct { + // VotingStartDelay is the delay before voting starts after proposal creation (in seconds) + VotingStartDelay int64 + // VotingPeriod is the duration during which votes are collected (in seconds) + VotingPeriod int64 + // VotingWeightSmoothingDuration is the period over which voting weight is averaged + // for proposal creation and cancellation threshold calculations (in seconds) + VotingWeightSmoothingDuration int64 + // Quorum is the percentage of total GNS supply required for proposal approval + Quorum int64 + // ProposalCreationThreshold is the minimum average voting weight required to create a proposal + ProposalCreationThreshold int64 + // ExecutionDelay is the waiting period after voting ends before a proposal can be executed (in seconds) + ExecutionDelay int64 + // ExecutionWindow is the time window during which an approved proposal can be executed (in seconds) + ExecutionWindow int64 +} + +// Reconfigure updates governance configuration. +// Only admin or governance contract can call this function. +// Updates all governance parameters and emits a "Reconfigure" event. +func Reconfigure( + cur realm, + votingStartDelay int64, + votingPeriod int64, + votingWeightSmoothingDuration int64, + quorum int64, + proposalCreationThreshold int64, + executionDelay int64, + executionWindow int64, +) int64 { + // Check if system is halted before proceeding + halt.AssertIsNotHaltedGovernance() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + // Mint and distribute GNS tokens as part of the process + en.MintAndDistributeGns(cross) + + // Store previous version for event emission + previousVersion := getCurrentConfigVersion() + + // Apply the new configuration + nextVersion, newCfg := reconfigure( + votingStartDelay, + votingPeriod, + votingWeightSmoothingDuration, + quorum, + proposalCreationThreshold, + executionDelay, + executionWindow, + ) + + // Emit configuration change event with all parameters + previousRealm := std.PreviousRealm() + std.Emit( + "Reconfigure", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "votingStartDelay", formatInt(newCfg.VotingStartDelay), + "votingPeriod", formatInt(newCfg.VotingPeriod), + "votingWeightSmoothingDuration", formatInt(newCfg.VotingWeightSmoothingDuration), + "quorum", formatInt(newCfg.Quorum), + "proposalCreationThreshold", formatInt(newCfg.ProposalCreationThreshold), + "executionDelay", formatInt(newCfg.ExecutionDelay), + "executionPeriod", formatInt(newCfg.ExecutionWindow), + "newConfigVersion", formatInt(nextVersion), + "prevConfigVersion", formatInt(previousVersion), + ) + + return nextVersion +} + +// reconfigure updates the Governor's configuration. +// Creates new configuration and stores it with incremented version number. +func reconfigure( + votingStartDelay int64, + votingPeriod int64, + votingWeightSmoothingDuration int64, + quorum int64, + proposalCreationThreshold int64, + executionDelay int64, + executionWindow int64, +) (int64, Config) { + // Create new configuration with provided parameters + cfg := Config{ + VotingStartDelay: votingStartDelay, + VotingPeriod: votingPeriod, + VotingWeightSmoothingDuration: votingWeightSmoothingDuration, + Quorum: quorum, + ProposalCreationThreshold: proposalCreationThreshold, + ExecutionDelay: executionDelay, + ExecutionWindow: executionWindow, + } + + // Generate next version number + nextVersion := nextConfigVersion() + + // Store the new configuration with version + setConfig(nextVersion, cfg) + + return nextVersion, cfg +} diff --git a/contract/r/gnoswap/v1/gov/governance/consts.gno b/contract/r/gnoswap/v1/gov/governance/consts.gno new file mode 100644 index 0000000..9c248a7 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/consts.gno @@ -0,0 +1,16 @@ +package governance + +const ( + // Governance can execute multiple messages in a single proposal + // each message is a string with the following format: + // *EXE**EXE* + // To execute a message, we need to parse the message and call the corresponding function + // with the given parameters + parameterSeparator = "*EXE*" + + messageSeparator = "*GOV*" + + maxTitleLength = 255 + maxDescriptionLength = 10_000 + maxNumberOfExecution = 10 +) diff --git a/contract/r/gnoswap/v1/gov/governance/counter.gno b/contract/r/gnoswap/v1/gov/governance/counter.gno new file mode 100644 index 0000000..6c9fae8 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/counter.gno @@ -0,0 +1,25 @@ +package governance + +// Counter manages unique incrementing IDs. +type Counter struct { + id int64 +} + +// NewCounter creates a new Counter starting at 0. +func NewCounter() *Counter { + return &Counter{ + id: 0, + } +} + +// next increments and returns the next ID. +func (c *Counter) next() int64 { + c.id++ + + return c.id +} + +// Get returns the current ID without incrementing. +func (c *Counter) Get() int64 { + return c.id +} diff --git a/contract/r/gnoswap/v1/gov/governance/doc.gno b/contract/r/gnoswap/v1/gov/governance/doc.gno new file mode 100644 index 0000000..3fe7f3d --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/doc.gno @@ -0,0 +1,5 @@ +// Package governance implements proposal lifecycle management and voting. +// It supports text proposals, parameter changes, and community pool spending. +// Proposals go through creation, voting, and execution phases with configurable +// parameters for voting delays, periods, and thresholds. +package governance diff --git a/contract/r/gnoswap/v1/gov/governance/errors.gno b/contract/r/gnoswap/v1/gov/governance/errors.gno new file mode 100644 index 0000000..b99d038 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/errors.gno @@ -0,0 +1,37 @@ +package governance + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errOutOfRange = errors.New("[GNOSWAP-GOVERNANCE-001] out of range for numeric value") + errInvalidInput = errors.New("[GNOSWAP-GOVERNANCE-002] invalid input") + errDataNotFound = errors.New("[GNOSWAP-GOVERNANCE-003] requested data not found") + errNotEnoughBalance = errors.New("[GNOSWAP-GOVERNANCE-004] not enough balance") + errUnableToVoteCanceledProposal = errors.New("[GNOSWAP-GOVERNANCE-005] unable to vote for canceled proposal") + errAlreadyVoted = errors.New("[GNOSWAP-GOVERNANCE-006] can not vote twice") + errNotEnoughVotingWeight = errors.New("[GNOSWAP-GOVERNANCE-007] not enough voting power") + errAlreadyCanceledProposal = errors.New("[GNOSWAP-GOVERNANCE-008] can not cancel already canceled proposal") + errUnableToCancleVotingProposal = errors.New("[GNOSWAP-GOVERNANCE-009] unable to cancel voting proposal") + errUnableToCancelProposalWithVoterEnoughDelegated = errors.New("[GNOSWAP-GOVERNANCE-010] unable to cancel proposal with voter has enough delegation") + errTextProposalNotExecutable = errors.New("[GNOSWAP-GOVERNANCE-011] can not execute text proposal") + errUnsupportedProposalType = errors.New("[GNOSWAP-GOVERNANCE-012] unsupported proposal type") + errInvalidProposalType = errors.New("[GNOSWAP-GOVERNANCE-013] invalid proposal type") + errUnableToVoteOutOfPeriod = errors.New("[GNOSWAP-GOVERNANCE-014] unable to vote out of voting period") + errInvalidMessageFormat = errors.New("[GNOSWAP-GOVERNANCE-015] invalid message format") + errProposalNotPassed = errors.New("[GNOSWAP-GOVERNANCE-016] proposal not passed") + errInvalidAddress = errors.New("[GNOSWAP-GOVERNANCE-017] invalid address") + errExecutionWindowNotStarted = errors.New("[GNOSWAP-GOVERNANCE-018] execution window not started") + errAlreadyActiveProposal = errors.New("[GNOSWAP-GOVERNANCE-019] already active proposal") + errProposalNotFound = errors.New("[GNOSWAP-GOVERNANCE-020] proposal not found") + errProposalNotExecutable = errors.New("[GNOSWAP-GOVERNANCE-021] proposal not executable") + errNotProposer = errors.New("[GNOSWAP-GOVERNANCE-022] not proposer") +) + +// makeErrorWithDetails creates an error with additional context. +func makeErrorWithDetails(err error, detail string) error { + return ufmt.Errorf("%s || %s", err.Error(), detail) +} diff --git a/contract/r/gnoswap/v1/gov/governance/getter_proposal.gno b/contract/r/gnoswap/v1/gov/governance/getter_proposal.gno new file mode 100644 index 0000000..10fff0b --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/getter_proposal.gno @@ -0,0 +1,71 @@ +package governance + +import "time" + +func mustGetProposal(proposalId int64) *Proposal { + proposal, ok := getProposal(proposalId) + if !ok { + panic(errDataNotFound) + } + + return proposal +} + +func GetProposerByProposalId(proposalId int64) string { + return mustGetProposal(proposalId).proposer.String() +} + +func GetProposalTypeByProposalId(proposalId int64) string { + return mustGetProposal(proposalId).data.proposalType.String() +} + +func GetYeaByProposalId(proposalId int64) int64 { + return mustGetProposal(proposalId).status.YesWeight() +} + +func GetNayByProposalId(proposalId int64) int64 { + return mustGetProposal(proposalId).status.NoWeight() +} + +func GetConfigVersionByProposalId(proposalId int64) int64 { + return mustGetProposal(proposalId).configVersion +} + +func GetQuorumAmountByProposalId(proposalId int64) int64 { + return mustGetProposal(proposalId).status.voteStatus.quorumAmount +} + +func GetTitleByProposalId(proposalId int64) string { + return mustGetProposal(proposalId).metadata.title +} + +func GetDescriptionByProposalId(proposalId int64) string { + return mustGetProposal(proposalId).metadata.description +} + +// GetExecutionStateByProposalId is deprecated. Use GetProposalStatusById instead. +// This function is kept for backward compatibility. +func GetExecutionStateByProposalId(proposalId int64) string { + currentAt := time.Now().Unix() + proposal := mustGetProposal(proposalId) + + return proposal.Status(currentAt) +} + +func GetLatestConfig() Config { + config, ok := getCurrentConfig() + if !ok { + panic(errDataNotFound) + } + + return config +} + +func GetConfig(configVersion int64) Config { + config, ok := getConfig(configVersion) + if !ok { + panic(errDataNotFound) + } + + return config +} diff --git a/contract/r/gnoswap/v1/gov/governance/getter_vote.gno b/contract/r/gnoswap/v1/gov/governance/getter_vote.gno new file mode 100644 index 0000000..d470d28 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/getter_vote.gno @@ -0,0 +1,32 @@ +package governance + +import ( + "std" +) + +func GetVoteWeight(proposalID int64, address std.Address) int64 { + proposalUserVotingInfo, ok := getProposalUserVotingInfo(proposalID, address) + if !ok { + panic(errDataNotFound) + } + + return proposalUserVotingInfo.VotedWeight() +} + +func GetVotedHeight(proposalID int64, address std.Address) int64 { + proposalUserVotingInfo, ok := getProposalUserVotingInfo(proposalID, address) + if !ok { + panic(errDataNotFound) + } + + return proposalUserVotingInfo.votedHeight +} + +func GetVotedAt(proposalID int64, address std.Address) int64 { + proposalUserVotingInfo, ok := getProposalUserVotingInfo(proposalID, address) + if !ok { + panic(errDataNotFound) + } + + return proposalUserVotingInfo.votedAt +} diff --git a/contract/r/gnoswap/v1/gov/governance/gnomod.toml b/contract/r/gnoswap/v1/gov/governance/gnomod.toml new file mode 100644 index 0000000..50d0119 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/gov/governance" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/gov/governance/governance_execute.gno b/contract/r/gnoswap/v1/gov/governance/governance_execute.gno new file mode 100644 index 0000000..d23d45e --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/governance_execute.gno @@ -0,0 +1,253 @@ +package governance + +import ( + "std" + "time" + + en "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" +) + +// Execute executes an approved proposal. +// +// Processes and implements governance decisions after successful voting. +// Enforces timelock delays and execution windows for security. +// Anyone can trigger execution to ensure decentralization. +// +// Parameters: +// - proposalID: ID of the proposal to execute +// +// Requirements: +// - Proposal must have passed (majority yes votes) +// - Quorum must be reached (50% of xGNS supply) +// - Timelock period must have elapsed (1 day default) +// - Must be within execution window (30 days default) +// - Proposal not already executed or cancelled +// +// Effects: +// - Executes proposal actions (parameter changes, treasury transfers) +// - Marks proposal as executed +// - Emits execution event +// - Refunds gas costs from treasury +// +// Returns executed proposal ID. +// Callable by anyone once proposal is executable. +func Execute(cur realm, proposalID int64) int64 { + // Check if execution is allowed (system not halted for execution) + halt.AssertIsNotHaltedGovernance() + + // Get caller information and current blockchain state + caller := std.PreviousRealm().Address() + currentHeight := std.ChainHeight() + currentAt := time.Now().Unix() + + // Mint and distribute GNS tokens as part of the execution process + en.MintAndDistributeGns(cross) + + // Attempt to execute the proposal with current context + proposal, err := executeProposal( + proposalID, + currentAt, + currentHeight, + caller, + ) + if err != nil { + panic(err) + } + + // Emit execution event for tracking and auditing + previousRealm := std.PreviousRealm() + std.Emit( + "Execute", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "proposalId", formatInt(proposalID), + ) + + return proposal.ID() +} + +// executeProposal handles core logic of proposal execution. +func executeProposal( + proposalID int64, + executedAt int64, + executedHeight int64, + executedBy std.Address, +) (*Proposal, error) { + // Retrieve the proposal from storage + proposal, ok := getProposal(proposalID) + if !ok { + return nil, errDataNotFound + } + + // Text proposals cannot be executed (they are informational only) + if proposal.IsTextType() { + return nil, errTextProposalNotExecutable + } + + // Verify proposal is in executable state (timing and voting requirements met) + if !proposal.IsExecutable(executedAt) { + return nil, errProposalNotExecutable + } + + // Mark proposal as executed in its status + err := proposal.execute(executedAt, executedHeight, executedBy) + if err != nil { + return nil, err + } + + // Create parameter registry for handling execution actions + parameterRegistry := createParameterHandlers() + + // Execute proposal based on its type + switch proposal.Type() { + case CommunityPoolSpend: + // Execute community pool spending (token transfers) + err = executeCommunityPoolSpend(proposal, parameterRegistry, executedAt, executedHeight, executedBy) + if err != nil { + return nil, err + } + case ParameterChange: + // Execute parameter changes (governance configuration updates) + err = executeParameterChange(proposal, parameterRegistry, executedAt, executedHeight, executedBy) + if err != nil { + return nil, err + } + } + + return proposal, nil +} + +// Cancel cancels a proposal in upcoming status. +// +// Allows proposers to withdraw their proposals before voting begins. +// Prevents accidental or malicious proposals from reaching vote. +// Safety mechanism for proposal errors or changed circumstances. +// +// Parameters: +// - proposalID: ID of the proposal to cancel +// +// Requirements: +// - Must be called by original proposer +// - Proposal must be in "upcoming" status +// - Voting must not have started yet +// - Proposal not already cancelled or executed +// +// Effects: +// - Sets proposal status to "cancelled" +// - Prevents future voting or execution +// - Emits cancellation event +// - Frees up proposer's proposal slot +// +// Returns cancelled proposal ID. +// Only callable by original proposer before voting begins. +func Cancel(cur realm, proposalID int64) int64 { + halt.AssertIsNotHaltedGovernance() + + caller := std.PreviousRealm().Address() + assertCallerIsProposer(proposalID, caller) + + // Get current blockchain state and caller information + currentHeight := std.ChainHeight() + currentAt := time.Now().Unix() + + // Mint and distribute GNS tokens as part of the process + en.MintAndDistributeGns(cross) + + // Attempt to cancel the proposal + proposal, err := cancel(proposalID, currentAt, currentHeight, caller) + if err != nil { + panic(err) + } + + // Emit cancellation event for tracking + previousRealm := std.PreviousRealm() + std.Emit( + "Cancel", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "proposalId", formatInt(proposalID), + ) + + return proposal.ID() +} + +// cancel handles core logic of proposal cancellation. +// Validates proposal state and updates status to canceled. +func cancel(proposalID, canceledAt, canceledHeight int64, canceledBy std.Address) (proposal *Proposal, err error) { + // Retrieve the proposal from storage + proposal, ok := getProposal(proposalID) + if !ok { + return nil, errDataNotFound + } + + // Attempt to cancel the proposal (this validates cancellation conditions) + err = proposal.cancel(canceledAt, canceledHeight, canceledBy) + if err != nil { + return nil, err + } + + return proposal, nil +} + +// executeCommunityPoolSpend executes community pool spending proposals. +// Handles token transfers from community pool to specified recipients. +func executeCommunityPoolSpend( + proposal *Proposal, + parameterRegistry *ParameterRegistry, + executedAt int64, + executedHeight int64, + executedBy std.Address, +) error { + // Verify token registration for community pool spending + if proposal.IsCommunityPoolSpendType() { + common.MustRegistered(proposal.CommunityPoolSpendTokenPath()) + } + + // Execute all parameter changes defined in the proposal + parameterChangesInfos := proposal.data.execution.ParameterChangesInfos() + for _, parameterChangeInfo := range parameterChangesInfos { + // Get the appropriate handler for this parameter change + handler, err := parameterRegistry.handler(parameterChangeInfo.pkgPath, parameterChangeInfo.function) + if err != nil { + return err + } + + // Execute the parameter change with provided parameters + err = handler.Execute(parameterChangeInfo.params) + if err != nil { + return err + } + } + + return nil +} + +// executeParameterChange executes parameter change proposals. +// Handles governance configuration updates and system parameter modifications. +func executeParameterChange( + proposal *Proposal, + parameterRegistry *ParameterRegistry, + executedAt int64, + executedHeight int64, + executedBy std.Address, +) error { + // Execute all parameter changes defined in the proposal + parameterChangesInfos := proposal.data.execution.ParameterChangesInfos() + for _, parameterChangeInfo := range parameterChangesInfos { + // Get the appropriate handler for this parameter change + handler, err := parameterRegistry.handler(parameterChangeInfo.pkgPath, parameterChangeInfo.function) + if err != nil { + return err + } + + // Execute the parameter change with provided parameters + err = handler.Execute(parameterChangeInfo.params) + if err != nil { + return err + } + } + + return nil +} diff --git a/contract/r/gnoswap/v1/gov/governance/governance_propose.gno b/contract/r/gnoswap/v1/gov/governance/governance_propose.gno new file mode 100644 index 0000000..3dafd6a --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/governance_propose.gno @@ -0,0 +1,461 @@ +package governance + +import ( + "std" + "time" + + "gno.land/r/gnoswap/gns" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/gov/staker" +) + +// ProposeText creates a text proposal for community discussion. +// +// Signal proposals for non-binding community sentiment. +// Used for policy discussions, roadmap planning, and community feedback. +// No on-chain execution, serves as formal governance record. +// +// Parameters: +// - title: Short, descriptive proposal title (max 100 chars recommended) +// - description: Full proposal content with rationale and context +// +// Requirements: +// - Caller must hold minimum 1,000 GNS tokens +// - No other active proposal from same address +// - Title and description must be non-empty +// +// Process: +// - 1 day delay before voting starts +// - 7 days voting period +// - Simple majority decides outcome +// - No execution phase (signal only) +// +// Returns new proposal ID. +func ProposeText( + cur realm, + title string, + description string, +) (newProposalId int64) { + halt.AssertIsNotHaltedGovernance() + + callerAddress := std.PreviousRealm().Address() + + createdAt := time.Now().Unix() + createdHeight := std.ChainHeight() + gnsBalance := gns.BalanceOf(callerAddress) + + config, ok := getCurrentConfig() + if !ok { + panic(errDataNotFound) + } + + // Check if caller already has an active proposal (one proposal per address) + if hasActiveProposal(callerAddress, createdAt) { + panic(errAlreadyActiveProposal) + } + + // Get snapshot of voting weights for proposal creation + userVotes, maxVotingWeight, err := getUserVotingInfoSnapshot( + createdAt, + config.VotingWeightSmoothingDuration, + ) + if err != nil { + panic(err) + } + + // Create the text proposal with metadata + proposal, err := createProposal( + Text, + config, + maxVotingWeight, + NewProposalMetadata(title, description), + NewProposalTextData(), + callerAddress, + gnsBalance, + createdAt, + createdHeight, + ) + if err != nil { + panic(err) + } + + // Store voting information for the proposal + success := updateProposalUserVotes(proposal, userVotes) + if !success { + panic(errDataNotFound) + } + + // Emit proposal creation event for indexing and tracking + previousRealm := std.PreviousRealm() + std.Emit( + "ProposeText", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "title", title, + "description", description, + "proposalId", formatInt(proposal.ID()), + "quorumAmount", formatInt(proposal.VotingQuorumAmount()), + "maxVotingWeight", formatInt(proposal.VotingMaxWeight()), + "configVersion", formatInt(proposal.ConfigVersion()), + "createdAt", formatInt(proposal.CreatedAt()), + ) + + return proposal.ID() +} + +// ProposeCommunityPoolSpend creates a treasury disbursement proposal. +// +// Allocates community pool funds for approved purposes. +// Supports grants, development funding, and protocol incentives. +// Automatic transfer on execution if approved. +// +// Parameters: +// - title: Proposal title describing purpose +// - description: Detailed justification and budget breakdown +// - to: Recipient address for funds +// - tokenPath: Token contract path (e.g., "gno.land/r/gnoswap/gns") +// - amount: Amount to transfer (in smallest unit) +// +// Requirements: +// - Caller must hold minimum 1,000 GNS tokens +// - Sufficient balance in community pool +// - Valid recipient address +// - Supported token type +// +// Security: +// - Enforces timelock after approval +// - Single transfer per proposal +// - Tracks all disbursements on-chain +// +// Returns new proposal ID. +func ProposeCommunityPoolSpend( + cur realm, + title string, + description string, + to std.Address, + tokenPath string, + amount int64, +) (newProposalId int64) { + halt.AssertIsNotHaltedGovernance() + halt.AssertIsNotHaltedWithdraw() + + callerAddress := std.PreviousRealm().Address() + + createdAt := time.Now().Unix() + createdHeight := std.ChainHeight() + gnsBalance := gns.BalanceOf(callerAddress) + + config, ok := getCurrentConfig() + if !ok { + panic(errDataNotFound) + } + + // Check if caller already has an active proposal (one proposal per address) + if hasActiveProposal(callerAddress, createdAt) { + panic(errAlreadyActiveProposal) + } + + // Get snapshot of voting weights for proposal creation + userVotes, maxVotingWeight, err := getUserVotingInfoSnapshot( + createdAt, + config.VotingWeightSmoothingDuration, + ) + if err != nil { + panic(err) + } + + // Create the community pool spend proposal with execution data + proposal, err := createProposal( + CommunityPoolSpend, + config, + maxVotingWeight, + NewProposalMetadata(title, description), + NewProposalCommunityPoolSpendData(tokenPath, to, amount, COMMUNITY_POOL_PATH), + callerAddress, + gnsBalance, + createdAt, + createdHeight, + ) + if err != nil { + panic(err) + } + + // Store voting information for the proposal + success := updateProposalUserVotes(proposal, userVotes) + if !success { + panic(errDataNotFound) + } + + // Emit proposal creation event for indexing and tracking + previousRealm := std.PreviousRealm() + std.Emit( + "ProposeCommunityPoolSpend", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "title", title, + "description", description, + "to", to.String(), + "tokenPath", tokenPath, + "amount", formatInt(amount), + "proposalId", formatInt(proposal.ID()), + "quorumAmount", formatInt(proposal.VotingQuorumAmount()), + "maxVotingWeight", formatInt(proposal.VotingMaxWeight()), + "configVersion", formatInt(proposal.ConfigVersion()), + "createdAt", formatInt(proposal.CreatedAt()), + ) + + return proposal.ID() +} + +// ProposeParameterChange creates a protocol parameter update proposal. +// +// Modifies system parameters through governance. +// Supports multiple parameter changes in single proposal. +// Changes apply atomically on execution. +// +// Parameters: +// - title: Clear description of changes +// - description: Rationale and impact analysis +// - numToExecute: Number of parameter changes +// - executions: JSON array of changes, each containing: +// - target: Contract address to modify +// - function: Function name to call +// - params: Parameters for the function +// +// Example executions format: +// +// [{ +// "target": "gno.land/r/gnoswap/v1/gov", +// "function": "SetVotingPeriod", +// "params": ["604800"] +// }] +// +// Requirements: +// - Valid JSON format for executions +// - Target contracts must exist +// - Functions must be governance-callable +// - Parameters must match function signatures +// +// Returns new proposal ID. +func ProposeParameterChange( + cur realm, + title string, + description string, + numToExecute int64, + executions string, +) (newProposalId int64) { + halt.AssertIsNotHaltedGovernance() + + callerAddress := std.PreviousRealm().Address() + + createdAt := time.Now().Unix() + createdHeight := std.ChainHeight() + gnsBalance := gns.BalanceOf(callerAddress) + + config, ok := getCurrentConfig() + if !ok { + panic(errDataNotFound) + } + + // Check if caller already has an active proposal (one proposal per address) + if hasActiveProposal(callerAddress, createdAt) { + panic(errAlreadyActiveProposal) + } + + // Get snapshot of voting weights for proposal creation + userVotes, maxVotingWeight, err := getUserVotingInfoSnapshot( + createdAt, + config.VotingWeightSmoothingDuration, + ) + if err != nil { + panic(err) + } + + // Create the parameter change proposal with execution data + proposal, err := createProposal( + ParameterChange, + config, + maxVotingWeight, + NewProposalMetadata(title, description), + NewProposalExecutionData(numToExecute, executions), + callerAddress, + gnsBalance, + createdAt, + createdHeight, + ) + if err != nil { + panic(err) + } + + // Store voting information for the proposal + success := updateProposalUserVotes(proposal, userVotes) + if !success { + panic(errDataNotFound) + } + + // Emit proposal creation event for indexing and tracking + previousRealm := std.PreviousRealm() + std.Emit( + "ProposeParameterChange", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "title", title, + "description", description, + "numToExecute", formatInt(numToExecute), + "executions", executions, + "proposalId", formatInt(proposal.ID()), + "quorumAmount", formatInt(proposal.VotingQuorumAmount()), + "maxVotingWeight", formatInt(proposal.VotingMaxWeight()), + "configVersion", formatInt(proposal.ConfigVersion()), + "createdAt", formatInt(proposal.CreatedAt()), + ) + + return proposal.ID() +} + +// createProposal handles proposal creation logic. +// Validates input data, checks proposer eligibility, and creates proposal object. +func createProposal( + proposalType ProposalType, + config Config, + maxVotingWeight int64, + proposalMetadata *ProposalMetadata, + proposalData *ProposalData, + proposerAddress std.Address, + proposerGnsBalance int64, + createdAt int64, + createdHeight int64, +) (*Proposal, error) { + // Validate proposal metadata (title and description) + err := proposalMetadata.Validate() + if err != nil { + return nil, err + } + + // Validate proposal data (type-specific validation) + err = proposalData.Validate() + if err != nil { + return nil, err + } + + // Check if proposer has enough GNS balance to create proposal + if proposerGnsBalance < config.ProposalCreationThreshold { + return nil, errNotEnoughBalance + } + + // Generate unique proposal ID + proposalID := nextProposalID() + + // Create proposal status with voting schedule and requirements + proposalStatus := NewProposalStatus( + config, + maxVotingWeight, + proposalType.IsExecutable(), + createdAt, + ) + + // Get current configuration version for tracking + configVersion := getCurrentConfigVersion() + + // Create the proposal object + proposal := NewProposal( + proposalID, + proposalStatus, + proposalMetadata, + proposalData, + proposerAddress, + configVersion, + createdAt, + createdHeight, + ) + + // Store the proposal in state + success := addProposal(proposal) + if !success { + return nil, errDataNotFound + } + + return proposal, nil +} + +// getUserVotingInfoSnapshot retrieves voting information snapshot for proposal creation. +// Calculates voting weights at specific time point for fair voting. +func getUserVotingInfoSnapshot( + current, + smoothingPeriod int64, +) (map[string]*VotingInfo, int64, error) { + // Calculate snapshot time by going back by smoothing period + snapshotTime := current - smoothingPeriod + + var votingInfos map[string]*VotingInfo + var maxVotingWeight int64 + var ok bool + + // Use custom snapshot function if available + if getUserVotingInfoSnapshotFn != nil { + votingInfos, maxVotingWeight, ok = getUserVotingInfoSnapshotFn(snapshotTime) + } else { + votingInfos, maxVotingWeight, ok = getUserVotingInfotWithDelegationSnapshots(snapshotTime) + } + + if !ok || maxVotingWeight <= 0 { + return votingInfos, maxVotingWeight, errNotEnoughVotingWeight + } + + return votingInfos, maxVotingWeight, nil +} + +// getUserVotingInfotWithDelegationSnapshots retrieves voting info from staker delegation snapshots. +// Integrates with staker contract to get actual delegation amounts. +func getUserVotingInfotWithDelegationSnapshots( + snapshotTime int64, +) (map[string]*VotingInfo, int64, bool) { + // Get delegation snapshots from staker contract + delegationSnapshots, ok := staker.GetDelegationSnapshots(snapshotTime) + if !ok { + return nil, 0, false + } + + maxVotingWeight := int64(0) + userVotes := make(map[string]*VotingInfo) + + // Process each delegation snapshot + for _, snapshot := range delegationSnapshots { + delegatorAddress := snapshot.DelegatorAddress() + delegationAmount := snapshot.DelegationAmount() + + // Create voting info for each delegator + userVotes[delegatorAddress.String()] = NewVotingInfo(delegationAmount, delegatorAddress) + maxVotingWeight += delegationAmount + } + + return userVotes, maxVotingWeight, true +} + +// updateProposalUserVotes stores voting information for specific proposal. +// Links voting eligibility data to proposal for later use during voting. +func updateProposalUserVotes( + proposal *Proposal, + userVotingInfos map[string]*VotingInfo, +) bool { + // Store the voting information mapping for this proposal + proposalUserVotingInfos.Set(formatInt(proposal.ID()), userVotingInfos) + + return true +} + +// hasActiveProposal checks if address already has active proposal. +// Enforces one-proposal-per-address rule to prevent spam. +func hasActiveProposal(proposerAddress std.Address, current int64) bool { + // Get all proposals for this address + proposals := getUserProposals(proposerAddress) + + // Check if any proposal is still active + for _, proposal := range proposals { + if proposal.IsActive(current) { + return true + } + } + + return false +} diff --git a/contract/r/gnoswap/v1/gov/governance/governance_vote.gno b/contract/r/gnoswap/v1/gov/governance/governance_vote.gno new file mode 100644 index 0000000..cedd8eb --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/governance_vote.gno @@ -0,0 +1,121 @@ +package governance + +import ( + "std" + "time" + + en "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" +) + +// Vote casts a vote on a proposal. +// +// Records on-chain vote with weight based on delegated xGNS. +// Uses 24-hour average voting power to prevent manipulation. +// Votes are final and cannot be changed. +// +// Parameters: +// - proposalID: ID of the proposal to vote on +// - yes: true for yes vote, false for no vote +// +// Vote Weight Calculation: +// - Based on delegated xGNS amount +// - 24-hour average before proposal creation +// - Prevents flash loan attacks +// - Includes both self-stake and delegations received +// +// Requirements: +// - Proposal must be in voting period +// - Voter must have xGNS delegated +// - Cannot vote twice on same proposal +// - Voting period typically 7 days +// +// Returns voting weight used as string. +func Vote(cur realm, proposalID int64, yes bool) string { + halt.AssertIsNotHaltedGovernance() + + // Get current blockchain state and caller information + currentHeight := std.ChainHeight() + currentAt := time.Now() + + // Mint and distribute GNS tokens as part of the voting process + en.MintAndDistributeGns(cross) + + // Extract voter address from realm context + voter := std.PreviousRealm().Address() + + // Process the vote and get updated vote tallies + userVote, totalYesVoteWeight, totalNoVoteWeight, err := vote( + proposalID, + voter, + yes, + currentHeight, + currentAt.Unix(), + ) + if err != nil { + panic(err) + } + + // Emit voting event for tracking and transparency + previousRealm := std.PreviousRealm() + std.Emit( + "Vote", + "prevAddr", previousRealm.Address().String(), + "prevPkgPath", previousRealm.PkgPath(), + "proposalId", formatInt(proposalID), + "voter", voter.String(), + "yes", userVote.VotingType(), + "voteWeight", formatInt(userVote.VotedWeight()), + "voteYes", formatInt(totalYesVoteWeight), + "voteNo", formatInt(totalNoVoteWeight), + ) + + return formatInt(userVote.VotedWeight()) +} + +// vote handles core voting logic. +func vote( + proposalID int64, + voterAddress std.Address, + votedYes bool, + votedHeight, + votedAt int64, +) (*VotingInfo, int64, int64, error) { + // Retrieve the proposal from storage + proposal, ok := getProposal(proposalID) + if !ok { + return nil, 0, 0, makeErrorWithDetails(errDataNotFound, "not found proposal") + } + + // Check if current time is within voting period + if !proposal.IsVotingPeriod(votedAt) { + return nil, 0, 0, makeErrorWithDetails(errUnableToVoteOutOfPeriod, "can not vote out of voting period") + } + + // Get user's voting information for this proposal + userVote, ok := getProposalUserVotingInfo(proposalID, voterAddress) + if !ok { + return nil, 0, 0, makeErrorWithDetails(errDataNotFound, "not found user's voting info") + } + + // Check if user has voting weight available + votingWeight := userVote.AvailableVoteWeight() + if votingWeight <= 0 { + return nil, 0, 0, makeErrorWithDetails(errNotEnoughVotingWeight, "no voting weight") + } + + // Record the vote in user's voting info (this also prevents double voting) + err := userVote.vote(votedYes, votingWeight, votedHeight, votedAt) + if err != nil { + return nil, 0, 0, err + } + + // Update proposal vote tallies + err = proposal.vote(votedYes, votingWeight) + if err != nil { + return nil, 0, 0, err + } + + // Return updated vote information and current tallies + return userVote, proposal.VotingYesWeight(), proposal.VotingNoWeight(), nil +} diff --git a/contract/r/gnoswap/v1/gov/governance/parameter_registry.gno b/contract/r/gnoswap/v1/gov/governance/parameter_registry.gno new file mode 100644 index 0000000..61d4558 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/parameter_registry.gno @@ -0,0 +1,529 @@ +package governance + +import ( + "std" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + en "gno.land/r/gnoswap/emission" + cp "gno.land/r/gnoswap/v1/community_pool" + + pl "gno.land/r/gnoswap/v1/pool" + pf "gno.land/r/gnoswap/v1/protocol_fee" + rr "gno.land/r/gnoswap/v1/router" + sr "gno.land/r/gnoswap/v1/staker" + + "gno.land/r/gnoswap/halt" +) + +// Package paths +const ( + GNS_PATH = "gno.land/r/gnoswap/gns" + HALT_PATH = "gno.land/r/gnoswap/halt" + ACCESS_PATH = "gno.land/r/gnoswap/access" + EMISSION_PATH = "gno.land/r/gnoswap/emission" + COMMON_PATH = "gno.land/r/gnoswap/v1/common" + POOL_PATH = "gno.land/r/gnoswap/v1/pool" + ROUTER_PATH = "gno.land/r/gnoswap/v1/router" + STAKER_PATH = "gno.land/r/gnoswap/v1/staker" + PROTOCOL_FEE_PATH = "gno.land/r/gnoswap/v1/protocol_fee" + COMMUNITY_POOL_PATH = "gno.land/r/gnoswap/v1/community_pool" + GOV_GOVERNANCE_PATH = "gno.land/r/gnoswap/v1/gov/governance" +) + +// ParameterHandler interface defines the contract for parameter execution handlers. +// Each handler is responsible for executing specific parameter changes in the system. +type ParameterHandler interface { + // Execute processes the parameters and applies the changes to the system + Execute(params []string) error +} + +// ParameterHandlerOptions contains the configuration and execution logic for a parameter handler. +// This struct encapsulates all information needed to identify and execute a parameter change. +type ParameterHandlerOptions struct { + pkgPath string // Package path of the target contract + function string // Function name to be called + paramCount int // Expected number of parameters + handlerFunc func([]string) error // Function that executes the parameter change +} + +// HandlerKey generates a unique key for this handler based on package path and function name. +// +// Returns: +// - string: unique identifier for the handler +func (h *ParameterHandlerOptions) HandlerKey() string { + return makeHandlerKey(h.pkgPath, h.function) +} + +// Execute validates parameter count and executes the handler function. +// This method ensures the correct number of parameters are provided before execution. +// +// Parameters: +// - params: slice of string parameters to pass to the handler +// +// Returns: +// - error: execution error if parameter count mismatch or handler execution fails +func (h *ParameterHandlerOptions) Execute(params []string) error { + // Validate parameter count matches expected count + if len(params) != h.paramCount { + return ufmt.Errorf("expected %d parameters, got %d", h.paramCount, len(params)) + } + + // Create realm context function and execute handler + fn := func(cur realm) error { + return h.handlerFunc(params) + } + + return fn(cross) +} + +// NewParameterHandlerOptions creates a new parameter handler with the specified configuration. +// +// Parameters: +// - pkgPath: package path of the target contract +// - function: function name to be called +// - paramCount: expected number of parameters +// - handlerFunc: function that executes the parameter change +// +// Returns: +// - ParameterHandler: configured parameter handler interface +func NewParameterHandlerOptions( + pkgPath, + function string, + paramCount int, + handlerFunc func([]string) error, +) ParameterHandler { + return &ParameterHandlerOptions{ + pkgPath: pkgPath, + function: function, + paramCount: paramCount, + handlerFunc: handlerFunc, + } +} + +// ParameterRegistry manages the collection of parameter handlers for governance execution. +// This registry allows proposals to execute parameter changes across different system contracts. +type ParameterRegistry struct { + handlers *avl.Tree // Tree storing handler configurations keyed by package:function +} + +// register adds a new parameter handler to the registry. +// Each handler is identified by a unique combination of package path and function name. +// +// Parameters: +// - handler: parameter handler configuration to register +func (r *ParameterRegistry) register(handler ParameterHandlerOptions) { + r.handlers.Set(handler.HandlerKey(), handler) +} + +// handler retrieves a parameter handler by package path and function name. +// This method is used during proposal execution to find the appropriate handler. +// +// Parameters: +// - pkgPath: package path of the target contract +// - function: function name to be called +// +// Returns: +// - ParameterHandler: the matching parameter handler +// - error: error if handler not found or casting fails +func (r *ParameterRegistry) handler(pkgPath, function string) (ParameterHandler, error) { + // Generate lookup key + key := makeHandlerKey(pkgPath, function) + + // Retrieve handler from registry + h, exists := r.handlers.Get(key) + if !exists { + return nil, ufmt.Errorf("handler not found for %s", key) + } + + // Cast to correct type + handler, ok := h.(ParameterHandlerOptions) + if !ok { + return nil, ufmt.Errorf("failed to cast handler %s to ParameterHandler", key) + } + + return &handler, nil +} + +// NewParameterRegistry creates a new empty parameter registry. +// +// Returns: +// - *ParameterRegistry: new registry instance +func NewParameterRegistry() *ParameterRegistry { + return &ParameterRegistry{handlers: avl.NewTree()} +} + +// makeHandlerKey creates a unique identifier for a handler based on package path and function. +// +// Parameters: +// - pkgPath: package path of the target contract +// - function: function name to be called +// +// Returns: +// - string: unique key in format "pkgPath:function" +func makeHandlerKey(pkgPath, function string) string { + return ufmt.Sprintf("%s:%s", pkgPath, function) +} + +// createParameterHandlers initializes and configures all supported parameter handlers. +// This function defines all the parameter changes that can be executed through governance proposals. +// It covers configuration changes for various system components including pools, staking, fees, etc. +// +// Returns: +// - *ParameterRegistry: fully configured registry with all supported handlers +func createParameterHandlers() *ParameterRegistry { + registry := NewParameterRegistry() + + // Define all handler configurations for different system components + handlers := []*ParameterHandlerOptions{ + // Access control + { + pkgPath: ACCESS_PATH, + function: "UpdateSwapWhiteList", + paramCount: 1, + handlerFunc: func(params []string) error { + // Update swap whitelist + access.UpdateSwapWhiteList(cross, std.Address(params[0])) + + return nil + }, + }, + { + pkgPath: ACCESS_PATH, + function: "RemoveFromSwapWhiteList", + paramCount: 1, + handlerFunc: func(params []string) error { + // Remove from swap whitelist + access.RemoveFromSwapWhiteList(cross, std.Address(params[0])) + + return nil + }, + }, + // Community pool token transfers + { + pkgPath: COMMUNITY_POOL_PATH, + function: "TransferToken", + paramCount: 3, + handlerFunc: func(params []string) error { + // Transfer tokens from community pool to specified address + cp.TransferToken( + cross, + params[0], // pkgPath + std.Address(params[1]), // to + parseNumber(params[2], kindInt64).(int64), // amount + ) + + return nil + }, + }, + // Emission distribution configuration + { + pkgPath: EMISSION_PATH, + function: "SetDistributionStartTime", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set distribution start time + en.SetDistributionStartTime(cross, parseInt64(params[0])) + + return nil + }, + }, + { + pkgPath: EMISSION_PATH, + function: "ChangeDistributionPct", + paramCount: 8, + handlerFunc: func(params []string) error { + // Parse distribution targets and percentages + target01 := parseNumber(params[0], kindInt).(int) // target01 + pct01 := parseNumber(params[1], kindInt64).(int64) // pct01 + target02 := parseNumber(params[2], kindInt).(int) // target02 + pct02 := parseNumber(params[3], kindInt64).(int64) // pct02 + target03 := parseNumber(params[4], kindInt).(int) // target03 + pct03 := parseNumber(params[5], kindInt64).(int64) // pct03 + target04 := parseNumber(params[6], kindInt).(int) // target04 + pct04 := parseNumber(params[7], kindInt64).(int64) // pct04 + + // Update emission distribution percentages + en.ChangeDistributionPct( + cross, + target01, // target01 + pct01, // pct01 + target02, // target02 + pct02, // pct02 + target03, // target03 + pct03, // pct03 + target04, // target04 + pct04, // pct04 + ) + + return nil + }, + }, + // Governance configuration changes + { + pkgPath: GOV_GOVERNANCE_PATH, + function: "Reconfigure", + paramCount: 7, + handlerFunc: func(params []string) error { + // Parse governance configuration parameters + votingStartDelay := parseInt64(params[0]) + votingPeriod := parseInt64(params[1]) + votingWeightSmoothingDuration := parseInt64(params[2]) + quorum := parseInt64(params[3]) + proposalCreationThreshold := parseInt64(params[4]) + executionDelay := parseInt64(params[5]) + executionWindow := parseInt64(params[6]) + + // Reconfigure governance parameters through governance process + Reconfigure( + cross, + votingStartDelay, + votingPeriod, + votingWeightSmoothingDuration, + quorum, + proposalCreationThreshold, + executionDelay, + executionWindow, + ) + + return nil + }, + }, + + // Pool protocol fee configuration + { + pkgPath: POOL_PATH, + function: "CollectProtocol", + paramCount: 6, + handlerFunc: func(params []string) error { + // Collect protocol fees + pl.CollectProtocol( + cross, + params[0], // token0Path + params[1], // token1Path + uint32(parseUint64(params[2])), // fee + std.Address(params[3]), // recipient + params[4], // amount0Requested + params[5], // amount1Requested + ) + + return nil + }, + }, + { + pkgPath: POOL_PATH, + function: "SetFeeProtocol", + paramCount: 2, + handlerFunc: func(params []string) error { + // Parse and validate fee protocol values + feeProtocol0 := parseInt64(params[0]) + feeProtocol1 := parseInt64(params[1]) + + // Validate fee protocol values are within uint8 range + if feeProtocol0 > 255 { + panic(ufmt.Sprintf("feeProtocol0 out of range: %d", feeProtocol0)) + } + + if feeProtocol1 > 255 { + panic(ufmt.Sprintf("feeProtocol1 out of range: %d", feeProtocol1)) + } + + // Set protocol fee percentages + pl.SetFeeProtocol( + cross, + uint8(feeProtocol0), // feeProtocol0 + uint8(feeProtocol1), // feeProtocol1 + ) + + return nil + }, + }, + // Pool creation fee + { + pkgPath: POOL_PATH, + function: "SetPoolCreationFee", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set fee required to create new pools + pl.SetPoolCreationFee(cross, parseInt64(params[0])) // fee + return nil + }, + }, + // Pool withdrawal fee + { + pkgPath: POOL_PATH, + function: "SetWithdrawalFee", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set fee for withdrawing from pools + pl.SetWithdrawalFee(cross, parseUint64(params[0])) // fee + return nil + }, + }, + + // Protocol fee distribution + { + pkgPath: PROTOCOL_FEE_PATH, + function: "SetDevOpsPct", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set percentage of protocol fees going to development operations + pf.SetDevOpsPct(cross, parseInt64(params[0])) // pct + return nil + }, + }, + + // Router swap fee + { + pkgPath: ROUTER_PATH, + function: "SetSwapFee", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set fee charged for token swaps + rr.SetSwapFee(cross, parseUint64(params[0])) // fee + return nil + }, + }, + + // Staker configuration handlers + { + pkgPath: STAKER_PATH, + function: "SetDepositGnsAmount", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set minimum GNS amount required for staking deposits + sr.SetDepositGnsAmount(cross, parseInt64(params[0])) // amount + return nil + }, + }, + { + pkgPath: STAKER_PATH, + function: "SetMinimumRewardAmount", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set minimum GNS amount required for staking deposits + sr.SetMinimumRewardAmount(cross, parseInt64(params[0])) // amount + return nil + }, + }, + { + pkgPath: STAKER_PATH, + function: "SetTokenMinimumRewardAmount", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set minimum GNS amount required for staking deposits + // params[0] is a string in the format "tokenPath:amount" + sr.SetTokenMinimumRewardAmount(cross, params[0]) // amount + return nil + }, + }, + { + pkgPath: STAKER_PATH, + function: "SetPoolTier", + paramCount: 2, + handlerFunc: func(params []string) error { + // Assign tier level to a specific pool + sr.SetPoolTier( + cross, + params[0], // pool + parseUint64(params[1]), // tier + ) + return nil + }, + }, + { + pkgPath: STAKER_PATH, + function: "ChangePoolTier", + paramCount: 2, + handlerFunc: func(params []string) error { + // Change existing pool's tier level + sr.ChangePoolTier( + cross, + params[0], // pool + parseUint64(params[1]), // tier + ) + return nil + }, + }, + { + pkgPath: STAKER_PATH, + function: "RemovePoolTier", + paramCount: 1, + handlerFunc: func(params []string) error { + // Remove tier assignment from a pool + sr.RemovePoolTier(cross, params[0]) // pool + return nil + }, + }, + { + pkgPath: STAKER_PATH, + function: "SetUnStakingFee", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set fee charged for unstaking operations + fee := parseInt64(params[0]) + sr.SetUnStakingFee(cross, fee) + return nil + }, + }, + { + pkgPath: STAKER_PATH, + function: "SetWarmUp", + paramCount: 2, + handlerFunc: func(params []string) error { + // Set warm-up period configuration for staking + percent := parseInt64(params[0]) + block := parseNumber(params[1], kindInt64).(int64) + sr.SetWarmUp(cross, percent, block) + + return nil + }, + }, + + // System halt controls + { + pkgPath: HALT_PATH, + function: "SetHaltLevel", + paramCount: 1, + handlerFunc: func(params []string) error { + // Set system-wide halt status + halt.SetHaltLevel(cross, halt.HaltLevel(params[0])) // true = halt, false = no halt + + return nil + }, + }, + { + pkgPath: HALT_PATH, + function: "SetOperationStatus", + paramCount: 2, + handlerFunc: func(params []string) error { + // Enable or disable specific operation types + opType := halt.OpType(params[0]) + allowed := parseBool(params[1]) + + halt.SetOperationStatus(cross, opType, allowed) + + return nil + }, + }, + } + + // Register all configured handlers in the registry + registerHandlers(registry, handlers) + + return registry +} + +// registerHandlers batch registers all configured handlers into the registry. +// This helper function processes the handler configuration array and adds each handler to the registry. +// +// Parameters: +// - registry: the parameter registry to add handlers to +// - handlerOptions: slice of handler configurations to register +func registerHandlers(registry *ParameterRegistry, handlerOptions []*ParameterHandlerOptions) { + for _, handlerOption := range handlerOptions { + registry.register(*handlerOption) + } +} diff --git a/contract/r/gnoswap/v1/gov/governance/parameter_registry_handler.gno b/contract/r/gnoswap/v1/gov/governance/parameter_registry_handler.gno new file mode 100644 index 0000000..a3081b1 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/parameter_registry_handler.gno @@ -0,0 +1,140 @@ +package governance + +import "gno.land/p/nt/ufmt" + +// ParameterRegistryHandler is an interface for handlers that need to manage state +// during parameter changes. This is an optional interface that handlers can implement +// if they need to save and restore state during governance operations. +type ParameterRegistryHandler interface { + // GetState returns the current state that should be preserved + GetState() interface{} + // RestoreState restores the handler to the given state + RestoreState(state interface{}) error +} + +// stateManager handles state preservation for parameter handlers that implement +// the ParameterRegistryHandler interface. This allows for rollback capabilities +// during failed governance executions. +type stateManager struct { + states map[string]*handlerState // Maps handler keys to their saved states +} + +// newStateManager creates a new state manager instance. +// +// Returns: +// - *stateManager: new state manager for parameter handlers +func newStateManager() *stateManager { + return &stateManager{ + states: make(map[string]*handlerState), + } +} + +// SaveState preserves the current state of a parameter handler if it supports state management. +// Only handlers that implement ParameterRegistryHandler will have their state saved. +// +// Parameters: +// - pkgPath: package path of the handler +// - function: function name of the handler +// - handler: the parameter handler instance +// +// Returns: +// - error: always nil (state saving is optional) +func (sm *stateManager) SaveState(pkgPath, function string, handler ParameterHandler) error { + key := makeHandlerKey(pkgPath, function) + + // Check if the handler implements the ParameterRegistryHandler interface + if sh, ok := handler.(ParameterRegistryHandler); ok { + // Save the current state for potential rollback + sm.states[key] = &handlerState{ + pkgPath: pkgPath, + function: function, + state: sh.GetState(), + handler: handler, + } + return nil + } + + // Handlers that do not implement the ParameterRegistryHandler interface do not need to save state + return nil +} + +// RestoreStates restores all saved handler states. +// This is used for rollback operations when governance execution fails. +// +// Returns: +// - error: restoration error if any handler fails to restore +func (sm *stateManager) RestoreStates() error { + for _, state := range sm.states { + // Verify handler still implements the interface + handler, ok := state.handler.(ParameterRegistryHandler) + if !ok { + return ufmt.Errorf("handler %s does not implement ParameterRegistryHandler", state.pkgPath) + } + + // Restore the saved state + err := handler.RestoreState(state.state) + if err != nil { + return ufmt.Errorf("failed to restore state for %s: %v", + makeHandlerKey(state.pkgPath, state.function), err) + } + } + + return nil +} + +// handlerState stores the preserved state information for a parameter handler. +type handlerState struct { + pkgPath string // Package path of the handler + function string // Function name of the handler + state any // Preserved state data + handler ParameterHandler // Reference to the handler instance +} + +// registryHandler wraps existing functions as a ParameterHandler interface. +// This is a simple wrapper for functions that don't need state management. +type registryHandler struct { + fn func(params []string) error // The wrapped function +} + +// NewRegistryHandler creates a new registry handler wrapper around a function. +// This allows simple functions to be used as parameter handlers without implementing +// the full ParameterRegistryHandler interface. +// +// Parameters: +// - fn: function to wrap as a parameter handler +// +// Returns: +// - ParameterRegistryHandler: wrapped function that implements the interface +func NewRegistryHandler(fn func(params []string) error) ParameterRegistryHandler { + return ®istryHandler{fn} +} + +// Execute runs the wrapped function with the provided parameters. +// +// Parameters: +// - params: parameters to pass to the wrapped function +// +// Returns: +// - error: execution error from the wrapped function +func (h *registryHandler) Execute(params []string) error { + return h.fn(params) +} + +// RestoreState is a no-op for simple registry handlers since they don't manage state. +// +// Parameters: +// - state: state data (ignored) +// +// Returns: +// - error: always nil +func (h *registryHandler) RestoreState(state interface{}) error { + return nil +} + +// GetState returns nil for simple registry handlers since they don't manage state. +// +// Returns: +// - interface{}: always nil +func (h *registryHandler) GetState() interface{} { + return nil +} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal.gno b/contract/r/gnoswap/v1/gov/governance/proposal.gno new file mode 100644 index 0000000..3f765bb --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/proposal.gno @@ -0,0 +1,280 @@ +package governance + +import ( + "std" +) + +// ProposalType defines the different types of proposals supported by the governance system. +// Each type has different execution behavior and validation requirements. +type ProposalType string + +const ( + Text ProposalType = "TEXT" // Informational proposals for community discussion + CommunityPoolSpend ProposalType = "COMMUNITY_POOL_SPEND" // Proposals to spend community pool funds + ParameterChange ProposalType = "PARAMETER_CHANGE" // Proposals to modify system parameters +) + +// String returns the human-readable string representation of the proposal type. +func (p ProposalType) String() string { + switch p { + case Text: + return "Text" + case CommunityPoolSpend: + return "CommunityPoolSpend" + case ParameterChange: + return "ParameterChange" + default: + return "Unknown" + } +} + +// IsExecutable determines whether this proposal type can be executed. +// Text proposals are informational only and cannot be executed. +func (p ProposalType) IsExecutable() bool { + switch p { + case Text: + return false + case CommunityPoolSpend, ParameterChange: + return true + default: + return false + } +} + +// Proposal represents a governance proposal with all its associated data and state. +// This is the core structure that tracks proposal lifecycle from creation to execution. +type Proposal struct { + id int64 // Unique identifier for the proposal + proposer std.Address // The address of the proposer + configVersion int64 // The version of the governance config used + status *ProposalStatus // Current status and voting information + metadata *ProposalMetadata // Title and description + data *ProposalData // Type-specific proposal data + createdAt int64 // Creation timestamp + createdHeight int64 // Block height at creation +} + +// ID returns the unique identifier of the proposal. +func (p *Proposal) ID() int64 { + return p.id +} + +// Type returns the type of this proposal. +func (p *Proposal) Type() ProposalType { + return p.data.ProposalType() +} + +// IsTextType checks if this is a text proposal. +func (p *Proposal) IsTextType() bool { + return p.Type() == Text +} + +// IsCommunityPoolSpendType checks if this is a community pool spend proposal. +func (p *Proposal) IsCommunityPoolSpendType() bool { + return p.Type() == CommunityPoolSpend +} + +// IsParameterChangeType checks if this is a parameter change proposal. +func (p *Proposal) IsParameterChangeType() bool { + return p.Type() == ParameterChange +} + +// Status returns the current status string of the proposal at the given time. +func (p *Proposal) Status(current int64) string { + return p.status.StatusType(current).String() +} + +// StatusType returns the current status type of the proposal at the given time. +func (p *Proposal) StatusType(current int64) ProposalStatusType { + return p.status.StatusType(current) +} + +// IsActive determines if the proposal is currently active (can be voted on or executed). +// A proposal is considered active if it's not rejected, expired, executed, or canceled. +func (p *Proposal) IsActive(current int64) bool { + // Text proposals become inactive once they pass (no execution needed) + if p.IsTextType() { + if p.status.IsPassed(current) { + return false + } + } + + // If the proposal is rejected, expired, executed, or canceled, it is not active + if p.status.IsRejected(current) || + p.status.IsExpired(current) || + p.status.IsExecuted(current) || + p.status.IsCanceled(current) { + return false + } + + return true +} + +// IsVotingPeriod checks if the proposal is currently in its voting period. +func (p *Proposal) IsVotingPeriod(votedAt int64) bool { + return p.StatusType(votedAt) == StatusActive +} + +// IsExecutable determines if the proposal can be executed at the given time. +// Only executable proposal types that have passed voting can be executed. +func (p *Proposal) IsExecutable(current int64) bool { + // Only certain proposal types can be executed + if !p.Type().IsExecutable() { + return false + } + + return p.status.IsExecutable(current) +} + +// Validate performs comprehensive validation of the proposal data and metadata. +// This ensures all proposal components meet requirements before storage. +func (p *Proposal) Validate() error { + // Validate type-specific proposal data + if err := p.data.Validate(); err != nil { + return err + } + + // Validate proposal metadata (title and description) + if err := p.metadata.Validate(); err != nil { + return err + } + + return nil +} + +// Title returns the proposal title. +func (p *Proposal) Title() string { + return p.metadata.Title() +} + +// Description returns the proposal description. +func (p *Proposal) Description() string { + return p.metadata.Description() +} + +// ConfigVersion returns the governance configuration version used for this proposal. +func (p *Proposal) ConfigVersion() int64 { + return p.configVersion +} + +// IsProposer checks if the given address is the proposer of this proposal. +func (p *Proposal) IsProposer(addr std.Address) bool { + return p.proposer == addr +} + +// Proposer returns the address of the proposal creator. +func (p *Proposal) Proposer() std.Address { + return p.proposer +} + +// CreatedAt returns the creation timestamp of the proposal. +func (p *Proposal) CreatedAt() int64 { + return p.status.schedule.createTime +} + +// VotingYesWeight returns the total weight of "yes" votes. +func (p *Proposal) VotingYesWeight() int64 { + return p.status.voteStatus.yea +} + +// VotingNoWeight returns the total weight of "no" votes. +func (p *Proposal) VotingNoWeight() int64 { + return p.status.voteStatus.nay +} + +// VotingTotalWeight returns total weight of all votes cast. +func (p *Proposal) VotingTotalWeight() int64 { + return p.status.voteStatus.TotalVoteWeight() +} + +// VotingQuorumAmount returns minimum vote weight required for proposal to pass. +func (p *Proposal) VotingQuorumAmount() int64 { + return p.status.voteStatus.quorumAmount +} + +// VotingMaxWeight returns maximum possible voting weight for this proposal. +func (p *Proposal) VotingMaxWeight() int64 { + return p.status.voteStatus.maxVotingWeight +} + +// CommunityPoolSpendTokenPath returns the token path for community pool spend proposals. +// Returns empty string for other proposal types. +func (p *Proposal) CommunityPoolSpendTokenPath() string { + if p.data == nil { + return "" + } + + return p.data.communityPoolSpend.tokenPath +} + +// vote records a vote for this proposal and updates vote tallies. +// This is an internal method called during voting process. +func (p *Proposal) vote(votedYes bool, weight int64) error { + return p.status.vote(votedYes, weight) +} + +// updateVoteStatus updates the voting status with new parameters. +// This is used for dynamic voting requirement adjustments. +func (p *Proposal) updateVoteStatus(maxVotingWeight, quorum int64) error { + return p.status.updateVoteStatus(maxVotingWeight, quorum) +} + +// execute marks the proposal as executed and records execution details. +// This method validates execution conditions before proceeding. +func (p *Proposal) execute(executedAt int64, executedHeight int64, executedBy std.Address) error { + // Verify proposal is in executable state + if !p.IsExecutable(executedAt) { + return errProposalNotExecutable + } + + // Mark proposal as executed + return p.status.execute(executedAt, executedHeight, executedBy) +} + +// cancel marks the proposal as canceled and records cancellation details. +// This method validates cancellation conditions before proceeding. +func (p *Proposal) cancel(canceledAt int64, canceledHeight int64, canceledBy std.Address) error { + if p.status.IsCanceled(canceledAt) { + return errAlreadyCanceledProposal + } + + if !p.status.IsUpcoming(canceledAt) { + return errUnableToCancleVotingProposal + } + + // Mark proposal as canceled + return p.status.cancel(canceledAt, canceledHeight, canceledBy) +} + +// NewProposal creates a new proposal instance with the provided parameters. +// NewProposal is the main constructor for creating governance proposals. +// - metadata: proposal title and description +// - data: type-specific proposal data +// - proposerAddress: address of the proposal creator +// - configVersion: governance configuration version +// - createdAt: creation timestamp +// - createdHeight: creation block height +// +// Returns: +// - *Proposal: newly created proposal instance +func NewProposal( + proposalID int64, + status *ProposalStatus, + metadata *ProposalMetadata, + data *ProposalData, + proposerAddress std.Address, + configVersion int64, + createdAt int64, + createdHeight int64, +) *Proposal { + return &Proposal{ + id: proposalID, + proposer: proposerAddress, + status: status, + metadata: metadata, + data: data, + configVersion: configVersion, + createdAt: createdAt, + createdHeight: createdHeight, + } +} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_action_status.gno b/contract/r/gnoswap/v1/gov/governance/proposal_action_status.gno new file mode 100644 index 0000000..001d45b --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/proposal_action_status.gno @@ -0,0 +1,128 @@ +package governance + +import "std" + +// ProposalActionStatus tracks the execution and cancellation status of a proposal. +// This structure manages the action-related state including who performed actions and when. +type ProposalActionStatus struct { + canceled bool // Whether the proposal has been canceled + canceledAt int64 // Timestamp when proposal was canceled + canceledHeight int64 // Block height when proposal was canceled + canceledBy std.Address // Who canceled the proposal + + executed bool // Whether the proposal has been executed + executedAt int64 // Timestamp when proposal was executed + executedHeight int64 // Block height when proposal was executed + executedBy std.Address // Who executed the proposal + + executable bool // Whether this proposal type supports execution +} + +// IsCanceled returns whether the proposal has been canceled. +// +// Returns: +// - bool: true if proposal has been canceled +func (p *ProposalActionStatus) IsCanceled() bool { + return p.canceled +} + +// CanceledBy returns the address that canceled the proposal. +// Only meaningful if IsCanceled() returns true. +// +// Returns: +// - std.Address: address of the canceller +func (p *ProposalActionStatus) CanceledBy() std.Address { + return p.canceledBy +} + +// IsExecuted returns whether the proposal has been executed. +// +// Returns: +// - bool: true if proposal has been executed +func (p *ProposalActionStatus) IsExecuted() bool { + return p.executed +} + +// ExecutedBy returns the address that executed the proposal. +// Only meaningful if IsExecuted() returns true. +// +// Returns: +// - std.Address: address of the executor +func (p *ProposalActionStatus) ExecutedBy() std.Address { + return p.executedBy +} + +// IsExecutable returns whether this proposal type can be executed. +// Text proposals return false, while other types return true. +// +// Returns: +// - bool: true if proposal type supports execution +func (p *ProposalActionStatus) IsExecutable() bool { + return p.executable +} + +// cancel marks the proposal as canceled and records cancellation details. +// This method validates that the proposal is eligible for cancellation. +// +// Parameters: +// - canceledAt: timestamp when cancellation occurred +// - canceledHeight: block height when cancellation occurred +// - canceledBy: address performing the cancellation +// +// Returns: +// - error: cancellation error if proposal cannot be canceled +func (p *ProposalActionStatus) cancel(canceledAt int64, canceledHeight int64, canceledBy std.Address) error { + // Only executable proposals can be canceled (text proposals cannot) + if !p.executable { + return errProposalNotExecutable + } + + // Record cancellation details + p.canceled = true + p.canceledAt = canceledAt + p.canceledHeight = canceledHeight + p.canceledBy = canceledBy + + return nil +} + +// execute marks the proposal as executed and records execution details. +// This method validates that the proposal is eligible for execution. +// +// Parameters: +// - executedAt: timestamp when execution occurred +// - executedHeight: block height when execution occurred +// - executedBy: address performing the execution +// +// Returns: +// - error: execution error if proposal cannot be executed +func (p *ProposalActionStatus) execute(executedAt int64, executedHeight int64, executedBy std.Address) error { + // Only executable proposals can be executed (text proposals cannot) + if !p.executable { + return errProposalNotExecutable + } + + // Record execution details + p.executed = true + p.executedAt = executedAt + p.executedHeight = executedHeight + p.executedBy = executedBy + + return nil +} + +// NewProposalActionStatus creates a new action status for a proposal. +// Initializes the status with default values and the executable flag. +// +// Parameters: +// - executable: whether this proposal type can be executed +// +// Returns: +// - *ProposalActionStatus: new action status instance +func NewProposalActionStatus(executable bool) *ProposalActionStatus { + return &ProposalActionStatus{ + canceled: false, // Proposal starts as not canceled + executed: false, // Proposal starts as not executed + executable: executable, // Set based on proposal type + } +} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_data.gno b/contract/r/gnoswap/v1/gov/governance/proposal_data.gno new file mode 100644 index 0000000..baaba28 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/proposal_data.gno @@ -0,0 +1,406 @@ +package governance + +import ( + "std" + "strings" + + "gno.land/p/nt/ufmt" +) + +// ProposalMetadata contains descriptive information about a proposal. +// This includes the title and description that are displayed to voters. +type ProposalMetadata struct { + title string // Proposal title (max 255 characters) + description string // Detailed proposal description (max 10,000 characters) +} + +// Title returns the proposal title. +// +// Returns: +// - string: proposal title +func (p *ProposalMetadata) Title() string { + return p.title +} + +// Description returns the proposal description. +// +// Returns: +// - string: proposal description +func (p *ProposalMetadata) Description() string { + return p.description +} + +// Validate performs comprehensive validation of the proposal metadata. +// Checks title and description length and content requirements. +// +// Returns: +// - error: validation error if metadata is invalid +func (p *ProposalMetadata) Validate() error { + // Validate title meets requirements + if err := p.validateTitle(p.title); err != nil { + return err + } + + // Validate description meets requirements + if err := p.validateDescription(p.description); err != nil { + return err + } + + return nil +} + +// validateTitle checks if the proposal title meets length and content requirements. +// +// Parameters: +// - title: title string to validate +// +// Returns: +// - error: validation error if title is invalid +func (p *ProposalMetadata) validateTitle(title string) error { + // Title cannot be empty + if title == "" { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("title is empty"), + ) + } + + // Title cannot exceed maximum length + if len(title) > maxTitleLength { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("title is too long, max length is %d", maxTitleLength), + ) + } + + return nil +} + +// validateDescription checks if the proposal description meets length and content requirements. +// +// Parameters: +// - description: description string to validate +// +// Returns: +// - error: validation error if description is invalid +func (p *ProposalMetadata) validateDescription(description string) error { + // Description cannot be empty + if description == "" { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("description is empty"), + ) + } + + // Description cannot exceed maximum length + if len(description) > maxDescriptionLength { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("description is too long, max length is %d", maxDescriptionLength), + ) + } + + return nil +} + +// NewProposalMetadata creates a new proposal metadata instance with trimmed input. +// +// Parameters: +// - title: proposal title +// - description: proposal description +// +// Returns: +// - *ProposalMetadata: new metadata instance with trimmed whitespace +func NewProposalMetadata(title string, description string) *ProposalMetadata { + return &ProposalMetadata{ + title: strings.TrimSpace(title), + description: strings.TrimSpace(description), + } +} + +// ProposalData contains the type-specific data for a proposal. +// This structure holds different data depending on the proposal type. +type ProposalData struct { + proposalType ProposalType // Type of proposal (Text, CommunityPoolSpend, ParameterChange) + communityPoolSpend CommunityPoolSpendInfo // Data for community pool spending proposals + execution ExecutionInfo // Data for parameter change proposals +} + +// ProposalType returns the type of this proposal. +// +// Returns: +// - ProposalType: the proposal type +func (p *ProposalData) ProposalType() ProposalType { + return p.proposalType +} + +// CommunityPoolSpend returns the community pool spending information. +// +// Returns: +// - CommunityPoolSpendInfo: community pool spending details +func (p *ProposalData) CommunityPoolSpend() CommunityPoolSpendInfo { + return p.communityPoolSpend +} + +// Execution returns the execution information for parameter changes. +// +// Returns: +// - ExecutionInfo: parameter change execution details +func (p *ProposalData) Execution() ExecutionInfo { + return p.execution +} + +// Validate performs type-specific validation of the proposal data. +// Different proposal types have different validation requirements. +// +// Returns: +// - error: validation error if data is invalid +func (p *ProposalData) Validate() error { + // Validate based on proposal type + if p.proposalType == Text { + return p.validateText() + } + + if p.proposalType == CommunityPoolSpend { + return p.validateCommunityPoolSpend() + } + + if p.proposalType == ParameterChange { + return p.validateParameterChange() + } + + return nil +} + +// validateText validates text proposal data. +// Text proposals have no additional validation requirements. +// +// Returns: +// - error: always nil for text proposals +func (p *ProposalData) validateText() error { + return nil +} + +// validateCommunityPoolSpend validates community pool spend proposal data. +// Checks recipient address, token path, and amount validity. +// +// Returns: +// - error: validation error if community pool spend data is invalid +func (p *ProposalData) validateCommunityPoolSpend() error { + // Validate recipient address + if !p.communityPoolSpend.to.IsValid() { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("to is invalid address"), + ) + } + + // Validate token path is provided + if p.communityPoolSpend.tokenPath == "" { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("tokenPath is empty"), + ) + } + + // Validate amount is positive + if p.communityPoolSpend.amount == 0 { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("amount is 0"), + ) + } + + return nil +} + +// validateParameterChange validates parameter change proposal data. +// Checks execution count, message format, and parameter validity. +// +// Returns: +// - error: validation error if parameter change data is invalid +func (p *ProposalData) validateParameterChange() error { + // Validate execution count is positive + if p.execution.num <= 0 { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("numToExecute is less than or equal to 0"), + ) + } + + // Validate execution messages are provided + if len(p.execution.msgs) == 0 { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("executions is empty"), + ) + } + + // Validate execution count matches message count + if len(p.execution.msgs) != int(p.execution.num) { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("executions is not equal to numToExecute"), + ) + } + + // Validate execution count doesn't exceed maximum + if p.execution.num > maxNumberOfExecution { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("numToExecute is greater than %d", maxNumberOfExecution), + ) + } + + // Validate parameter change message format + parameterChangesInfos := p.execution.ParameterChangesInfos() + if len(parameterChangesInfos) != int(p.execution.num) { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("invalid parameter change info"), + ) + } + + return nil +} + +// NewProposalData creates a new proposal data instance with the specified components. +// +// Parameters: +// - proposalType: type of the proposal +// - communityPoolSpend: community pool spending information +// - execution: parameter change execution information +// +// Returns: +// - *ProposalData: new proposal data instance +func NewProposalData(proposalType ProposalType, communityPoolSpend CommunityPoolSpendInfo, execution ExecutionInfo) *ProposalData { + return &ProposalData{ + proposalType: proposalType, + communityPoolSpend: communityPoolSpend, + execution: execution, + } +} + +// NewProposalTextData creates proposal data for a text proposal. +// Text proposals have no additional data requirements. +// +// Returns: +// - *ProposalData: proposal data configured for text proposal +func NewProposalTextData() *ProposalData { + return NewProposalData( + Text, + CommunityPoolSpendInfo{}, + ExecutionInfo{}, + ) +} + +// NewProposalCommunityPoolSpendData creates proposal data for a community pool spend proposal. +// Automatically generates the execution message for the token transfer. +// +// Parameters: +// - tokenPath: path of the token to transfer +// - to: recipient address for the transfer +// - amount: amount of tokens to transfer +// - communityPoolPackagePath: package path of the community pool contract +// +// Returns: +// - *ProposalData: proposal data configured for community pool spending +func NewProposalCommunityPoolSpendData( + tokenPath string, + to std.Address, + amount int64, + communityPoolPackagePath string, +) *ProposalData { + // Create execution message for the token transfer + executionInfoMessage := makeExecuteMessage( + communityPoolPackagePath, + "TransferToken", + []string{tokenPath, to.String(), ufmt.Sprintf("%d", amount)}, + ) + + return NewProposalData( + CommunityPoolSpend, + CommunityPoolSpendInfo{to, tokenPath, amount}, + ExecutionInfo{ + num: 1, + msgs: []string{executionInfoMessage}, + }, + ) +} + +// NewProposalExecutionData creates proposal data for a parameter change proposal. +// Parses the execution string to create the execution structure. +// +// Parameters: +// - numToExecute: number of parameter changes to execute +// - executions: encoded execution string with parameter changes +// +// Returns: +// - *ProposalData: proposal data configured for parameter changes +func NewProposalExecutionData(numToExecute int64, executions string) *ProposalData { + // Split execution string into individual messages + msgs := strings.Split(executions, messageSeparator) + + return NewProposalData( + ParameterChange, + CommunityPoolSpendInfo{}, + ExecutionInfo{numToExecute, msgs}, + ) +} + +// CommunityPoolSpendInfo contains information for community pool spending proposals. +type CommunityPoolSpendInfo struct { + to std.Address // Recipient address for token transfer + tokenPath string // Path of the token to transfer + amount int64 // Amount of tokens to transfer +} + +// ExecutionInfo contains information for parameter change execution. +// Messages are encoded strings that specify function calls and parameters. +type ExecutionInfo struct { + num int64 // Number of parameter changes to execute + msgs []string // Execution messages separated by messageSeparator (*GOV*) +} + +// ParameterChangesInfos parses the execution messages and returns structured parameter change information. +// Each message is expected to be in format: pkgPath*EXE*function*EXE*params +// +// Returns: +// - []ParameterChangeInfo: slice of parsed parameter change information +func (e *ExecutionInfo) ParameterChangesInfos() []ParameterChangeInfo { + // Return empty slice if no executions + if e.num <= 0 { + return []ParameterChangeInfo{} + } + + infos := make([]ParameterChangeInfo, 0) + + // Parse each execution message + for _, msg := range e.msgs { + // Split message into components: pkgPath, function, params + params := strings.Split(msg, parameterSeparator) + if len(params) != 3 { + continue // Skip malformed messages + } + + pkgPath := params[0] + function := params[1] + executionParams := strings.Split(params[2], ",") + + // Create parameter change info structure + infos = append(infos, ParameterChangeInfo{ + pkgPath: pkgPath, + function: function, + params: executionParams, + }) + } + + return infos +} + +// ParameterChangeInfo represents a single parameter change to be executed. +type ParameterChangeInfo struct { + pkgPath string // Package path of the target contract + function string // Function name to call + params []string // Parameters to pass to the function +} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_manager.gno b/contract/r/gnoswap/v1/gov/governance/proposal_manager.gno new file mode 100644 index 0000000..34e8018 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/proposal_manager.gno @@ -0,0 +1,95 @@ +package governance + +import "std" + +// ProposalManager manages the association between users and their created proposals. +// This structure provides efficient lookup of proposals by user address and maintains +// the relationship for governance operations and queries. +type ProposalManager struct { + userProposals map[string]map[int64]bool // Maps user address to their proposal IDs +} + +// GetUserProposals retrieves all proposal IDs created by a specific user. +// Returns an empty slice if the user has no proposals. +func (pm *ProposalManager) GetUserProposals(user std.Address) []int64 { + // Check if user has any proposals + _, ok := pm.userProposals[user.String()] + if !ok { + return []int64{} + } + + proposalIDs := make([]int64, 0) + + // Collect all proposal IDs for this user + for proposalID := range pm.userProposals[user.String()] { + proposalIDs = append(proposalIDs, proposalID) + } + + return proposalIDs +} + +// HasProposal checks if a specific user has created a specific proposal. +// Returns true if the user created the specified proposal. +func (pm *ProposalManager) HasProposal(user std.Address, proposalID int64) bool { + // First check if user has any proposals + proposals, ok := pm.userProposals[user.String()] + if !ok { + return false + } + + // Then check if specific proposal exists for this user + _, ok = proposals[proposalID] + if !ok { + return false + } + + return true +} + +// addProposal associates a proposal with its creator. +// This is called when a new proposal is created to establish the relationship. +// +// Parameters: +// - user: address of the proposal creator +// - proposalID: ID of the created proposal +func (pm *ProposalManager) addProposal(user std.Address, proposalID int64) { + // Initialize user's proposal map if it doesn't exist + if _, ok := pm.userProposals[user.String()]; !ok { + pm.userProposals[user.String()] = make(map[int64]bool) + } + + // Add the proposal to the user's list + pm.userProposals[user.String()][proposalID] = true +} + +// removeProposal removes the association between a user and proposal. +// This could be used for cleanup operations (though currently not used in practice). +// +// Parameters: +// - user: address of the proposal creator +// - proposalID: ID of the proposal to remove +func (pm *ProposalManager) removeProposal(user std.Address, proposalID int64) { + // Exit early if user doesn't have the proposal + if !pm.HasProposal(user, proposalID) { + return + } + + // Double-check user exists (defensive programming) + if _, ok := pm.userProposals[user.String()]; !ok { + return + } + + // Remove the proposal from user's list + delete(pm.userProposals[user.String()], proposalID) +} + +// NewProposalManager creates a new proposal manager instance. +// Initializes the internal data structures for managing user-proposal relationships. +// +// Returns: +// - *ProposalManager: new proposal manager instance +func NewProposalManager() *ProposalManager { + return &ProposalManager{ + userProposals: make(map[string]map[int64]bool), + } +} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_schedule_status.gno b/contract/r/gnoswap/v1/gov/governance/proposal_schedule_status.gno new file mode 100644 index 0000000..89d01db --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/proposal_schedule_status.gno @@ -0,0 +1,108 @@ +package governance + +// ProposalScheduleStatus represents the pre-calculated time schedule for a proposal. +// This structure defines all the important timestamps in a proposal's lifecycle, +// from creation through voting to execution and expiration. +type ProposalScheduleStatus struct { + createTime int64 // When the proposal was created + activeTime int64 // When voting starts (CreateTime + VotingStartDelay) + votingEndTime int64 // When voting ends (ActiveTime + VotingPeriod) + executableTime int64 // When execution window starts (VotingEndTime + ExecutionDelay) + expiredTime int64 // When execution window ends (ExecutableTime + ExecutionWindow) +} + +// IsPassedCreatedAt checks if the current time has passed the proposal creation time. +// This is always true once a proposal exists. +// +// Parameters: +// - current: timestamp to check against +// +// Returns: +// - bool: true if current time is at or after creation time +func (p *ProposalScheduleStatus) IsPassedCreatedAt(current int64) bool { + return p.createTime <= current +} + +// IsPassedActiveAt checks if the current time has passed the voting start time. +// When true, the proposal enters its active voting period. +// +// Parameters: +// - current: timestamp to check against +// +// Returns: +// - bool: true if voting period has started +func (p *ProposalScheduleStatus) IsPassedActiveAt(current int64) bool { + return p.activeTime <= current +} + +// IsPassedVotingEndedAt checks if the current time has passed the voting end time. +// When true, no more votes can be cast on the proposal. +// +// Parameters: +// - current: timestamp to check against +// +// Returns: +// - bool: true if voting period has ended +func (p *ProposalScheduleStatus) IsPassedVotingEndedAt(current int64) bool { + return p.votingEndTime <= current +} + +// IsPassedExecutableAt checks if the current time has passed the execution start time. +// When true, approved proposals can be executed (after execution delay). +// +// Parameters: +// - current: timestamp to check against +// +// Returns: +// - bool: true if execution window has started +func (p *ProposalScheduleStatus) IsPassedExecutableAt(current int64) bool { + return p.executableTime <= current +} + +// IsPassedExpiredAt checks if the current time has passed the execution expiration time. +// When true, the proposal can no longer be executed and has expired. +// +// Parameters: +// - current: timestamp to check against +// +// Returns: +// - bool: true if execution window has expired +func (p *ProposalScheduleStatus) IsPassedExpiredAt(current int64) bool { + return p.expiredTime <= current +} + +// NewProposalScheduleStatus creates a new schedule status with calculated timestamps. +// This constructor takes the governance timing parameters and calculates all +// important timestamps for the proposal's lifecycle. +// +// Parameters: +// - votingStartDelay: delay before voting starts (seconds) +// - votingPeriod: duration of voting period (seconds) +// - executionDelay: delay before execution can start (seconds) +// - executionWindow: window during which execution is allowed (seconds) +// - createdAt: timestamp when proposal was created +// +// Returns: +// - *ProposalScheduleStatus: new schedule status with calculated times +func NewProposalScheduleStatus( + votingStartDelay, + votingPeriod, + executionDelay, + executionWindow, + createdAt int64, +) *ProposalScheduleStatus { + // Calculate all phase timestamps based on creation time and configuration + createTime := createdAt + activeTime := createTime + votingStartDelay // When voting can start + votingEndTime := activeTime + votingPeriod // When voting ends + executableTime := votingEndTime + executionDelay // When execution can start + expiredTime := executableTime + executionWindow // When execution window closes + + return &ProposalScheduleStatus{ + createTime: createTime, + activeTime: activeTime, + votingEndTime: votingEndTime, + executableTime: executableTime, + expiredTime: expiredTime, + } +} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_status.gno b/contract/r/gnoswap/v1/gov/governance/proposal_status.gno new file mode 100644 index 0000000..2caeaec --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/proposal_status.gno @@ -0,0 +1,314 @@ +package governance + +import ( + "std" +) + +// ProposalStatusType represents the current status of a proposal in its lifecycle. +// These statuses determine what actions are available for a proposal. +type ProposalStatusType int + +const ( + _ ProposalStatusType = iota + StatusUpcoming // Proposal created but voting hasn't started yet + StatusActive // Proposal is in voting period + StatusPassed // Proposal has passed but hasn't been executed (or is text proposal) + StatusRejected // Proposal failed to meet voting requirements + StatusExecutable // Proposal can be executed (passed and in execution window) + StatusExecuted // Proposal has been successfully executed + StatusExpired // Proposal execution window has passed + StatusCanceled // Proposal has been canceled +) + +// String returns the string representation of ProposalStatusType for display purposes. +// +// Returns: +// - string: human-readable status name +func (s ProposalStatusType) String() string { + switch s { + case StatusUpcoming: + return "upcoming" + case StatusActive: + return "active" + case StatusPassed: + return "passed" + case StatusRejected: + return "rejected" + case StatusExecutable: + return "executable" + case StatusExecuted: + return "executed" + case StatusExpired: + return "expired" + case StatusCanceled: + return "canceled" + default: + return "unknown" + } +} + +// ProposalStatus manages the complete status of a proposal including scheduling, voting, and actions. +// This is the central status tracking structure that coordinates different aspects of proposal state. +type ProposalStatus struct { + schedule *ProposalScheduleStatus // Time-based scheduling information + actionStatus *ProposalActionStatus // Execution and cancellation status + voteStatus *ProposalVoteStatus // Voting tallies and requirements +} + +// StatusType determines the current status of the proposal based on timing, voting, and actions. +// This is the main status calculation method that considers all factors. +// +// Parameters: +// - current: current timestamp to evaluate status at +// +// Returns: +// - ProposalStatusType: current status of the proposal +func (p *ProposalStatus) StatusType(current int64) ProposalStatusType { + // Check action-based statuses first (these override time-based statuses) + if p.actionStatus.IsExecuted() { + return StatusExecuted + } + + if p.actionStatus.IsCanceled() { + return StatusCanceled + } + + // Check time-based statuses + if !p.schedule.IsPassedActiveAt(current) { + return StatusUpcoming + } + + if !p.schedule.IsPassedVotingEndedAt(current) { + return StatusActive + } + + // Check voting outcome + if p.voteStatus.IsRejected() { + return StatusRejected + } + + // For passed proposals, check execution status + if !p.actionStatus.IsExecutable() || !p.schedule.IsPassedExecutableAt(current) { + return StatusPassed + } + + if !p.schedule.IsPassedExpiredAt(current) { + return StatusExecutable + } + + return StatusExpired +} + +// IsUpcoming checks if the proposal is in upcoming status. +// +// Parameters: +// - current: timestamp to check status at +// +// Returns: +// - bool: true if proposal is upcoming +func (p *ProposalStatus) IsUpcoming(current int64) bool { + return p.StatusType(current) == StatusUpcoming +} + +// IsActive checks if the proposal is in active voting status. +// +// Parameters: +// - current: timestamp to check status at +// +// Returns: +// - bool: true if proposal is active (voting period) +func (p *ProposalStatus) IsActive(current int64) bool { + return p.StatusType(current) == StatusActive +} + +// IsPassed checks if the proposal has passed voting. +// +// Parameters: +// - current: timestamp to check status at +// +// Returns: +// - bool: true if proposal has passed +func (p *ProposalStatus) IsPassed(current int64) bool { + return p.StatusType(current) == StatusPassed +} + +// IsRejected checks if the proposal has been rejected by voting. +// +// Parameters: +// - current: timestamp to check status at +// +// Returns: +// - bool: true if proposal was rejected +func (p *ProposalStatus) IsRejected(current int64) bool { + return p.StatusType(current) == StatusRejected +} + +// IsExecutable checks if the proposal is in executable status. +// +// Parameters: +// - current: timestamp to check status at +// +// Returns: +// - bool: true if proposal can be executed +func (p *ProposalStatus) IsExecutable(current int64) bool { + return p.StatusType(current) == StatusExecutable +} + +// IsExpired checks if the proposal execution window has expired. +// +// Parameters: +// - current: timestamp to check status at +// +// Returns: +// - bool: true if proposal has expired +func (p *ProposalStatus) IsExpired(current int64) bool { + return p.StatusType(current) == StatusExpired +} + +// IsExecuted checks if the proposal has been executed. +// +// Parameters: +// - current: timestamp to check status at +// +// Returns: +// - bool: true if proposal has been executed +func (p *ProposalStatus) IsExecuted(current int64) bool { + return p.StatusType(current) == StatusExecuted +} + +// IsCanceled checks if the proposal has been canceled. +// +// Parameters: +// - current: timestamp to check status at +// +// Returns: +// - bool: true if proposal has been canceled +func (p *ProposalStatus) IsCanceled(current int64) bool { + return p.StatusType(current) == StatusCanceled +} + +// YesWeight returns the total weight of "yes" votes. +// +// Returns: +// - int64: total "yes" vote weight +func (p *ProposalStatus) YesWeight() int64 { + return p.voteStatus.YesWeight() +} + +// NoWeight returns the total weight of "no" votes. +// +// Returns: +// - int64: total "no" vote weight +func (p *ProposalStatus) NoWeight() int64 { + return p.voteStatus.NoWeight() +} + +// TotalVoteWeight returns the total weight of all votes cast. +// +// Returns: +// - int64: total vote weight +func (p *ProposalStatus) TotalVoteWeight() int64 { + return p.voteStatus.TotalVoteWeight() +} + +// DiffVoteWeight returns the absolute difference between yes and no votes. +// +// Returns: +// - int64: absolute difference in vote weights +func (p *ProposalStatus) DiffVoteWeight() int64 { + return p.voteStatus.DiffVoteWeight() +} + +// cancel marks the proposal as canceled with the provided details. +// This delegates to the action status for actual cancellation logic. +// +// Parameters: +// - canceledAt: timestamp when proposal was canceled +// - canceledHeight: block height when proposal was canceled +// - canceledBy: address that canceled the proposal +// +// Returns: +// - error: cancellation error if operation fails +func (p *ProposalStatus) cancel(canceledAt int64, canceledHeight int64, canceledBy std.Address) error { + return p.actionStatus.cancel(canceledAt, canceledHeight, canceledBy) +} + +// execute marks the proposal as executed with the provided details. +// This delegates to the action status for actual execution logic. +// +// Parameters: +// - executedAt: timestamp when proposal was executed +// - executedHeight: block height when proposal was executed +// - executedBy: address that executed the proposal +// +// Returns: +// - error: execution error if operation fails +func (p *ProposalStatus) execute(executedAt int64, executedHeight int64, executedBy std.Address) error { + return p.actionStatus.execute(executedAt, executedHeight, executedBy) +} + +// vote records a vote on the proposal and updates vote tallies. +// This delegates to the vote status for actual vote recording. +// +// Parameters: +// - votedYes: true for "yes" vote, false for "no" vote +// - weight: voting weight to apply +// +// Returns: +// - error: voting error if operation fails +func (p *ProposalStatus) vote(votedYes bool, weight int64) error { + if votedYes { + return p.voteStatus.addYesVoteWeight(weight) + } + + return p.voteStatus.addNoVoteWeight(weight) +} + +// updateVoteStatus updates the voting parameters and recalculates requirements. +// This is used when voting parameters change dynamically. +// +// Parameters: +// - maxVotingWeight: updated maximum voting weight +// - quorum: updated quorum percentage +// +// Returns: +// - error: update error if operation fails +func (p *ProposalStatus) updateVoteStatus(maxVotingWeight, quorum int64) error { + return p.voteStatus.updateVoteStatus(maxVotingWeight, quorum) +} + +// NewProposalStatus creates a new proposal status with the specified configuration. +// This initializes all status components with the governance configuration and timing. +// +// Parameters: +// - config: governance configuration to use +// - maxVotingWeight: maximum voting weight for this proposal +// - executable: whether this proposal type can be executed +// - createdAt: timestamp when proposal was created +// +// Returns: +// - *ProposalStatus: new proposal status instance +func NewProposalStatus( + config Config, + maxVotingWeight int64, + executable bool, + createdAt int64, +) *ProposalStatus { + return &ProposalStatus{ + // Initialize time-based scheduling + schedule: NewProposalScheduleStatus( + config.VotingStartDelay, + config.VotingPeriod, + config.ExecutionDelay, + config.ExecutionWindow, + createdAt, + ), + // Initialize action status (execution/cancellation tracking) + actionStatus: NewProposalActionStatus(executable), + // Initialize vote status with voting requirements + voteStatus: NewProposalVoteStatus( + maxVotingWeight, + config.Quorum, + ), + } +} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_vote_status.gno b/contract/r/gnoswap/v1/gov/governance/proposal_vote_status.gno new file mode 100644 index 0000000..244ecc2 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/proposal_vote_status.gno @@ -0,0 +1,163 @@ +package governance + +// ProposalVoteStatus tracks the voting tallies and requirements for a proposal. +// This structure manages vote counting, quorum calculation, and voting outcome determination. +type ProposalVoteStatus struct { + yea int64 // Total weight of "yes" votes collected + nay int64 // Total weight of "no" votes collected + maxVotingWeight int64 // The max voting weight at the time of proposal creation + quorumAmount int64 // How many total votes must be collected for the proposal to be valid +} + +// TotalVoteWeight returns the total weight of all votes cast (yes + no). +// +// Returns: +// - int64: combined weight of all votes +func (p *ProposalVoteStatus) TotalVoteWeight() int64 { + return p.yea + p.nay +} + +// DiffVoteWeight returns the absolute difference between yes and no votes. +// This can be used to determine the margin of victory or defeat. +// +// Returns: +// - int64: absolute difference between yes and no vote weights +func (p *ProposalVoteStatus) DiffVoteWeight() int64 { + if p.yea > p.nay { + return p.yea - p.nay + } + + return p.nay - p.yea +} + +// YesWeight returns the total weight of "yes" votes. +// +// Returns: +// - int64: total "yes" vote weight +func (p *ProposalVoteStatus) YesWeight() int64 { + return p.yea +} + +// NoWeight returns the total weight of "no" votes. +// +// Returns: +// - int64: total "no" vote weight +func (p *ProposalVoteStatus) NoWeight() int64 { + return p.nay +} + +// IsVotingFinished determines if voting has effectively ended due to mathematical impossibility +// of changing the outcome. This happens when the remaining uncast votes cannot change the result. +// +// Returns: +// - bool: true if voting outcome is mathematically determined +func (p *ProposalVoteStatus) IsVotingFinished() bool { + totalVotes := p.TotalVoteWeight() + + // If we haven't reached quorum yet, voting is not finished + if totalVotes < p.quorumAmount { + return false + } + + // Calculate remaining votes that could still be cast + remainingVotes := p.maxVotingWeight - totalVotes + + // If the difference between yes/no is greater than remaining votes, + // the outcome cannot change, so voting is effectively finished + return remainingVotes-p.DiffVoteWeight() <= 0 +} + +// IsRejected determines if the proposal has been rejected by voting. +// A proposal is rejected if voting is finished and it did not pass. +// +// Returns: +// - bool: true if proposal has been rejected +func (p *ProposalVoteStatus) IsRejected() bool { + // Only consider rejection if voting is finished + if !p.IsVotingFinished() { + return false + } + + // Proposal is rejected if it didn't pass + return !p.IsPassed() +} + +// IsPassed determines if the proposal has passed the voting requirements. +// A proposal passes if it receives at least the quorum amount of "yes" votes. +// +// Returns: +// - bool: true if proposal has passed +func (p *ProposalVoteStatus) IsPassed() bool { + return p.yea >= p.quorumAmount +} + +// addYesVoteWeight adds the specified weight to the "yes" vote tally. +// This is called when a user votes "yes" on the proposal. +// +// Parameters: +// - yea: vote weight to add to "yes" votes +// +// Returns: +// - error: always nil (reserved for future validation) +func (p *ProposalVoteStatus) addYesVoteWeight(yea int64) error { + p.yea += yea + + return nil +} + +// addNoVoteWeight adds the specified weight to the "no" vote tally. +// This is called when a user votes "no" on the proposal. +// +// Parameters: +// - nay: vote weight to add to "no" votes +// +// Returns: +// - error: always nil (reserved for future validation) +func (p *ProposalVoteStatus) addNoVoteWeight(nay int64) error { + p.nay += nay + + return nil +} + +// updateVoteStatus updates the voting parameters and recalculates the quorum requirement. +// This can be used if voting parameters change dynamically. +// +// Parameters: +// - maxVotingWeight: updated maximum voting weight +// - quorum: updated quorum percentage +// +// Returns: +// - error: always nil (reserved for future validation) +func (p *ProposalVoteStatus) updateVoteStatus(maxVotingWeight, quorum int64) error { + // Update maximum voting weight + p.maxVotingWeight = maxVotingWeight + + // Recalculate quorum amount based on new parameters + p.quorumAmount = maxVotingWeight * quorum / 100 + + return nil +} + +// NewProposalVoteStatus creates a new vote status for a proposal. +// Initializes vote tallies to zero and calculates the quorum requirement. +// +// Parameters: +// - maxVotingWeight: maximum possible voting weight for this proposal +// - quorum: quorum percentage required for passage (0-100) +// +// Returns: +// - *ProposalVoteStatus: new vote status instance +func NewProposalVoteStatus( + maxVotingWeight int64, + quorum int64, +) *ProposalVoteStatus { + // Calculate the absolute vote weight needed to meet quorum + quorumAmount := maxVotingWeight * quorum / 100 + + return &ProposalVoteStatus{ + yea: 0, // Start with no "yes" votes + nay: 0, // Start with no "no" votes + maxVotingWeight: maxVotingWeight, // Set maximum possible votes + quorumAmount: quorumAmount, // Set required votes for passage + } +} diff --git a/contract/r/gnoswap/v1/gov/governance/state.gno b/contract/r/gnoswap/v1/gov/governance/state.gno new file mode 100644 index 0000000..b3e8dcb --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/state.gno @@ -0,0 +1,239 @@ +package governance + +import ( + "std" + + "gno.land/p/nt/avl" +) + +// Global state variables for governance system +var ( + configCounter *Counter // Counter for generating config version numbers + proposalCounter *Counter // Counter for generating unique proposal IDs + + configs *avl.Tree // Tree storing governance configurations by version + proposals *avl.Tree // Tree storing all proposals by ID + proposalManager *ProposalManager // Manager for user-proposal associations + proposalUserVotingInfos *avl.Tree // Tree storing voting info for each proposal by user + + // Function to retrieve user voting snapshots (can be overridden for testing) + getUserVotingInfoSnapshotFn func(snapshotTime int64) (map[string]*VotingInfo, int64, bool) +) + +// init initializes the governance system state when the contract is deployed. +// This function sets up all necessary data structures and default configurations. +func init() { + initConfig() + initProposal() + initStakerDelegationSnapshots() +} + +// initConfig initializes the governance configuration system. +// Sets up the configuration counter and creates the default initial configuration. +func initConfig() { + configCounter = NewCounter() + configs = avl.NewTree() + + // Create the initial governance configuration with default parameters + nextConfigVersion := nextConfigVersion() + config := Config{ + VotingStartDelay: 86400, // 1 day - delay before voting starts + VotingPeriod: 604800, // 7 days - duration for collecting votes + VotingWeightSmoothingDuration: 86400, // 1 day - period for averaging voting weight + Quorum: 50, // 50% of total xGNS supply required + ProposalCreationThreshold: 1_000_000_000, // 1 billion - minimum balance to create proposals + ExecutionDelay: 86400, // 1 day - waiting period before execution + ExecutionWindow: 2592000, // 30 days - window for executing proposals + } + + setConfig(nextConfigVersion, config) +} + +// initProposal initializes the proposal management system. +// Sets up counters, storage trees, and management structures for proposals. +func initProposal() { + proposalCounter = NewCounter() + proposals = avl.NewTree() + proposalManager = NewProposalManager() + proposalUserVotingInfos = avl.NewTree() +} + +// initStakerDelegationSnapshots initializes the voting snapshot function. +// Sets up the default function to retrieve voting weights from staker contract. +func initStakerDelegationSnapshots() { + getUserVotingInfoSnapshotFn = func(snapshotTime int64) (map[string]*VotingInfo, int64, bool) { + return getUserVotingInfotWithDelegationSnapshots(snapshotTime) + } +} + +// getCurrentConfigVersion returns the current governance configuration version. +// +// Returns: +// - int64: current configuration version number +func getCurrentConfigVersion() int64 { + return configCounter.Get() +} + +// nextConfigVersion increments and returns the next configuration version number. +// This is used when creating new governance configurations. +// +// Returns: +// - int64: next configuration version number +func nextConfigVersion() int64 { + return configCounter.next() +} + +// getCurrentProposalID returns the current proposal ID (last assigned). +// +// Returns: +// - int64: current proposal ID +func getCurrentProposalID() int64 { + return proposalCounter.Get() +} + +// nextProposalID increments and returns the next unique proposal ID. +// This is used when creating new proposals. +// +// Returns: +// - int64: next unique proposal ID +func nextProposalID() int64 { + return proposalCounter.next() +} + +// getConfig retrieves a specific governance configuration by version number. +// +// Parameters: +// - version: configuration version to retrieve +// +// Returns: +// - Config: governance configuration for the specified version +// - bool: true if configuration exists, false otherwise +func getConfig(version int64) (cfg Config, ok bool) { + if val, exists := configs.Get(formatInt(version)); !exists { + return + } else { + cfg, ok = val.(Config) + } + + return +} + +// setConfig stores a governance configuration with the specified version number. +// +// Parameters: +// - version: configuration version number +// - config: governance configuration to store +func setConfig(version int64, config Config) { + configs.Set(formatInt(version), config) +} + +// getCurrentConfig retrieves the current active governance configuration. +// +// Returns: +// - Config: current governance configuration +// - bool: true if configuration exists, false otherwise +func getCurrentConfig() (Config, bool) { + return getConfig(getCurrentConfigVersion()) +} + +// getProposal retrieves a specific proposal by its ID. +// +// Parameters: +// - proposalID: unique identifier of the proposal +// +// Returns: +// - *Proposal: pointer to the proposal if found +// - bool: true if proposal exists, false otherwise +func getProposal(proposalID int64) (proposal *Proposal, ok bool) { + if val, exists := proposals.Get(formatInt(proposalID)); !exists { + return + } else { + proposal, ok = val.(*Proposal) + } + return +} + +// addProposal stores a new proposal in the system. +// Also registers the proposal with the proposal manager for user tracking. +// +// Parameters: +// - proposal: proposal to store +// +// Returns: +// - bool: true if proposal was successfully added +func addProposal(proposal *Proposal) bool { + id := proposal.ID() + // Store proposal in main proposals tree + proposals.Set(formatInt(id), proposal) + + // Register proposal with user in proposal manager + proposalManager.addProposal(proposal.Proposer(), id) + + return true +} + +// getUserProposals retrieves all proposals created by a specific user. +// +// Parameters: +// - user: address of the user +// +// Returns: +// - []*Proposal: slice of proposals created by the user +func getUserProposals(user std.Address) (proposals []*Proposal) { + // Get proposal IDs for this user + proposalIDs := proposalManager.GetUserProposals(user) + + // Retrieve each proposal by ID + for _, proposalID := range proposalIDs { + if proposal, ok := getProposal(proposalID); !ok { + continue // Skip if proposal not found (shouldn't happen) + } else { + proposals = append(proposals, proposal) + } + } + return +} + +// getProposalUserVotingInfos retrieves all voting information for a specific proposal. +// Returns a mapping of user addresses to their voting information. +// +// Parameters: +// - proposalID: unique identifier of the proposal +// +// Returns: +// - map[string]*VotingInfo: mapping of user addresses to voting information +// - bool: true if voting information exists for the proposal +func getProposalUserVotingInfos(proposalID int64) (userVotingInfos map[string]*VotingInfo, ok bool) { + id := formatInt(proposalID) + if userVotingInfosI, exists := proposalUserVotingInfos.Get(id); !exists { + return + } else { + userVotingInfos, ok = userVotingInfosI.(map[string]*VotingInfo) + } + return +} + +// getProposalUserVotingInfo retrieves voting information for a specific user on a specific proposal. +// +// Parameters: +// - proposalID: unique identifier of the proposal +// - userAddress: address of the user +// +// Returns: +// - *VotingInfo: voting information for the user +// - bool: true if voting information exists for the user +func getProposalUserVotingInfo(proposalID int64, userAddress std.Address) (*VotingInfo, bool) { + // First get all voting info for the proposal + userVotingInfos, exists := getProposalUserVotingInfos(proposalID) + if !exists { + return nil, false + } + + // Then lookup the specific user's voting info + val, exists := userVotingInfos[userAddress.String()] + if !exists { + return nil, false + } + + return val, true +} diff --git a/contract/r/gnoswap/v1/gov/governance/utils.gno b/contract/r/gnoswap/v1/gov/governance/utils.gno new file mode 100644 index 0000000..5355928 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/utils.gno @@ -0,0 +1,159 @@ +package governance + +import ( + "encoding/base64" + "strconv" + "strings" + + "gno.land/p/nt/avl" + + "gno.land/p/onbloc/json" + "gno.land/p/nt/ufmt" +) + +// iterTree iterates over an AVL tree and applies a callback function to each element. +func iterTree(tree *avl.Tree, cb func(key string, value any) bool) { + tree.Iterate("", "", cb) +} + +// strToInt converts a string to an integer. +func strToInt(str string) int { + res, err := strconv.Atoi(str) + if err != nil { + panic(err.Error()) + } + + return res +} + +// marshal marshals a JSON node to a string. +func marshal(data *json.Node) string { + b, err := json.Marshal(data) + if err != nil { + panic(err.Error()) + } + + return string(b) +} + +// b64Encode encodes a string to base64. +func b64Encode(data string) string { + return string(base64.StdEncoding.EncodeToString([]byte(data))) +} + +// b64Decode decodes a base64 string. +func b64Decode(data string) string { + decoded, err := base64.StdEncoding.DecodeString(data) + if err != nil { + panic(err.Error()) + } + return string(decoded) +} + +// formatInt formats an integer to a string. +func formatInt(v any) string { + switch v := v.(type) { + case int8: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} + +// parseInt64 parses a string to int64. +func parseInt64(s string) int64 { + num, err := strconv.ParseInt(s, 10, 64) + if err != nil { + panic(ufmt.Sprintf("invalid int64 value: %s", s)) + } + + return num +} + +// parseUint64 parses a string to uint64. +func parseUint64(s string) uint64 { + num, err := strconv.ParseUint(s, 10, 64) + if err != nil { + panic(ufmt.Sprintf("invalid uint64 value: %s", s)) + } + + return num +} + +// formatBool formats a boolean to a string. +func formatBool(v bool) string { + return strconv.FormatBool(v) +} + +// numberKind represents the type of number to parse. +type numberKind int + +const ( + kindInt numberKind = iota + kindInt64 + kindUint64 +) + +// parseNumber parses a string to a number (int, int64, or uint64). +func parseNumber(s string, kind numberKind) any { + switch kind { + case kindInt: + num, err := strconv.ParseInt(s, 10, 64) + if err != nil { + panic(ufmt.Sprintf("invalid int value: %s", s)) + } + return int(num) + case kindInt64: + num, err := strconv.ParseInt(s, 10, 64) + if err != nil { + panic(ufmt.Sprintf("invalid int64 value: %s", s)) + } + return num + case kindUint64: + num, err := strconv.ParseUint(s, 10, 64) + if err != nil { + panic(ufmt.Sprintf("invalid uint64 value: %s", s)) + } + return num + default: + panic(ufmt.Sprintf("unsupported number kind: %v", kind)) + } +} + +// parseBool parses a string to a boolean. +func parseBool(s string) bool { + switch s { + case "true": + return true + case "false": + return false + default: + panic(ufmt.Sprintf("invalid bool value: %s", s)) + } +} + +// makeExecuteMessage creates a message to execute a function. +// Message format: *EXE**EXE*. +func makeExecuteMessage(pkgPath, function string, params []string) string { + messageParams := make([]string, 0) + + messageParams = append(messageParams, pkgPath) + messageParams = append(messageParams, function) + messageParams = append(messageParams, strings.Join(params, ",")) + + return strings.Join(messageParams, parameterSeparator) +} + +// parseMessage parses an execution message into its components. +func parseMessage(msg string) (pkgPath string, function string, params []string, err error) { + parts := strings.Split(msg, parameterSeparator) + if len(parts) != 3 { + return "", "", nil, errInvalidMessageFormat + } + + return parts[0], parts[1], strings.Split(parts[2], ","), nil +} diff --git a/contract/r/gnoswap/v1/gov/governance/voting_info.gno b/contract/r/gnoswap/v1/gov/governance/voting_info.gno new file mode 100644 index 0000000..8a60d9f --- /dev/null +++ b/contract/r/gnoswap/v1/gov/governance/voting_info.gno @@ -0,0 +1,150 @@ +package governance + +import ( + "std" +) + +// VotingInfo tracks voting-related information for a specific user on a specific proposal. +// This structure maintains the user's voting eligibility, voting history, and voting power. +type VotingInfo struct { + voterAddress std.Address // Address of the voter + availableVoteWeight int64 // Total voting weight available to this user for this proposal + votedWeight int64 // Actual weight used when voting (0 if not voted) + votedHeight int64 // Block height when vote was cast + votedAt int64 // Timestamp when vote was cast + votedYes bool // True if voted "yes", false if voted "no" + voted bool // True if user has already voted +} + +// VotingType returns a human-readable string representation of the vote choice. +// +// Returns: +// - string: "yes" or "no" based on voting choice +func (v *VotingInfo) VotingType() string { + if v.votedYes { + return "yes" + } + + return "no" +} + +// IsVoted checks if the user has already cast their vote. +// +// Returns: +// - bool: true if user has voted on this proposal +func (v *VotingInfo) IsVoted() bool { + return v.voted +} + +// VotedYes checks if the user voted "yes" on the proposal. +// Only meaningful if IsVoted() returns true. +// +// Returns: +// - bool: true if user voted "yes" +func (v *VotingInfo) VotedYes() bool { + return v.votedYes +} + +// VotedNo checks if the user voted "no" on the proposal. +// Only meaningful if IsVoted() returns true. +// +// Returns: +// - bool: true if user voted "no" +func (v *VotingInfo) VotedNo() bool { + return !v.votedYes +} + +// AvailableVoteWeight returns the total voting weight available to this user. +// This weight is determined at proposal creation time based on delegation snapshots. +// +// Returns: +// - int64: available voting weight +func (v *VotingInfo) AvailableVoteWeight() int64 { + return v.availableVoteWeight +} + +// VotedWeight returns the weight actually used when voting. +// Returns 0 if the user hasn't voted yet. +// +// Returns: +// - int64: weight used for voting, or 0 if not voted +func (v *VotingInfo) VotedWeight() int64 { + if !v.voted { + return 0 + } + + return v.votedWeight +} + +// voteYes records a "yes" vote with the specified weight and timing information. +// This is an internal helper method that delegates to the main vote function. +// +// Parameters: +// - weight: voting weight to use for this vote +// - votedHeight: block height when vote is cast +// - votedAt: timestamp when vote is cast +// +// Returns: +// - error: voting error if vote cannot be recorded +func (v *VotingInfo) voteYes(weight int64, votedHeight int64, votedAt int64) error { + return v.vote(true, weight, votedHeight, votedAt) +} + +// voteNo records a "no" vote with the specified weight and timing information. +// This is an internal helper method that delegates to the main vote function. +// +// Parameters: +// - weight: voting weight to use for this vote +// - votedHeight: block height when vote is cast +// - votedAt: timestamp when vote is cast +// +// Returns: +// - error: voting error if vote cannot be recorded +func (v *VotingInfo) voteNo(weight int64, votedHeight int64, votedAt int64) error { + return v.vote(false, weight, votedHeight, votedAt) +} + +// vote records a vote with the specified choice, weight, and timing information. +// This is the core voting method that prevents double voting and records all vote details. +// +// Parameters: +// - votedYes: true for "yes" vote, false for "no" vote +// - weight: voting weight to use for this vote +// - votedHeight: block height when vote is cast +// - votedAt: timestamp when vote is cast +// +// Returns: +// - error: voting error if user has already voted +func (v *VotingInfo) vote(votedYes bool, weight int64, votedHeight int64, votedAt int64) error { + // Prevent double voting - each user can only vote once per proposal + if v.voted { + return errAlreadyVoted + } + + // Record all voting details + v.votedWeight = weight + v.votedHeight = votedHeight + v.votedAt = votedAt + v.voted = true + v.votedYes = votedYes + + return nil +} + +// NewVotingInfo creates a new voting information structure for a user. +// This constructor initializes the voting eligibility based on delegation snapshots. +// +// Parameters: +// - availableVoteWeight: total voting weight available to this user +// - voterAddress: address of the voter +// +// Returns: +// - *VotingInfo: newly created voting information structure +func NewVotingInfo(availableVoteWeight int64, voterAddress std.Address) *VotingInfo { + return &VotingInfo{ + availableVoteWeight: availableVoteWeight, + voterAddress: voterAddress, + // Other fields are initialized to zero values (false, 0) + // voted starts as false, indicating no vote has been cast + } +} diff --git a/contract/r/gnoswap/v1/gov/staker/api_delegation.gno b/contract/r/gnoswap/v1/gov/staker/api_delegation.gno new file mode 100644 index 0000000..ff3b9be --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/api_delegation.gno @@ -0,0 +1,25 @@ +package staker + +import ( + "gno.land/r/gnoswap/v1/gov/xgns" +) + +// GetTotalxGnsSupply returns the total amount of xGNS supply. +func GetTotalxGnsSupply() int64 { + return xgns.TotalSupply() +} + +// GetTotalVoteWeight returns the total amount of xGNS used for voting. +func GetTotalVoteWeight() int64 { + return xgns.VotingSupply() +} + +// GetTotalDelegated returns the total amount of xGNS delegated. +func GetTotalDelegated() int64 { + return totalDelegatedAmount +} + +// GetTotalLockedAmount returns the total amount of locked GNS. +func GetTotalLockedAmount() int64 { + return totalLockedAmount +} diff --git a/contract/r/gnoswap/v1/gov/staker/api_staker.gno b/contract/r/gnoswap/v1/gov/staker/api_staker.gno new file mode 100644 index 0000000..5c11a68 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/api_staker.gno @@ -0,0 +1,77 @@ +package staker + +import ( + "std" + "time" + + "gno.land/p/onbloc/json" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/v1/protocol_fee" +) + +// GetLockedAmount returns total locked GNS. +func GetLockedAmount() int64 { + lockedAmount := int64(0) + + delegations.Iterate("", "", func(key string, value any) bool { + delegation, ok := value.(*Delegation) + if !ok { + panic(ufmt.Sprintf("failed to cast delegations's element to *Delegation: %T", value)) + } + lockedAmount += delegation.DelegatedAmount() + return false + }) + + return lockedAmount +} + +// GetClaimableRewardByAddress returns claimable reward for address. +func GetClaimableRewardByAddress(addr std.Address) string { + return GetClaimableRewardByRewardID(addr.String()) +} + +// GetClaimableRewardByLaunchpad returns claimable reward for launchpad. +func GetClaimableRewardByLaunchpad(addr std.Address) string { + return GetClaimableRewardByRewardID(makeLaunchpadRewardID(addr.String())) +} + +// GetClaimableRewardByRewardID returns claimable reward by ID. +func GetClaimableRewardByRewardID(rewardID string) string { + func(cur realm) { + emission.MintAndDistributeGns(cross) + protocol_fee.DistributeProtocolFee(cross) + }(cross) + + emissionDistributedAmount := emission.GetAccuDistributedToGovStaker() + emissionReward, _ := emissionRewardManager.GetClaimableRewardAmount(emissionDistributedAmount, rewardID, time.Now().Unix()) + + protocolFeeDistributedAmounts := getDistributedProtocolFees() + protocolFeeRewards, _ := protocolFeeRewardManager.GetClaimableRewardAmounts(protocolFeeDistributedAmounts, rewardID, time.Now().Unix()) + + if emissionReward == 0 && len(protocolFeeRewards) == 0 { + return "" + } + + data := json.Builder(). + WriteString("height", formatInt(std.ChainHeight())). + WriteString("now", formatInt(time.Now().Unix())). + WriteString("emissionReward", formatInt(emissionReward)). + Node() + + // Always include protocolFees array, even if empty + pfArr := json.ArrayNode("", nil) + for tokenPath, protocolFeeReward := range protocolFeeRewards { + if protocolFeeReward > 0 { + pfObj := json.Builder(). + WriteString("tokenPath", tokenPath). + WriteString("amount", formatInt(protocolFeeReward)). + Node() + pfArr.AppendArray(pfObj) + } + } + data.AppendObject("protocolFees", pfArr) + + return marshal(data) +} diff --git a/contract/r/gnoswap/v1/gov/staker/assert.gno b/contract/r/gnoswap/v1/gov/staker/assert.gno new file mode 100644 index 0000000..3e52a7d --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/assert.gno @@ -0,0 +1,27 @@ +package staker + +import "gno.land/p/nt/ufmt" + +// assertIsValidDelegateAmount validates that the delegation amount meets system requirements. +// This function checks minimum amount and multiple requirements. +// +// Parameters: +// - amount: amount to validate +// +// Returns: +// - error: nil if valid, error describing validation failure +func assertIsValidDelegateAmount(amount int64) { + if amount < minimumAmount { + panic(makeErrorWithDetails( + errLessThanMinimum, + ufmt.Sprintf("minimum amount to delegate is %d (requested:%d)", minimumAmount, amount), + )) + } + + if amount%minimumAmount != 0 { + panic(makeErrorWithDetails( + errInvalidAmount, + ufmt.Sprintf("amount must be multiple of %d", minimumAmount), + )) + } +} diff --git a/contract/r/gnoswap/v1/gov/staker/consts.gno b/contract/r/gnoswap/v1/gov/staker/consts.gno new file mode 100644 index 0000000..fa12dd1 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/consts.gno @@ -0,0 +1,12 @@ +package staker + +import ( + u256 "gno.land/p/gnoswap/uint256" +) + +const ( + GNOT string = "gnot" + minimumAmount = 1_000_000 // 1 GNS +) + +var q128 = u256.MustFromDecimal("340282366920938463463374607431768211456") diff --git a/contract/r/gnoswap/v1/gov/staker/counter.gno b/contract/r/gnoswap/v1/gov/staker/counter.gno new file mode 100644 index 0000000..4c2cd21 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/counter.gno @@ -0,0 +1,13 @@ +package staker + +type Counter struct { + id int64 +} + +func NewCounter() *Counter { return &Counter{id: 0} } +func (c *Counter) Get() int64 { return c.id } + +func (c *Counter) next() int64 { + c.id++ + return c.id +} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation.gno b/contract/r/gnoswap/v1/gov/staker/delegation.gno new file mode 100644 index 0000000..d5a0e30 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/delegation.gno @@ -0,0 +1,164 @@ +package staker + +import "std" + +// Error message constants +const ( + errCollectAmountExceedsCollectable = "amount to collect is greater than collectable amount" + errOverflowInCollectedAmount = "overflow in delegation collected amount: current(%d) + collect(%d)" +) + +// DelegationType represents the type of delegation operation +type DelegationType string + +const ( + DelegateType DelegationType = "DELEGATE" + UnDelegateType DelegationType = "UNDELEGATE" +) + +func (d DelegationType) String() string { return string(d) } +func (d DelegationType) IsDelegate() bool { return d == DelegateType } +func (d DelegationType) IsUnDelegate() bool { return d == UnDelegateType } + +// Delegation represents a delegation between two addresses +type Delegation struct { + id int64 + delegateAmount int64 + unDelegateAmount int64 + collectedAmount int64 + delegateFrom std.Address + delegateTo std.Address + createdHeight int64 + createdAt int64 + withdraws []*DelegationWithdraw +} + +// NewDelegation creates a new delegation +func NewDelegation( + id int64, + delegateFrom, delegateTo std.Address, + delegateAmount, createdHeight, createdAt int64, +) *Delegation { + return &Delegation{ + id: id, + delegateFrom: delegateFrom, + delegateTo: delegateTo, + delegateAmount: delegateAmount, + createdHeight: createdHeight, + createdAt: createdAt, + unDelegateAmount: 0, + collectedAmount: 0, + withdraws: make([]*DelegationWithdraw, 0), + } +} + +// Basic getters +func (d *Delegation) ID() int64 { return d.id } +func (d *Delegation) DelegateFrom() std.Address { return d.delegateFrom } +func (d *Delegation) DelegateTo() std.Address { return d.delegateTo } +func (d *Delegation) CreatedAt() int64 { return d.createdAt } + +// Amount getters +func (d *Delegation) TotalDelegatedAmount() int64 { return d.delegateAmount } +func (d *Delegation) UnDelegatedAmount() int64 { return d.unDelegateAmount } +func (d *Delegation) CollectedAmount() int64 { return d.collectedAmount } + +// Calculated amounts +func (d *Delegation) DelegatedAmount() int64 { return d.delegateAmount - d.unDelegateAmount } +func (d *Delegation) LockedAmount() int64 { return d.delegateAmount - d.collectedAmount } +func (d *Delegation) IsEmpty() bool { return d.LockedAmount() == 0 } + +// CollectableAmount calculates the total amount that can be collected at the given time +func (d *Delegation) CollectableAmount(currentTime int64) (total int64) { + for _, withdraw := range d.withdraws { + total += withdraw.CollectableAmount(currentTime) + } + return +} + +// unDelegate processes an undelegation with lockup period +func (d *Delegation) unDelegate( + amount, currentHeight, currentTimestamp, unDelegationLockupPeriod int64, +) { + d.unDelegateAmount += amount + d.withdraws = append(d.withdraws, NewDelegationWithdraw( + d.id, + amount, + currentHeight, + currentTimestamp, + unDelegationLockupPeriod, + )) +} + +// UnDelegateWithoutLockup processes an immediate undelegation without lockup +func (d *Delegation) unDelegateWithoutLockup( + amount, currentHeight, currentTime int64, +) { + d.unDelegateAmount += amount + d.collectedAmount += amount + d.withdraws = append(d.withdraws, NewDelegationWithdrawWithoutLockup( + d.id, + amount, + currentHeight, + currentTime, + )) +} + +// Collect processes the collection of available amounts +func (d *Delegation) collect(amount, currentTime int64) error { + if amount > d.CollectableAmount(currentTime) { + return makeErrorWithDetails( + errInvalidAmount, + errCollectAmountExceedsCollectable, + ) + } + + return d.processCollection(amount, currentTime) +} + +// processCollection handles the actual collection logic +func (d *Delegation) processCollection(amount, currentTime int64) error { + remainingToCollect := amount + + for _, withdraw := range d.withdraws { + if remainingToCollect <= 0 { + break + } + + if !withdraw.IsCollectable(currentTime) { + continue + } + + collectableAmount := withdraw.CollectableAmount(currentTime) + if collectableAmount <= 0 { + continue + } + + amountToCollect := min(remainingToCollect, collectableAmount) + + if err := withdraw.collect(amountToCollect, currentTime); err != nil { + return err + } + + updatedAmount, err := addToCollectedAmount(d.collectedAmount, amountToCollect) + if err != nil { + return err + } + d.collectedAmount = updatedAmount + remainingToCollect -= amountToCollect + } + + return nil +} + +func addToCollectedAmount(collectedAmount, amount int64) (int64, error) { + return collectedAmount + amount, nil +} + +// min returns the smaller of two int64 values +func min(a, b int64) int64 { + if a < b { + return a + } + return b +} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_history.gno b/contract/r/gnoswap/v1/gov/staker/delegation_history.gno new file mode 100644 index 0000000..c0ba83f --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/delegation_history.gno @@ -0,0 +1,61 @@ +package staker + +// DelegationHistory represents a chronological list of delegation records +// used to track delegation changes over time for snapshot calculations +type DelegationHistory []*DelegationRecord + +// getRecordsBy retrieves delegation records that occurred at or after the specified snapshot time. +// This method is used to filter historical records for calculating delegation snapshots at specific points in time. +// +// Parameters: +// - snapshotTime: timestamp to filter records from (inclusive) +// +// Returns: +// - DelegationHistory: filtered records occurring at or after snapshotTime +func (dh DelegationHistory) getRecordsBy(snapshotTime int64) DelegationHistory { + records := make(DelegationHistory, 0) + + historyIndex := -1 + + // Find the first record at or after the snapshot time + for index, record := range dh { + if record.CreatedAt() >= snapshotTime { + historyIndex = index + break + } + } + + // If no records found at or after snapshot time, return empty slice + if historyIndex == -1 { + return records + } + + // Return all records from the found index onwards + records = append(records, dh[historyIndex:]...) + + return records +} + +// addRecord appends a new delegation record to the history. +// This method maintains the chronological order of delegation events. +// +// Parameters: +// - delegationRecord: the delegation record to add to history +// +// Returns: +// - DelegationHistory: updated history with the new record appended +func (dh DelegationHistory) addRecord(delegationRecord *DelegationRecord) DelegationHistory { + return append(dh, delegationRecord) +} + +// removeRecordsBy removes historical records that occurred before the specified time. +// This method is used for cleanup operations to remove old historical data. +// +// Parameters: +// - previousTime: cutoff timestamp for record removal +// +// Returns: +// - DelegationHistory: filtered history containing only records at or after previousTime +func (dh DelegationHistory) removeRecordsBy(previousTime int64) DelegationHistory { + return dh.getRecordsBy(previousTime) +} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_mananger.gno b/contract/r/gnoswap/v1/gov/staker/delegation_mananger.gno new file mode 100644 index 0000000..32b4d4d --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/delegation_mananger.gno @@ -0,0 +1,144 @@ +package staker + +import ( + "std" +) + +// DelegationManager manages the mapping between users and their delegation IDs. +// It provides efficient lookup and management of user delegations organized by delegator and delegatee addresses. +type DelegationManager struct { + // userDelegations maps delegator address -> delegatee address -> list of delegation IDs + // This nested mapping allows efficient retrieval of delegations by both delegator and delegatee + userDelegations map[string]map[string][]int64 +} + +// GetUserDelegationIDsWithDelegatee retrieves all delegation IDs for a specific delegator-delegatee pair. +// This method is used to find delegations from a specific user to a specific delegate. +// +// Parameters: +// - delegator: address of the user who delegated tokens +// - delegatee: address of the user who received the delegation +// +// Returns: +// - []int64: list of delegation IDs for the specified pair +func (dm *DelegationManager) GetUserDelegationIDsWithDelegatee(delegator std.Address, delegatee std.Address) []int64 { + delegatorAddress := delegator.String() + delegateeAddress := delegatee.String() + + return dm.userDelegations[delegatorAddress][delegateeAddress] +} + +// GetUserDelegationIDs retrieves all delegation IDs for a specific delegator across all delegatees. +// This method is used to find all delegations made by a specific user. +// +// Parameters: +// - delegator: address of the user whose delegations to retrieve +// +// Returns: +// - []int64: list of all delegation IDs for the delegator +func (dm *DelegationManager) GetUserDelegationIDs(delegator std.Address) []int64 { + delegatorAddress := delegator.String() + delegationIDs := make([]int64, 0) + + // Return empty slice if no delegations exist for this user + if dm.userDelegations[delegatorAddress] == nil { + return delegationIDs + } + + // Collect delegation IDs from all delegatees + for _, toDelegations := range dm.userDelegations[delegatorAddress] { + delegationIDs = append(delegationIDs, toDelegations...) + } + + return delegationIDs +} + +// addDelegation adds a delegation ID to the manager's tracking system. +// This method creates the necessary nested map structure if it doesn't exist +// and ensures no duplicate delegation IDs are stored. +// +// Parameters: +// - delegator: address of the user who made the delegation +// - delegatee: address of the user who received the delegation +// - delegationID: unique identifier for the delegation +func (dm *DelegationManager) addDelegation(delegator, delegatee std.Address, delegationID int64) { + delegatorAddress := delegator.String() + delegateeAddress := delegatee.String() + + // Initialize delegator map if it doesn't exist + if _, ok := dm.userDelegations[delegatorAddress]; !ok { + dm.userDelegations[delegatorAddress] = make(map[string][]int64) + } + + // Initialize delegatee slice if it doesn't exist + if _, ok := dm.userDelegations[delegatorAddress][delegateeAddress]; !ok { + dm.userDelegations[delegatorAddress][delegateeAddress] = make([]int64, 0) + } + + // Check for duplicate delegation IDs before adding + delegationIDs := dm.userDelegations[delegatorAddress][delegateeAddress] + for _, id := range delegationIDs { + if id == delegationID { + return + } + } + + // Add the new delegation ID + dm.userDelegations[delegatorAddress][delegateeAddress] = append( + delegationIDs, + delegationID, + ) +} + +// removeDelegation removes a delegation ID from the manager's tracking system. +// This method finds and removes the specified delegation ID from the appropriate slice. +// +// Parameters: +// - delegator: address of the user who made the delegation +// - delegatee: address of the user who received the delegation +// - delegationID: unique identifier for the delegation to remove +func (dm *DelegationManager) removeDelegation(delegator, delegatee std.Address, delegationID int64) { + delegatorAddress := delegator.String() + delegateeAddress := delegatee.String() + + // Check if delegator exists in the map + userDelegations, ok := dm.userDelegations[delegatorAddress] + if !ok { + return + } + + // Check if delegatee exists for this delegator + delegationIDs, ok := userDelegations[delegateeAddress] + if !ok { + return + } + + index := -1 + + // Find the index of the delegation ID to remove + for i, id := range delegationIDs { + if id == delegationID { + index = i + break + } + } + + // Remove the delegation ID if found + if index != -1 { + dm.userDelegations[delegatorAddress][delegateeAddress] = append( + delegationIDs[:index], + delegationIDs[index+1:]..., + ) + } +} + +// NewDelegationManager creates a new instance of DelegationManager. +// This factory function initializes the nested map structure for tracking user delegations. +// +// Returns: +// - *DelegationManager: initialized delegation manager instance +func NewDelegationManager() *DelegationManager { + return &DelegationManager{ + userDelegations: make(map[string]map[string][]int64), + } +} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_record.gno b/contract/r/gnoswap/v1/gov/staker/delegation_record.gno new file mode 100644 index 0000000..681c198 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/delegation_record.gno @@ -0,0 +1,156 @@ +package staker + +import ( + "std" +) + +// DelegationRecord represents a single delegation event in the system. +// This struct tracks delegation or undelegation actions with their associated metadata +// and is used for historical tracking and snapshot calculations. +type DelegationRecord struct { + // delegationType indicates whether this is a delegation or undelegation action + delegationType DelegationType + // delegateFrom is the address of the user who initiated the delegation + delegateFrom std.Address + // delegateTo is the address of the user who received the delegation + delegateTo std.Address + // delegateAmount is the amount delegated (set only for delegation actions) + delegateAmount int64 + // unDelegateAmount is the amount undelegated (set only for undelegation actions) + unDelegateAmount int64 + // createdAt is the timestamp when this record was created + createdAt int64 +} + +// DelegationType returns the type of delegation action (DELEGATE or UNDELEGATE). +// +// Returns: +// - DelegationType: the type of this delegation record +func (d *DelegationRecord) DelegationType() DelegationType { + return d.delegationType +} + +// DelegateAmount returns the amount that was delegated. +// This value is non-zero only for delegation actions. +// +// Returns: +// - int64: amount delegated (0 for undelegation records) +func (d *DelegationRecord) DelegateAmount() int64 { + return d.delegateAmount +} + +// UnDelegateAmount returns the amount that was undelegated. +// This value is non-zero only for undelegation actions. +// +// Returns: +// - int64: amount undelegated (0 for delegation records) +func (d *DelegationRecord) UnDelegateAmount() int64 { + return d.unDelegateAmount +} + +// DelegateFrom returns the address of the user who initiated the delegation. +// +// Returns: +// - std.Address: delegator's address +func (d *DelegationRecord) DelegateFrom() std.Address { + return d.delegateFrom +} + +// DelegateTo returns the address of the user who received the delegation. +// +// Returns: +// - std.Address: delegatee's address +func (d *DelegationRecord) DelegateTo() std.Address { + return d.delegateTo +} + +// CreatedAt returns the timestamp when this delegation record was created. +// +// Returns: +// - int64: creation timestamp +func (d *DelegationRecord) CreatedAt() int64 { + return d.createdAt +} + +// NewDelegationRecord creates a new delegation record with the specified parameters. +// This factory function properly sets either delegateAmount or unDelegateAmount based on the delegation type. +// +// Parameters: +// - delegationType: type of delegation action (DELEGATE or UNDELEGATE) +// - delegationAmount: amount being delegated or undelegated +// - delegateFrom: address of the delegator +// - delegateTo: address of the delegatee +// - createdAt: timestamp of the action +// +// Returns: +// - *DelegationRecord: newly created delegation record +func NewDelegationRecord( + delegationType DelegationType, + delegationAmount int64, + delegateFrom std.Address, + delegateTo std.Address, + createdAt int64, +) *DelegationRecord { + delegateAmount := int64(0) + unDelegateAmount := int64(0) + + // Set the appropriate amount field based on delegation type + if delegationType.IsDelegate() { + delegateAmount = delegationAmount + } else { + unDelegateAmount = delegationAmount + } + + return &DelegationRecord{ + delegationType: delegationType, + delegateAmount: delegateAmount, + unDelegateAmount: unDelegateAmount, + delegateFrom: delegateFrom, + delegateTo: delegateTo, + createdAt: createdAt, + } +} + +// NewDelegationDelegateRecordBy creates a delegation record from an existing Delegation instance. +// This factory function is used to create historical records for delegation actions. +// +// Parameters: +// - delegation: the delegation instance to create a record from +// +// Returns: +// - *DelegationRecord: delegation record representing the delegation action +func NewDelegationDelegateRecordBy( + delegation *Delegation, +) *DelegationRecord { + return NewDelegationRecord( + DelegateType, + delegation.DelegatedAmount(), + delegation.DelegateFrom(), + delegation.DelegateTo(), + delegation.CreatedAt(), + ) +} + +// NewDelegationWithdrawRecordBy creates an undelegation record for a withdrawal action. +// This factory function is used to create historical records for undelegation actions. +// +// Parameters: +// - delegation: the delegation instance being withdrawn from +// - withdrawAmount: amount being withdrawn +// - currentTime: timestamp of the withdrawal action +// +// Returns: +// - *DelegationRecord: delegation record representing the undelegation action +func NewDelegationWithdrawRecordBy( + delegation *Delegation, + withdrawAmount int64, + currentTime int64, +) *DelegationRecord { + return NewDelegationRecord( + UnDelegateType, + withdrawAmount, + delegation.DelegateFrom(), + delegation.DelegateTo(), + currentTime, + ) +} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_snapshot.gno b/contract/r/gnoswap/v1/gov/staker/delegation_snapshot.gno new file mode 100644 index 0000000..08a66c5 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/delegation_snapshot.gno @@ -0,0 +1,159 @@ +package staker + +import "std" + +// DelegationSnapshot represents a point-in-time view of delegation states. +// It maps delegatee addresses to their corresponding delegation snapshot items, +// providing efficient lookup and manipulation of delegation states at specific timestamps. +type DelegationSnapshot map[string]*DelegationSnapshotItem + +// clone creates a deep copy of the delegation snapshot. +// This method is used to create independent copies for historical calculations +// without modifying the original snapshot state. +// +// Returns: +// - DelegationSnapshot: deep copy of the current snapshot +func (d *DelegationSnapshot) clone() DelegationSnapshot { + clone := make(DelegationSnapshot) + + for k, v := range *d { + clone[k] = v.clone() + } + + return clone +} + +// addRecord applies a delegation record to the snapshot, updating delegation amounts. +// This method creates new snapshot items if they don't exist and removes empty items. +// +// Parameters: +// - delegationRecord: the delegation record to apply to the snapshot +// +// Returns: +// - DelegationSnapshot: updated snapshot with the record applied +func (d DelegationSnapshot) addRecord(delegationRecord *DelegationRecord) DelegationSnapshot { + delegateTo := delegationRecord.DelegateTo() + delegateToStr := delegateTo.String() + + // Create new snapshot item if it doesn't exist + _, ok := d[delegateToStr] + if !ok { + d[delegateToStr] = NewDelegationSnapshotItem(delegateTo) + } + + // Apply the delegation record to the snapshot item + d[delegateToStr].addRecord(delegationRecord) + + // Remove empty snapshot items to keep the map clean + if d[delegateToStr].IsEmpty() { + delete(d, delegateToStr) + } + + return d +} + +// subRecord subtracts a delegation record from the snapshot. +// This method is used for calculating historical snapshots by removing +// the effects of delegation records that occurred after a specific time. +// +// Parameters: +// - delegationRecord: the delegation record to subtract from the snapshot +// +// Returns: +// - DelegationSnapshot: updated snapshot with the record subtracted +func (d DelegationSnapshot) subRecord(delegationRecord *DelegationRecord) DelegationSnapshot { + delegateTo := delegationRecord.DelegateTo() + delegateToStr := delegateTo.String() + + // Create new snapshot item if it doesn't exist + _, ok := d[delegateToStr] + if !ok { + d[delegateToStr] = NewDelegationSnapshotItem(delegateTo) + } + + // Subtract the delegation record from the snapshot item + d[delegateToStr].subRecord(delegationRecord) + + return d +} + +// DelegationSnapshotItem represents delegation information for a specific delegatee. +// It tracks the total delegation amount and the delegator's address. +type DelegationSnapshotItem struct { + // delegationAmount is the total amount delegated to this delegatee + delegationAmount int64 + // delegatorAddress is the address of the delegatee receiving delegations + delegatorAddress std.Address +} + +// DelegatorAddress returns the address of the delegatee. +// +// Returns: +// - std.Address: delegatee's address +func (d *DelegationSnapshotItem) DelegatorAddress() std.Address { + return d.delegatorAddress +} + +// DelegationAmount returns the total delegation amount for this delegatee. +// +// Returns: +// - int64: total delegated amount +func (d *DelegationSnapshotItem) DelegationAmount() int64 { + return d.delegationAmount +} + +// IsEmpty checks if the delegation amount is zero. +// Empty snapshot items are typically removed from the snapshot map. +// +// Returns: +// - bool: true if delegation amount is zero, false otherwise +func (d *DelegationSnapshotItem) IsEmpty() bool { + return d.delegationAmount == 0 +} + +// clone creates a deep copy of the delegation snapshot item. +// +// Returns: +// - *DelegationSnapshotItem: independent copy of the snapshot item +func (d *DelegationSnapshotItem) clone() *DelegationSnapshotItem { + return &DelegationSnapshotItem{ + delegatorAddress: d.delegatorAddress, + delegationAmount: d.delegationAmount, + } +} + +// addRecord applies a delegation record to this snapshot item. +// It increases the delegation amount for delegate actions and decreases for undelegate actions. +// +// Parameters: +// - delegationRecord: the delegation record to apply +func (d *DelegationSnapshotItem) addRecord(delegationRecord *DelegationRecord) { + d.delegationAmount += delegationRecord.DelegateAmount() + d.delegationAmount -= delegationRecord.UnDelegateAmount() +} + +// subRecord subtracts the delegation amount from the snapshot by the delegation record. +// It is used to get previous delegation snapshots by reversing the effects of current delegation records. +// This method performs the inverse operation of addRecord. +// +// Parameters: +// - delegationRecord: the delegation record to subtract from the snapshot +func (d *DelegationSnapshotItem) subRecord(delegationRecord *DelegationRecord) { + d.delegationAmount -= delegationRecord.DelegateAmount() + d.delegationAmount += delegationRecord.UnDelegateAmount() +} + +// NewDelegationSnapshotItem creates a new delegation snapshot item for a delegatee. +// The initial delegation amount is set to zero. +// +// Parameters: +// - delegatorAddress: address of the delegatee +// +// Returns: +// - *DelegationSnapshotItem: new snapshot item with zero delegation amount +func NewDelegationSnapshotItem(delegatorAddress std.Address) *DelegationSnapshotItem { + return &DelegationSnapshotItem{ + delegatorAddress: delegatorAddress, + delegationAmount: 0, + } +} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_withdraw.gno b/contract/r/gnoswap/v1/gov/staker/delegation_withdraw.gno new file mode 100644 index 0000000..4a26819 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/delegation_withdraw.gno @@ -0,0 +1,177 @@ +package staker + +// DelegationWithdraw represents a pending withdrawal from a delegation. +// This struct tracks undelegated amounts that are subject to lockup periods +// and manages the collection process once the lockup period expires. +type DelegationWithdraw struct { + // delegationID is the unique identifier of the associated delegation + delegationID int64 + // unDelegateAmount is the total amount that was undelegated + unDelegateAmount int64 + // unDelegatedHeight is the height when the undelegation occurred + unDelegatedHeight int64 + // unDelegatedAt is the timestamp when the undelegation occurred + unDelegatedAt int64 + // collectedAmount is the amount that has already been collected + collectedAmount int64 + // collectableTime is the timestamp when collection becomes available + collectableTime int64 + // collectedAt is the timestamp when collection occurred + collectedAt int64 + // collected indicates whether the withdrawal has been fully collected + collected bool +} + +// DelegationID returns the unique identifier of the associated delegation. +// +// Returns: +// - int64: delegation ID +func (d *DelegationWithdraw) DelegationID() int64 { + return d.delegationID +} + +// UnDelegateAmount returns the total amount that was undelegated. +// +// Returns: +// - int64: undelegated amount +func (d *DelegationWithdraw) UnDelegateAmount() int64 { + return d.unDelegateAmount +} + +// UnDelegatedAt returns the timestamp when the undelegation occurred. +// +// Returns: +// - int64: undelegation timestamp +func (d *DelegationWithdraw) UnDelegatedAt() int64 { + return d.unDelegatedAt +} + +// CollectableAmount calculates the amount available for collection at the given time. +// Returns zero if the withdrawal is not yet collectable or has been fully collected. +// +// Parameters: +// - currentTime: current timestamp to check collectability against +// +// Returns: +// - int64: amount available for collection +func (d *DelegationWithdraw) CollectableAmount(currentTime int64) int64 { + if d.IsCollectable(currentTime) { + return d.unDelegateAmount - d.collectedAmount + } + + return 0 +} + +// IsCollectable determines whether the withdrawal can be collected at the given time. +// A withdrawal is collectable if: +// - The undelegated amount is positive +// - There is remaining uncollected amount +// - The current time is at or after the collectable time +// +// Parameters: +// - currentTime: current timestamp to check against +// +// Returns: +// - bool: true if the withdrawal can be collected, false otherwise +func (d *DelegationWithdraw) IsCollectable(currentTime int64) bool { + if d.unDelegateAmount <= 0 { + return false + } + + if d.unDelegateAmount-d.collectedAmount <= 0 { + return false + } + + if currentTime < d.collectableTime { + return false + } + + return true +} + +// IsCollected returns whether the withdrawal has been fully collected. +// +// Returns: +// - bool: true if fully collected, false otherwise +func (d *DelegationWithdraw) IsCollected() bool { + return d.collected +} + +// collect processes the collection of the specified amount from this withdrawal. +// This method validates collectability and updates the collection state. +// +// Parameters: +// - amount: amount to collect +// - currentTime: current timestamp +// +// Returns: +// - error: nil on success, error if collection is not allowed +func (d *DelegationWithdraw) collect(amount int64, currentTime int64) error { + if !d.IsCollectable(currentTime) { + return errInvalidAmount + } + + d.collected = true + d.collectedAt = currentTime + d.collectedAmount += amount + + return nil +} + +// NewDelegationWithdraw creates a new delegation withdrawal with lockup period. +// The withdrawal will be collectable after the lockup period expires. +// +// Parameters: +// - delegationID: unique identifier of the associated delegation +// - unDelegateAmount: amount being withdrawn +// - createdAt: timestamp when the withdrawal was created +// - unDelegationLockupPeriod: duration of the lockup period in seconds +// +// Returns: +// - *DelegationWithdraw: new withdrawal instance with lockup +func NewDelegationWithdraw( + delegationID, + unDelegateAmount, + createdHeight, + createdAt, + unDelegationLockupPeriod int64, +) *DelegationWithdraw { + return &DelegationWithdraw{ + delegationID: delegationID, + unDelegateAmount: unDelegateAmount, + unDelegatedHeight: createdHeight, + unDelegatedAt: createdAt, + collectableTime: createdAt + unDelegationLockupPeriod, + collectedAmount: 0, + collectedAt: 0, + collected: false, + } +} + +// NewDelegationWithdrawWithoutLockup creates a new delegation withdrawal that is immediately collectable. +// This is used for special cases like redelegation where no lockup period is required. +// +// Parameters: +// - delegationID: unique identifier of the associated delegation +// - unDelegateAmount: amount being withdrawn +// - createdAt: timestamp when the withdrawal was created +// +// Returns: +// - *DelegationWithdraw: new withdrawal instance that is immediately collected +func NewDelegationWithdrawWithoutLockup( + delegationID, + unDelegateAmount, + createdHeight, + createdAt int64, +) *DelegationWithdraw { + return &DelegationWithdraw{ + delegationID: delegationID, + unDelegateAmount: unDelegateAmount, + unDelegatedHeight: createdHeight, + unDelegatedAt: createdAt, + collectableTime: createdAt, + collectedAmount: unDelegateAmount, + collectedAt: createdAt, + collected: true, + } +} diff --git a/contract/r/gnoswap/v1/gov/staker/doc.gno b/contract/r/gnoswap/v1/gov/staker/doc.gno new file mode 100644 index 0000000..cdd5c05 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/doc.gno @@ -0,0 +1,4 @@ +// Package staker manages GNS token staking and delegation functionality. +// It handles delegation of voting power, distribution of protocol rewards, +// and implements a 7-day lockup period for unstaking operations. +package staker diff --git a/contract/r/gnoswap/v1/gov/staker/emission_reward_manager.gno b/contract/r/gnoswap/v1/gov/staker/emission_reward_manager.gno new file mode 100644 index 0000000..aa946ba --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/emission_reward_manager.gno @@ -0,0 +1,235 @@ +package staker + +import ( + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + u256 "gno.land/p/gnoswap/uint256" +) + +var errFailedToCastRewardState = "failed to cast rewardStates's element to *EmissionRewardState: %T" + +// EmissionRewardManager manages the distribution of emission rewards to stakers. +type EmissionRewardManager struct { + // rewardStates maps address to EmissionRewardState for tracking individual staker rewards + rewardStates *avl.Tree // address -> EmissionRewardState + + // accumulatedRewardX128PerStake tracks the cumulative reward per unit of stake with 128-bit precision + accumulatedRewardX128PerStake *u256.Uint + // distributedAmount tracks the total amount of rewards distributed + distributedAmount int64 + // accumulatedTimestamp tracks the last timestamp when rewards were accumulated + accumulatedTimestamp int64 + // totalStakedAmount tracks the total amount of tokens staked in the system + totalStakedAmount int64 +} + +// GetAccumulatedRewardX128PerStake returns the accumulated reward per stake with 128-bit precision. +func (e *EmissionRewardManager) GetAccumulatedRewardX128PerStake() *u256.Uint { + return e.accumulatedRewardX128PerStake +} + +// GetAccumulatedTimestamp returns the last timestamp when rewards were accumulated. +func (e *EmissionRewardManager) GetAccumulatedTimestamp() int64 { + return e.accumulatedTimestamp +} + +// GetTotalStakedAmount returns the total amount of tokens staked in the system. +func (e *EmissionRewardManager) GetTotalStakedAmount() int64 { + return e.totalStakedAmount +} + +// GetDistributedAmount returns the total amount of rewards distributed. +func (e *EmissionRewardManager) GetDistributedAmount() int64 { + return e.distributedAmount +} + +// GetClaimableRewardAmount calculates the claimable reward amount for a specific address. +func (e *EmissionRewardManager) GetClaimableRewardAmount( + currentDistributedAmount int64, + address string, + currentTimestamp int64, +) (int64, error) { + rewardStateI, ok := e.rewardStates.Get(address) + if !ok { + return 0, nil + } + + rewardState, ok := rewardStateI.(*EmissionRewardState) + if !ok { + return 0, ufmt.Errorf( + "failed to cast rewardStates's element to *EmissionRewardState: %T", + rewardStateI, + ) + } + + accumulatedRewardX128PerStake, err := e.calculateAccumulatedRewardX128PerStake( + currentDistributedAmount, + currentTimestamp, + ) + if err != nil { + return 0, err + } + + return rewardState.GetClaimableRewardAmount(accumulatedRewardX128PerStake, currentTimestamp) +} + +// calculateAccumulatedRewardX128PerStake calculates the updated accumulated reward per stake. +func (e *EmissionRewardManager) calculateAccumulatedRewardX128PerStake( + currentDistributedAmount int64, + currentTimestamp int64, +) (*u256.Uint, error) { + // If we're looking at a past timestamp, return current state + if currentTimestamp < e.accumulatedTimestamp { + return e.accumulatedRewardX128PerStake, nil + } + + // If no tokens are staked, no rewards to distribute + if e.totalStakedAmount == 0 { + return e.accumulatedRewardX128PerStake, nil + } + + // Newly distributed rewards since last update + distributedAmountDelta := currentDistributedAmount - e.distributedAmount + if distributedAmountDelta <= 0 { + // Non-positive delta. nothing to do more. + return e.accumulatedRewardX128PerStake, nil + } + + // Reward per stake for the new distribution + distributedAmountDeltaX128PerStake := u256.Zero().Div( + u256.Zero().Lsh(u256.NewUintFromInt64(distributedAmountDelta), 128), + u256.NewUintFromInt64(e.totalStakedAmount), + ) + + // Add to accumulated reward per stake + accumulatedReward := u256.Zero().Add(e.accumulatedRewardX128PerStake, distributedAmountDeltaX128PerStake) + return accumulatedReward, nil +} + +// updateAccumulatedRewardX128PerStake updates the internal accumulated reward state. +// This method should be called before any stake changes to ensure accurate reward calculations. +// Updates accumulated reward per stake with current distribution data. +func (e *EmissionRewardManager) updateAccumulatedRewardX128PerStake( + currentDistributedAmount int64, + currentTimestamp int64, +) error { + // DO NOT apply out-of-order timestamps + if currentTimestamp < e.accumulatedTimestamp { + return nil + } + + // to avoid accumulating a large delta later. + if e.totalStakedAmount == 0 { + return nil + } + + // Update accumulated reward state + accumulatedRewardX128PerStake, err := e.calculateAccumulatedRewardX128PerStake( + currentDistributedAmount, + currentTimestamp, + ) + if err != nil { + return err + } + + e.accumulatedRewardX128PerStake = accumulatedRewardX128PerStake.Clone() + e.distributedAmount = currentDistributedAmount + e.accumulatedTimestamp = currentTimestamp + + return nil +} + +// addStake adds a stake for an address and updates their reward state. +// This method ensures rewards are properly calculated before the stake change. +// Adds stake for specified address and updates reward calculations. +func (e *EmissionRewardManager) addStake(address string, amount int64, currentTimestamp int64) error { + rewardState, ok, err := e.getRewardState(address) + if err != nil { + return err + } + if !ok { + // if the address is unseen, initialize a snapshot to avoid nil deref + rewardState = NewEmissionRewardState(e.accumulatedRewardX128PerStake.Clone()) + } + + err = rewardState.addStakeWithUpdateRewardDebtX128(amount, e.accumulatedRewardX128PerStake, currentTimestamp) + if err != nil { + return err + } + + e.rewardStates.Set(address, rewardState) + e.totalStakedAmount = e.totalStakedAmount + amount + return nil +} + +// removeStake removes a stake for an address and updates their reward state. +// This method ensures rewards are properly calculated before the stake change. +// Removes stake for specified address and updates reward calculations. +func (e *EmissionRewardManager) removeStake(address string, amount int64, currentTimestamp int64) error { + rewardState, ok, err := e.getRewardState(address) + if err != nil { + return err + } + if !ok { + // if the address is unseen, initialize a snapshot to avoid nil deref + rewardState = NewEmissionRewardState(e.accumulatedRewardX128PerStake.Clone()) + } + + err = rewardState.removeStakeWithUpdateRewardDebtX128(amount, e.accumulatedRewardX128PerStake, currentTimestamp) + if err != nil { + return err + } + + // persist updated state + e.rewardStates.Set(address, rewardState) + e.totalStakedAmount -= amount + if e.totalStakedAmount < 0 { + e.totalStakedAmount = 0 // defensive clamp + } + + return nil +} + +// claimRewards processes reward claiming for an address. +// This method calculates and returns the amount of rewards claimed. +// Claims available rewards for specified address. +func (e *EmissionRewardManager) claimRewards(address string, currentTimestamp int64) (claimedRewardAmount int64, err error) { + rewardState, ok, err := e.getRewardState(address) + if err != nil || !ok { + return 0, err + } + + claimedRewardAmount, cErr := rewardState.claimRewardsWithUpdateRewardDebtX128(e.accumulatedRewardX128PerStake, currentTimestamp) + if cErr != nil { + return 0, cErr + } + + e.rewardStates.Set(address, rewardState) + return claimedRewardAmount, nil +} + +// NewEmissionRewardManager creates a new instance of EmissionRewardManager. +// This factory function initializes all tracking structures for emission reward management. +// NewEmissionRewardManager creates new emission reward manager instance. +func NewEmissionRewardManager() *EmissionRewardManager { + return &EmissionRewardManager{ + accumulatedRewardX128PerStake: u256.NewUint(0), + accumulatedTimestamp: 0, + totalStakedAmount: 0, + distributedAmount: 0, + rewardStates: avl.NewTree(), + } +} + +func (e *EmissionRewardManager) getRewardState(addr string) (*EmissionRewardState, bool, error) { + ri, ok := e.rewardStates.Get(addr) + if !ok { + return nil, false, nil + } + rs, castOk := ri.(*EmissionRewardState) + if !castOk { + return nil, false, ufmt.Errorf(errFailedToCastRewardState, ri) + } + return rs, true, nil +} diff --git a/contract/r/gnoswap/v1/gov/staker/emission_reward_state.gno b/contract/r/gnoswap/v1/gov/staker/emission_reward_state.gno new file mode 100644 index 0000000..b609d92 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/emission_reward_state.gno @@ -0,0 +1,250 @@ +package staker + +import ( + "errors" + + u256 "gno.land/p/gnoswap/uint256" +) + +var errNotClaimable = errors.New("not claimable") + +// EmissionRewardState tracks emission reward information for an individual staker. +// This struct maintains reward debt, accumulated rewards, and claiming history +// to ensure accurate reward calculations and prevent double-claiming. +type EmissionRewardState struct { + // rewardDebtX128 represents the reward debt with 128-bit precision scaling + // Used to calculate rewards earned since the last update + rewardDebtX128 *u256.Uint + // accumulatedRewardAmount is the total rewards accumulated but not yet claimed + accumulatedRewardAmount int64 + // accumulatedTimestamp is the last timestamp when rewards were accumulated + accumulatedTimestamp int64 + // claimedRewardAmount is the total amount of rewards that have been claimed + claimedRewardAmount int64 + // claimedTimestamp is the last timestamp when rewards were claimed + claimedTimestamp int64 + // stakedAmount is the current amount of tokens staked by this address + stakedAmount int64 +} + +// IsClaimable checks if rewards can be claimed at the given timestamp. +// Rewards are claimable if the current timestamp is greater than the last claimed timestamp. +// +// Parameters: +// - currentTimestamp: current timestamp to check against +// +// Returns: +// - bool: true if rewards can be claimed, false otherwise +func (e *EmissionRewardState) IsClaimable(currentTimestamp int64) bool { + return e.claimedTimestamp < currentTimestamp +} + +// GetClaimableRewardAmount calculates the total amount of rewards that can be claimed. +// This includes both accumulated rewards and newly earned rewards based on current state. +// +// Parameters: +// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake +// - currentTimestamp: current timestamp +// +// Returns: +// - int64: total claimable reward amount +func (e *EmissionRewardState) GetClaimableRewardAmount( + accumulatedRewardX128PerStake *u256.Uint, + currentTimestamp int64, +) (int64, error) { + rewardAmount, err := e.calculateClaimableRewards(accumulatedRewardX128PerStake, currentTimestamp) + if err != nil { + return 0, err + } + return e.accumulatedRewardAmount + rewardAmount, nil +} + +// calculateClaimableRewards calculates newly earned rewards since the last update. +// Uses the difference between current and stored reward debt to calculate earnings. +// +// Parameters: +// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake +// - currentTimestamp: current timestamp +// +// Returns: +// - int64: newly earned reward amount since last update +// - error: nil on success, error if calculation fails +func (e *EmissionRewardState) calculateClaimableRewards( + accumulatedRewardX128PerStake *u256.Uint, + currentTimestamp int64, +) (int64, error) { + // Don't calculate rewards for past timestamps or when nothing is staked + if currentTimestamp < e.accumulatedTimestamp || e.stakedAmount == 0 { + return 0, nil + } + + // Calculate the difference in accumulated rewards per stake since last update + // Using modular arithmetic for accumulator values - underflow is allowed and handled correctly + rewardDebtDeltaX128 := u256.Zero().Sub( + accumulatedRewardX128PerStake, + e.rewardDebtX128, + ) + + // Calculate reward amount by multiplying reward debt delta by staked amount and dividing by Q128 + // rewardAmount = (rewardDebtDeltaX128 * stakedAmount) / Q128 + rewardAmount := u256.MulDiv( + rewardDebtDeltaX128, + u256.NewUintFromInt64(e.stakedAmount), + q128, + ) + return safeConvertToInt64(rewardAmount), nil +} + +// addStake increases the staked amount for this address. +// This method should be called when a user increases their stake. +// +// Parameters: +// - amount: amount of stake to add +func (e *EmissionRewardState) addStake(amount int64) { + e.adjustStake(amount) +} + +// removeStake decreases the staked amount for this address. +// This method should be called when a user decreases their stake. +// +// Parameters: +// - amount: amount of stake to remove +func (e *EmissionRewardState) removeStake(amount int64) { + e.adjustStake(-amount) +} + +// adjustStake is a small internal helper to centralize bound checks and math. +func (e *EmissionRewardState) adjustStake(delta int64) { + if delta == 0 { + return + } + // clamp at zero on underflow + newAmt := e.stakedAmount + delta + if newAmt < 0 { + newAmt = 0 + } + e.stakedAmount = newAmt +} + +// claimRewards processes reward claiming and updates the claim state. +// This method validates claimability and transfers accumulated rewards to claimed status. +// +// Parameters: +// - currentTimestamp: current timestamp +// +// Returns: +// - int64: amount of rewards claimed +// - error: nil on success, error if claiming is not allowed +func (e *EmissionRewardState) claimRewards(currentTimestamp int64) (int64, error) { + if !e.IsClaimable(currentTimestamp) { + return 0, errNotClaimable + } + claimedRewardAmount := e.accumulatedRewardAmount - e.claimedRewardAmount + e.claimedRewardAmount = e.accumulatedRewardAmount + e.claimedTimestamp = currentTimestamp + return claimedRewardAmount, nil +} + +// updateRewardDebtX128 updates the reward debt and accumulates new rewards. +// This method should be called before any stake changes to ensure accurate reward tracking. +// +// Parameters: +// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake +// - currentTimestamp: current timestamp +func (e *EmissionRewardState) updateRewardDebtX128( + accumulatedRewardX128PerStake *u256.Uint, + currentTimestamp int64, +) error { + rewardAmount, err := e.calculateClaimableRewards(accumulatedRewardX128PerStake, currentTimestamp) + if err != nil { + return err + } + + // Accumulate newly earned rewards + if rewardAmount != 0 { + e.accumulatedRewardAmount += rewardAmount + } + + // Deep copy to avoid aliasing with external state + e.rewardDebtX128 = accumulatedRewardX128PerStake.Clone() + e.accumulatedTimestamp = currentTimestamp + return nil +} + +// addStakeWithUpdateRewardDebtX128 adds stake and updates reward debt in one operation. +// This ensures rewards are properly calculated before the stake change takes effect. +// +// Parameters: +// - amount: amount of stake to add +// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake +// - currentTimestamp: current timestamp +func (e *EmissionRewardState) addStakeWithUpdateRewardDebtX128( + amount int64, + accumulatedRewardX128PerStake *u256.Uint, + currentTimestamp int64, +) error { + if err := e.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { + return err + } + e.addStake(amount) + return nil +} + +// removeStakeWithUpdateRewardDebtX128 removes stake and updates reward debt in one operation. +// This ensures rewards are properly calculated before the stake change takes effect. +// +// Parameters: +// - amount: amount of stake to remove +// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake +// - currentTimestamp: current timestamp +func (e *EmissionRewardState) removeStakeWithUpdateRewardDebtX128( + amount int64, + accumulatedRewardX128PerStake *u256.Uint, + currentTimestamp int64, +) error { + if err := e.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { + return err + } + e.removeStake(amount) + return nil +} + +// claimRewardsWithUpdateRewardDebtX128 claims rewards and updates reward debt in one operation. +// This ensures all rewards are properly calculated before claiming. +// +// Parameters: +// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake +// - currentTimestamp: current timestamp +// +// Returns: +// - int64: amount of rewards claimed +// - error: nil on success, error if claiming fails +func (e *EmissionRewardState) claimRewardsWithUpdateRewardDebtX128( + accumulatedRewardX128PerStake *u256.Uint, + currentTimestamp int64, +) (int64, error) { + if err := e.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { + return 0, err + } + return e.claimRewards(currentTimestamp) +} + +// NewEmissionRewardState creates a new emission reward state for a staker. +// This factory function initializes the state with the current system reward debt. +// +// Parameters: +// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake +// +// Returns: +// - *EmissionRewardState: new emission reward state instance +func NewEmissionRewardState(accumulatedRewardX128PerStake *u256.Uint) *EmissionRewardState { + return &EmissionRewardState{ + // Deep copy the input to snapshot the current accumulator value. + rewardDebtX128: accumulatedRewardX128PerStake.Clone(), + accumulatedRewardAmount: 0, + accumulatedTimestamp: 0, + claimedRewardAmount: 0, + claimedTimestamp: 0, + stakedAmount: 0, + } +} diff --git a/contract/r/gnoswap/v1/gov/staker/errors.gno b/contract/r/gnoswap/v1/gov/staker/errors.gno new file mode 100644 index 0000000..781fecc --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/errors.gno @@ -0,0 +1,32 @@ +package staker + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errNoPermission = errors.New("[GNOSWAP-GOV_STAKER-001] caller has no permission") + errDataNotFound = errors.New("[GNOSWAP-GOV_STAKER-002] requested data not found") + errTransferFailed = errors.New("[GNOSWAP-GOV_STAKER-003] transfer failed") + errInvalidAmount = errors.New("[GNOSWAP-GOV_STAKER-004] invalid amount") + errNoDelegatedAmount = errors.New("[GNOSWAP-GOV_STAKER-005] zero delegated amount") + errNoDelegatedTarget = errors.New("[GNOSWAP-GOV_STAKER-006] did not delegated to that address") + errNotEnoughDelegated = errors.New("[GNOSWAP-GOV_STAKER-007] not enough delegated") + errInvalidAddress = errors.New("[GNOSWAP-GOV_STAKER-008] invalid address") + errFutureTime = errors.New("[GNOSWAP-GOV_STAKER-009] can not use future time") + errNotEnoughBalance = errors.New("[GNOSWAP-GOV_STAKER-010] not enough balance") + errLessThanMinimum = errors.New("[GNOSWAP-GOV_STAKER-011] can not delegate less than minimum amount") +) + +func makeErrorWithDetails(err error, detail string) error { + return ufmt.Errorf("%s || %s", err.Error(), detail) +} + +// checkTransferError checks transfer error. +func checkTransferError(err error) { + if err != nil { + panic(makeErrorWithDetails(errTransferFailed, err.Error())) + } +} diff --git a/contract/r/gnoswap/v1/gov/staker/getter_delegation_snapshot.gno b/contract/r/gnoswap/v1/gov/staker/getter_delegation_snapshot.gno new file mode 100644 index 0000000..e80b520 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/getter_delegation_snapshot.gno @@ -0,0 +1,34 @@ +package staker + +// GetDelegationSnapshots retrieves the delegation snapshot at a specific point in time. +// This function reconstructs historical delegation states by taking the current snapshot +// and reversing the effects of delegation records that occurred after the specified time. +// +// The algorithm works by: +// 1. Cloning the current delegation snapshot +// 2. Getting all delegation records that occurred at or after the snapshot time +// 3. Subtracting each record in reverse chronological order to restore the historical state +// +// Parameters: +// - snapshotTime: timestamp to retrieve the snapshot for +// +// Returns: +// - DelegationSnapshot: delegation state at the specified time +// - bool: true if snapshot was successfully calculated, false otherwise +func GetDelegationSnapshots(snapshotTime int64) (DelegationSnapshot, bool) { + // Get current delegation snapshots and create a working copy + delegationSnapshots := getDelegationSnapshots() + currentDelegationSnapshot := delegationSnapshots.clone() + + // Get delegation history and filter for records after snapshot time + delegationHistory := getDelegationHistory() + historyRecords := delegationHistory.getRecordsBy(snapshotTime) + + // Apply records in reverse order to reconstruct historical state + // This effectively "undoes" all delegation changes that happened after snapshotTime + for i := len(historyRecords) - 1; i >= 0; i-- { + currentDelegationSnapshot = currentDelegationSnapshot.subRecord(historyRecords[i]) + } + + return currentDelegationSnapshot, true +} diff --git a/contract/r/gnoswap/v1/gov/staker/gnomod.toml b/contract/r/gnoswap/v1/gov/staker/gnomod.toml new file mode 100644 index 0000000..b1c9071 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/gov/staker" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_manager.gno b/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_manager.gno new file mode 100644 index 0000000..af4c17b --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_manager.gno @@ -0,0 +1,298 @@ +package staker + +import ( + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + u256 "gno.land/p/gnoswap/uint256" +) + +// ProtocolFeeRewardManager manages the distribution of protocol fee rewards to stakers. +// Unlike emission rewards, protocol fees can come from multiple tokens, requiring +// separate tracking and distribution mechanisms for each token type. +type ProtocolFeeRewardManager struct { + // rewardStates maps address to ProtocolFeeRewardState for tracking individual staker rewards + rewardStates *avl.Tree // address -> ProtocolFeeRewardState + + // accumulatedProtocolFeeX128PerStake maps token path to accumulated fee per stake with 128-bit precision + accumulatedProtocolFeeX128PerStake map[string]*u256.Uint + // protocolFeeAmounts maps token path to total distributed protocol fee amounts + protocolFeeAmounts map[string]int64 + // accumulatedTimestamp tracks the last timestamp when fees were accumulated + accumulatedTimestamp int64 + // totalStakedAmount tracks the total amount of tokens staked in the system + totalStakedAmount int64 +} + +// GetAccumulatedProtocolFeeX128PerStake returns the accumulated protocol fee per stake for a specific token. +// +// Parameters: +// - token: token path to get accumulated fee for +// +// Returns: +// - *u256.Uint: accumulated protocol fee per stake for the token (scaled by 2^128) +func (p *ProtocolFeeRewardManager) GetAccumulatedProtocolFeeX128PerStake(token string) *u256.Uint { + return p.accumulatedProtocolFeeX128PerStake[token] +} + +// GetAccumulatedTimestamp returns the last timestamp when protocol fees were accumulated. +// +// Returns: +// - int64: last accumulated timestamp +func (p *ProtocolFeeRewardManager) GetAccumulatedTimestamp() int64 { + return p.accumulatedTimestamp +} + +// GetClaimableRewardAmounts calculates the claimable reward amounts for all tokens for a specific address. +// This method computes rewards based on current protocol fee distribution state and staking history. +// +// Parameters: +// - protocolFeeAmounts: current protocol fee amounts for all tokens +// - address: staker's address to calculate rewards for +// - currentTimestamp: current timestamp +// +// Returns: +// - map[string]int64: map of token path to claimable reward amount +func (p *ProtocolFeeRewardManager) GetClaimableRewardAmounts( + protocolFeeAmounts map[string]int64, + address string, + currentTimestamp int64, +) (map[string]int64, error) { + rewardStateI, ok := p.rewardStates.Get(address) + if !ok { + return make(map[string]int64), nil + } + + rewardState, ok := rewardStateI.(*ProtocolFeeRewardState) + if !ok { + return nil, ufmt.Errorf( + "failed to cast rewardStates's element to *ProtocolFeeRewardState: %T", + rewardStateI, + ) + } + + accumulatedRewardX128PerStake, _, err := p.calculateAccumulatedRewardX128PerStake( + protocolFeeAmounts, + currentTimestamp, + ) + if err != nil { + return nil, err + } + + return rewardState.GetClaimableRewardAmounts(accumulatedRewardX128PerStake, currentTimestamp) +} + +// calculateAccumulatedRewardX128PerStake calculates the updated accumulated reward per stake for all tokens. +// This method computes new accumulated reward rates based on newly distributed protocol fees. +// +// Parameters: +// - protocolFeeAmounts: current protocol fee amounts for all tokens +// - currentTimestamp: current timestamp +// +// Returns: +// - map[string]*u256.Uint: updated accumulated reward per stake for each token +// - map[string]int64: updated protocol fee amounts for each token +func (p *ProtocolFeeRewardManager) calculateAccumulatedRewardX128PerStake( + protocolFeeAmounts map[string]int64, + currentTimestamp int64, +) (map[string]*u256.Uint, map[string]int64, error) { + // If we're looking at a past timestamp, return current state + if p.accumulatedTimestamp > currentTimestamp { + return p.accumulatedProtocolFeeX128PerStake, p.protocolFeeAmounts, nil + } + + accumulatedProtocolFeesX128PerStake := make(map[string]*u256.Uint) + changedProtocolFeeAmounts := make(map[string]int64) + + // Process each token's protocol fees + for token, protocolFeeAmount := range protocolFeeAmounts { + previousProtocolFeeAmount, ok := p.protocolFeeAmounts[token] + if !ok { + previousProtocolFeeAmount = 0 + } + + protocolFeeDelta := protocolFeeAmount - previousProtocolFeeAmount + + // If no new fees for this token, keep existing rate + if protocolFeeDelta <= 0 { + accumulatedProtocolFeesX128PerStake[token] = p.accumulatedProtocolFeeX128PerStake[token] + if accumulatedProtocolFeesX128PerStake[token] == nil { + accumulatedProtocolFeesX128PerStake[token] = u256.NewUint(0) + } + } + + // Scale the fee delta by 2^128 for precision + protocolFeeDeltaX128 := u256.NewUintFromInt64(protocolFeeDelta) + protocolFeeDeltaX128 = u256.Zero().Lsh(protocolFeeDeltaX128, 128) + + protocolFeeDeltaX128PerStake := u256.Zero() + + // Calculate fee per stake if there are staked tokens + if p.totalStakedAmount > 0 { + feePerStake := u256.Zero().Div(protocolFeeDeltaX128, u256.NewUintFromInt64(p.totalStakedAmount)) + protocolFeeDeltaX128PerStake = feePerStake + } + + // Get current accumulated fee per stake for this token + accumulatedProtocolFeeX128PerStake := u256.Zero() + if p.accumulatedProtocolFeeX128PerStake[token] != nil { + accumulatedProtocolFeeX128PerStake = p.accumulatedProtocolFeeX128PerStake[token] + } + + // Add the new fee per stake to the accumulated amount + accumulatedProtocolFeeX128PerStake = u256.Zero().Add(accumulatedProtocolFeeX128PerStake, protocolFeeDeltaX128PerStake) + accumulatedProtocolFeesX128PerStake[token] = accumulatedProtocolFeeX128PerStake.Clone() + + changedProtocolFeeAmounts[token] = protocolFeeAmount + } + + return accumulatedProtocolFeesX128PerStake, changedProtocolFeeAmounts, nil +} + +// updateAccumulatedProtocolFeeX128PerStake updates the internal accumulated protocol fee state. +// This method should be called before any stake changes to ensure accurate reward calculations. +// +// Parameters: +// - protocolFeeAmounts: current protocol fee amounts for all tokens +// - currentTimestamp: current timestamp +func (p *ProtocolFeeRewardManager) updateAccumulatedProtocolFeeX128PerStake( + protocolFeeAmounts map[string]int64, + currentTimestamp int64, +) error { + // Don't update if we're looking at a past timestamp + if p.accumulatedTimestamp > currentTimestamp { + return nil + } + + accumulatedProtocolFeeX128PerStake, changedProtocolFeeAmounts, err := p.calculateAccumulatedRewardX128PerStake( + protocolFeeAmounts, + currentTimestamp, + ) + if err != nil { + return err + } + + p.accumulatedProtocolFeeX128PerStake = accumulatedProtocolFeeX128PerStake + p.protocolFeeAmounts = changedProtocolFeeAmounts + p.accumulatedTimestamp = currentTimestamp + + return nil +} + +// addStake adds a stake for an address and updates their protocol fee reward state. +// This method ensures rewards are properly calculated before the stake change. +// +// Parameters: +// - address: staker's address +// - amount: amount of stake to add +// - currentTimestamp: current timestamp +func (p *ProtocolFeeRewardManager) addStake(address string, amount int64, currentTimestamp int64) error { + rewardStateI, ok := p.rewardStates.Get(address) + if !ok { + rewardStateI = NewProtocolFeeRewardState(p.accumulatedProtocolFeeX128PerStake) + } + + rewardState, ok := rewardStateI.(*ProtocolFeeRewardState) + if !ok { + return ufmt.Errorf( + "failed to cast rewardStates's element to *ProtocolFeeRewardState: %T", + rewardStateI, + ) + } + + err := rewardState.addStakeWithUpdateRewardDebtX128(amount, p.accumulatedProtocolFeeX128PerStake, currentTimestamp) + if err != nil { + return err + } + + p.rewardStates.Set(address, rewardState) + + p.totalStakedAmount = p.totalStakedAmount + amount + + return nil +} + +// removeStake removes a stake for an address and updates their protocol fee reward state. +// This method ensures rewards are properly calculated before the stake change. +// +// Parameters: +// - address: staker's address +// - amount: amount of stake to remove +// - currentTimestamp: current timestamp +func (p *ProtocolFeeRewardManager) removeStake(address string, amount int64, currentTimestamp int64) error { + rewardStateI, ok := p.rewardStates.Get(address) + if !ok { + rewardStateI = NewProtocolFeeRewardState(p.accumulatedProtocolFeeX128PerStake) + } + + rewardState, ok := rewardStateI.(*ProtocolFeeRewardState) + if !ok { + return ufmt.Errorf( + "failed to cast rewardStates's element to *ProtocolFeeRewardState: %T", + rewardStateI, + ) + } + + err := rewardState.removeStakeWithUpdateRewardDebtX128(amount, p.accumulatedProtocolFeeX128PerStake, currentTimestamp) + if err != nil { + return err + } + + p.rewardStates.Set(address, rewardState) + + p.totalStakedAmount = p.totalStakedAmount - amount + + return nil +} + +// claimRewards processes protocol fee reward claiming for an address. +// This method calculates and returns the amounts of rewards claimed for each token. +// +// Parameters: +// - address: staker's address claiming rewards +// - currentTimestamp: current timestamp +// +// Returns: +// - map[string]int64: map of token path to claimed reward amount +// - error: nil on success, error if claiming fails +func (p *ProtocolFeeRewardManager) claimRewards(address string, currentTimestamp int64) (map[string]int64, error) { + rewardStateI, ok := p.rewardStates.Get(address) + if !ok { + return make(map[string]int64), nil + } + + rewardState, ok := rewardStateI.(*ProtocolFeeRewardState) + if !ok { + return nil, ufmt.Errorf( + "failed to cast rewardStates's element to *ProtocolFeeRewardState: %T", + rewardStateI, + ) + } + + claimedRewards, err := rewardState.claimRewardsWithUpdateRewardDebtX128( + p.accumulatedProtocolFeeX128PerStake, + currentTimestamp, + ) + if err != nil { + return nil, err + } + + p.rewardStates.Set(address, rewardState) + + return claimedRewards, nil +} + +// NewProtocolFeeRewardManager creates a new instance of ProtocolFeeRewardManager. +// This factory function initializes all tracking structures for multi-token protocol fee reward management. +// +// Returns: +// - *ProtocolFeeRewardManager: new protocol fee reward manager instance +func NewProtocolFeeRewardManager() *ProtocolFeeRewardManager { + return &ProtocolFeeRewardManager{ + rewardStates: avl.NewTree(), + protocolFeeAmounts: make(map[string]int64), + accumulatedProtocolFeeX128PerStake: make(map[string]*u256.Uint), + accumulatedTimestamp: 0, + totalStakedAmount: 0, + } +} diff --git a/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_state.gno b/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_state.gno new file mode 100644 index 0000000..ae6f3f5 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_state.gno @@ -0,0 +1,298 @@ +package staker + +import ( + "errors" + + u256 "gno.land/p/gnoswap/uint256" +) + +// ProtocolFeeRewardState tracks protocol fee reward information for an individual staker across multiple tokens. +// Unlike emission rewards which are single-token, protocol fees can come from various trading pairs, +// requiring separate tracking and calculation for each token type. +type ProtocolFeeRewardState struct { + // rewardDebtX128 maps token path to reward debt with 128-bit precision scaling + // Used to calculate rewards earned since the last update for each token + rewardDebtX128 map[string]*u256.Uint + // accumulatedRewards maps token path to total rewards accumulated but not yet claimed + accumulatedRewards map[string]int64 + // claimedRewards maps token path to total amount of rewards that have been claimed + claimedRewards map[string]int64 + // accumulatedTimestamp is the last timestamp when rewards were accumulated + accumulatedTimestamp int64 + // claimedTimestamp is the last timestamp when rewards were claimed + claimedTimestamp int64 + // stakedAmount is the current amount of tokens staked by this address + stakedAmount int64 +} + +// IsClaimable checks if rewards can be claimed at the given timestamp. +// Rewards are claimable if the current timestamp is greater than the last claimed timestamp. +// +// Parameters: +// - currentTimestamp: current timestamp to check against +// +// Returns: +// - bool: true if rewards can be claimed, false otherwise +func (p *ProtocolFeeRewardState) IsClaimable(currentTimestamp int64) bool { + return p.claimedTimestamp < currentTimestamp +} + +// GetClaimableRewardAmounts calculates the claimable reward amounts for all tokens. +// This includes both accumulated rewards and newly earned rewards based on current state. +// +// Parameters: +// - accumulatedRewardsX128PerStake: current system-wide accumulated rewards per stake for all tokens +// - currentTimestamp: current timestamp +// +// Returns: +// - map[string]int64: map of token path to claimable reward amount +// - error: nil on success, error if claiming is not allowed +func (p *ProtocolFeeRewardState) GetClaimableRewardAmounts( + accumulatedRewardsX128PerStake map[string]*u256.Uint, + currentTimestamp int64, +) (map[string]int64, error) { + rewardAmounts, err := p.calculateClaimableRewards(accumulatedRewardsX128PerStake, currentTimestamp) + if err != nil { + return nil, err + } + + return rewardAmounts, nil +} + +// calculateClaimableRewards calculates newly earned rewards for all tokens since the last update. +// This method uses the difference between current and stored reward debt to calculate earnings. +// +// Parameters: +// - accumulatedRewardsX128PerStake: current system-wide accumulated rewards per stake for all tokens +// - currentTimestamp: current timestamp +// +// Returns: +// - map[string]int64: map of token path to newly earned reward amount +func (p *ProtocolFeeRewardState) calculateClaimableRewards( + accumulatedRewardsX128PerStake map[string]*u256.Uint, + currentTimestamp int64, +) (map[string]int64, error) { + // Don't calculate rewards for past timestamps + if p.accumulatedTimestamp >= currentTimestamp { + return p.accumulatedRewards, nil + } + + rewardAmounts := make(map[string]int64) + + // Calculate rewards for each token type + for token, accumulatedRewardX128PerStake := range accumulatedRewardsX128PerStake { + // Initialize reward debt if it doesn't exist for this token + if p.rewardDebtX128[token] == nil { + p.rewardDebtX128[token] = u256.Zero() + } + + // Calculate the difference in accumulated rewards per stake since last update + // Using modular arithmetic for accumulator values - underflow is allowed and handled correctly + rewardDebtDeltaX128 := u256.Zero().Sub( + accumulatedRewardX128PerStake, + p.rewardDebtX128[token], + ) + + // Multiply by staked amount to get total reward for this staker and token + rewardAmount := u256.MulDiv( + rewardDebtDeltaX128, + u256.NewUintFromInt64(p.stakedAmount), + q128, + ) + + rewardAmounts[token] = safeConvertToInt64(rewardAmount) + } + + return rewardAmounts, nil +} + +// addStake increases the staked amount for this address. +// This method should be called when a user increases their stake. +// +// Parameters: +// - amount: amount of stake to add +func (p *ProtocolFeeRewardState) addStake(amount int64) { + p.stakedAmount = p.stakedAmount + amount +} + +// removeStake decreases the staked amount for this address. +// This method should be called when a user decreases their stake. +// +// Parameters: +// - amount: amount of stake to remove +func (p *ProtocolFeeRewardState) removeStake(amount int64) { + p.stakedAmount = p.stakedAmount - amount +} + +// claimRewards processes reward claiming for all tokens and updates the claim state. +// This method validates claimability and transfers accumulated rewards to claimed status. +// +// Parameters: +// - currentTimestamp: current timestamp +// +// Returns: +// - map[string]int64: map of token path to claimed reward amount +// - error: nil on success, error if claiming is not allowed +func (p *ProtocolFeeRewardState) claimRewards(currentTimestamp int64) (map[string]int64, error) { + if !p.IsClaimable(currentTimestamp) { + return nil, errors.New("not claimable") + } + + if p.accumulatedTimestamp < currentTimestamp { + return nil, errors.New("must update reward debt before claiming rewards") + } + + currentClaimedRewards := make(map[string]int64) + + // Calculate and update claimed amounts for each token + for token, rewardAmount := range p.accumulatedRewards { + currentClaimedRewards[token] = rewardAmount - p.claimedRewards[token] + p.claimedRewards[token] = rewardAmount + } + + p.claimedTimestamp = currentTimestamp + + return currentClaimedRewards, nil +} + +// updateRewardDebtX128 updates the reward debt and accumulates new rewards for all tokens. +// This method should be called before any stake changes to ensure accurate reward tracking. +// +// Parameters: +// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake for all tokens +// - currentTimestamp: current timestamp +func (p *ProtocolFeeRewardState) updateRewardDebtX128( + accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, + currentTimestamp int64, +) error { + // Don't update if we're looking at a past timestamp + if p.accumulatedTimestamp >= currentTimestamp { + return nil + } + + // Calculate and accumulate new rewards for all tokens + rewardAmounts, err := p.calculateClaimableRewards(accumulatedProtocolFeeX128PerStake, currentTimestamp) + if err != nil { + return err + } + + p.rewardDebtX128 = cloneAccumulatedProtocolFeeX128PerStake(accumulatedProtocolFeeX128PerStake) + + // Add newly calculated rewards to accumulated amounts + for token, rewardAmount := range rewardAmounts { + p.accumulatedRewards[token] = p.accumulatedRewards[token] + rewardAmount + } + + p.accumulatedTimestamp = currentTimestamp + + return nil +} + +// addStakeWithUpdateRewardDebtX128 adds stake and updates reward debt in one operation. +// This ensures rewards are properly calculated before the stake change takes effect. +// +// Parameters: +// - amount: amount of stake to add +// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake +// - currentTimestamp: current timestamp +func (p *ProtocolFeeRewardState) addStakeWithUpdateRewardDebtX128( + amount int64, + accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, + currentTimestamp int64, +) error { + err := p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp) + if err != nil { + return err + } + + p.addStake(amount) + + return nil +} + +// removeStakeWithUpdateRewardDebtX128 removes stake and updates reward debt in one operation. +// This ensures rewards are properly calculated before the stake change takes effect. +// +// Parameters: +// - amount: amount of stake to remove +// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake +// - currentTimestamp: current timestamp +func (p *ProtocolFeeRewardState) removeStakeWithUpdateRewardDebtX128( + amount int64, + accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, + currentTimestamp int64, +) error { + err := p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp) + if err != nil { + return err + } + + p.removeStake(amount) + + return nil +} + +// claimRewardsWithUpdateRewardDebtX128 claims rewards and updates reward debt in one operation. +// This ensures all rewards are properly calculated before claiming. +// +// Parameters: +// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake +// - currentTimestamp: current timestamp +// +// Returns: +// - map[string]int64: map of token path to claimed reward amount +// - error: nil on success, error if claiming fails +func (p *ProtocolFeeRewardState) claimRewardsWithUpdateRewardDebtX128( + accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, + currentTimestamp int64, +) (map[string]int64, error) { + p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp) + + return p.claimRewards(currentTimestamp) +} + +// NewProtocolFeeRewardState creates a new protocol fee reward state for a staker. +// This factory function initializes the state with the current system reward debt for all tokens. +// +// Parameters: +// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake for all tokens +// +// Returns: +// - *ProtocolFeeRewardState: new protocol fee reward state instance +func NewProtocolFeeRewardState( + accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, +) *ProtocolFeeRewardState { + rewardDebtX128 := make(map[string]*u256.Uint) + + // Clone reward debt for each token to avoid reference issues + for token, accumulatedProtocolFeeX128PerStake := range accumulatedProtocolFeeX128PerStake { + rewardDebtX128[token] = accumulatedProtocolFeeX128PerStake.Clone() + } + + return &ProtocolFeeRewardState{ + rewardDebtX128: rewardDebtX128, + claimedRewards: map[string]int64{}, + accumulatedRewards: map[string]int64{}, + stakedAmount: 0, + accumulatedTimestamp: 0, + claimedTimestamp: 0, + } +} + +// cloneAccumulatedProtocolFeeX128PerStake creates a deep copy of the accumulated protocol fee map. +// This utility function prevents reference sharing between different reward states. +// +// Parameters: +// - accumulatedProtocolFeeX128PerStake: map to clone +// +// Returns: +// - map[string]*u256.Uint: deep copy of the input map +func cloneAccumulatedProtocolFeeX128PerStake(accumulatedProtocolFeeX128PerStake map[string]*u256.Uint) map[string]*u256.Uint { + clone := make(map[string]*u256.Uint) + + for token, item := range accumulatedProtocolFeeX128PerStake { + clone[token] = item.Clone() + } + + return clone +} diff --git a/contract/r/gnoswap/v1/gov/staker/staker_delegate.gno b/contract/r/gnoswap/v1/gov/staker/staker_delegate.gno new file mode 100644 index 0000000..5600b6d --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/staker_delegate.gno @@ -0,0 +1,469 @@ +package staker + +import ( + "std" + "time" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/gns" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/referral" + "gno.land/r/gnoswap/v1/gov/xgns" +) + +// Delegate delegates GNS tokens to an address. +// +// Converts GNS to xGNS and assigns voting power. +// Primary mechanism for participating in governance. +// Can delegate to self or any other address. +// +// Parameters: +// - to: Address to receive voting power (can be self) +// - amount: Amount of GNS to stake and delegate +// - referrer: Optional referral address for tracking +// +// Process: +// 1. Transfers GNS from caller +// 2. Mints equivalent xGNS (1:1 ratio) +// 3. Assigns voting power to target address +// 4. Creates delegation snapshot for voting +// +// Requirements: +// - Minimum 1 GNS delegation +// - Valid target address +// - Sufficient GNS balance +// - Approval for GNS transfer +// +// Returns delegated amount. +func Delegate( + cur realm, + to std.Address, + amount int64, + referrer string, +) int64 { + halt.AssertIsNotHaltedGovStaker() + + prevRealm := std.PreviousRealm() + access.AssertIsUser(prevRealm) + access.AssertIsValidAddress(to) + + assertIsValidDelegateAmount(amount) + + caller := prevRealm.Address() + from := caller + currentRealm := std.CurrentRealm() + currentHeight := std.ChainHeight() + currentTimestamp := time.Now().Unix() + + emission.MintAndDistributeGns(cross) + + delegation, err := delegate( + from, + to, + amount, + currentHeight, + currentTimestamp, + ) + if err != nil { + panic(err) + } + + gns.TransferFrom(cross, from, currentRealm.Address(), amount) + xgns.Mint(cross, from, amount) + + registeredReferrer := registerReferrer(caller, referrer) + + std.Emit( + "Delegate", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "from", delegation.DelegateFrom().String(), + "to", delegation.DelegateTo().String(), + "amount", formatInt(delegation.DelegatedAmount()), + "referrer", registeredReferrer, + ) + + return amount +} + +// Undelegate undelegates xGNS from the existing delegate. +// +// Initiates withdrawal of staked GNS with lockup period. +// Voting power removed immediately, tokens locked for 7 days. +// Prevents governance attacks through time delay. +// +// Parameters: +// - from: Address currently delegated to +// - amount: Amount of xGNS to undelegate +// +// Process: +// 1. Removes voting power immediately +// 2. Burns xGNS tokens +// 3. Creates withdrawal request with timestamp +// 4. Locks GNS for 7-day cooldown period +// +// Requirements: +// - Must have delegated to target address +// - Sufficient delegated amount +// - Cannot undelegate during active votes +// +// After 7 days, use Collect() to claim GNS. +// Returns undelegated amount. +func Undelegate( + cur realm, + from std.Address, + amount int64, +) int64 { + halt.AssertIsNotHaltedGovStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsValidAddress(from) + + assertIsValidDelegateAmount(amount) + + currentHeight := std.ChainHeight() + currentTimestamp := time.Now().Unix() + + emission.MintAndDistributeGns(cross) + + unDelegationAmount, err := unDelegate( + caller, + from, + amount, + currentHeight, + currentTimestamp, + ) + if err != nil { + panic(err) + } + + prevRealm := std.PreviousRealm() + std.Emit( + "Undelegate", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "from", caller.String(), + "to", from.String(), + "amount", formatInt(unDelegationAmount), + ) + + return unDelegationAmount +} + +// Redelegate redelegates xGNS from existing delegate to another. +// +// Atomic operation to change delegation target. +// Maintains voting power continuity without unstaking. +// Useful for vote delegation services and dao coordination. +// +// Parameters: +// - delegatee: Current address delegated to +// - newDelegatee: New address to delegate to +// - amount: Amount of xGNS to redelegate +// +// Process: +// 1. Validates current delegation exists +// 2. Removes voting power from old delegatee +// 3. Assigns voting power to new delegatee +// 4. Updates delegation snapshots +// +// Requirements: +// - Must have active delegation to current delegatee +// - Both addresses must be valid +// - Amount must not exceed current delegation +// - Cannot redelegate to same address +// +// No lockup period - instant redelegation. +// Returns redelegated amount. +func Redelegate( + cur realm, + delegatee, + newDelegatee std.Address, + amount int64, +) int64 { + halt.AssertIsNotHaltedGovStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsValidAddress(delegatee) + access.AssertIsValidAddress(newDelegatee) + + assertIsValidDelegateAmount(amount) + + currentHeight := std.ChainHeight() + currentTimestamp := time.Now().Unix() + delegator := caller + + emission.MintAndDistributeGns(cross) + + unDelegationAmount, err := unDelegateWithoutLockup( + delegator, + delegatee, + amount, + currentHeight, + currentTimestamp, + ) + if err != nil { + panic(err) + } + + delegation, err := delegate( + delegator, + newDelegatee, + unDelegationAmount, + currentHeight, + currentTimestamp, + ) + + prevRealm := std.PreviousRealm() + std.Emit( + "Undelegate", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "from", delegation.DelegateFrom().String(), + "to", delegation.DelegateTo().String(), + "amount", formatInt(delegation.DelegatedAmount()), + ) + + std.Emit( + "Redelegate", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "from", delegation.DelegateFrom().String(), + "to", delegation.DelegateTo().String(), + "amount", formatInt(delegation.DelegatedAmount()), + ) + + return amount +} + +// CollectUndelegatedGns collects undelegated GNS tokens. +// Allows users to collect GNS tokens that completed undelegation lockup period. +// Burns xGNS and returns GNS tokens. +func CollectUndelegatedGns(cur realm) int64 { + halt.AssertIsNotHaltedGovStaker() + halt.AssertIsNotHaltedWithdraw() + + prevRealm := std.PreviousRealm() + caller := prevRealm.Address() + currentTime := time.Now().Unix() + + emission.MintAndDistributeGns(cross) + + collectedAmount, err := collectDelegations(caller, currentTime) + if err != nil { + panic(err) + } + + if collectedAmount == 0 { + return 0 + } + + xgns.Burn(cross, caller, collectedAmount) + gns.Transfer(cross, caller, collectedAmount) + + totalLockedAmount -= collectedAmount + if totalLockedAmount < 0 { + totalLockedAmount = 0 + } + + std.Emit( + "CollectUndelegatedGns", + "prevAddr", prevRealm.Address().String(), + "prevRealm", prevRealm.PkgPath(), + "from", prevRealm.Address().String(), + "to", caller.String(), + "collectedAmount", formatInt(collectedAmount), + ) + + return collectedAmount +} + +// delegate processes delegation operations. +// Validates delegation amount, creates delegation records, and updates reward tracking. +func delegate( + from std.Address, + to std.Address, + amount, + currentHeight, + currentTimestamp int64, +) (*Delegation, error) { + delegationID := nextDelegationID() + delegation := NewDelegation( + delegationID, + from, + to, + amount, + currentHeight, + currentTimestamp, + ) + delegationRecord := NewDelegationDelegateRecordBy(delegation) + + addDelegation(delegationID, delegation) + addDelegationRecord(delegationRecord) + addStakeEmissionReward(from.String(), amount, currentTimestamp) + addStakeProtocolFeeReward(from.String(), amount, time.Now().Unix()) + + totalDelegatedAmount += amount + totalLockedAmount += amount + + return delegation, nil +} + +// unDelegate processes undelegation operations with lockup. +// Validates undelegation amount, processes withdrawals, and updates reward tracking. +func unDelegate( + delegator, + delegatee std.Address, + amount, + currentHeight, + currentTimestamp int64, +) (int64, error) { + delegations := getUserDelegationsWithDelegatee(delegator, delegatee) + if len(delegations) == 0 { + return 0, nil + } + + unDelegationAmount := amount + + // Process undelegation across multiple delegation records if necessary + for _, delegation := range delegations { + if delegation.IsEmpty() { + removeDelegation(delegation.ID()) + continue + } + + currentUnDelegationAmount := unDelegationAmount + + if currentUnDelegationAmount > delegation.DelegatedAmount() { + currentUnDelegationAmount = delegation.DelegatedAmount() + } + + delegation.unDelegate( + currentUnDelegationAmount, + currentHeight, + currentTimestamp, + unDelegationLockupPeriod, + ) + + delegationRecord := NewDelegationWithdrawRecordBy(delegation, currentUnDelegationAmount, currentTimestamp) + + setDelegation(delegation.ID(), delegation) + addDelegationRecord(delegationRecord) + removeStakeEmissionReward(delegator.String(), currentUnDelegationAmount, currentTimestamp) + removeStakeProtocolFeeReward(delegator.String(), currentUnDelegationAmount, currentTimestamp) + + unDelegationAmount -= currentUnDelegationAmount + if unDelegationAmount <= 0 { + break + } + } + + totalDelegatedAmount -= amount + + if totalDelegatedAmount < 0 { + totalDelegatedAmount = 0 + } + + return amount, nil +} + +// unDelegateWithoutLockup processes undelegation without lockup. +// Used for redelegation where tokens are immediately available. +func unDelegateWithoutLockup( + delegator, + delegatee std.Address, + amount, + currentHeight, + currentTime int64, +) (int64, error) { + delegations := getUserDelegationsWithDelegatee(delegator, delegatee) + if len(delegations) == 0 { + return 0, nil + } + + unDelegationAmount := amount + + // Process undelegation across multiple delegation records if necessary + for _, delegation := range delegations { + if delegation.IsEmpty() { + removeDelegation(delegation.ID()) + continue + } + + currentUnDelegationAmount := unDelegationAmount + + if currentUnDelegationAmount > delegation.DelegatedAmount() { + currentUnDelegationAmount = delegation.DelegatedAmount() + } + + delegation.unDelegateWithoutLockup( + currentUnDelegationAmount, + currentHeight, + currentTime, + ) + + unDelegationAmount -= currentUnDelegationAmount + if unDelegationAmount <= 0 { + break + } + } + + totalDelegatedAmount -= amount + + if totalDelegatedAmount < 0 { + totalDelegatedAmount = 0 + } + + return amount, nil +} + +// collectDelegations processes collection of undelegated tokens. +// Iterates through user delegations and collects available amounts. +func collectDelegations(user std.Address, currentTime int64) (int64, error) { + collectedAmount := int64(0) + + delegations := getUserDelegations(user) + if len(delegations) == 0 { + return collectedAmount, nil + } + + // Collect from all available delegations + for _, delegation := range delegations { + collectableAmount := delegation.CollectableAmount(currentTime) + + if collectableAmount == 0 { + continue + } + + err := delegation.collect(collectableAmount, currentTime) + if err != nil { + return collectedAmount, err + } + + collectedAmount, err = addToCollectedAmount(collectedAmount, collectableAmount) + if err != nil { + return collectedAmount, err + } + + // Remove empty delegations to keep storage clean + if delegation.IsEmpty() { + removeDelegation(delegation.ID()) + } + } + + return collectedAmount, nil +} + +// registerReferrer registers or validates referrer for delegation. +// Handles referral system integration for delegation operations. +func registerReferrer(caller std.Address, referrer string) string { + success := referral.TryRegister(cross, caller, referrer) + actualReferrer := referrer + + if !success { + actualReferrer = referral.GetReferral(referrer) + } + + return actualReferrer +} diff --git a/contract/r/gnoswap/v1/gov/staker/staker_delegation_snapshot.gno b/contract/r/gnoswap/v1/gov/staker/staker_delegation_snapshot.gno new file mode 100644 index 0000000..96ad779 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/staker_delegation_snapshot.gno @@ -0,0 +1,82 @@ +package staker + +import ( + "std" + "time" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" +) + +// CleanStakerDelegationSnapshotByAdmin cleans old delegation history records. +// This administrative function removes delegation history records older than the specified threshold +// to prevent unlimited growth of historical data and optimize storage usage. +// +// The cleanup process: +// 1. Calculates cutoff time by subtracting threshold from current time +// 2. Filters delegation history to keep only records after cutoff time +// 3. Updates the delegation history with filtered records +// +// Parameters: +// - cur: realm context for cross-realm calls +// - threshold: time threshold in seconds (records older than this will be removed) +// +// Panics: +// - if caller is not admin +// +// Note: This operation is irreversible and will permanently remove historical data +func CleanStakerDelegationSnapshotByAdmin(cur realm, threshold int64) { + halt.AssertIsNotHaltedGovStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + // Calculate cutoff time by subtracting threshold from current time + cutoffTimestamp := time.Now().Unix() - threshold + + // Filter records after cutoff time + delegationHistory := getDelegationHistory() + recentDelegationHistory := delegationHistory.getRecordsBy(cutoffTimestamp) + + // Update delegation history with filtered records + setDelegationHistory(recentDelegationHistory) +} + +// SetUnDelegationLockupPeriodByAdmin sets the undelegation lockup period. +// This administrative function configures the time period that undelegated tokens +// must wait before they can be collected by users. +// +// The lockup period serves as a security mechanism to: +// - Prevent rapid delegation/undelegation cycles +// - Provide time for governance decisions to take effect +// - Maintain system stability during volatile periods +// +// Parameters: +// - cur: realm context for cross-realm calls +// - period: lockup period in seconds (must be non-negative) +// +// Panics: +// - if caller is not admin +// - if period is negative +// +// Note: This change affects all future undelegation operations +func SetUnDelegationLockupPeriodByAdmin(cur realm, period int64) { + halt.AssertIsNotHaltedGovStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + if period < 0 { + panic("period must be greater than 0") + } + + setUnDelegationLockupPeriod(period) + + previousRealm := std.PreviousRealm() + std.Emit( + "SetUnDelegationLockupPeriod", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "period", formatInt(period), + ) +} diff --git a/contract/r/gnoswap/v1/gov/staker/staker_reward.gno b/contract/r/gnoswap/v1/gov/staker/staker_reward.gno new file mode 100644 index 0000000..879ec76 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/staker_reward.gno @@ -0,0 +1,280 @@ +package staker + +import ( + "std" + "time" + + "gno.land/p/nt/ufmt" + prbac "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoland/wugnot" + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/gns" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" + "gno.land/r/gnoswap/v1/gov/xgns" +) + +const WUGNOT_PATH string = "gno.land/r/gnoland/wugnot" + +// CollectReward collects accumulated rewards based on xGNS holdings. +// +// Claims all pending rewards from governance staking. +// Distributes protocol fees and emission rewards proportionally. +// Multi-token rewards system based on xGNS share. +// +// Reward Types: +// 1. Emission rewards: GNS from protocol emission +// 2. Protocol fees: Various tokens from swap/pool fees +// 3. Withdrawal fees: 1% of liquidity provider rewards +// 4. Pool creation fees: 100 GNS per pool +// +// Distribution Formula: +// +// userReward = (userXGNS / totalXGNS) * accumulatedRewards +// +// Process: +// 1. Calculates share based on xGNS balance +// 2. Claims GNS emission rewards +// 3. Claims protocol fee rewards (all tokens) +// 4. Transfers all rewards to caller +// 5. Resets user's reward tracking +// +// No parameters required - automatically determines caller's rewards. +// Transfers rewards directly to caller. +func CollectReward(cur realm) { + halt.AssertIsNotHaltedGovStaker() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + from := std.CurrentRealm().Address() + currentTimestamp := time.Now().Unix() + + emissionReward, protocolFeeRewards, err := claimRewards(caller.String(), currentTimestamp) + if err != nil { + panic(err) + } + + // Transfer emission rewards (GNS tokens) if any + if emissionReward > 0 { + gns.Transfer(cross, caller, emissionReward) + + previousRealm := std.PreviousRealm() + std.Emit( + "CollectEmissionReward", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "from", from.String(), + "to", caller.String(), + "emissionRewardAmount", formatInt(emissionReward), + ) + } + + // Transfer protocol fee rewards for each token type + for tokenPath, amount := range protocolFeeRewards { + if amount > 0 { + err := transferToken(tokenPath, from, caller, amount) + if err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "CollectProtocolFeeReward", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "tokenPath", tokenPath, + "from", from.String(), + "to", caller.String(), + "collectedAmount", formatInt(amount), + ) + } + } +} + +// CollectRewardFromLaunchPad collects rewards for launchpad project wallets. +// +// Parameters: +// - to: recipient address for rewards +// +// Only callable by launchpad contract. +func CollectRewardFromLaunchPad(cur realm, to std.Address) { + halt.AssertIsNotHaltedGovStaker() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + access.AssertIsLaunchpad(caller) + + from := std.CurrentRealm().Address() + currentTimestamp := time.Now().Unix() + + launchpadRewardID := makeLaunchpadRewardID(to.String()) + _, exists := getLaunchpadProjectDeposit(launchpadRewardID) + if !exists { + panic(makeErrorWithDetails( + errNoDelegatedAmount, + ufmt.Sprintf("%s is not project wallet from launchpad", to.String()), + )) + } + + emissionReward, protocolFeeRewards, err := claimRewardsFromLaunchpad(to.String(), currentTimestamp) + if err != nil { + panic(err) + } + + // Transfer emission rewards (GNS tokens) to project wallet if any + if emissionReward > 0 { + gns.Transfer(cross, to, emissionReward) + + previousRealm := std.PreviousRealm() + std.Emit( + "CollectEmissionFromLaunchPad", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "from", from.String(), + "to", to.String(), + "emissionRewardAmount", formatInt(emissionReward), + ) + } + + // Transfer protocol fee rewards to project wallet for each token type + for tokenPath, amount := range protocolFeeRewards { + if amount > 0 { + err := transferToken(tokenPath, from, to, amount) + if err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "CollectProtocolFeeFromLaunchPad", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "tokenPath", tokenPath, + "from", from.String(), + "to", to.String(), + "collectedAmount", formatInt(amount), + ) + } + } +} + +// SetAmountByProjectWallet sets the amount of reward for the project wallet. +// This function is exclusively callable by the launchpad contract to manage +// xGNS balances for project wallets that participate in launchpad offerings. +// +// The function handles both adding and removing stakes: +// - When adding: mints xGNS to launchpad address and starts reward accumulation +// - When removing: burns xGNS from launchpad address and stops reward accumulation +// Adjusts stake amount for project wallet address. +// Panics: +// - if caller is not the launchpad contract +// - if system is halted for withdrawals +// - if access control operations fail +func SetAmountByProjectWallet(cur realm, addr std.Address, amount int64, add bool) { + halt.AssertIsNotHaltedGovStaker() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + currentTimestamp := time.Now().Unix() + + access.AssertIsLaunchpad(caller) + + launchpadAddr, exists := access.GetAddress(prbac.ROLE_LAUNCHPAD.String()) + if !exists { + panic(ufmt.Sprintf("launchpad address not found")) + } + + if add { + // Add stake for the project wallet and mint xGNS to launchpad + err := addStakeFromLaunchpad(addr.String(), amount, currentTimestamp) + if err != nil { + panic(err) + } + + xgns.Mint(cross, launchpadAddr, amount) + } else { + // Remove stake for the project wallet and burn xGNS from launchpad + err := removeStakeFromLaunchpad(addr.String(), amount, currentTimestamp) + if err != nil { + panic(err) + } + + xgns.Burn(cross, launchpadAddr, amount) + } +} + +// claimRewards claims both emission and protocol fee rewards. +// Coordinates claiming process for both reward types. +func claimRewards(rewardID string, currentTimestamp int64) (int64, map[string]int64, error) { + emissionReward, err := claimRewardsEmissionReward(rewardID, currentTimestamp) + if err != nil { + return 0, nil, err + } + + protocolFeeRewards, err := claimRewardsProtocolFeeReward(rewardID, currentTimestamp) + if err != nil { + return 0, nil, err + } + + return emissionReward, protocolFeeRewards, nil +} + +// claimRewardsFromLaunchpad claims rewards for launchpad project wallets. +// Uses special reward ID format for launchpad integration. +func claimRewardsFromLaunchpad(address string, currentTimestamp int64) (int64, map[string]int64, error) { + launchpadRewardID := makeLaunchpadRewardID(address) + + return claimRewards(launchpadRewardID, currentTimestamp) +} + +// transferToken transfers tokens from the staker contract to a recipient address. +// transferToken handles token transfers for reward distribution. +// Supports both native GNOT (through wUGNOT unwrapping) and GRC20 tokens. +func transferToken( + tokenPath string, + from, to std.Address, + amount int64, +) error { + common.MustRegistered(tokenPath) + + // Validate recipient address + if !to.IsValid() { + return makeErrorWithDetails( + errInvalidAddress, + ufmt.Sprintf("invalid address %s to transfer protocol fee", to.String()), + ) + } + + // Validate transfer amount + if amount < 0 { + return makeErrorWithDetails( + errInvalidAmount, + ufmt.Sprintf("invalid amount %d to transfer protocol fee", amount), + ) + } + + // Check sufficient balance + balance := common.BalanceOf(tokenPath, from) + if balance < amount { + return makeErrorWithDetails( + errNotEnoughBalance, + ufmt.Sprintf("not enough %s balance(%d) to collect(%d)", tokenPath, balance, amount), + ) + } + + // Handle native GNOT transfer through wUGNOT unwrapping + isGnoNativeCoin := tokenPath == WUGNOT_PATH + if isGnoNativeCoin { + wugnot.Withdraw(cross, amount) + + sendCoin := std.Coin{Denom: "ugnot", Amount: amount} + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(from, to, std.Coins{sendCoin}) + + return nil + } + + // Handle GRC20 token transfer + return common.Transfer(cross, tokenPath, to, amount) +} diff --git a/contract/r/gnoswap/v1/gov/staker/state.gno b/contract/r/gnoswap/v1/gov/staker/state.gno new file mode 100644 index 0000000..dff6663 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/state.gno @@ -0,0 +1,523 @@ +package staker + +import ( + "std" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/emission" + pf "gno.land/r/gnoswap/v1/protocol_fee" +) + +// Global state variables for staker contract +var ( + // unDelegationLockupPeriod defines the time period (in seconds) that undelegated tokens must wait before collection + unDelegationLockupPeriod int64 + + // delegationCounter provides unique delegation IDs for new delegations + delegationCounter *Counter + // delegations stores all delegation records indexed by delegation ID + delegations *avl.Tree + // delegationManager tracks user delegation relationships and provides efficient lookup + delegationManager *DelegationManager + + // delegationHistory maintains chronological record of all delegation events for snapshot calculations + delegationHistory DelegationHistory + // delegationSnapshots stores current delegation state for each delegatee + delegationSnapshots DelegationSnapshot + + // emissionRewardManager handles distribution and tracking of GNS emission rewards to stakers + emissionRewardManager *EmissionRewardManager + // protocolFeeRewardManager handles distribution and tracking of multi-token protocol fee rewards + protocolFeeRewardManager *ProtocolFeeRewardManager + + // launchpadProjectDeposits tracks xGNS deposits for launchpad project wallets + launchpadProjectDeposits *avl.Tree // project owner address -> deposit amount + + // emissionRewardBalance tracks the current balance of emission rewards (unused) + emissionRewardBalance int64 + // protocolFeeBalances tracks current balances of protocol fees by token (unused) + protocolFeeBalances map[string]int64 + + // totalDelegated tracks the total amount of xGNS delegated + totalDelegatedAmount int64 + // totalLockedAmount tracks the total amount of locked GNS + totalLockedAmount int64 +) + +// init initializes the global state variables with default values and empty structures +func init() { + // Default lockup period is 7 days + unDelegationLockupPeriod = 60 * 60 * 24 * 7 // 7 days + + // Initialize totalDelegated and totalLockedAmount + totalDelegatedAmount = 0 + totalLockedAmount = 0 + + // Initialize delegation tracking structures + delegationCounter = NewCounter() + delegations = avl.NewTree() + delegationManager = NewDelegationManager() + + // Initialize delegation history and snapshot tracking + delegationHistory = make(DelegationHistory, 0) + delegationSnapshots = make(DelegationSnapshot) + + // Initialize reward management systems + emissionRewardManager = NewEmissionRewardManager() + protocolFeeRewardManager = NewProtocolFeeRewardManager() + + // Initialize launchpad integration + launchpadProjectDeposits = avl.NewTree() +} + +// getUnDelegationLockupPeriod returns the current undelegation lockup period in seconds. +// +// Returns: +// - int64: lockup period in seconds +func getUnDelegationLockupPeriod() int64 { + return unDelegationLockupPeriod +} + +// setUnDelegationLockupPeriod updates the undelegation lockup period. +// This affects all future undelegation operations. +// +// Parameters: +// - period: new lockup period in seconds +func setUnDelegationLockupPeriod(period int64) { + unDelegationLockupPeriod = period +} + +// getCurrentDelegationID returns the current delegation counter value. +// +// Returns: +// - int64: current delegation ID counter +func getCurrentDelegationID() int64 { + return delegationCounter.Get() +} + +// nextDelegationID generates and returns the next unique delegation ID. +// +// Returns: +// - int64: next available delegation ID +func nextDelegationID() int64 { + return delegationCounter.next() +} + +// getDelegations returns the delegation storage tree. +// +// Returns: +// - *avl.Tree: delegation storage tree +func getDelegations() *avl.Tree { + return delegations +} + +// getDelegation retrieves a delegation by its ID. +// +// Parameters: +// - delegationID: unique identifier of the delegation +// +// Returns: +// - *Delegation: delegation instance or nil if not found +func getDelegation(delegationID int64) *Delegation { + id := formatInt(delegationID) + delegation, ok := delegations.Get(id) + if !ok { + return nil + } + + if delegation, ok := delegation.(*Delegation); !ok { + panic(ufmt.Sprintf("failed to cast delegations's element to *Delegation: %T", delegation)) + } else { + return delegation + } +} + +// setDelegation stores or updates a delegation in the storage tree. +// +// Parameters: +// - delegationID: unique identifier of the delegation +// - delegation: delegation instance to store +// +// Returns: +// - bool: true if successfully stored +func setDelegation(delegationID int64, delegation *Delegation) bool { + id := formatInt(delegationID) + + delegations.Set(id, delegation) + + return true +} + +// addDelegation adds a new delegation to storage and updates the delegation manager. +// +// Parameters: +// - delegationID: unique identifier of the delegation +// - delegation: delegation instance to add +// +// Returns: +// - bool: true if successfully added +func addDelegation(delegationID int64, delegation *Delegation) bool { + if ok := setDelegation(delegationID, delegation); !ok { + return false + } + + delegationManager.addDelegation( + delegation.DelegateFrom(), + delegation.DelegateTo(), + delegationID, + ) + + return true +} + +// removeDelegation removes a delegation from storage and updates the delegation manager. +// +// Parameters: +// - delegationID: unique identifier of the delegation to remove +// +// Returns: +// - bool: true if successfully removed +func removeDelegation(delegationID int64) bool { + delegation := getDelegation(delegationID) + if delegation == nil { + return false + } + + id := formatInt(delegation.ID()) + _, ok := delegations.Remove(id) + + delegationManager.removeDelegation( + delegation.DelegateFrom(), + delegation.DelegateTo(), + delegationID, + ) + + return ok +} + +// getUserDelegations retrieves all delegations for a specific user. +// +// Parameters: +// - user: user's address +// +// Returns: +// - []*Delegation: list of user's delegations +func getUserDelegations(user std.Address) (delegations []*Delegation) { + for _, delegationID := range delegationManager.GetUserDelegationIDs(user) { + delegations = append(delegations, getDelegation(delegationID)) + } + return +} + +// getUserDelegationsWithDelegatee retrieves all delegations from a user to a specific delegatee. +// Note: Current implementation returns all user delegations regardless of delegatee (potential bug). +// +// Parameters: +// - user: user's address +// - delegatee: delegatee's address (currently unused) +// +// Returns: +// - []*Delegation: list of user's delegations to the delegatee +func getUserDelegationsWithDelegatee(user std.Address, delegatee std.Address) (delegations []*Delegation) { + for _, delegationID := range delegationManager.GetUserDelegationIDs(user) { + delegations = append(delegations, getDelegation(delegationID)) + } + return +} + +// getDelegationHistory returns the current delegation history. +// +// Returns: +// - DelegationHistory: chronological list of delegation records +func getDelegationHistory() DelegationHistory { + return delegationHistory +} + +// addDelegationRecord adds a new delegation record to history and updates snapshots. +// +// Parameters: +// - delegationRecord: delegation record to add +func addDelegationRecord(delegationRecord *DelegationRecord) { + delegationHistory = delegationHistory.addRecord(delegationRecord) + delegationSnapshots = delegationSnapshots.addRecord(delegationRecord) +} + +// setDelegationHistory replaces the current delegation history. +// +// Parameters: +// - history: new delegation history to set +func setDelegationHistory(history DelegationHistory) { + delegationHistory = history +} + +// getDelegationSnapshots returns the current delegation snapshots. +// +// Returns: +// - DelegationSnapshot: current delegation state for all delegatees +func getDelegationSnapshots() DelegationSnapshot { + return delegationSnapshots +} + +// setDelegationSnapshots replaces the current delegation snapshots. +// +// Parameters: +// - snapshot: new delegation snapshot to set +func setDelegationSnapshots(snapshot DelegationSnapshot) { + delegationSnapshots = snapshot +} + +// addStakeEmissionReward adds stake to emission reward tracking for an address. +// This method updates the emission reward distribution state and adds stake for the specified address. +// +// Parameters: +// - address: staker's address +// - amount: amount of stake to add +// - currentTimestamp: current timestamp +func addStakeEmissionReward(address string, amount int64, currentTimestamp int64) error { + distributedAmount := emission.GetAccuDistributedToGovStaker() + + err := emissionRewardManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp) + if err != nil { + return err + } + + return emissionRewardManager.addStake(address, amount, currentTimestamp) +} + +// removeStakeEmissionReward removes stake from emission reward tracking for an address. +// This method updates the emission reward distribution state and removes stake for the specified address. +// +// Parameters: +// - address: staker's address +// - amount: amount of stake to remove +// - currentTimestamp: current timestamp +func removeStakeEmissionReward(address string, amount int64, currentTimestamp int64) error { + distributedAmount := emission.GetAccuDistributedToGovStaker() + + err := emissionRewardManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp) + if err != nil { + return err + } + + return emissionRewardManager.removeStake(address, amount, currentTimestamp) +} + +// claimRewardsEmissionReward claims emission rewards for an address. +// This method updates the emission reward distribution state and processes reward claiming. +// +// Parameters: +// - address: staker's address claiming rewards +// - currentTimestamp: current timestamp +// +// Returns: +// - int64: amount of emission rewards claimed +// - error: nil on success, error if claiming fails +func claimRewardsEmissionReward(address string, currentTimestamp int64) (int64, error) { + distributedAmount := emission.GetAccuDistributedToGovStaker() + + err := emissionRewardManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp) + if err != nil { + return 0, err + } + + return emissionRewardManager.claimRewards(address, currentTimestamp) +} + +// addStakeProtocolFeeReward adds stake to protocol fee reward tracking for an address. +// This method distributes protocol fees and updates the protocol fee reward state. +// +// Parameters: +// - address: staker's address +// - amount: amount of stake to add +// - currentTimestamp: current timestamp +func addStakeProtocolFeeReward(address string, amount int64, currentTimestamp int64) error { + pf.DistributeProtocolFee(cross) + + distributedAmounts := getDistributedProtocolFees() + + err := protocolFeeRewardManager.updateAccumulatedProtocolFeeX128PerStake(distributedAmounts, currentTimestamp) + if err != nil { + return err + } + + return protocolFeeRewardManager.addStake(address, amount, currentTimestamp) +} + +// removeStakeProtocolFeeReward removes stake from protocol fee reward tracking for an address. +// This method distributes protocol fees and updates the protocol fee reward state. +// +// Parameters: +// - address: staker's address +// - amount: amount of stake to remove +// - currentTimestamp: current timestamp +func removeStakeProtocolFeeReward(address string, amount int64, currentTimestamp int64) error { + pf.DistributeProtocolFee(cross) + + distributedAmounts := getDistributedProtocolFees() + + err := protocolFeeRewardManager.updateAccumulatedProtocolFeeX128PerStake(distributedAmounts, currentTimestamp) + if err != nil { + return err + } + + return protocolFeeRewardManager.removeStake(address, amount, currentTimestamp) +} + +// claimRewardsProtocolFeeReward claims protocol fee rewards for an address. +// This method distributes protocol fees and processes reward claiming for all token types. +// +// Parameters: +// - address: staker's address claiming rewards +// - currentTimestamp: current timestamp +// +// Returns: +// - map[string]int64: protocol fee rewards claimed by token +// - error: nil on success, error if claiming fails +func claimRewardsProtocolFeeReward(address string, currentTimestamp int64) (map[string]int64, error) { + pf.DistributeProtocolFee(cross) + + distributedAmounts := getDistributedProtocolFees() + + err := protocolFeeRewardManager.updateAccumulatedProtocolFeeX128PerStake(distributedAmounts, currentTimestamp) + if err != nil { + return nil, err + } + + return protocolFeeRewardManager.claimRewards(address, currentTimestamp) +} + +// getDistributedProtocolFees retrieves the current distributed protocol fee amounts for all tokens. +// This method queries the protocol fee contract for accumulated distributions. +// +// Returns: +// - map[string]int64: distributed amounts by token path +func getDistributedProtocolFees() map[string]int64 { + return pf.GetAccuTransfersToGovStaker() +} + +// getLaunchpadProjectDeposit retrieves the deposit amount for a launchpad project. +// +// Parameters: +// - ownerAddress: project owner's address identifier +// +// Returns: +// - int64: deposit amount +// - bool: true if project exists, false otherwise +func getLaunchpadProjectDeposit(ownerAddress string) (int64, bool) { + deposit, ok := launchpadProjectDeposits.Get(ownerAddress) + if !ok { + return 0, false + } + + amount, ok := deposit.(int64) + if !ok { + panic(ufmt.Sprintf("failed to cast deposit to int64: %T", deposit)) + } + + return amount, true +} + +// setLaunchpadProjectDeposit sets the deposit amount for a launchpad project. +// +// Parameters: +// - ownerAddress: project owner's address identifier +// - deposit: deposit amount to set +// +// Returns: +// - bool: true if successfully set +func setLaunchpadProjectDeposit(ownerAddress string, deposit int64) bool { + launchpadProjectDeposits.Set(ownerAddress, deposit) + + return true +} + +// removeLaunchpadProjectDeposit removes a launchpad project deposit record. +// +// Parameters: +// - ownerAddress: project owner's address identifier +// +// Returns: +// - bool: true if successfully removed +func removeLaunchpadProjectDeposit(ownerAddress string) bool { + _, ok := launchpadProjectDeposits.Remove(ownerAddress) + + return ok +} + +// addStakeFromLaunchpad adds stake for a launchpad project and updates reward tracking. +// This method creates a special reward ID for launchpad projects and manages their deposit tracking. +// +// Parameters: +// - address: project wallet address +// - amount: amount of stake to add +// - currentTimestamp: current timestamp +func addStakeFromLaunchpad(address string, amount int64, currentTimestamp int64) error { + launchpadRewardID := makeLaunchpadRewardID(address) + err := addStakeEmissionReward(launchpadRewardID, amount, currentTimestamp) + if err != nil { + return err + } + + err = addStakeProtocolFeeReward(launchpadRewardID, amount, currentTimestamp) + if err != nil { + return err + } + + deposit, exists := getLaunchpadProjectDeposit(launchpadRewardID) + if !exists { + deposit = 0 + } + + deposit += amount + setLaunchpadProjectDeposit(launchpadRewardID, deposit) + + return nil +} + +// removeStakeFromLaunchpad removes stake for a launchpad project and updates reward tracking. +// This method manages launchpad project deposit tracking and ensures non-negative deposits. +// +// Parameters: +// - address: project wallet address +// - amount: amount of stake to remove +// - currentTimestamp: current timestamp +func removeStakeFromLaunchpad(address string, amount int64, currentTimestamp int64) error { + launchpadRewardID := makeLaunchpadRewardID(address) + err := removeStakeEmissionReward(launchpadRewardID, amount, currentTimestamp) + if err != nil { + return err + } + + err = removeStakeProtocolFeeReward(launchpadRewardID, amount, currentTimestamp) + if err != nil { + return err + } + + deposit, exists := getLaunchpadProjectDeposit(launchpadRewardID) + if !exists { + deposit = 0 + } + + deposit -= amount + if deposit < 0 { + deposit = 0 + } + + setLaunchpadProjectDeposit(launchpadRewardID, deposit) + + return nil +} + +// makeLaunchpadRewardID creates a special reward identifier for launchpad projects. +// This ensures launchpad project rewards are tracked separately from regular user stakes. +// +// Parameters: +// - address: project wallet address +// +// Returns: +// - string: formatted launchpad reward ID +func makeLaunchpadRewardID(address string) string { + return ufmt.Sprintf("launchpad:%s", address) +} diff --git a/contract/r/gnoswap/v1/gov/staker/util.gno b/contract/r/gnoswap/v1/gov/staker/util.gno new file mode 100644 index 0000000..7620b34 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/staker/util.gno @@ -0,0 +1,124 @@ +package staker + +import ( + b64 "encoding/base64" + "strconv" + + "gno.land/p/nt/avl" + "gno.land/p/onbloc/json" + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" +) + +// marshal data to json string +func marshal(data *json.Node) string { + b, err := json.Marshal(data) + if err != nil { + panic(err.Error()) + } + + return string(b) +} + +// b64Encode encodes data to base64 string +func b64Encode(data string) string { + return string(b64.StdEncoding.EncodeToString([]byte(data))) +} + +// formatUint formats a uint64 to a string +func formatUint(v uint64) string { + return strconv.FormatUint(v, 10) +} + +// formatInt formats an int64 to a string +func formatInt(v int64) string { + return strconv.FormatInt(v, 10) +} + +// getUint64FromTree returns the uint64 value from the tree +func getUint64FromTree(tree *avl.Tree, key string) uint64 { + value, exists := tree.Get(key) + if !exists { + return 0 + } + + v, ok := value.(uint64) + if !ok { + panic(ufmt.Sprintf("failed to cast value to uint64: %T", value)) + } + + return v +} + +// updateUint64InTree updates the uint64 value in the tree +func updateUint64InTree(tree *avl.Tree, key string, delta uint64, add bool) uint64 { + current := getUint64FromTree(tree, key) + var newValue uint64 + if add { + newValue = current + delta + } else { + if current < delta { + panic(makeErrorWithDetails( + errNotEnoughBalance, + ufmt.Sprintf("not enough balance: current(%d) < requested(%d)", current, delta), + )) + } + newValue = current - delta + } + + tree.Set(key, newValue) + + return newValue +} + +// getOrCreateInnerTree returns the inner tree for the given key +func getOrCreateInnerTree(tree *avl.Tree, key string) *avl.Tree { + value, exists := tree.Get(key) + if !exists { + innerTree := avl.NewTree() + tree.Set(key, innerTree) + return innerTree + } + + v, ok := value.(*avl.Tree) + if !ok { + panic(ufmt.Sprintf("failed to cast value to *avl.Tree: %T", value)) + } + + return v +} + +// milliToSec converts milliseconds to seconds +func milliToSec(ms int64) int64 { + var msPerSec int64 = 1000 + return ms / msPerSec +} + +// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. +// +// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds +// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. +// +// Parameters: +// - value (*u256.Uint): The unsigned 256-bit integer to be converted. +// +// Returns: +// - int64: The converted value if it falls within the int64 range. +// +// Panics: +// - If the `value` exceeds the range of int64, the function will panic with an error indicating +// the overflow and the original value. +func safeConvertToInt64(value *u256.Uint) int64 { + const INT64_MAX = 9223372036854775807 + const MAX_INT64 = "9223372036854775807" + + res, overflow := value.Uint64WithOverflow() + if overflow || res > uint64(INT64_MAX) { + panic(ufmt.Sprintf( + "amount(%s) overflows int64 range (max %s)", + value.ToString(), + MAX_INT64, + )) + } + return int64(res) +} diff --git a/contract/r/gnoswap/v1/gov/xgns/doc.gno b/contract/r/gnoswap/v1/gov/xgns/doc.gno new file mode 100644 index 0000000..aae22e1 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/xgns/doc.gno @@ -0,0 +1,4 @@ +// Package xgns implements the GRC20-compliant xGNS token that represents +// staked GNS tokens. It manages minting/burning operations and tracks +// voting power for governance. +package xgns diff --git a/contract/r/gnoswap/v1/gov/xgns/errors.gno b/contract/r/gnoswap/v1/gov/xgns/errors.gno new file mode 100644 index 0000000..8f9fb02 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/xgns/errors.gno @@ -0,0 +1,14 @@ +package xgns + +import ( + "errors" +) + +var errNoPermission = errors.New("[GNOSWAP-XGNS-001] caller has no permission") + +// checkErr panics if an error occurs. +func checkErr(err error) { + if err != nil { + panic(err.Error()) + } +} diff --git a/contract/r/gnoswap/v1/gov/xgns/gnomod.toml b/contract/r/gnoswap/v1/gov/xgns/gnomod.toml new file mode 100644 index 0000000..875d4b1 --- /dev/null +++ b/contract/r/gnoswap/v1/gov/xgns/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/gov/xgns" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/gov/xgns/xgns.gno b/contract/r/gnoswap/v1/gov/xgns/xgns.gno new file mode 100644 index 0000000..ae021ad --- /dev/null +++ b/contract/r/gnoswap/v1/gov/xgns/xgns.gno @@ -0,0 +1,126 @@ +package xgns + +import ( + "std" + "strings" + + "gno.land/p/demo/tokens/grc20" + "gno.land/p/nt/ownable" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + + prbac "gno.land/p/gnoswap/rbac" +) + +var ( + admin = ownable.NewWithAddress(std.DerivePkgAddr(prbac.ROLE_GOV_STAKER.String())) + token, ledger = grc20.NewToken("XGNS", "xGNS", 6) +) + +// TotalSupply returns the total supply of xGNS tokens. +func TotalSupply() int64 { + return token.TotalSupply() +} + +// VotingSupply returns total supply eligible for voting. +func VotingSupply() int64 { + total := token.TotalSupply() + launchpad, ok := access.GetAddress(prbac.ROLE_LAUNCHPAD.String()) + if !ok { + panic(ufmt.Sprintf("launchpad address not found")) + } + + return total - token.BalanceOf(launchpad) +} + +// BalanceOf returns token balance for address. +// +// Parameters: +// - owner: address to check balance for +// +// Returns balance amount. +func BalanceOf(owner std.Address) int64 { + return token.BalanceOf(owner) +} + +// Render returns a formatted representation of the token state. +func Render(path string) string { + parts := strings.Split(path, "/") + c := len(parts) + + switch { + case path == "": + return token.RenderHome() + case c == 2 && parts[0] == "balance": + balance := token.BalanceOf(std.Address(parts[1])) + return ufmt.Sprintf("%d\n", balance) + default: + return "404\n" + } +} + +// Mint mints tokens to address. +// +// Parameters: +// - to: recipient address +// - amount: amount to mint +// +// Only callable by governance staker contract. +func Mint(cur realm, to std.Address, amount int64) { + halt.AssertIsNotHaltedXGns() + + caller := std.PreviousRealm().Address() + access.AssertIsGovStaker(caller) + + checkErr(ledger.Mint(to, amount)) +} + +// MintByLaunchPad mints tokens to address. +// +// Parameters: +// - to: recipient address +// - amount: amount to mint +// +// Only callable by launchpad contract. +func MintByLaunchPad(cur realm, to std.Address, amount int64) { + halt.AssertIsNotHaltedXGns() + + caller := std.PreviousRealm().Address() + access.AssertIsLaunchpad(caller) + + checkErr(ledger.Mint(to, amount)) +} + +// Burn burns tokens from address. +// +// Parameters: +// - from: address to burn from +// - amount: amount to burn +// +// Only callable by governance staker contract. +func Burn(cur realm, from std.Address, amount int64) { + halt.AssertIsNotHaltedXGns() + + caller := std.PreviousRealm().Address() + access.AssertIsGovStaker(caller) + + checkErr(ledger.Burn(from, amount)) +} + +// BurnByLaunchPad burns tokens from address. +// +// Parameters: +// - from: address to burn from +// - amount: amount to burn +// +// Only callable by launchpad contract. +func BurnByLaunchPad(cur realm, from std.Address, amount int64) { + halt.AssertIsNotHaltedXGns() + + caller := std.PreviousRealm().Address() + access.AssertIsLaunchpad(caller) + + checkErr(ledger.Burn(from, amount)) +} diff --git a/contract/r/gnoswap/v1/launchpad/README.md b/contract/r/gnoswap/v1/launchpad/README.md new file mode 100644 index 0000000..ce17447 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/README.md @@ -0,0 +1,66 @@ +# Launchpad + +Token distribution platform for early-stage projects. + +## Overview + +Launchpad enables new projects to distribute tokens to GNS stakers with tiered lock periods and pro-rata reward distribution. For more details about the concept, check out [docs](https://docs.gnoswap.io/core-concepts/launchpad). + +## Configuration + +- **Pool Tiers**: 30, 90, 180 days +- **Minimum Start Delay**: 7 days +- **Auto-delegation**: Staked GNS converts to xGNS +- **Tier Allocation**: Customizable per project + +## Core Features + +- GNS staking for project token rewards +- Multiple tier durations with different rewards +- Automatic xGNS delegation for governance +- Pro-rata distribution based on stake size +- Conditional participation requirements + +## Key Functions + +### `CreateProject` +Creates new token distribution project. + +### `DepositGns` +Stakes GNS to earn project tokens. + +### `CollectRewardByDepositId` +Claims earned project tokens. + +### `CollectDepositGns` +Withdraws GNS after lock period. + +### `TransferLeftFromProjectByAdmin` +Refunds unclaimed rewards to project. + +## Usage + +```go +// Create project +projectId := CreateProject( + name, tokenPath, recipient, amount, + conditionTokens, conditionAmounts, + tier30Ratio, tier90Ratio, tier180Ratio, + startTime +) + +// Stake GNS +depositId := DepositGns(projectTierId, amount, referrer) + +// Collect rewards +CollectRewardByDepositId(depositId) + +// Withdraw after lock period +CollectDepositGns(depositId) +``` + +## Security + +- GNS locked until tier period ends +- Automatic governance delegation +- Conditional requirements prevent abuse \ No newline at end of file diff --git a/contract/r/gnoswap/v1/launchpad/api_deposit.gno b/contract/r/gnoswap/v1/launchpad/api_deposit.gno new file mode 100644 index 0000000..819f25c --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/api_deposit.gno @@ -0,0 +1,18 @@ +package launchpad + +// ApiGetDepositByDepositId retrieves deposit information by deposit ID. +func ApiGetDepositByDepositId(depositId string) string { + deposit, exist := deposits.Get(depositId) + if !exist { + return "" + } + + builder := metaBuilder() + d, ok := deposit.(*Deposit) + if !ok { + panic("failed to cast deposit to *Deposit") + } + depositBuilder(builder, d) + + return marshal(builder.Node()) +} diff --git a/contract/r/gnoswap/v1/launchpad/api_project.gno b/contract/r/gnoswap/v1/launchpad/api_project.gno new file mode 100644 index 0000000..1b8b60a --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/api_project.gno @@ -0,0 +1,92 @@ +package launchpad + +import ( + "std" + "strconv" + + "gno.land/p/nt/ufmt" +) + +// ApiGetProjectAndTierStatisticsByProjectId retrieves project and tier statistics by project ID. +func ApiGetProjectAndTierStatisticsByProjectId(projectId string) string { + project, err := getProject(projectId) + if err != nil { + return "" + } + + builder := metaBuilder().WriteString("projectId", projectId) + projectBuilder(builder, project) + + for _, duration := range []int64{30, 90, 180} { + if tier, err := project.getTier(duration); err == nil { + tierBuilder(builder, ufmt.Sprintf("tier%d", duration), tier) + } + } + + return marshal(builder.Node()) +} + +// ApiGetProjectStatisticsByProjectId retrieves project statistics by project ID. +func ApiGetProjectStatisticsByProjectId(projectId string) string { + project, err := getProject(projectId) + if err != nil { + return "" + } + + builder := metaBuilder().WriteString("projectId", projectId) + projectBuilder(builder, project) + + return marshal(builder.Node()) +} + +// ApiGetTierStatisticsByProjectId retrieves tier statistics by project ID. +func ApiGetTierStatisticsByProjectId(projectId string) string { + project, err := getProject(projectId) + if err != nil { + return "" + } + + builder := metaBuilder().WriteString("projectId", projectId) + + for _, duration := range []int64{30, 90, 180} { + if tier, err := project.getTier(duration); err == nil { + tierBuilder(builder, ufmt.Sprintf("tier%d", duration), tier) + } + } + + return marshal(builder.Node()) +} + +// ApiGetProjectStatisticsByProjectTierId retrieves project statistics by project tier ID. +func ApiGetProjectStatisticsByProjectTierId(tierId string) string { + projectId, duration := parseProjectTierID(tierId) + project, err := getProject(projectId) + if err != nil { + return "" + } + + tier, err := project.getTier(duration) + if err != nil { + return "" + } + + builder := metaBuilder().WriteString("projectId", projectId) + tierBuilder(builder, "tier", tier) + + return marshal(builder.Node()) +} + +// ApiGetProjectActiveOf retrieves project active status by project ID. +func ApiGetProjectActiveOf(projectId string) string { + project, err := getProject(projectId) + if err != nil { + return "" + } + projectActiveResult := project.IsActive(std.ChainHeight()) + builder := (metaBuilder(). + WriteString("projectId", project.id). + WriteString("isActive", strconv.FormatBool(projectActiveResult)). + WriteString("currentHeight", strconv.FormatInt(std.ChainHeight(), 10))). + WriteString("startTime", strconv.FormatInt(project.standardTier().startTime, 10)) + return marshal(builder.Node()) +} diff --git a/contract/r/gnoswap/v1/launchpad/api_reward.gno b/contract/r/gnoswap/v1/launchpad/api_reward.gno new file mode 100644 index 0000000..e2d0b9f --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/api_reward.gno @@ -0,0 +1,26 @@ +package launchpad + +import ( + "std" + + gs "gno.land/r/gnoswap/v1/gov/staker" +) + +// ApiGetProjectRecipientRewardByProjectId retrieves the claimable reward for a project recipient by project ID. +func ApiGetProjectRecipientRewardByProjectId(projectId string) string { + project, exist := projects.Get(projectId) + if !exist { + return "0" + } + + return gs.GetClaimableRewardByAddress(project.(*Project).recipient) +} + +// ApiGetProjectRecipientRewardByAddress retrieves the claimable reward for a recipient by address. +func ApiGetProjectRecipientRewardByAddress(address std.Address) string { + if !address.IsValid() { + return "0" + } + + return gs.GetClaimableRewardByAddress(address) +} diff --git a/contract/r/gnoswap/v1/launchpad/assert.gno b/contract/r/gnoswap/v1/launchpad/assert.gno new file mode 100644 index 0000000..783e9a3 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/assert.gno @@ -0,0 +1,62 @@ +package launchpad + +import ( + "std" + + "gno.land/p/nt/ufmt" +) + +// assertIsDepositOwner asserts that the caller is the owner of the deposit. +// Panics if the caller is not the owner of the deposit. +func assertIsDepositOwner(depositID string, caller std.Address) { + deposit, err := getDeposit(depositID) + if err != nil { + panic(err.Error()) + } + + if !deposit.IsOwner(caller) { + panic(makeErrorWithDetails(errInvalidOwner, ufmt.Sprintf("(%s)", caller.String())).Error()) + } +} + +// assertIsValidAmount panics if the amount is zero. +func assertIsValidAmount(amount int64) { + if amount < minimumDepositAmount { + panic(makeErrorWithDetails( + errInvalidAmount, + ufmt.Sprintf("amount(%d) should greater than minimum deposit amount(%d)", amount, minimumDepositAmount), + )) + } + + if (amount % minimumDepositAmount) != 0 { + panic(makeErrorWithDetails( + errInvalidAmount, + ufmt.Sprintf("amount(%d) must be a multiple of 1_000_000", amount), + )) + } +} + +// assertHasProject asserts that the caller is the owner of at least one project. +// Panics if the caller is not the owner of any project. +func assertHasProject(caller std.Address) { + hasProject := false + + projects.Iterate("", "", func(key string, value interface{}) bool { + project, ok := value.(*Project) + if !ok { + panic(ufmt.Sprintf("failed to cast projects's element to *Project: %T", value)) + } + + hasProject = project.IsOwner(caller) + + // if true, break the loop + return hasProject + }) + + if !hasProject { + panic(makeErrorWithDetails( + errInvalidOwner, + ufmt.Sprintf("caller %s is not the owner of any project", caller.String()), + )) + } +} diff --git a/contract/r/gnoswap/v1/launchpad/consts.gno b/contract/r/gnoswap/v1/launchpad/consts.gno new file mode 100644 index 0000000..5b23243 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/consts.gno @@ -0,0 +1,44 @@ +package launchpad + +import ( + u256 "gno.land/p/gnoswap/uint256" +) + +const ( + projectTier30 = int64(30) + projectTier90 = int64(90) + projectTier180 = int64(180) + + dayTime = int64(24 * 60 * 60) // 86400 + + minimumDepositAmount = int64(1_000_000) + + stringSplitterPad = "*PAD*" + + projectMinimumStartDelayTime = int64(60 * 60) // 1 hour +) + +// contract paths +const ( + GOV_XGNS_PATH string = "gno.land/r/gnoswap/v1/gov/xgns" +) + +var projectTierDurations = []int64{ + projectTier30, + projectTier90, + projectTier180, +} + +var projectTierDurationTimes = map[int64]int64{ + projectTier30: dayTime * projectTier30, // 30 days + projectTier90: dayTime * projectTier90, // 90 days + projectTier180: dayTime * projectTier180, // 180 days +} + +var projectTierRewardCollectableDuration = map[int64]int64{ + projectTier30: dayTime * 3, // 3 days + projectTier90: dayTime * 7, // 7 days + projectTier180: dayTime * 14, // 14 days +} + +var q128 = u256.MustFromDecimal("340282366920938463463374607431768211456") diff --git a/contract/r/gnoswap/v1/launchpad/counter.gno b/contract/r/gnoswap/v1/launchpad/counter.gno new file mode 100644 index 0000000..7fede4a --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/counter.gno @@ -0,0 +1,25 @@ +package launchpad + +// Counter manages unique incrementing IDs. +type Counter struct { + id int64 +} + +// NewCounter creates a new Counter starting at 0. +func NewCounter() *Counter { + return &Counter{ + id: 0, + } +} + +// next increments the counter and returns the next ID. +func (c *Counter) next() int64 { + c.id++ + + return c.id +} + +// Get returns the current ID without incrementing. +func (c *Counter) Get() int64 { + return c.id +} diff --git a/contract/r/gnoswap/v1/launchpad/deposit.gno b/contract/r/gnoswap/v1/launchpad/deposit.gno new file mode 100644 index 0000000..cf23148 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/deposit.gno @@ -0,0 +1,120 @@ +package launchpad + +import ( + "std" +) + +// Deposit represents a deposit made by a user in a launchpad project. +// +// This struct contains the necessary data and methods to manage and distribute +// rewards for a specific deposit. +// +// Fields: +// - depositor (std.Address): The address of the depositor. +// - id (string): The unique identifier for the deposit. +// - projectID (string): The ID of the project associated with the deposit. +// - tier (int64): The tier of the deposit. +// - depositAmount (int64): The amount of the deposit. +// - withdrawnHeight (int64): The height at which the deposit was withdrawn. +// - withdrawnTime (int64): The time when the deposit was withdrawn. +// - createdTime (int64): The time when the deposit was created. +// - endTime (int64): The time when the deposit ends. +type Deposit struct { + depositor std.Address + + id string + projectID string + tier int64 // 30, 60, 180 // instead of tierId + depositAmount int64 + withdrawnHeight int64 + withdrawnTime int64 + createdHeight int64 + createdAt int64 + endTime int64 +} + +func (d *Deposit) ID() string { + return d.id +} + +func (d *Deposit) ProjectID() string { + return d.projectID +} + +func (d *Deposit) ProjectTierID() string { + return makeProjectTierID(d.projectID, d.tier) +} + +func (d *Deposit) Tier() int64 { + return d.tier +} + +func (d *Deposit) Depositor() std.Address { + return d.depositor +} + +func (d *Deposit) DepositAmount() int64 { + return d.depositAmount +} + +func (d *Deposit) CreatedHeight() int64 { + return d.createdHeight +} + +func (d *Deposit) DepositTime() int64 { + return d.createdAt +} + +func (d *Deposit) WithdrawnTime() int64 { + return d.withdrawnTime +} + +func (d *Deposit) IsOwner(address std.Address) bool { + return d.depositor.String() == address.String() +} + +func (d *Deposit) EndTime() int64 { + return d.endTime +} + +func (d *Deposit) IsEnded(currentTime int64) bool { + return d.endTime < currentTime +} + +func (d *Deposit) IsWithdrawn() bool { + return d.withdrawnTime > 0 && d.withdrawnHeight > 0 +} + +func (d *Deposit) withdraw(currentHeight, currentTime int64) int64 { + d.withdrawnTime = currentTime + d.withdrawnHeight = currentHeight + + previousDepositAmount := d.depositAmount + d.depositAmount = 0 + + return previousDepositAmount +} + +// NewDeposit returns a pointer to a new Deposit with the given values. +func NewDeposit( + depositID string, + projectID string, + tier int64, + depositor std.Address, + depositAmount int64, + createdHeight int64, + createdTime int64, + endTime int64, +) *Deposit { + return &Deposit{ + id: depositID, + projectID: projectID, + tier: tier, + depositor: depositor, + depositAmount: depositAmount, + withdrawnHeight: 0, + createdHeight: createdHeight, + createdAt: createdTime, + endTime: endTime, + } +} diff --git a/contract/r/gnoswap/v1/launchpad/errors.gno b/contract/r/gnoswap/v1/launchpad/errors.gno new file mode 100644 index 0000000..eac0992 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/errors.gno @@ -0,0 +1,46 @@ +package launchpad + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errNoLeftReward = errors.New("[GNOSWAP-LAUNCHPAD-001] no left reward") + errInvalidAddress = errors.New("[GNOSWAP-LAUNCHPAD-002] invalid address") + errDataNotFound = errors.New("[GNOSWAP-LAUNCHPAD-003] requested data not found") + errActiveProject = errors.New("[GNOSWAP-LAUNCHPAD-004] project is active") + errInactiveProject = errors.New("[GNOSWAP-LAUNCHPAD-005] project is inactive") + errInactiveTier = errors.New("[GNOSWAP-LAUNCHPAD-006] pool is inactive") + errInvalidInput = errors.New("[GNOSWAP-LAUNCHPAD-007] invalid input data") + errDuplicateProject = errors.New("[GNOSWAP-LAUNCHPAD-008] can not create same project in same block") + errInvalidTier = errors.New("[GNOSWAP-LAUNCHPAD-009] invalid pool") + errInsufficientBalance = errors.New("[GNOSWAP-LAUNCHPAD-010] insufficient balance") + errInvalidLength = errors.New("[GNOSWAP-LAUNCHPAD-011] invalid length") + errNotEnoughBalance = errors.New("[GNOSWAP-LAUNCHPAD-012] not enough balance") + errInvalidCondition = errors.New("[GNOSWAP-LAUNCHPAD-013] invalid transfer condition") + errConvertFail = errors.New("[GNOSWAP-LAUNCHPAD-014] convert fail") + errNotUserCaller = errors.New("[GNOSWAP-LAUNCHPAD-015] only user caller") + errInvalidData = errors.New("[GNOSWAP-LAUNCHPAD-016] invalid data") + errInvalidAmount = errors.New("[GNOSWAP-LAUNCHPAD-017] invalid amount") + errDuplicateDeposit = errors.New("[GNOSWAP-LAUNCHPAD-018] duplicate deposit") + errInvalidRewardState = errors.New("[GNOSWAP-LAUNCHPAD-019] invalid reward state") + errNotExistDeposit = errors.New("[GNOSWAP-LAUNCHPAD-020] not exist deposit") + errAlreadyExistDeposit = errors.New("[GNOSWAP-LAUNCHPAD-021] already exist deposit") + errInvalidProjectId = errors.New("[GNOSWAP-LAUNCHPAD-022] invalid project id") + errAlreadyCollected = errors.New("[GNOSWAP-LAUNCHPAD-023] already collected") + errNotYetClaimReward = errors.New("[GNOSWAP-LAUNCHPAD-024] not yet claim reward") + errInvalidCaller = errors.New("[GNOSWAP-LAUNCHPAD-025] invalid caller") + errInvalidOwner = errors.New("[GNOSWAP-LAUNCHPAD-026] invalid owner") + errInvalidAvgBlockTime = errors.New("[GNOSWAP-LAUNCHPAD-027] invalid average block time") + errInvalidTime = errors.New("[GNOSWAP-LAUNCHPAD-028] invalid time") + errTierHasParticipants = errors.New("[GNOSWAP-LAUNCHPAD-029] tier has participants") + errNotYetEndedProject = errors.New("[GNOSWAP-LAUNCHPAD-030] project lock period is not over yet") + errTransferFailed = errors.New("[GNOSWAP-LAUNCHPAD-031] transfer failed") +) + +// makeErrorWithDetails creates an error with additional context. +func makeErrorWithDetails(err error, details string) error { + return ufmt.Errorf("%s || %s", err.Error(), details) +} diff --git a/contract/r/gnoswap/v1/launchpad/gnomod.toml b/contract/r/gnoswap/v1/launchpad/gnomod.toml new file mode 100644 index 0000000..a5a449f --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/launchpad" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/launchpad/json_builder.gno b/contract/r/gnoswap/v1/launchpad/json_builder.gno new file mode 100644 index 0000000..415d2ed --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/json_builder.gno @@ -0,0 +1,102 @@ +package launchpad + +import ( + "std" + "strings" + "time" + + "gno.land/p/onbloc/json" + "gno.land/p/nt/ufmt" +) + +// projectStatsBuilder adds ProjectStats fields to JSON +func projectStatsBuilder(b *json.NodeBuilder, project *Project) *json.NodeBuilder { + return b. + WriteString("totalDeposit", ufmt.Sprintf("%d", project.TotalDepositAmount())). + WriteString("actualDeposit", ufmt.Sprintf("%d", project.CurrentDepositAmount())). + WriteString("totalParticipant", ufmt.Sprintf("%d", project.TotalDepositCount())). + WriteString("actualParticipant", ufmt.Sprintf("%d", project.CurrentDepositCount())). + WriteString("totalCollected", ufmt.Sprintf("%d", project.TotalCollectedAmount())) +} + +// refundInfoBuilder adds RefundInfo fields to JSON +func refundInfoBuilder(b *json.NodeBuilder, project *Project) *json.NodeBuilder { + return b. + WriteString("refundedAmount", ufmt.Sprintf("%d", project.RemainingAmount())) +} + +// TierBuilder adds Tier fields to JSON +func tierBuilder(b *json.NodeBuilder, prefix string, tier *ProjectTier) *json.NodeBuilder { + // Add tiers info + b.WriteString(prefix+"Id", tier.id) + b.WriteString(prefix+"TierAmount", ufmt.Sprintf("%d", tier.TotalDistributeAmount())) + b.WriteString(prefix+"TierAmountPerSecondX128", tier.DistributeAmountPerSecondX128().ToString()) + b.WriteString(prefix+"Started", ufmt.Sprintf("%d", tier.StartTime())) + b.WriteString(prefix+"Ended", ufmt.Sprintf("%d", tier.EndTime())) + b.WriteString(prefix+"TotalDepositAmount", ufmt.Sprintf("%d", tier.TotalDepositAmount())) + b.WriteString(prefix+"ActualDepositAmount", ufmt.Sprintf("%d", tier.CurrentDepositAmount())) + b.WriteString(prefix+"TotalParticipant", ufmt.Sprintf("%d", tier.TotalDepositCount())) + b.WriteString(prefix+"ActualParticipant", ufmt.Sprintf("%d", tier.CurrentDepositCount())) + b.WriteString(prefix+"UserCollectedAmount", ufmt.Sprintf("%d", tier.TotalCollectedAmount())) + return b +} + +// ProjectBuilder adds Project fields to JSON +func projectBuilder(b *json.NodeBuilder, project *Project) *json.NodeBuilder { + b.WriteString("name", project.name) + b.WriteString("tokenPath", project.tokenPath) + b.WriteString("depositAmount", ufmt.Sprintf("%d", project.depositAmount)) + b.WriteString("recipient", project.recipient.String()) + + tokenPaths := []string{} + amounts := []string{} + + for _, condition := range project.getConditions() { + tokenPaths = append(tokenPaths, condition.TokenPath()) + amounts = append(amounts, ufmt.Sprintf("%d", condition.MinimumAmount())) + } + b.WriteString("conditionsToken", strings.Join(tokenPaths, ",")) + b.WriteString("conditionsAmount", strings.Join(amounts, ",")) + + // Add time info + b.WriteString("createdTime", ufmt.Sprintf("%d", project.CreatedAt())) + b.WriteString("startedTime", ufmt.Sprintf("%d", project.standardTier().StartTime())) + b.WriteString("endedTime", ufmt.Sprintf("%d", project.standardTier().EndTime())) + + // Add refund info + refundInfoBuilder(b, project) + + return b +} + +// DepositBuilder adds Deposit fields to JSON +func depositBuilder(b *json.NodeBuilder, deposit *Deposit) *json.NodeBuilder { + return b. + WriteString("depositId", deposit.id). + WriteString("projectId", deposit.ProjectID()). + WriteString("tier", ufmt.Sprintf("%d", deposit.Tier())). + WriteString("depositor", deposit.Depositor().String()). + WriteString("amount", ufmt.Sprintf("%d", deposit.DepositAmount())). + WriteString("depositHeight", ufmt.Sprintf("%d", deposit.CreatedHeight())). + WriteString("depositTime", ufmt.Sprintf("%d", deposit.DepositTime())) +} + +// MetaBuilder adds metadata fields to JSON +func metaBuilder() *json.NodeBuilder { + height := std.ChainHeight() + now := time.Now().Unix() + + return json.Builder(). + WriteString("height", ufmt.Sprintf("%d", height)). + WriteString("now", ufmt.Sprintf("%d", now)) +} + +// Marshals a JSON node to a string, panics if marshalling fails +func marshal(data *json.Node) string { + b, err := json.Marshal(data) + if err != nil { + panic(err.Error()) + } + + return string(b) +} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_deposit.gno b/contract/r/gnoswap/v1/launchpad/launchpad_deposit.gno new file mode 100644 index 0000000..e84b377 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/launchpad_deposit.gno @@ -0,0 +1,188 @@ +package launchpad + +import ( + "std" + "time" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/gns" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/referral" + "gno.land/r/gnoswap/v1/common" + gov_staker "gno.land/r/gnoswap/v1/gov/staker" + "gno.land/r/gnoswap/v1/gov/xgns" +) + +// DepositGns deposits GNS tokens to a launchpad project tier. +// +// Parameters: +// - targetProjectTierID: format "{projectId}:{tierType}" +// - depositAmount: amount of GNS to deposit +// - referrer: referral address (optional) +// +// Returns deposit ID. +func DepositGns(cur realm, targetProjectTierID string, depositAmount int64, referrer string) string { + halt.AssertIsNotHaltedLaunchpad() + + previousRealm := std.PreviousRealm() + access.AssertIsUser(previousRealm) + + assertIsValidAmount(depositAmount) + + projectID, tierDuration := parseProjectTierID(targetProjectTierID) + caller := previousRealm.Address() + + deposit, rewardState, isFirstDeposit, distributeAmountPerSecondX128, err := depositGns( + projectID, + tierDuration, + depositAmount, + caller, + ) + if err != nil { + panic(err.Error()) + } + + actualReferrer, success := registerReferral(referrer, caller) + if !success { + actualReferrer = referral.GetReferral(std.PreviousRealm().Address().String()) + } + + if isFirstDeposit { + std.Emit( + "FirstDepositForProjectTier", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "targetProjectTierId", targetProjectTierID, + "amount", formatInt(depositAmount), + "depositId", deposit.ID(), + "claimableTime", formatInt(rewardState.ClaimableTime()), + "tierAmountPerSecondX128", distributeAmountPerSecondX128, + ) + } + + std.Emit( + "DepositGns", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "targetProjectTierId", targetProjectTierID, + "amount", formatInt(depositAmount), + "depositId", deposit.ID(), + "claimableTime", formatInt(rewardState.ClaimableTime()), + "referrer", actualReferrer, + ) + + launchpadAddress := std.CurrentRealm().Address() + + // stake governance token to the project + err = stakeGovernance(projectID, depositAmount, launchpadAddress, caller) + if err != nil { + panic(err.Error()) + } + + return deposit.ID() +} + +// depositGns deposits GNS to a project tier. +func depositGns( + projectID string, + tierDuration int64, + depositAmount int64, + callerAddress std.Address, +) (*Deposit, *RewardState, bool, string, error) { + project, err := getProject(projectID) + if err != nil { + return nil, nil, false, "", err + } + + balanceOfFn := func(tokenPath string, caller std.Address) int64 { + if tokenPath == GOV_XGNS_PATH { + return xgns.BalanceOf(caller) + } + + return common.BalanceOf(tokenPath, caller) + } + + err = project.CheckConditions(callerAddress, balanceOfFn) + if err != nil { + return nil, nil, false, "", err + } + + projectTier, err := project.getTier(tierDuration) + if err != nil { + return nil, nil, false, "", err + } + + currentTime := time.Now().Unix() + currentHeight := std.ChainHeight() + + if !projectTier.isActivated(currentTime) { + return nil, nil, false, "", makeErrorWithDetails(errInactiveProject, projectID) + } + + depositID := nextDepositID() + deposit := NewDeposit( + depositID, + projectID, + tierDuration, + callerAddress, + depositAmount, + currentHeight, + currentTime, + projectTier.endTime, + ) + deposits.Set(depositID, deposit) + + projectTier.deposit(deposit) + + rewardManager, err := getProjectTierRewardManager(projectTier.ID()) + if err != nil { + return nil, nil, false, "", err + } + + isFirstDeposit := !rewardManager.IsInitialized() + + rewardState := rewardManager.addRewardStateByDeposit(deposit) + + err = rewardManager.updateRewardPerDepositX128(projectTier.CurrentDepositAmount(), currentHeight, currentTime) + if err != nil { + return nil, nil, false, "", err + } + + return deposit, + rewardState, + isFirstDeposit, + rewardManager.DistributeAmountPerSecondX128().ToString(), + nil +} + +// registerReferral registers a referral for a caller. +func registerReferral(referrer string, callerAddress std.Address) (string, bool) { + success := referral.TryRegister(cross, callerAddress, referrer) + actualReferrer := referrer + if !success { + actualReferrer = referral.GetReferral(callerAddress.String()) + } + + return actualReferrer, success +} + +// stakeGovernance stakes governance token to the project. +func stakeGovernance(projectID string, depositAmount int64, launchpadAddress std.Address, callerAddress std.Address) error { + project, err := getProject(projectID) + if err != nil { + return err + } + + gov_staker.SetAmountByProjectWallet(cross, project.Recipient(), depositAmount, true) + + gns.TransferFrom( + cross, + callerAddress, + launchpadAddress, + depositAmount, + ) + + xgns.MintByLaunchPad(cross, launchpadAddress, depositAmount) + + return nil +} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_project.gno b/contract/r/gnoswap/v1/launchpad/launchpad_project.gno new file mode 100644 index 0000000..9631b8a --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/launchpad_project.gno @@ -0,0 +1,470 @@ +package launchpad + +import ( + "errors" + "std" + "strconv" + "strings" + "time" + + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" +) + +// CreateProject creates a new launchpad project with tiered allocations. +// +// Parameters: +// - name: project name +// - tokenPath: reward token contract path +// - recipient: project recipient address +// - depositAmount: amount of tokens to deposit +// - conditionTokens: comma-separated token paths for conditions +// - conditionAmounts: comma-separated minimum amounts for conditions +// - tier30Ratio: allocation ratio for 30-day tier +// - tier90Ratio: allocation ratio for 90-day tier +// - tier180Ratio: allocation ratio for 180-day tier +// - startTime: unix timestamp for project start +// +// Returns project ID. +// Only callable by admin or governance. +func CreateProject( + cur realm, + name string, + tokenPath string, + recipient std.Address, + depositAmount int64, + conditionTokens string, + conditionAmounts string, + tier30Ratio int64, + tier90Ratio int64, + tier180Ratio int64, + startTime int64, +) string { + halt.AssertIsNotHaltedLaunchpad() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + launchpadAddr := std.CurrentRealm().Address() + currentHeight := std.ChainHeight() + currentTime := time.Now().Unix() + + params := &createProjectParams{ + name: name, + tokenPath: tokenPath, + recipient: recipient, + depositAmount: depositAmount, + conditionTokens: conditionTokens, + conditionAmounts: conditionAmounts, + tier30Ratio: tier30Ratio, + tier90Ratio: tier90Ratio, + tier180Ratio: tier180Ratio, + startTime: startTime, + currentTime: currentTime, + currentHeight: currentHeight, + minimumStartDelayTime: projectMinimumStartDelayTime, + } + + project, err := createProject(params) + if err != nil { + panic(err) + } + + tokenBalance := common.BalanceOf(tokenPath, caller) + if tokenBalance < depositAmount { + panic( + makeErrorWithDetails( + errInsufficientBalance, + ufmt.Sprintf("caller(%s) balance(%d) < depositAmount(%d)", launchpadAddr.String(), tokenBalance, depositAmount), + ), + ) + } + + err = common.TransferFrom( + cross, + tokenPath, + caller, + launchpadAddr, + depositAmount, + ) + if err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "CreateProject", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "name", name, + "tokenPath", tokenPath, + "recipient", recipient.String(), + "depositAmount", formatInt(depositAmount), + "conditionsToken", params.conditionTokens, + "conditionsAmount", params.conditionAmounts, + "tier30Ratio", formatInt(params.tier30Ratio), + "tier90Ratio", formatInt(params.tier90Ratio), + "tier180Ratio", formatInt(params.tier180Ratio), + "startTime", formatInt(params.startTime), + "projectId", project.ID(), + "tier30Amount", formatInt(project.tiers[projectTier30].TotalDistributeAmount()), + "tier30EndTime", formatInt(project.tiers[projectTier30].EndTime()), + "tier90Amount", formatInt(project.tiers[projectTier90].TotalDistributeAmount()), + "tier90EndTime", formatInt(project.tiers[projectTier90].EndTime()), + "tier180Amount", formatInt(project.tiers[projectTier180].TotalDistributeAmount()), + "tier180EndTime", formatInt(project.tiers[projectTier180].EndTime()), + ) + + return project.ID() +} + +// createProject creates a new project with the given parameters. +// This function validates the input parameters, creates the project structure, +// and sets up the project tiers and reward managers. +// Returns the created project and any error. +func createProject(params *createProjectParams) (*Project, error) { + if err := params.validate(); err != nil { + return nil, err + } + + // create project + project := NewProject( + params.name, + params.tokenPath, + params.depositAmount, + params.recipient, + params.currentHeight, + params.currentTime, + ) + + // check duplicate project + if projects.Has(project.ID()) { + return nil, makeErrorWithDetails( + errDuplicateProject, + ufmt.Sprintf("project(%s) already exists", project.ID()), + ) + } + + projectConditions, err := NewProjectConditionsWithError(params.conditionTokens, params.conditionAmounts) + if err != nil { + return nil, err + } + + for _, condition := range projectConditions { + project.addProjectCondition(condition.tokenPath, condition) + } + + projectTierRatios := map[int64]int64{ + projectTier30: params.tier30Ratio, + projectTier90: params.tier90Ratio, + projectTier180: params.tier180Ratio, + } + + accumulatedTierDistributeAmount := int64(0) + + for _, duration := range projectTierDurations { + rewardCollectableDuration := projectTierRewardCollectableDuration[duration] + tierDurationTime := projectTierDurationTimes[duration] + tierDistributeAmount := params.depositAmount * projectTierRatios[duration] / 100 + accumulatedTierDistributeAmount += tierDistributeAmount + + // if the last tier, distribute the remaining amount + if duration == projectTier180 { + remainTierDistributeAmount := params.depositAmount - accumulatedTierDistributeAmount + tierDistributeAmount += remainTierDistributeAmount + } + + projectTier := NewProjectTier( + project.ID(), + rewardCollectableDuration, + tierDistributeAmount, + params.startTime, + params.startTime+tierDurationTime, + ) + project.addProjectTier(duration, projectTier) + + projectTierRewardManagers.Set(projectTier.ID(), NewRewardManager( + projectTier.TotalDistributeAmount(), + projectTier.StartTime(), + projectTier.EndTime(), + params.startTime, + params.startTime+tierDurationTime, + )) + } + + projects.Set(project.ID(), project) + + return project, nil +} + +// TransferLeftFromProjectByAdmin transfers the remaining rewards of a project to a specified recipient. +// Only admin or governance can call this function. Returns the amount of rewards transferred. +func TransferLeftFromProjectByAdmin(cur realm, projectID string, recipient std.Address) int64 { + halt.AssertIsNotHaltedLaunchpad() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + currentHeight := std.ChainHeight() + currentTime := time.Now().Unix() + + projectLeftReward, err := transferLeftFromProject(projectID, recipient, currentTime) + if err != nil { + panic(err) + } + + project, err := getProject(projectID) + if err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "TransferLeftFromProjectByAdmin", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "projectId", projectID, + "recipient", recipient.String(), + "tokenPath", project.tokenPath, + "leftReward", formatInt(projectLeftReward), + "tier30Full", formatInt(project.tiers[projectTier30].TotalDepositAmount()), + "tier30Left", formatInt(project.tiers[projectTier30].calculateLeftReward()), + "tier90Full", formatInt(project.tiers[projectTier90].TotalDepositAmount()), + "tier90Left", formatInt(project.tiers[projectTier90].calculateLeftReward()), + "tier180Full", formatInt(project.tiers[projectTier180].TotalDepositAmount()), + "tier180Left", formatInt(project.tiers[projectTier180].calculateLeftReward()), + "currentHeight", formatInt(currentHeight), + "currentTime", formatInt(currentTime), + ) + + return projectLeftReward +} + +// transferLeftFromProject transfers the remaining rewards of a project to a specified recipient. +// This function is called by an admin to transfer any unclaimed rewards from a project to a recipient address. +// It validates the project ID, checks the recipient conditions, calculates the remaining rewards, and performs the transfer. +// Returns the amount of rewards transferred to the recipient and any error. +func transferLeftFromProject(projectID string, recipient std.Address, currentTime int64) (int64, error) { + project, err := getProject(projectID) + if err != nil { + return 0, err + } + + if err := validateRefundProject(project, recipient, currentTime); err != nil { + return 0, err + } + + emission.MintAndDistributeGns(cross) + + accumTotalDistributeAmount := int64(0) + accumLeftReward := int64(0) + accumCollectedReward := int64(0) + + tierMap := project.getTiers() + for _, tier := range tierMap { + if !tier.isEnded(currentTime) { + return 0, errors.New(ufmt.Sprintf("tier(%d) is not ended", tier.ID())) + } + + if tier.CurrentDepositCount() > 0 { + return 0, errors.New(ufmt.Sprintf("tier(%d) has (%d) participants", tier.ID(), tier.CurrentDepositCount())) + } + + leftReward := tier.calculateLeftReward() + accumLeftReward += leftReward + accumCollectedReward += tier.TotalCollectedAmount() + accumTotalDistributeAmount += tier.TotalDistributeAmount() + } + + if accumTotalDistributeAmount != accumCollectedReward+accumLeftReward { + return 0, errors.New(ufmt.Sprintf("accumTotalDistributeAmount(%d) != accumCollectedReward(%d)+accumLeftReward(%d)", accumTotalDistributeAmount, accumCollectedReward, accumLeftReward)) + } + + projectLeftReward := project.RemainingAmount() + + if projectLeftReward > 0 { + if err := common.Transfer(cross, project.tokenPath, recipient, int64(projectLeftReward)); err != nil { + return 0, makeErrorWithDetails(errTransferFailed, ufmt.Sprintf("token(%s), amount(%d)", project.tokenPath, projectLeftReward)) + } + } + + return projectLeftReward, nil +} + +// validateTransferLeft validates the transfer of remaining tokens +func validateRefundProject(project *Project, recipient std.Address, currentTime int64) error { + if !recipient.IsValid() { + return errors.New(ufmt.Sprintf("invalid recipient address(%s)", recipient.String())) + } + + return project.validateRefundRemainingAmount(currentTime) +} + +type createProjectParams struct { + name string + tokenPath string + recipient std.Address + depositAmount int64 + conditionTokens string + conditionAmounts string + tier30Ratio int64 + tier90Ratio int64 + tier180Ratio int64 + startTime int64 + currentTime int64 + currentHeight int64 + minimumStartDelayTime int64 +} + +func (p *createProjectParams) validate() error { + if err := p.validateName(); err != nil { + return err + } + + if err := p.validateTokenPath(); err != nil { + return err + } + + if err := p.validateRecipient(); err != nil { + return err + } + + if err := p.validateDepositAmount(); err != nil { + return err + } + + if err := p.validateRatio(); err != nil { + return err + } + + if err := p.validateStartTime(p.currentTime, p.minimumStartDelayTime); err != nil { + return err + } + + if err := p.validateConditions(); err != nil { + return err + } + + return nil +} + +// validateName checks if the project name is valid. +func (p *createProjectParams) validateName() error { + if p.name == "" { + return makeErrorWithDetails(errInvalidInput, "project name cannot be empty") + } + + if len(p.name) > 100 { + return makeErrorWithDetails(errInvalidInput, "project name is too long") + } + + return nil +} + +// validateTokenPath validates the token path is not empty and is registered. +func (p *createProjectParams) validateTokenPath() error { + if p.tokenPath == "" { + return makeErrorWithDetails(errInvalidInput, "tokenPath cannot be empty") + } + + if err := common.IsRegistered(p.tokenPath); err != nil && !isGovernanceToken(p.tokenPath) { + return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) not registered", p.tokenPath)) + } + + return nil +} + +// validateRecipient checks if the recipient address is valid. +func (p *createProjectParams) validateRecipient() error { + if !p.recipient.IsValid() { + return makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("recipient address(%s)", p.recipient.String())) + } + + return nil +} + +// validateDepositAmount ensures that the deposit amount is greater than zero. +func (p *createProjectParams) validateDepositAmount() error { + if p.depositAmount == 0 { + return makeErrorWithDetails(errInvalidInput, "deposit amount cannot be 0") + } + + if p.depositAmount < 0 { + return makeErrorWithDetails(errInvalidInput, "deposit amount cannot be negative") + } + + return nil +} + +// validateRatio checks if the sum of the tier ratios equals 100. +func (p *createProjectParams) validateRatio() error { + sum := p.tier30Ratio + p.tier90Ratio + p.tier180Ratio + if sum != 100 { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("invalid ratio, sum of all tiers(30:%d, 90:%d, 180:%d) should be 100", p.tier30Ratio, p.tier90Ratio, p.tier180Ratio), + ) + } + + return nil +} + +// validateStartTime checks if the start time is available with minimum delay requirement. +func (p *createProjectParams) validateStartTime(now int64, minimumStartDelayTime int64) error { + availableStartTime := now + minimumStartDelayTime + + if p.startTime < availableStartTime { + return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("start time(%d) must be greater than now(%d)", p.startTime, availableStartTime)) + } + + return nil +} + +func (p *createProjectParams) validateConditions() error { + if p.conditionTokens == "" && p.conditionAmounts == "" { + return nil + } + + tokenPaths := strings.Split(p.conditionTokens, stringSplitterPad) + minimumAmounts := strings.Split(p.conditionAmounts, stringSplitterPad) + + if len(tokenPaths) != len(minimumAmounts) { + return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("conditionTokens and conditionAmounts are not matched")) + } + + tokenPathMap := make(map[string]bool) + + for _, tokenPath := range tokenPaths { + err := common.IsRegistered(tokenPath) + if err != nil && !isGovernanceToken(tokenPath) { + return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) not registered", tokenPath)) + } + + if tokenPathMap[tokenPath] { + return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) is duplicated", tokenPath)) + } + + tokenPathMap[tokenPath] = true + } + + for _, amountStr := range minimumAmounts { + minimumAmount, err := strconv.ParseInt(amountStr, 10, 64) + if err != nil { + return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("invalid condition amount(%s)", amountStr)) + } + + if minimumAmount <= 0 { + return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("condition amount(%s) is not available", amountStr)) + } + } + + return nil +} + +func isGovernanceToken(tokenPath string) bool { + return tokenPath == GOV_XGNS_PATH +} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_protocol_fee.gno b/contract/r/gnoswap/v1/launchpad/launchpad_protocol_fee.gno new file mode 100644 index 0000000..c394173 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/launchpad_protocol_fee.gno @@ -0,0 +1,24 @@ +package launchpad + +import ( + "std" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + gov_staker "gno.land/r/gnoswap/v1/gov/staker" +) + +// CollectProtocolFee collects protocol fee from gov/staker for project recipient wallets. +// Only users can call this function. +func CollectProtocolFee(cur realm) { + halt.AssertIsNotHaltedLaunchpad() + halt.AssertIsNotHaltedWithdraw() + + previousRealm := std.PreviousRealm() + access.AssertIsUser(previousRealm) + + caller := previousRealm.Address() + assertHasProject(caller) + + gov_staker.CollectRewardFromLaunchPad(cross, caller) +} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_reward.gno b/contract/r/gnoswap/v1/launchpad/launchpad_reward.gno new file mode 100644 index 0000000..b342e1a --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/launchpad_reward.gno @@ -0,0 +1,77 @@ +package launchpad + +import ( + "std" + "time" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" +) + +// CollectRewardByDepositId collects reward from a specific deposit. +// +// Parameters: +// - depositID: ID of the deposit to collect from +// +// Returns amount of reward collected. +// Only callable by deposit owner. +func CollectRewardByDepositId(cur realm, depositID string) int64 { + halt.AssertIsNotHaltedLaunchpad() + halt.AssertIsNotHaltedWithdraw() + + previousRealm := std.PreviousRealm() + access.AssertIsUser(previousRealm) + + caller := previousRealm.Address() + assertIsDepositOwner(depositID, caller) + + deposit := mustGetDeposit(depositID) + currentHeight := std.ChainHeight() + currentTime := time.Now().Unix() + rewardAmount, err := collectDepositReward(deposit, currentHeight, currentTime) + if err != nil { + panic(err) + } + + std.Emit( + "CollectRewardByDepositId", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "depositId", depositID, + "amount", formatInt(rewardAmount), + ) + + return rewardAmount +} + +// collectDepositReward calculates and collects the reward for a deposit. +func collectDepositReward(deposit *Deposit, currentHeight, currentTime int64) (int64, error) { + if currentTime <= 0 { + return 0, makeErrorWithDetails(errInvalidTime, "currentTime must be positive") + } + + // Get project tier and reward manager + projectTier, err := getProjectTier(deposit.ProjectID(), deposit.Tier()) + if err != nil { + return 0, err + } + + rewardManager, err := getProjectTierRewardManager(projectTier.ID()) + if err != nil { + return 0, err + } + + // Update reward state before collection + err = rewardManager.updateRewardPerDepositX128(projectTier.CurrentDepositAmount(), currentHeight, currentTime) + if err != nil { + return 0, err + } + + // Collect reward + rewardAmount, err := rewardManager.collectReward(deposit.ID(), currentTime) + if err != nil { + return 0, err + } + + return rewardAmount, nil +} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_withdraw.gno b/contract/r/gnoswap/v1/launchpad/launchpad_withdraw.gno new file mode 100644 index 0000000..9c9bccc --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/launchpad_withdraw.gno @@ -0,0 +1,116 @@ +package launchpad + +import ( + "std" + "time" + + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" + gov_staker "gno.land/r/gnoswap/v1/gov/staker" +) + +// CollectDepositGns collects rewards from a deposit. +// +// Parameters: +// - depositID: ID of the deposit to collect from +// +// Returns amount collected and any error. +func CollectDepositGns(cur realm, depositID string) (int64, error) { + halt.AssertIsNotHaltedLaunchpad() + halt.AssertIsNotHaltedWithdraw() + + previousRealm := std.PreviousRealm() + access.AssertIsUser(previousRealm) + + caller := previousRealm.Address() + assertIsDepositOwner(depositID, caller) + + emission.MintAndDistributeGns(cross) + + deposit := mustGetDeposit(depositID) + + currentTime := time.Now().Unix() + recipient, withdrawalAmount, err := withdrawDeposit(deposit, std.ChainHeight(), currentTime) + if err != nil { + panic(err.Error()) + } + + unStakeGovernance(recipient, withdrawalAmount) + + std.Emit( + "CollectDepositGns", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "depositId", depositID, + "amount", formatInt(withdrawalAmount), + ) + + return withdrawalAmount, nil +} + +// withdrawDeposit withdraws a deposit and updates the reward manager. +func withdrawDeposit(deposit *Deposit, currentHeight, currentTime int64) (std.Address, int64, error) { + // Input validation + if deposit == nil { + return "", 0, makeErrorWithDetails(errNotExistDeposit, "deposit is nil") + } + + if currentTime <= 0 { + return "", 0, makeErrorWithDetails(errInvalidTime, "currentTime must be positive") + } + + // State validation + if deposit.IsWithdrawn() { + return "", 0, makeErrorWithDetails(errAlreadyCollected, ufmt.Sprintf("(%s)", deposit.ID())) + } + + if !deposit.IsEnded(currentTime) { + return "", 0, makeErrorWithDetails(errNotYetEndedProject, ufmt.Sprintf("(%s)", deposit.ID())) + } + + // Get project and tier information + project, err := getProject(deposit.ProjectID()) + if err != nil { + return "", 0, err + } + + projectTier, err := project.getTier(deposit.Tier()) + if err != nil { + return "", 0, err + } + + // Get reward manager and update rewards before withdrawal + rewardManager, err := getProjectTierRewardManager(projectTier.ID()) + if err != nil { + return "", 0, err + } + + // Update rewards with current deposit amount + err = rewardManager.updateRewardPerDepositX128(projectTier.CurrentDepositAmount(), currentHeight, currentTime) + if err != nil { + return "", 0, err + } + + // Process withdrawal from project tier + projectTier.withdraw(deposit) + + // Update rewards with new deposit amount after withdrawal + err = rewardManager.updateRewardPerDepositX128(projectTier.CurrentDepositAmount(), currentHeight, currentTime) + if err != nil { + return "", 0, err + } + + // Finalize withdrawal + withdrawalAmount := deposit.withdraw(currentHeight, currentTime) + deposits.Set(deposit.ID(), deposit) + + return project.Recipient(), withdrawalAmount, nil +} + +// unStakeGovernance removes the staked amount from governance system +func unStakeGovernance(recipient std.Address, withdrawalAmount int64) { + gov_staker.SetAmountByProjectWallet(cross, recipient, withdrawalAmount, false) +} diff --git a/contract/r/gnoswap/v1/launchpad/project.gno b/contract/r/gnoswap/v1/launchpad/project.gno new file mode 100644 index 0000000..892b947 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/project.gno @@ -0,0 +1,256 @@ +package launchpad + +import ( + "errors" + "std" + + "gno.land/p/nt/ufmt" +) + +// Project represents a launchpad project. +// +// This struct contains the necessary data and methods to manage and distribute +// rewards for a specific project. +// +// Fields: +// - id (string): The unique identifier for the project, formatted as "{tokenPath}:{createdHeight}". +// - name (string): The name of the project. +// - tokenPath (string): The path of the token associated with the project. +// - depositAmount (int64): The total amount of tokens deposited for the project. +// - recipient (std.Address): The address to receive the project's rewards. +// - conditions (map[string]*ProjectCondition): A map of token paths to their associated conditions. +// - tiers (map[int64]*ProjectTier): A map of tier durations to their associated tiers. +// - tiersRatios (map[int64]int64): A map of tier durations to their associated ratios. +// - createdBlockTimeInfo (BlockTimeInfo): The block time and height information for the creation of the project. +type Project struct { + id string // 'tokenPath:createdHeight' + name string + tokenPath string + depositAmount int64 + recipient std.Address // string + conditions map[string]*ProjectCondition // tokenPath -> Condition + tiers map[int64]*ProjectTier + tiersRatios map[int64]int64 + createdHeight int64 + createdAt int64 +} + +func (p *Project) ID() string { + return p.id +} + +func (p *Project) Name() string { + return p.name +} + +func (p *Project) TokenPath() string { + return p.tokenPath +} + +func (p *Project) DepositAmount() int64 { + return p.depositAmount +} + +func (p *Project) Recipient() std.Address { + return p.recipient +} + +func (p *Project) TiersRatios() map[int64]int64 { + return p.tiersRatios +} + +func (p *Project) CreatedAt() int64 { + return p.createdAt +} + +func (p *Project) CreatedHeight() int64 { + return p.createdHeight +} + +func (p *Project) StartTime() int64 { + return p.standardTier().StartTime() +} + +func (p *Project) EndTime() int64 { + return p.standardTier().EndTime() +} + +func (p *Project) IsActive(currentTime int64) bool { + return p.standardTier().isActivated(currentTime) +} + +func (p *Project) IsEnded(currentTime int64) bool { + return p.standardTier().isEnded(currentTime) +} + +func (p *Project) IsOwner(caller std.Address) bool { + return p.recipient == caller +} + +func (p *Project) RemainingAmount() int64 { + remainingAmount := int64(0) + + for _, tier := range p.getTiers() { + remainingAmount += tier.calculateLeftReward() + } + + return remainingAmount +} + +func (p *Project) CheckConditions(caller std.Address, balanceOfFunc func(tokenPath string, caller std.Address) int64) error { + conditions := p.getConditions() + if conditions == nil { + return makeErrorWithDetails(errInvalidData, "conditions is nil") + } + + for _, condition := range conditions { + // xGNS(or GNS) may have a zero condition + if !condition.IsAvailable() { + continue + } + + tokenPath := condition.TokenPath() + balance := balanceOfFunc(tokenPath, caller) + + if err := condition.CheckBalanceCondition(tokenPath, balance); err != nil { + return err + } + } + + return nil +} + +func (p *Project) TotalDepositCount() int64 { + totalRecipient := int64(0) + + for _, tier := range p.getTiers() { + totalRecipient += tier.totalDepositCount + } + + return totalRecipient +} + +func (p *Project) TotalDepositAmount() int64 { + totalDepositAmount := int64(0) + + for _, tier := range p.getTiers() { + totalDepositAmount += tier.TotalDepositAmount() + } + + return totalDepositAmount +} + +func (p *Project) CurrentDepositCount() int64 { + totalDepositCount := int64(0) + + for _, tier := range p.getTiers() { + totalDepositCount += tier.CurrentDepositCount() + } + + return totalDepositCount +} + +func (p *Project) CurrentDepositAmount() int64 { + totalDepositAmount := int64(0) + + for _, tier := range p.getTiers() { + totalDepositAmount += tier.CurrentDepositAmount() + } + + return totalDepositAmount +} + +func (p *Project) TotalCollectedAmount() int64 { + totalCollectedAmount := int64(0) + + for _, tier := range p.getTiers() { + totalCollectedAmount += tier.TotalCollectedAmount() + } + + return totalCollectedAmount +} + +func (p *Project) getConditions() map[string]*ProjectCondition { + return p.conditions +} + +func (p *Project) getTiers() map[int64]*ProjectTier { + return p.tiers +} + +func (p *Project) getTier(duration int64) (*ProjectTier, error) { + tier, exists := p.tiers[duration] + if !exists { + return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("tier(%s) not found", duration)) + } + + return tier, nil +} + +func (p *Project) standardTier() *ProjectTier { + projectTier, exists := p.tiers[projectTier180] + if !exists { + return nil + } + + return projectTier +} + +func (p *Project) validateRefundRemainingAmount(currentTime int64) error { + if !p.IsEnded(currentTime) { + return errors.New( + ufmt.Sprintf("project not ended yet(current:%d, endTime: %d)", currentTime, p.EndTime()), + ) + } + + if p.RemainingAmount() == 0 { + return errors.New( + ufmt.Sprintf("project has no remaining amount"), + ) + } + + return nil +} + +func (p *Project) addProjectTier(tierDuration int64, projectTier *ProjectTier) { + p.tiers[tierDuration] = projectTier +} + +func (p *Project) addProjectCondition(tokenPath string, condition *ProjectCondition) { + p.conditions[tokenPath] = condition +} + +func NewProject( + name string, + tokenPath string, + depositAmount int64, + recipient std.Address, + createdHeight int64, + createdAt int64, +) *Project { + return &Project{ + id: makeProjectID(tokenPath, createdHeight), + name: name, + tokenPath: tokenPath, + depositAmount: depositAmount, + recipient: recipient, + conditions: make(map[string]*ProjectCondition), + tiers: make(map[int64]*ProjectTier), + createdHeight: createdHeight, + createdAt: createdAt, + } +} + +// makeProjectID generates a unique project ID based on the given token path and the current block height. +// +// The generated ID combines the `tokenPath` and the current block height in the following format: +// "{tokenPath}:{height}" +// +// Parameters: +// - tokenPath (string): The path of the token associated with the project. +// +// Returns: +// - string: A unique project ID in the format "tokenPath:height". +func makeProjectID(tokenPath string, createdHeight int64) string { + return ufmt.Sprintf("%s:%d", tokenPath, createdHeight) +} diff --git a/contract/r/gnoswap/v1/launchpad/project_condition.gno b/contract/r/gnoswap/v1/launchpad/project_condition.gno new file mode 100644 index 0000000..5407813 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/project_condition.gno @@ -0,0 +1,98 @@ +package launchpad + +import ( + "strconv" + "strings" + + "gno.land/p/nt/ufmt" +) + +// ProjectCondition represents a condition for a project. +// +// This struct contains the necessary data and methods to manage and distribute +// rewards for a specific project. +// +// Fields: +// - tokenPath (string): The path of the token associated with the project. +// - minimumAmount (int64): The minimum amount of the token required for the project. +type ProjectCondition struct { + tokenPath string + minimumAmount int64 +} + +func (p *ProjectCondition) TokenPath() string { + return p.tokenPath +} + +func (p *ProjectCondition) MinimumAmount() int64 { + return p.minimumAmount +} + +func (p *ProjectCondition) IsAvailable() bool { + return p.tokenPath != "" && p.minimumAmount > 0 +} + +func (p *ProjectCondition) CheckBalanceCondition(inputTokenPath string, inputAmount int64) error { + if p.tokenPath != inputTokenPath { + return makeErrorWithDetails( + errInvalidData, + ufmt.Sprintf("token path(%s) is not matched", inputTokenPath), + ) + } + + if inputAmount < p.minimumAmount { + return makeErrorWithDetails( + errInvalidData, + ufmt.Sprintf("input amount(%d) is less than minimum amount(%d)", inputAmount, p.minimumAmount), + ) + } + + return nil +} + +func NewProjectCondition(tokenPath string, minimumAmount int64) *ProjectCondition { + return &ProjectCondition{ + tokenPath: tokenPath, + minimumAmount: minimumAmount, + } +} + +func NewProjectConditionsWithError(conditionTokens string, conditionAmounts string) ([]*ProjectCondition, error) { + if conditionTokens == "" && conditionAmounts == "" { + return []*ProjectCondition{}, nil + } + + conditions := []*ProjectCondition{} + + tokenPaths := strings.Split(conditionTokens, stringSplitterPad) + minimumAmounts := strings.Split(conditionAmounts, stringSplitterPad) + + for index, tokenPath := range tokenPaths { + if index >= len(minimumAmounts) { + return nil, makeErrorWithDetails( + errInvalidData, + ufmt.Sprintf("condition amount(%s) is not matched with condition token(%s)", conditionAmounts, conditionTokens), + ) + } + + minimumAmount, err := strconv.ParseInt(minimumAmounts[index], 10, 64) + if err != nil { + return nil, makeErrorWithDetails( + errInvalidData, + ufmt.Sprintf("condition amount(%s) is not a valid integer", minimumAmounts[index]), + ) + } + + condition := NewProjectCondition(tokenPath, minimumAmount) + if !condition.IsAvailable() { + return nil, makeErrorWithDetails( + errInvalidData, + ufmt.Sprintf("condition(%s) is not available", condition.TokenPath()), + ) + } + + conditions = append(conditions, condition) + } + + return conditions, nil +} diff --git a/contract/r/gnoswap/v1/launchpad/project_tier.gno b/contract/r/gnoswap/v1/launchpad/project_tier.gno new file mode 100644 index 0000000..7ee0a72 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/project_tier.gno @@ -0,0 +1,174 @@ +package launchpad + +import ( + "gno.land/p/nt/ufmt" + + u256 "gno.land/p/gnoswap/uint256" +) + +// ProjectTier represents a tier within a project. +// +// This struct contains the necessary data and methods to manage and distribute +// rewards for a specific tier of a project. +// +// Fields: +// - distributeAmountPerSecondX128 (u256.Uint): The amount of tokens to be distributed per second, represented as a Q128 fixed-point number. +// - startTime (int64): The time for the start of the tier. +// - endTime (int64): The time for the end of the tier. +// - id (string): The unique identifier for the tier, formatted as "{projectID}:duration". +// - totalDistributeAmount (int64): The total amount of tokens to be distributed for the tier. +// - totalDepositAmount (int64): The total amount of tokens deposited for the tier. +// - totalWithdrawAmount (int64): The total amount of tokens withdrawn from the tier. +// - totalDepositCount (int64): The total number of deposits made to the tier. +// - totalWithdrawCount (int64): The total number of withdrawals from the tier. +// - totalCollectedAmount (int64): The total amount of tokens collected as rewards for the tier. +type ProjectTier struct { + distributeAmountPerSecondX128 *u256.Uint // distribute amount per second, Q128 + startTime int64 + endTime int64 + + id string // '{projectId}:duration' // duartion == 30, 90, 180 + totalDistributeAmount int64 + totalDepositAmount int64 // accumulated deposit amount + totalWithdrawAmount int64 // accumulated withdraw amount + totalDepositCount int64 // accumulated deposit count + totalWithdrawCount int64 // accumulated withdraw count + totalCollectedAmount int64 // total collected amount by user (reward) +} + +func (t *ProjectTier) ID() string { + return t.id +} + +func (t *ProjectTier) TotalDistributeAmount() int64 { + return t.totalDistributeAmount +} + +func (t *ProjectTier) TotalCollectedAmount() int64 { + return t.totalCollectedAmount +} + +func (t *ProjectTier) TotalDepositAmount() int64 { + return t.totalDepositAmount +} + +func (t *ProjectTier) TotalWithdrawAmount() int64 { + return t.totalWithdrawAmount +} + +func (t *ProjectTier) TotalDepositCount() int64 { + return t.totalDepositCount +} + +func (t *ProjectTier) TotalWithdrawCount() int64 { + return t.totalWithdrawCount +} + +func (t *ProjectTier) CurrentDepositCount() int64 { + return t.totalDepositCount - t.totalWithdrawCount +} + +func (t *ProjectTier) CurrentDepositAmount() int64 { + return t.totalDepositAmount - t.totalWithdrawAmount +} + +func (t *ProjectTier) DistributeAmountPerSecondX128() *u256.Uint { + return t.distributeAmountPerSecondX128 +} + +func (t *ProjectTier) isActivated(currentTime int64) bool { + return t.startTime <= currentTime && currentTime < t.endTime +} + +func (t *ProjectTier) isEnded(currentTime int64) bool { + return t.endTime < currentTime +} + +func (t *ProjectTier) isFirstDeposit() bool { + return t.totalDepositCount == 0 +} + +func (t *ProjectTier) StartTime() int64 { + return t.startTime +} + +func (t *ProjectTier) EndTime() int64 { + return t.endTime +} + +func (t *ProjectTier) deposit(deposit *Deposit) { + t.totalDepositAmount += deposit.DepositAmount() + t.totalDepositCount++ +} + +func (t *ProjectTier) withdraw(deposit *Deposit) { + t.totalWithdrawAmount += deposit.DepositAmount() + t.totalWithdrawCount++ +} + +func (t *ProjectTier) setStartTime(time int64) { + t.startTime = time +} + +func (t *ProjectTier) setEndTime(time int64) { + t.endTime = time +} + +func (t *ProjectTier) calculateLeftReward() int64 { + return t.totalDistributeAmount - t.totalCollectedAmount +} + +func (t *ProjectTier) updateDistributeAmountPerSecond() { + // Use time duration instead of block count + distributeTimeDuration := t.endTime - t.startTime + if distributeTimeDuration <= 0 { + return + } + + totalDistributeAmountX128 := u256.Zero().Mul(u256.NewUintFromInt64(t.totalDistributeAmount), q128.Clone()) + // Divide by time duration in seconds + distributeAmountPerSecondX128 := u256.Zero().Div(totalDistributeAmountX128, u256.NewUintFromInt64(distributeTimeDuration)) + + t.distributeAmountPerSecondX128 = distributeAmountPerSecondX128 +} + +// NewProjectTier returns a pointer to a new ProjectTier with the given values. +func NewProjectTier( + projectID string, + tierDuration int64, + totalDistributeAmount int64, + startTime int64, + endTime int64, +) *ProjectTier { + tier := &ProjectTier{ + id: makeProjectTierID(projectID, tierDuration), + totalDistributeAmount: totalDistributeAmount, + distributeAmountPerSecondX128: u256.Zero(), + startTime: startTime, + endTime: endTime, + totalDepositAmount: 0, + totalWithdrawAmount: 0, + totalDepositCount: 0, + totalWithdrawCount: 0, + totalCollectedAmount: 0, + } + + tier.updateDistributeAmountPerSecond() + + return tier +} + +// makeProjectTierID generates a unique tier ID based on the given project ID and the tier duration. +// +// The generated ID combines the `projectId` and the `duration` in the following format: +// "{projectId}:{duration}" +// +// Parameters: +// - projectId (string): The unique ID of the project associated with the tier. +// - duration (uint64): The duration of the tier (e.g., 30, 90, 180 days). +// +// Returns: +// - string: A unique tier ID in the format "projectId:duration". +func makeProjectTierID(projectID string, duration int64) string { + return ufmt.Sprintf("%s:%d", projectID, duration) +} diff --git a/contract/r/gnoswap/v1/launchpad/reward_manager.gno b/contract/r/gnoswap/v1/launchpad/reward_manager.gno new file mode 100644 index 0000000..96bfd4b --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/reward_manager.gno @@ -0,0 +1,311 @@ +package launchpad + +import ( + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + u256 "gno.land/p/gnoswap/uint256" +) + +// RewardManager manages the distribution of rewards for a project tier. +// +// This struct contains the necessary data and methods to calculate and track +// rewards for deposits associated with a project tier. +// +// Fields: +// - rewards (avl.Tree): A map of deposit IDs to their associated reward states. +// - distributeAmountPerSecondX128 (u256.Uint): The amount of tokens to be distributed per second, represented as a Q128 fixed-point number. +// - accumulatedRewardPerDepositX128 (u256.Uint): The accumulated reward per GNS stake, represented as a Q128 fixed-point number. +// - totalDistributeAmount (int64): The total amount of tokens to be distributed. +// - totalClaimedAmount (int64): The total amount of tokens claimed. +// - distributeStartTime (int64): The start time of the reward calculation. +// - distributeEndTime (int64): The end time of the reward calculation. +// - accumulatedDistributeAmount (int64): The accumulated amount of tokens distributed. +// - accumulatedHeight (int64): The last height when reward was calculated. +// - rewardClaimableDuration (int64): The duration of reward claimable. +type RewardManager struct { + rewards *avl.Tree // depositId -> RewardState + + distributeAmountPerSecondX128 *u256.Uint // distribute amount per second, Q128 + accumulatedRewardPerDepositX128 *u256.Uint // accumulated reward per GNS stake, Q128 + + totalDistributeAmount int64 // total distributed amount + totalClaimedAmount int64 // total claimed amount + distributeStartTime int64 // start time of reward calculation + distributeEndTime int64 // end time of reward calculation + accumulatedDistributeAmount int64 // accumulated distribute amount + accumulatedHeight int64 // last height when reward was calculated + accumulatedTime int64 // last time when reward was calculated + rewardClaimableDuration int64 // duration of reward claimable +} + +func (r *RewardManager) IsInitialized() bool { + return r.rewards.Size() > 0 +} + +func (r *RewardManager) DistributeAmountPerSecondX128() *u256.Uint { + return r.distributeAmountPerSecondX128 +} + +func (r *RewardManager) AccumulatedHeight() int64 { + return r.accumulatedHeight +} + +func (r *RewardManager) AccumulatedTime() int64 { + return r.accumulatedTime +} + +func (r *RewardManager) DistributeEndTime() int64 { + return r.distributeEndTime +} + +func (r *RewardManager) AccumulatedRewardPerDepositX128() *u256.Uint { + return r.accumulatedRewardPerDepositX128 +} + +func (r *RewardManager) AccumulatedReward() int64 { + res := u256.Zero().Rsh(r.accumulatedRewardPerDepositX128, 128) + return safeConvertToInt64(res) +} + +func (r *RewardManager) getDepositRewardState(depositId string) (*RewardState, error) { + rewardStateI, exists := r.rewards.Get(depositId) + if !exists { + return nil, makeErrorWithDetails(errNotExistDeposit, ufmt.Sprintf("(%s)", depositId)) + } + + rewardState, ok := rewardStateI.(*RewardState) + if !ok { + return nil, ufmt.Errorf("failed to cast rewardState to *RewardState: %T", rewardStateI) + } + + return rewardState, nil +} + +func (r *RewardManager) calculateRewardPerDepositX128(rewardPerSecondX128 *u256.Uint, totalStaked int64, currentTime int64) (*u256.Uint, error) { + accumulatedTime := r.accumulatedTime + if r.distributeStartTime > accumulatedTime { + accumulatedTime = r.distributeStartTime + } + + // not started yet + if currentTime < accumulatedTime { + return u256.Zero(), nil + } + + // past distribute end time + if accumulatedTime > r.distributeEndTime { + return u256.Zero(), nil + } + + // past distribute end time, set to distribute end time + if currentTime > r.distributeEndTime { + currentTime = r.distributeEndTime + } + + if rewardPerSecondX128.IsZero() { + return nil, makeErrorWithDetails( + errNoLeftReward, + ufmt.Sprintf("rewardPerSecond(%d)", rewardPerSecondX128), + ) + } + + // no left reward + if totalStaked == 0 { + return u256.Zero(), nil + } + + // timeDuration * rewardPerSecond / totalStaked + timeDuration := currentTime - accumulatedTime + rewardPerDepositX128 := u256.MulDiv( + u256.NewUintFromInt64(timeDuration), + rewardPerSecondX128, + u256.NewUintFromInt64(totalStaked), + ) + + return rewardPerDepositX128, nil +} + +func (r *RewardManager) addRewardStateByDeposit(deposit *Deposit) *RewardState { + claimableTime := deposit.DepositTime() + r.rewardClaimableDuration + + rewardState := NewRewardState( + r.AccumulatedRewardPerDepositX128(), + deposit.DepositAmount(), + deposit.DepositTime(), + r.distributeEndTime, + claimableTime, + ) + + // if the first deposit, set the distribute start height + if !r.IsInitialized() { + rewardState.setDistributeStartTime(r.distributeStartTime) + rewardState.setDistributeEndTime(r.distributeEndTime) + rewardState.setAccumulatedTime(r.distributeStartTime) + rewardState.setPriceDebtX128(u256.Zero()) + } + + return r.addRewardState(deposit, rewardState) +} + +func (r *RewardManager) addRewardState(deposit *Deposit, rewardState *RewardState) *RewardState { + r.rewards.Set(deposit.ID(), rewardState) + + return rewardState +} + +func (r *RewardManager) addRewardPerDepositX128(rewardPerDepositX128 *u256.Uint, currentHeight, currentTime int64) error { + if rewardPerDepositX128.IsZero() { + return nil + } + + if r.accumulatedTime > currentTime || r.distributeStartTime > currentTime { + return nil + } + + if currentTime > r.distributeEndTime { + currentTime = r.distributeEndTime + } + + accumulated := u256.Zero().Add(r.accumulatedRewardPerDepositX128, rewardPerDepositX128) + r.accumulatedRewardPerDepositX128 = accumulated + r.accumulatedHeight = currentHeight + r.accumulatedTime = currentTime + + return nil +} + +// updateRewardPerDepositX128 updates the reward per deposit state. +// This function calculates and updates the accumulated reward per deposit +// based on the current total deposit amount and height. +// +// Parameters: +// - totalDepositAmount (int64): Current total deposit amount +// - height (int64): Current blockchain height +// - time (int64): Current timestamp +// +// Returns: +// - error: If the update fails +func (r *RewardManager) updateRewardPerDepositX128(totalDepositAmount int64, currentHeight, currentTime int64) error { + if currentTime <= 0 { + return makeErrorWithDetails(errInvalidTime, "time must be positive") + } + + // Calculate and update rewards + rewardPerDepositX128, err := r.calculateRewardPerDepositX128( + r.distributeAmountPerSecondX128, + totalDepositAmount, + currentTime, + ) + if err != nil { + return err + } + + err = r.addRewardPerDepositX128(rewardPerDepositX128, currentHeight, currentTime) + if err != nil { + return err + } + + return nil +} + +func (r *RewardManager) updateDistributeAmountPerSecondX128(totalDistributeAmount int64, distributeStartTime int64, distributeEndTime int64) { + // Use time duration for per-second calculation + timeDuration := distributeEndTime - distributeStartTime + if timeDuration <= 0 { + return + } + + totalDistributeAmountX128 := u256.Zero().Lsh( + u256.NewUintFromInt64(totalDistributeAmount), + 128, + ) + + // Divide by time duration in seconds + amountPerSecondX128 := u256.Zero().Div( + totalDistributeAmountX128, + u256.NewUintFromInt64(timeDuration), + ) + + r.distributeAmountPerSecondX128 = amountPerSecondX128 + r.distributeStartTime = distributeStartTime + r.distributeEndTime = distributeEndTime +} + +// collectReward processes the reward collection for a specific deposit. +// This function ensures that the reward collection is valid and updates +// the claimed amount accordingly. +// +// Parameters: +// - depositId (string): The ID of the deposit +// - currentHeight (int64): Current blockchain height +// +// Returns: +// - int64: The amount of reward collected +// - error: If the collection fails +func (r *RewardManager) collectReward(depositId string, currentTime int64) (int64, error) { + if currentTime < r.accumulatedTime { + return 0, makeErrorWithDetails( + errInvalidRewardState, + ufmt.Sprintf("currentTime %d is less than AccumulatedTime %d", currentTime, r.accumulatedTime), + ) + } + + rewardState, err := r.getDepositRewardState(depositId) + if err != nil { + return 0, err + } + + if !rewardState.IsClaimable(currentTime) { + return 0, makeErrorWithDetails( + errInvalidRewardState, + ufmt.Sprintf("currentTime %d is less than claimableTime %d", currentTime, rewardState.ClaimableTime()), + ) + } + + if currentTime < rewardState.DistributeStartTime() { + return 0, makeErrorWithDetails( + errInvalidRewardState, + ufmt.Sprintf("currentTime %d is less than DistributeStartTime %d", currentTime, rewardState.DistributeStartTime()), + ) + } + + claimableReward := rewardState.calculateClaimableReward(r.accumulatedRewardPerDepositX128) + if claimableReward == 0 { + return 0, nil + } + + rewardState.setClaimedAmount(rewardState.ClaimedAmount() + claimableReward) + r.rewards.Set(depositId, rewardState) + r.totalClaimedAmount += claimableReward + + return claimableReward, nil +} + +// NewRewardManager returns a pointer to a new RewardManager with the given values. +func NewRewardManager( + totalDistributeAmount int64, + distributeStartTime int64, + distributeEndTime int64, + currentHeight int64, + currentTime int64, +) *RewardManager { + manager := &RewardManager{ + totalDistributeAmount: totalDistributeAmount, + distributeStartTime: distributeStartTime, + distributeEndTime: distributeEndTime, + totalClaimedAmount: 0, + accumulatedDistributeAmount: 0, + accumulatedHeight: 0, + accumulatedTime: 0, + accumulatedRewardPerDepositX128: u256.Zero(), + distributeAmountPerSecondX128: u256.Zero(), + rewardClaimableDuration: 0, + rewards: avl.NewTree(), + } + + manager.updateDistributeAmountPerSecondX128(totalDistributeAmount, distributeStartTime, distributeEndTime) + manager.updateRewardPerDepositX128(0, currentHeight, currentTime) + + return manager +} diff --git a/contract/r/gnoswap/v1/launchpad/reward_state.gno b/contract/r/gnoswap/v1/launchpad/reward_state.gno new file mode 100644 index 0000000..8f48605 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/reward_state.gno @@ -0,0 +1,158 @@ +package launchpad + +import ( + u256 "gno.land/p/gnoswap/uint256" +) + +// RewardState represents the state of a reward for a deposit. +// It contains the necessary data to manage and distribute rewards for a specific deposit. +type RewardState struct { + priceDebtX128 *u256.Uint // price debt per GNS stake, Q128 + claimableTime int64 // time when reward can be claimed + + depositAmount int64 // amount of GNS staked + distributeStartTime int64 // time when launchpad started staking + distributeEndTime int64 // end time of reward calculation + accumulatedRewardAmount int64 // calculated, not collected + accumulatedHeight int64 // last height when reward was calculated + accumulatedTime int64 // last time when reward was calculated + claimedAmount int64 // amount of reward claimed so far +} + +func (r *RewardState) PriceDebtX128() *u256.Uint { + return r.priceDebtX128 +} + +func (r *RewardState) setPriceDebtX128(v *u256.Uint) { + r.priceDebtX128 = v +} + +func (r *RewardState) DepositAmount() int64 { + return r.depositAmount +} + +func (r *RewardState) setDepositAmount(v int64) { + r.depositAmount = v +} + +func (r *RewardState) AccumulatedRewardAmount() int64 { + return r.accumulatedRewardAmount +} + +func (r *RewardState) setAccumulatedRewardAmount(v int64) { + r.accumulatedRewardAmount = v +} + +func (r *RewardState) ClaimedAmount() int64 { + return r.claimedAmount +} + +func (r *RewardState) setClaimedAmount(v int64) { + r.claimedAmount = v +} + +func (r *RewardState) DistributeStartTime() int64 { + return r.distributeStartTime +} + +func (r *RewardState) setDistributeStartTime(v int64) { + r.distributeStartTime = v +} + +func (r *RewardState) DistributeEndTime() int64 { + return r.distributeEndTime +} + +func (r *RewardState) setDistributeEndTime(v int64) { + r.distributeEndTime = v +} + +func (r *RewardState) AccumulatedHeight() int64 { + return r.accumulatedHeight +} + +func (r *RewardState) setAccumulatedHeight(v int64) { + r.accumulatedHeight = v +} + +func (r *RewardState) AccumulatedTime() int64 { + return r.accumulatedTime +} + +func (r *RewardState) setAccumulatedTime(v int64) { + r.accumulatedTime = v +} + +func (r *RewardState) IsClaimable(currentTime int64) bool { + return currentTime >= r.claimableTime +} + +func (r *RewardState) ClaimableTime() int64 { + return r.claimableTime +} + +func (r *RewardState) setClaimableTime(v int64) { + r.claimableTime = v +} + +// calculateReward calculates the total reward amount based on +// the accumulated reward per deposit. +// Returns the total reward amount. +func (r *RewardState) calculateReward(accumRewardPerDepositX128 *u256.Uint) int64 { + if accumRewardPerDepositX128 == nil || r.PriceDebtX128() == nil { + return 0 + } + + actualRewardPerDepositX128 := u256.Zero().Sub(accumRewardPerDepositX128, r.PriceDebtX128()) + if actualRewardPerDepositX128.IsZero() { + return 0 + } + + reward := u256.Zero().Mul(actualRewardPerDepositX128, u256.NewUintFromInt64(r.DepositAmount())) + reward = u256.Zero().Rsh(reward, 128) + + return safeConvertToInt64(reward) +} + +// calculateClaimableReward calculates the amount of reward that can be claimed +// based on the current accumulated reward per deposit. +// Returns the amount of reward that can be claimed. +func (r *RewardState) calculateClaimableReward(accumRewardPerDepositX128 *u256.Uint) int64 { + if accumRewardPerDepositX128 == nil { + return 0 + } + + // Return 0 if accumulated reward is less than price debt + if accumRewardPerDepositX128.Lt(r.priceDebtX128) { + return 0 + } + + reward := r.calculateReward(accumRewardPerDepositX128) + claimedAmount := r.ClaimedAmount() + + if reward <= claimedAmount { + return 0 + } + + return reward - claimedAmount +} + +// NewRewardState returns a pointer to a new RewardState with the given values. +func NewRewardState( + accumulatedRewardPerDepositX128 *u256.Uint, + depositAmount, + distributeStartTime, + distributeEndTime int64, + claimableTime int64, +) *RewardState { + return &RewardState{ + priceDebtX128: accumulatedRewardPerDepositX128, + depositAmount: depositAmount, + distributeStartTime: distributeStartTime, + distributeEndTime: distributeEndTime, + claimableTime: claimableTime, + accumulatedRewardAmount: 0, + claimedAmount: 0, + accumulatedHeight: 0, + } +} diff --git a/contract/r/gnoswap/v1/launchpad/state.gno b/contract/r/gnoswap/v1/launchpad/state.gno new file mode 100644 index 0000000..e1f7974 --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/state.gno @@ -0,0 +1,104 @@ +package launchpad + +import ( + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" +) + +var ( + // projectId -> Project + projects *avl.Tree + + // projectTierId -> RewardManager + projectTierRewardManagers *avl.Tree + + // Counter for generating unique deposit IDs + depositCounter *Counter + + // depositId -> Deposit, Tree storing all deposits by ID + deposits *avl.Tree +) + +func init() { + projects = avl.NewTree() + projectTierRewardManagers = avl.NewTree() + + depositCounter = NewCounter() + deposits = avl.NewTree() +} + +func getProject(projectID string) (*Project, error) { + project, ok := projects.Get(projectID) + if !ok { + return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("project(%s) not found", projectID)) + } + + p, ok := project.(*Project) + if !ok { + return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("project(%s) not found", projectID)) + } + + return p, nil +} + +func getProjectTier(projectID string, tierDuration int64) (*ProjectTier, error) { + project, err := getProject(projectID) + if err != nil { + return nil, err + } + + tier, ok := project.tiers[tierDuration] + if !ok { + return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("tier(%d) not found", tierDuration)) + } + + return tier, nil +} + +func getProjectTierRewardManager(projectTierID string) (*RewardManager, error) { + rewardManager, ok := projectTierRewardManagers.Get(projectTierID) + if !ok { + return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("reward manager(%s) not found", projectTierID)) + } + + manager, ok := rewardManager.(*RewardManager) + if !ok { + return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("reward manager(%s) not found", projectTierID)) + } + + return manager, nil +} + +func mustGetDeposit(depositID string) *Deposit { + deposit, err := getDeposit(depositID) + if err != nil { + panic(err) + } + + return deposit +} + +func getDeposit(depositID string) (*Deposit, error) { + depositI, ok := deposits.Get(depositID) + if !ok { + return nil, makeErrorWithDetails(errNotExistDeposit, ufmt.Sprintf("(%s)", depositID)) + } + + deposit, ok := depositI.(*Deposit) + if !ok { + return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("deposit(%s) not found", depositID)) + } + + return deposit, nil +} + +// getCurrentDepositID returns the current deposit ID (last assigned). +func getCurrentDepositID() string { + return formatInt(depositCounter.Get()) +} + +// nextDepositID increments and returns the next unique deposit ID. +// This is used when creating new deposits. +func nextDepositID() string { + return formatInt(depositCounter.next()) +} diff --git a/contract/r/gnoswap/v1/launchpad/utils.gno b/contract/r/gnoswap/v1/launchpad/utils.gno new file mode 100644 index 0000000..ec4d59d --- /dev/null +++ b/contract/r/gnoswap/v1/launchpad/utils.gno @@ -0,0 +1,76 @@ +package launchpad + +import ( + "strconv" + "strings" + + "gno.land/p/nt/ufmt" + + u256 "gno.land/p/gnoswap/uint256" +) + +// formatInt returns the string representation of the int64 value. +func formatInt(value int64) string { + return strconv.FormatInt(value, 10) +} + +// parseProjectTierID parses a project tier ID into its project ID and duration. +// Returns the project ID {tokenPath}:{createdHeight} and the duration of the project tier (30, 90, 180). +func parseProjectTierID(projectTierID string) (string, int64) { + parts := strings.Split(projectTierID, ":") + if len(parts) != 3 { + panic(makeErrorWithDetails( + errInvalidData, + ufmt.Sprintf("(%s)", projectTierID), + )) + } + + projectID := ufmt.Sprintf("%s:%s", parts[0], parts[1]) + + tierDuration, err := strconv.ParseInt(parts[2], 10, 64) + if err != nil { + panic(makeErrorWithDetails( + errInvalidData, + ufmt.Sprintf("(%s)", projectTierID), + )) + } + + // Validate tier duration + if tierDuration != projectTier30 && tierDuration != projectTier90 && tierDuration != projectTier180 { + panic(makeErrorWithDetails( + errInvalidTier, + ufmt.Sprintf("pool type(%d) is not available", tierDuration), + )) + } + + return projectID, tierDuration +} + +// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. +// +// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds +// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. +// +// Parameters: +// - value (*u256.Uint): The unsigned 256-bit integer to be converted. +// +// Returns: +// - int64: The converted value if it falls within the int64 range. +// +// Panics: +// - If the `value` exceeds the range of int64, the function will panic with an error indicating +// the overflow and the original value. +func safeConvertToInt64(value *u256.Uint) int64 { + const INT64_MAX = 9223372036854775807 + const MAX_INT64 = "9223372036854775807" + + res, overflow := value.Uint64WithOverflow() + if overflow || res > uint64(INT64_MAX) { + panic(ufmt.Sprintf( + "amount(%s) overflows int64 range (max %s)", + value.ToString(), + MAX_INT64, + )) + } + return int64(res) +} diff --git a/contract/r/gnoswap/v1/pool/README.md b/contract/r/gnoswap/v1/pool/README.md new file mode 100644 index 0000000..70b4b55 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/README.md @@ -0,0 +1,131 @@ +# Pool + +Concentrated liquidity AMM pools with tick-based pricing. + +## Overview + +Pool contracts implement Uniswap V3-style concentrated liquidity, allowing LPs to provide liquidity within custom price ranges for maximum capital efficiency. + +## Configuration + +- **Pool Creation Fee**: 100 GNS (default) +- **Protocol Fee**: 0-10% of swap fees per token +- **Withdrawal Fee**: 1% on collected fees +- **Fee Tiers**: 0.01%, 0.05%, 0.3%, 1% +- **Tick Spacing**: Auto-set by fee tier +- **Max Liquidity Per Tick**: 2^128 - 1 + +## Core Concepts + +### Concentrated Liquidity +Liquidity providers concentrate capital within custom price ranges instead of 0-∞. This allows LPs to allocate capital where it's most likely to generate fees - near the current price for volatile pairs, or within tight ranges for stable pairs. Capital efficiency can improve by orders of magnitude depending on range selection and pair volatility. For more details, check out [GnoSwap Docs](https://docs.gnoswap.io/core-concepts/amm/concentrated-liquidity). + +### Tick System +- Price space divided into discrete ticks (0.01% apart) +- Each tick represents ~0.01% price change +- Positions defined by upper/lower tick boundaries +- Liquidity activated only when price in range + +## Key Functions + +### `CreatePool` +Deploys new trading pair. +- Requires 100 GNS creation fee +- Valid fee tier required +- Initial price via sqrtPriceX96 +- Unique token pair per fee tier + +### `Mint` +Adds liquidity to position (called by Position contract). +- Calculates token amounts from liquidity +- Updates tick bitmap +- Transfers tokens from owner +- Returns actual amounts used + +### `Burn` +Removes liquidity without collecting tokens. +- Two-step: burn then collect +- Calculates owed amounts +- Updates position state + +### `Collect` +Claims tokens from burned position + fees. +- Transfers principal and fees +- Updates tokensOwed +- Applies withdrawal fee + +### `Swap` +Core swap execution (called by Router). +- Iterates through ticks +- Updates price and liquidity +- Calculates fees +- Maintains TWAP oracle + +## Technical Details + +### Price Math + +**Q96 Format**: Prices stored as `sqrtPriceX96 = sqrt(price) * 2^96` + +``` +Price 1:1 → sqrtPriceX96 = 79228162514264337593543950336 +Price 1:4 → sqrtPriceX96 = 39614081257132168796771975168 +Price 100:1 → sqrtPriceX96 = 792281625142643375935439503360 +``` + +**Tick to Price**: `price = 1.0001^tick` +``` +tick 0 = price 1 +tick 6932 = price ~2 +tick -6932 = price ~0.5 +``` + +### Liquidity Math + +**Range Liquidity Formula**: +``` +L = amount / (sqrt(upper) - sqrt(lower)) // current < lower +L = amount * sqrt(current) / (upper - current) // lower < current < upper +L = amount / (sqrt(current) - sqrt(lower)) // current > upper +``` + +**Impermanent Loss**: +- Narrow range: Higher fees, higher IL +- Wide range: Lower fees, lower IL +- Stable pairs: ±0.1% ranges optimal +- Volatile pairs: ±10%+ ranges recommended + +### Fee Mechanics + +**Swap Fees**: +- Charged on input amount +- Accumulates as feeGrowthGlobal +- Distributed pro-rata to in-range liquidity + +**Fee Calculation**: +``` +fees = feeGrowthInside * liquidity +feeGrowthInside = feeGrowthGlobal - feeGrowthOutside +``` + +**Protocol fees**: +- Optional 0-10% of swap fees +- Configurable per pool +- Sent to protocol fee contract + +## Security + +### Reentrancy Protection +- Pools lock during swaps (`slot0.unlocked`) +- External calls after state updates +- Checks-effects-interactions pattern + +### Price Manipulation +- TWAP oracle resists manipulation +- Large swaps limited by liquidity +- Slippage protection required + +### Rounding +- Division rounds down (favors protocol) +- Minimum liquidity enforced +- Full precision for amounts \ No newline at end of file diff --git a/contract/r/gnoswap/v1/pool/api.gno b/contract/r/gnoswap/v1/pool/api.gno new file mode 100644 index 0000000..53c302f --- /dev/null +++ b/contract/r/gnoswap/v1/pool/api.gno @@ -0,0 +1,53 @@ +package pool + +import ( + b64 "encoding/base64" + "strconv" + "strings" + + "gno.land/p/onbloc/json" + + "gno.land/p/nt/ufmt" +) + +func ApiGetPool(poolPath string) string { + if !pools.Has(poolPath) { + return "" + } + + node := json.ObjectNode("", map[string]*json.Node{ + "stat": newStatNode().JSON(), + "response": newRpcPool(poolPath).JSON(), + }) + + return marshal(node) +} + +func posKeyDivide(posKey string) (string, int32, int32) { + kDec, _ := b64.StdEncoding.DecodeString(posKey) + posKey = string(kDec) + + res := strings.Split(posKey, "__") + if len(res) != 3 { + panic(newErrorWithDetail( + errInvalidPositionKey, + ufmt.Sprintf("invalid posKey(%s)", posKey), + )) + } + + owner, _tickLower, _tickUpper := res[0], res[1], res[2] + + tickLower, _ := strconv.Atoi(_tickLower) + tickUpper, _ := strconv.Atoi(_tickUpper) + + return owner, int32(tickLower), int32(tickUpper) +} + +func marshal(node *json.Node) string { + b, err := json.Marshal(node) + if err != nil { + panic(err.Error()) + } + + return string(b) +} diff --git a/contract/r/gnoswap/v1/pool/assert.gno b/contract/r/gnoswap/v1/pool/assert.gno new file mode 100644 index 0000000..e9090b5 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/assert.gno @@ -0,0 +1,65 @@ +package pool + +import ( + "std" + + "gno.land/p/nt/ufmt" +) + +// assertIsNotEqualsTokens asserts that the token0Path and token1Path are not equal. +func assertIsNotEqualsTokens(token0Path, token1Path string) { + if token0Path == token1Path { + panic(newErrorWithDetail( + errDuplicateTokenInPool, + ufmt.Sprintf("expected token0Path(%s) != token1Path(%s)", token0Path, token1Path), + )) + } +} + +// assertIsSupportedFeeTier asserts that the fee is a supported fee tier. +func assertIsSupportedFeeTier(fee uint32) { + if !isValidFeeTier(fee) { + panic(newErrorWithDetail( + errUnsupportedFeeTier, + ufmt.Sprintf("expected fee(%d) to be one of %d, %d, %d, %d", fee, FeeTier100, FeeTier500, FeeTier3000, FeeTier10000), + )) + } +} + +// assertIsNotExistsPoolPath asserts that the pool path does not exist. +func assertIsNotExistsPoolPath(token0Path, token1Path string, fee uint32) { + poolPath := GetPoolPath(token0Path, token1Path, fee) + + if pools.Has(poolPath) { + panic(newErrorWithDetail( + errPoolAlreadyExists, + ufmt.Sprintf("expected poolPath(%s:%s:%d) not to exist", token0Path, token1Path, fee), + )) + } +} + +// assertIsValidTicks validates the tick range for a liquidity position. +func assertIsValidTicks(tickLower, tickUpper int32) { + if err := validateTicks(tickLower, tickUpper); err != nil { + panic(err) + } +} + +// assertAmountSpecifiedIsNotZero asserts that the amountSpecified is not zero. +func assertAmountSpecifiedIsNotZero(amountSpecified string) { + if amountSpecified == "0" { + panic(newErrorWithDetail( + errInvalidSwapAmount, + ufmt.Sprintf("amountSpecified == 0"), + )) + } +} + +func assertPayerIsPreviousRealmOrOriginCaller(payer std.Address) { + if payer != std.PreviousRealm().Address() && payer != std.OriginCaller() { + panic(newErrorWithDetail( + errInvalidPayer, + ufmt.Sprintf("expected payer(%s) to be the previous realm or the caller", payer), + )) + } +} diff --git a/contract/r/gnoswap/v1/pool/doc.gno b/contract/r/gnoswap/v1/pool/doc.gno new file mode 100644 index 0000000..bca2f92 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/doc.gno @@ -0,0 +1,11 @@ +// Package pool implements GnoSwap's concentrated liquidity pools based on Uniswap V3. +// It manages liquidity positions, executes swaps, and maintains pool state including +// price, liquidity, and fee calculations. +// +// The pool contract is the core of the GnoSwap AMM, supporting: +// - Concentrated liquidity within custom price ranges +// - Multiple fee tiers (0.01%, 0.05%, 0.3%, 1%) +// - Single-tick and cross-tick swaps +// - Protocol fee collection +// - Tick bitmap optimization for gas efficiency +package pool diff --git a/contract/r/gnoswap/v1/pool/errors.gno b/contract/r/gnoswap/v1/pool/errors.gno new file mode 100644 index 0000000..4f04149 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/errors.gno @@ -0,0 +1,50 @@ +package pool + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +// Error definitions for pool operations +var ( + errNoPermission = errors.New("[GNOSWAP-POOL-001] caller has no permission") + errUnsupportedFeeTier = errors.New("[GNOSWAP-POOL-002] unsupported fee tier") + errPoolAlreadyExists = errors.New("[GNOSWAP-POOL-003] pool already created") + errInvalidTickMinMaxRange = errors.New("[GNOSWAP-POOL-004] tickLower and tickUpper are not within the valid range") + errOutOfRange = errors.New("[GNOSWAP-POOL-005] out of range for numeric value") + errInvalidInput = errors.New("[GNOSWAP-POOL-006] invalid input data") + errInvalidPositionKey = errors.New("[GNOSWAP-POOL-007] invalid position key") + errDataNotFound = errors.New("[GNOSWAP-POOL-008] requested data not found") + errLiquidityCalculation = errors.New("[GNOSWAP-POOL-009] invalid liquidity calculated") + errZeroLiquidity = errors.New("[GNOSWAP-POOL-010] zero liquidity") + errDuplicateTokenInPool = errors.New("[GNOSWAP-POOL-011] same token used in single pool") + errTokenSortOrder = errors.New("[GNOSWAP-POOL-012] tokens must be in lexicographical order") + errTickLowerInvalid = errors.New("[GNOSWAP-POOL-013] tickLower is invalid") + errTickUpperInvalid = errors.New("[GNOSWAP-POOL-014] tickUpper is invalid") + errInvalidSwapAmount = errors.New("[GNOSWAP-POOL-015] invalid swap amount") + errInvalidProtocolFeePct = errors.New("[GNOSWAP-POOL-016] invalid protocol fee percentage") + errInvalidWithdrawalFeePct = errors.New("[GNOSWAP-POOL-017] invalid withdrawal fee percentage") + errLockedPool = errors.New("[GNOSWAP-POOL-018] can't swap while pool is locked") + errPriceOutOfRange = errors.New("[GNOSWAP-POOL-019] swap price out of range") + errMustBeNegative = errors.New("[GNOSWAP-POOL-020] negative value expected") + errTransferFailed = errors.New("[GNOSWAP-POOL-021] token transfer failed") + errInvalidTickAndTickSpacing = errors.New("[GNOSWAP-POOL-022] invalid tick and tick spacing requested") + errInvalidAddress = errors.New("[GNOSWAP-POOL-023] invalid address") + errInvalidTickRange = errors.New("[GNOSWAP-POOL-024] tickLower is greater than or equal to tickUpper") + errUnderflow = errors.New("[GNOSWAP-POOL-025] underflow") + errOverFlow = errors.New("[GNOSWAP-POOL-026] overflow") + errBalanceUpdateFailed = errors.New("[GNOSWAP-POOL-027] balance update failed") + errInvalidPayer = errors.New("[GNOSWAP-POOL-028] invalid payer") +) + +// newErrorWithDetail adds detail to an error message. +func newErrorWithDetail(err error, detail string) string { + finalErr := ufmt.Errorf("%s || %s", err.Error(), detail) + return finalErr.Error() +} + +// makeErrorWithDetails creates an error with additional context. +func makeErrorWithDetails(err error, details string) error { + return ufmt.Errorf("%s || %s", err.Error(), details) +} diff --git a/contract/r/gnoswap/v1/pool/factory_param.gno b/contract/r/gnoswap/v1/pool/factory_param.gno new file mode 100644 index 0000000..ced5113 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/factory_param.gno @@ -0,0 +1,143 @@ +package pool + +import ( + "strings" + + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" +) + +var Q192 = u256.Zero().Lsh(u256.One(), 192) + +var ( + minSqrtRatio = u256.MustFromDecimal(MIN_SQRT_RATIO) + maxSqrtRatio = u256.MustFromDecimal(MAX_SQRT_RATIO) +) + +const ( + FeeTier100 uint32 = 100 + FeeTier500 uint32 = 500 + FeeTier3000 uint32 = 3000 + FeeTier10000 uint32 = 10000 +) + +const ( + GNOT string = "gnot" + WRAPPED_WUGNOT string = "gno.land/r/gnoland/wugnot" +) + +const ( + MIN_SQRT_RATIO string = "4295128739" + MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" +) + +// poolCreateConfig holds the essential parameters for creating a new pool. +type poolCreateConfig struct { + token0Path string + token1Path string + fee uint32 + sqrtPriceX96 *u256.Uint + tickSpacing int32 +} + +// newPoolParams defines the essential parameters for creating a new pool. +func newPoolParams( + token0Path string, + token1Path string, + fee uint32, + sqrtPriceX96 string, + tickSpacing int32, +) *poolCreateConfig { + price := u256.MustFromDecimal(sqrtPriceX96) + return &poolCreateConfig{ + token0Path: token0Path, + token1Path: token1Path, + fee: fee, + sqrtPriceX96: price, + tickSpacing: tickSpacing, + } +} + +func (p *poolCreateConfig) SqrtPriceX96() *u256.Uint { return p.sqrtPriceX96 } +func (p *poolCreateConfig) TickSpacing() int32 { return p.tickSpacing } +func (p *poolCreateConfig) Token0Path() string { return p.token0Path } +func (p *poolCreateConfig) Token1Path() string { return p.token1Path } +func (p *poolCreateConfig) Fee() uint32 { return p.fee } + +func (p *poolCreateConfig) updateWithWrapping() (*poolCreateConfig, error) { + token0Path, token1Path := p.wrap() + + // Always validate that the price is within valid range + if err := validateSqrtPriceX96(p.sqrtPriceX96); err != nil { + return nil, err + } + + if !p.isInOrder() { + token0Path, token1Path = token1Path, token0Path + + // newPrice = 2^192 / oldPrice + newPrice := u256.Zero().Div(Q192, p.sqrtPriceX96) + + // Check if calculated price is within valid range + if err := validateSqrtPriceX96(newPrice); err != nil { + return nil, err + } + + p.sqrtPriceX96 = newPrice + } + return newPoolParams(token0Path, token1Path, p.fee, p.sqrtPriceX96.ToString(), GetFeeAmountTickSpacing(p.fee)), nil +} + +func (p *poolCreateConfig) isSameTokenPath() bool { + return p.token0Path == p.token1Path +} + +// isInOrder checks if token paths are in lexicographical (or, alphabetical) order +func (p *poolCreateConfig) isInOrder() bool { + if strings.Compare(p.token0Path, p.token1Path) < 0 { + return true + } + return false +} + +func (p *poolCreateConfig) wrap() (string, string) { + if p.token0Path == GNOT { + p.token0Path = WRAPPED_WUGNOT + } + if p.token1Path == GNOT { + p.token1Path = WRAPPED_WUGNOT + } + return p.token0Path, p.token1Path +} + +func (p *poolCreateConfig) poolPath() string { + return GetPoolPath(p.token0Path, p.token1Path, p.fee) +} + +func (p *poolCreateConfig) isSupportedFee(feeTier uint32) bool { + switch feeTier { + case FeeTier100, FeeTier500, FeeTier3000, FeeTier10000: + return true + } + return false +} + +// validateSqrtPriceX96 validates that the given sqrtPriceX96 is within valid range +func validateSqrtPriceX96(sqrtPriceX96 *u256.Uint) error { + if sqrtPriceX96.Lt(minSqrtRatio) || sqrtPriceX96.Gt(maxSqrtRatio) { + return makeErrorWithDetails( + errOutOfRange, + ufmt.Sprintf("sqrtPriceX96(%s) is out of range", sqrtPriceX96.ToString()), + ) + } + return nil +} + +func isValidFeeTier(feeTier uint32) bool { + switch feeTier { + case FeeTier100, FeeTier500, FeeTier3000, FeeTier10000: + return true + } + + return false +} diff --git a/contract/r/gnoswap/v1/pool/getter.gno b/contract/r/gnoswap/v1/pool/getter.gno new file mode 100644 index 0000000..cd909b6 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/getter.gno @@ -0,0 +1,183 @@ +package pool + +import ( + "strings" + + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" +) + +// GetPool retrieves a pool instance based on the provided token paths and fee tier. +func GetPool(token0Path, token1Path string, fee uint32) *Pool { + poolPath := GetPoolPath(token0Path, token1Path, fee) + return mustGetPool(poolPath).Clone() +} + +// GetPoolPath generates a unique pool path string based on the token paths and fee tier. +func GetPoolPath(token0Path, token1Path string, fee uint32) string { + // All the token paths in the pool are sorted in alphabetical order. + if strings.Compare(token1Path, token0Path) < 0 { + token0Path, token1Path = token1Path, token0Path + } + + return ufmt.Sprintf("%s:%s:%d", token0Path, token1Path, fee) +} + +// GetFeeAmountTickSpacing retrieves the tick spacing associated with a given fee amount. +func GetFeeAmountTickSpacing(fee uint32) (spacing int32) { + feeStr := formatUint(fee) + iTickSpacing, exist := feeAmountTickSpacing.Get(feeStr) + if !exist { + panic(newErrorWithDetail( + errUnsupportedFeeTier, + ufmt.Sprintf("expected fee(%d) to be one of %d, %d, %d, %d", fee, FeeTier100, FeeTier500, FeeTier3000, FeeTier10000), + )) + } + + spacing, ok := iTickSpacing.(int32) + if !ok { + panic("failed to cast tick spacing to int32") + } + + return spacing +} + +func GetToken0Path(poolPath string) string { + return mustGetPool(poolPath).Token0Path() +} + +func GetToken1Path(poolPath string) string { + return mustGetPool(poolPath).Token1Path() +} + +func GetFee(poolPath string) uint32 { + return mustGetPool(poolPath).Fee() +} + +func GetBalanceToken0(poolPath string) string { + return mustGetPool(poolPath).BalanceToken0().ToString() +} + +func GetBalanceToken1(poolPath string) string { + return mustGetPool(poolPath).BalanceToken1().ToString() +} + +func GetTickSpacing(poolPath string) int32 { + return mustGetPool(poolPath).TickSpacing() +} + +func GetMaxLiquidityPerTick(poolPath string) string { + return mustGetPool(poolPath).MaxLiquidityPerTick().ToString() +} + +func GetSlot0FeeProtocol(poolPath string) uint8 { + return mustGetPool(poolPath).Slot0FeeProtocol() +} + +func GetSlot0Unlocked(poolPath string) bool { + return mustGetPool(poolPath).Slot0Unlocked() +} + +func GetFeeGrowthGlobal0X128(poolPath string) string { + return mustGetPool(poolPath).FeeGrowthGlobal0X128().ToString() +} + +func GetFeeGrowthGlobal1X128(poolPath string) string { + return mustGetPool(poolPath).FeeGrowthGlobal1X128().ToString() +} + +func GetProtocolFeesToken0(poolPath string) string { + return mustGetPool(poolPath).ProtocolFeesToken0().ToString() +} + +func GetProtocolFeesToken1(poolPath string) string { + return mustGetPool(poolPath).ProtocolFeesToken1().ToString() +} + +func GetLiquidity(poolPath string) string { + return mustGetPool(poolPath).Liquidity().ToString() +} + +func GetPositionFeeGrowthInside0LastX128(poolPath, key string) string { + return mustGetPool(poolPath).PositionFeeGrowthInside0LastX128(key).ToString() +} + +func GetPositionFeeGrowthInside1LastX128(poolPath, key string) string { + return mustGetPool(poolPath).PositionFeeGrowthInside1LastX128(key).ToString() +} + +func GetPositionTokensOwed0(poolPath, key string) string { + return mustGetPool(poolPath).PositionTokensOwed0(key).ToString() +} + +func GetPositionTokensOwed1(poolPath, key string) string { + return mustGetPool(poolPath).PositionTokensOwed1(key).ToString() +} + +func GetTickLiquidityGross(poolPath string, tick int32) string { + return mustGetPool(poolPath).GetTickLiquidityGross(tick).ToString() +} + +func GetTickLiquidityNet(poolPath string, tick int32) string { + return mustGetPool(poolPath).GetTickLiquidityNet(tick).ToString() +} + +func GetTickFeeGrowthOutside0X128(poolPath string, tick int32) string { + return mustGetPool(poolPath).GetTickFeeGrowthOutside0X128(tick).ToString() +} + +func GetTickFeeGrowthOutside1X128(poolPath string, tick int32) string { + return mustGetPool(poolPath).GetTickFeeGrowthOutside1X128(tick).ToString() +} + +func GetTickCumulativeOutside(poolPath string, tick int32) int64 { + return mustGetPool(poolPath).GetTickCumulativeOutside(tick) +} + +func GetTickSecondsPerLiquidityOutsideX128(poolPath string, tick int32) string { + return mustGetPool(poolPath).GetTickSecondsPerLiquidityOutsideX128(tick).ToString() +} + +func GetTickSecondsOutside(poolPath string, tick int32) uint32 { + return mustGetPool(poolPath).GetTickSecondsOutside(tick) +} + +func GetTickInitialized(poolPath string, tick int32) bool { + return mustGetPool(poolPath).GetTickInitialized(tick) +} + +func GetSlot0Tick(poolPath string) int32 { + return mustGetPool(poolPath).Slot0Tick() +} + +func GetSlot0SqrtPriceX96(poolPath string) *u256.Uint { + return u256.Zero().Set(mustGetPool(poolPath).Slot0SqrtPriceX96()) +} + +func GetFeeGrowthGlobalX128(poolPath string) (*u256.Uint, *u256.Uint) { + pool := mustGetPool(poolPath) + return u256.Zero().Set(pool.FeeGrowthGlobal0X128()), u256.Zero().Set(pool.FeeGrowthGlobal1X128()) +} + +func GetTickFeeGrowthOutsideX128(poolPath string, tick int32) (*u256.Uint, *u256.Uint) { + pool := mustGetPool(poolPath) + return u256.Zero().Set(pool.GetTickFeeGrowthOutside0X128(tick)), u256.Zero().Set(pool.GetTickFeeGrowthOutside1X128(tick)) +} + +func GetPositionFeeGrowthInsideLastX128(poolPath, key string) (*u256.Uint, *u256.Uint) { + pool := mustGetPool(poolPath) + return u256.Zero().Set(pool.PositionFeeGrowthInside0LastX128(key)), u256.Zero().Set(pool.PositionFeeGrowthInside1LastX128(key)) +} + +func GetPositionLiquidity(poolPath, key string) *u256.Uint { + return u256.Zero().Set(mustGetPool(poolPath).PositionLiquidity(key)) +} + +func ExistsPoolPath(poolPath string) bool { + return pools.Has(poolPath) +} + +func GetTickCumulative(poolPath string, secondsAgo int64) int64 { + pool := mustGetPool(poolPath) + return pool.Observation().TickCumulative() +} diff --git a/contract/r/gnoswap/v1/pool/gnomod.toml b/contract/r/gnoswap/v1/pool/gnomod.toml new file mode 100644 index 0000000..6c73620 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/pool" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/pool/json.gno b/contract/r/gnoswap/v1/pool/json.gno new file mode 100644 index 0000000..6f128ac --- /dev/null +++ b/contract/r/gnoswap/v1/pool/json.gno @@ -0,0 +1,275 @@ +package pool + +import ( + "std" + "strconv" + "time" + + "gno.land/p/onbloc/json" + + u256 "gno.land/p/gnoswap/uint256" +) + +// JsonResponse is an interface that all JSON response types must implement. +type JsonResponse interface { + JSON() *json.Node +} + +type RpcPool struct { + PoolPath string `json:"poolPath"` + + Token0Path string `json:"token0Path"` + Token1Path string `json:"token1Path"` + + Token0Balance string `json:"token0Balance"` + Token1Balance string `json:"token1Balance"` + + Fee uint32 `json:"fee"` + + TickSpacing int32 `json:"tickSpacing"` + + MaxLiquidityPerTick string `json:"maxLiquidityPerTick"` + + Slot0SqrtPriceX96 string `json:"sqrtPriceX96"` + Slot0Tick int32 `json:"tick"` + Slot0FeeProtocol uint8 `json:"feeProtocol"` + Slot0Unlocked bool `json:"unlocked"` + + FeeGrowthGlobal0X128 string `json:"feeGrowthGlobal0X128"` + FeeGrowthGlobal1X128 string `json:"feeGrowthGlobal1X128"` + + Token0ProtocolFee string `json:"token0ProtocolFee"` + Token1ProtocolFee string `json:"token1ProtocolFee"` + + Liquidity string `json:"liquidity"` + + Ticks RpcTicks `json:"ticks"` + + TickBitmaps RpcTickBitmaps `json:"tickBitmaps"` + + Positions RpcPositions `json:"positions"` +} + +func newRpcPool(poolPath string) RpcPool { + rpcPool := RpcPool{} + pool := mustGetPool(poolPath).Clone() + + rpcPool.PoolPath = poolPath + + rpcPool.Token0Path = pool.token0Path + rpcPool.Token1Path = pool.token1Path + + rpcPool.Token0Balance = pool.balances.token0.ToString() + rpcPool.Token1Balance = pool.balances.token1.ToString() + + rpcPool.Fee = pool.fee + + rpcPool.TickSpacing = pool.tickSpacing + + rpcPool.MaxLiquidityPerTick = pool.maxLiquidityPerTick.ToString() + + rpcPool.Slot0SqrtPriceX96 = pool.slot0.sqrtPriceX96.ToString() + rpcPool.Slot0Tick = pool.slot0.tick + rpcPool.Slot0FeeProtocol = pool.slot0.feeProtocol + rpcPool.Slot0Unlocked = pool.slot0.unlocked + + rpcPool.FeeGrowthGlobal0X128 = pool.feeGrowthGlobal0X128.ToString() + rpcPool.FeeGrowthGlobal1X128 = pool.feeGrowthGlobal1X128.ToString() + + rpcPool.Token0ProtocolFee = pool.protocolFees.token0.ToString() + rpcPool.Token1ProtocolFee = pool.protocolFees.token1.ToString() + + rpcPool.Liquidity = pool.liquidity.ToString() + + rpcPool.Ticks = RpcTicks{} + pool.ticks.Iterate("", "", func(tickStr string, iTickInfo any) bool { + tick, err := strconv.ParseInt(tickStr, 10, 32) + if err != nil { + panic(err) + } + tickInfo, ok := iTickInfo.(TickInfo) + if !ok { + panic("failed to cast tick info to TickInfo") + } + + rpcPool.Ticks[int32(tick)] = RpcTickInfo{ + LiquidityGross: tickInfo.liquidityGross.ToString(), + LiquidityNet: tickInfo.liquidityNet.ToString(), + FeeGrowthOutside0X128: tickInfo.feeGrowthOutside0X128.ToString(), + FeeGrowthOutside1X128: tickInfo.feeGrowthOutside1X128.ToString(), + TickCumulativeOutside: tickInfo.tickCumulativeOutside, + SecondsPerLiquidityOutsideX: tickInfo.secondsPerLiquidityOutsideX128.ToString(), + SecondsOutside: tickInfo.secondsOutside, + Initialized: tickInfo.initialized, + } + + return false + }) + + rpcPool.TickBitmaps = RpcTickBitmaps{} + pool.tickBitmaps.Iterate("", "", func(tickStr string, iTickBitmap any) bool { + tick, err := strconv.ParseInt(tickStr, 10, 16) + if err != nil { + panic(err) + } + bm, ok := iTickBitmap.(*u256.Uint) + if !ok { + panic("failed to cast tick bitmap to *u256.Uint") + } + pool.setTickBitmap(int16(tick), bm) + return false + }) + + rpcPositions := []RpcPosition{} + pool.positions.Iterate("", "", func(posKey string, iPositionInfo any) bool { + owner, tickLower, tickUpper := posKeyDivide(posKey) + posInfo, ok := iPositionInfo.(PositionInfo) + if !ok { + panic("failed to cast position info to PositionInfo") + } + + rpcPositions = append(rpcPositions, RpcPosition{ + Owner: owner, + TickLower: tickLower, + TickUpper: tickUpper, + Liquidity: posInfo.liquidity.ToString(), + Token0Owed: posInfo.tokensOwed0.ToString(), + Token1Owed: posInfo.tokensOwed1.ToString(), + }) + + return false + }) + + rpcPool.Positions = rpcPositions + + return rpcPool +} + +func (r RpcPool) JSON() *json.Node { + return makePoolNode(r) +} + +func makePoolNode(pool RpcPool) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "poolPath": json.StringNode("poolPath", pool.PoolPath), + "token0Path": json.StringNode("token0Path", pool.Token0Path), + "token1Path": json.StringNode("token1Path", pool.Token1Path), + "token0Balance": json.StringNode("token0Balance", pool.Token0Balance), + "token1Balance": json.StringNode("token1Balance", pool.Token1Balance), + "fee": json.NumberNode("fee", float64(pool.Fee)), + "tickSpacing": json.NumberNode("tickSpacing", float64(pool.TickSpacing)), + "maxLiquidityPerTick": json.StringNode("maxLiquidityPerTick", pool.MaxLiquidityPerTick), + "sqrtPriceX96": json.StringNode("sqrtPriceX96", pool.Slot0SqrtPriceX96), + "tick": json.NumberNode("tick", float64(pool.Slot0Tick)), + "feeProtocol": json.NumberNode("feeProtocol", float64(pool.Slot0FeeProtocol)), + "unlocked": json.BoolNode("unlocked", pool.Slot0Unlocked), + "feeGrowthGlobal0X128": json.StringNode("feeGrowthGlobal0X128", pool.FeeGrowthGlobal0X128), + "feeGrowthGlobal1X128": json.StringNode("feeGrowthGlobal1X128", pool.FeeGrowthGlobal1X128), + "token0ProtocolFee": json.StringNode("token0ProtocolFee", pool.Token0ProtocolFee), + "token1ProtocolFee": json.StringNode("token1ProtocolFee", pool.Token1ProtocolFee), + "liquidity": json.StringNode("liquidity", pool.Liquidity), + "ticks": pool.Ticks.JSON(), + "tickBitmaps": pool.TickBitmaps.JSON(), + "positions": pool.Positions.JSON(), + }) +} + +type RpcTickInfo struct { + LiquidityGross string `json:"liquidityGross"` + LiquidityNet string `json:"liquidityNet"` + + FeeGrowthOutside0X128 string `json:"feeGrowthOutside0X128"` + FeeGrowthOutside1X128 string `json:"feeGrowthOutside1X128"` + + TickCumulativeOutside int64 `json:"tickCumulativeOutside"` + + SecondsPerLiquidityOutsideX string `json:"secondsPerLiquidityOutsideX"` + SecondsOutside uint32 `json:"secondsOutside"` + + Initialized bool `json:"initialized"` +} + +func (r RpcTickInfo) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "liquidityGross": json.StringNode("liquidityGross", r.LiquidityGross), + "liquidityNet": json.StringNode("liquidityNet", r.LiquidityNet), + "feeGrowthOutside0X128": json.StringNode("feeGrowthOutside0X128", r.FeeGrowthOutside0X128), + "feeGrowthOutside1X128": json.StringNode("feeGrowthOutside1X128", r.FeeGrowthOutside1X128), + "tickCumulativeOutside": json.NumberNode("tickCumulativeOutside", float64(r.TickCumulativeOutside)), + "secondsPerLiquidityOutsideX": json.StringNode("secondsPerLiquidityOutsideX", r.SecondsPerLiquidityOutsideX), + "secondsOutside": json.NumberNode("secondsOutside", float64(r.SecondsOutside)), + "initialized": json.BoolNode("initialized", r.Initialized), + }) +} + +type RpcTickBitmaps map[int16]string // tick(wordPos) => bitmap(tickWord ^ mask) + +func (r RpcTickBitmaps) JSON() *json.Node { + tickBitmapsJson := map[string]*json.Node{} + for tick, tickBitmap := range r { + tickBitmapsJson[strconv.Itoa(int(tick))] = json.StringNode("", tickBitmap) + } + return json.ObjectNode("", tickBitmapsJson) +} + +type RpcTicks map[int32]RpcTickInfo // tick => RpcTickInfo + +func (r RpcTicks) JSON() *json.Node { + ticksJson := map[string]*json.Node{} + for tick, tickInfo := range r { + ticksJson[strconv.Itoa(int(tick))] = tickInfo.JSON() + } + return json.ObjectNode("", ticksJson) +} + +type RpcPositions []RpcPosition + +func (r RpcPositions) JSON() *json.Node { + positionsJson := make([]*json.Node, len(r)) + for i, pos := range r { + positionsJson[i] = pos.JSON() + } + return json.ArrayNode("", positionsJson) +} + +type RpcPosition struct { + Owner string `json:"owner"` + + TickLower int32 `json:"tickLower"` + TickUpper int32 `json:"tickUpper"` + + Liquidity string `json:"liquidity"` + + Token0Owed string `json:"token0Owed"` + Token1Owed string `json:"token1Owed"` +} + +func (r RpcPosition) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "owner": json.StringNode("owner", r.Owner), + "tickLower": json.NumberNode("tickLower", float64(r.TickLower)), + "tickUpper": json.NumberNode("tickUpper", float64(r.TickUpper)), + "liquidity": json.StringNode("liquidity", r.Liquidity), + "token0Owed": json.StringNode("token0Owed", r.Token0Owed), + "token1Owed": json.StringNode("token1Owed", r.Token1Owed), + }) +} + +type statNode struct { + height int64 + timestamp int64 +} + +func newStatNode() statNode { + return statNode{ + height: std.ChainHeight(), + timestamp: time.Now().Unix(), + } +} + +func (s statNode) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "height": json.NumberNode("height", float64(s.height)), + "timestamp": json.NumberNode("timestamp", float64(s.timestamp)), + }) +} diff --git a/contract/r/gnoswap/v1/pool/liquidity_math.gno b/contract/r/gnoswap/v1/pool/liquidity_math.gno new file mode 100644 index 0000000..eee500f --- /dev/null +++ b/contract/r/gnoswap/v1/pool/liquidity_math.gno @@ -0,0 +1,43 @@ +package pool + +import ( + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/p/nt/ufmt" +) + +// liquidityMathAddDelta calculates the new liquidity by applying the delta liquidity to the current liquidity. +// If delta liquidity is negative, it subtracts the absolute value of delta liquidity from the current liquidity. +// If delta liquidity is positive, it adds the absolute value of delta liquidity to the current liquidity. +// Returns the new liquidity as a uint256 value. +func liquidityMathAddDelta(x *u256.Uint, y *i256.Int) *u256.Uint { + if x == nil || y == nil { + panic(newErrorWithDetail( + errInvalidInput, + "x or y is nil", + )) + } + + absDelta := y.Abs() + + // Subtract or add based on the sign of y + if y.IsNeg() { + z := u256.Zero().Sub(x, absDelta) + if z.Gte(x) { + panic(newErrorWithDetail( + errLiquidityCalculation, + ufmt.Sprintf("Condition failed: (z must be < x) (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString()), + )) + } + return z + } + z := u256.Zero().Add(x, absDelta) + if z.Lt(x) { + panic(newErrorWithDetail( + errLiquidityCalculation, + ufmt.Sprintf("Condition failed: (z must be >= x) (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString()), + )) + } + return z +} diff --git a/contract/r/gnoswap/v1/pool/manager.gno b/contract/r/gnoswap/v1/pool/manager.gno new file mode 100644 index 0000000..7301e84 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/manager.gno @@ -0,0 +1,275 @@ +package pool + +import ( + "std" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" + + en "gno.land/r/gnoswap/emission" + pf "gno.land/r/gnoswap/v1/protocol_fee" + + "gno.land/r/gnoswap/gns" +) + +const GNS_PATH string = "gno.land/r/gnoswap/gns" + +var ( + feeAmountTickSpacing = avl.NewTree() // feeBps(uint32) -> tickSpacing(int32) + pools = avl.NewTree() // poolPath -> *Pool + + // slot0FeeProtocol represents the protocol fee percentage (0-10). + // This parameter can be modified through governance. + slot0FeeProtocol uint8 = 0 +) + +func init() { + setFeeAmountTickSpacing(100, 1) // 0.01% + setFeeAmountTickSpacing(500, 10) // 0.05% + setFeeAmountTickSpacing(3000, 60) // 0.3% + setFeeAmountTickSpacing(10000, 200) // 1% +} + +// CreatePool creates a new concentrated liquidity pool. +// +// Deploys new AMM pool for token pair with specified fee tier. +// Charges 100 GNS creation fee to prevent spam. +// Sets initial price and tick spacing based on fee tier. +// +// Parameters: +// - token0Path, token1Path: Token contract paths (ordered by address) +// - fee: Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%) +// - sqrtPriceX96: Initial sqrt price in Q64.96 format +// +// Tick spacing by fee tier: +// - 0.01%: 1 tick +// - 0.05%: 10 ticks +// - 0.30%: 60 ticks +// - 1.00%: 200 ticks +// +// Requirements: +// - Tokens must be different +// - Fee tier must be supported +// - Pool must not already exist +// - Caller must have 100 GNS for creation fee +func CreatePool( + cur realm, + token0Path string, + token1Path string, + fee uint32, + sqrtPriceX96 string, +) { + halt.AssertIsNotHaltedPool() + + assertIsNotEqualsTokens(token0Path, token1Path) + assertIsSupportedFeeTier(fee) + assertIsNotExistsPoolPath(token0Path, token1Path, fee) + + en.MintAndDistributeGns(cross) + + poolInfo := newPoolParams( + token0Path, + token1Path, + fee, + sqrtPriceX96, + GetFeeAmountTickSpacing(fee), + ) + + poolInfo, err := poolInfo.updateWithWrapping() + if err != nil { + panic(err) + } + + // check if wrapped token paths are registered + common.MustRegistered(poolInfo.token0Path) + common.MustRegistered(poolInfo.token1Path) + + pool := newPool(poolInfo) + pools.Set(poolInfo.poolPath(), pool) + + if poolCreationFee > 0 { + gns.TransferFrom(cross, std.PreviousRealm().Address(), protocolFeeAddr, poolCreationFee) + pf.AddToProtocolFee(cross, GNS_PATH, poolCreationFee) + + previousRealm := std.PreviousRealm() + std.Emit( + "PoolCreationFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "poolPath", poolInfo.poolPath(), + "feeTokenPath", GNS_PATH, + "feeAmount", formatInt(poolCreationFee), + ) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "CreatePool", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "token0Path", token0Path, + "token1Path", token1Path, + "fee", formatUint(fee), + "sqrtPriceX96", sqrtPriceX96, + "poolPath", pool.PoolPath(), + "tick", formatInt(pool.Slot0Tick()), + "tickSpacing", formatInt(poolInfo.TickSpacing()), + ) +} + +// SetFeeProtocol sets the protocol fee percentage for all pools. +// +// Parameters: +// - feeProtocol0, feeProtocol1: fee percentages (0-10) +// +// Only callable by admin or governance. +func SetFeeProtocol(cur realm, feeProtocol0, feeProtocol1 uint8) { + halt.AssertIsNotHaltedPool() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + err := setFeeProtocolInternal(feeProtocol0, feeProtocol1, "SetFeeProtocol") + if err != nil { + panic(err) + } +} + +// setFeeAmountTickSpacing associates a tick spacing value with a fee amount. +func setFeeAmountTickSpacing(fee uint32, tickSpacing int32) { + feeStr := formatUint(fee) + feeAmountTickSpacing.Set(feeStr, tickSpacing) +} + +// mustGetPool retrieves a pool instance by its path and ensures it exists. +func mustGetPool(poolPath string) (pool *Pool) { + iPool, exist := pools.Get(poolPath) + if !exist { + panic(newErrorWithDetail( + errDataNotFound, + ufmt.Sprintf("expected poolPath(%s) to exist", poolPath), + )) + } + + pool, ok := iPool.(*Pool) + if !ok { + panic(ufmt.Sprintf("failed to cast pool to *Pool: %T", iPool)) + } + return pool +} + +func mustGetPoolBy(token0Path, token1Path string, fee uint32) *Pool { + poolPath := GetPoolPath(token0Path, token1Path, fee) + return mustGetPool(poolPath) +} + +// setFeeProtocolInternal updates the protocol fee for all pools and emits an event. +func setFeeProtocolInternal(feeProtocol0, feeProtocol1 uint8, eventName string) error { + oldFee := slot0FeeProtocol + newFee, err := setFeeProtocol(feeProtocol0, feeProtocol1) + if err != nil { + return err + } + + feeProtocol0Old := oldFee % 16 + feeProtocol1Old := oldFee >> 4 + + previousRealm := std.PreviousRealm() + std.Emit( + eventName, + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "prevFeeProtocol0", formatUint(feeProtocol0Old), + "prevFeeProtocol1", formatUint(feeProtocol1Old), + "feeProtocol0", formatUint(feeProtocol0), + "feeProtocol1", formatUint(feeProtocol1), + "newFee", formatUint(newFee), + ) + + return nil +} + +// setFeeProtocol updates the protocol fee configuration for all managed pools. +// +// This function combines the protocol fee values for token0 and token1 into a single `uint8` value, +// where: +// - Lower 4 bits store feeProtocol0 (for token0). +// - Upper 4 bits store feeProtocol1 (for token1). +// +// The updated fee protocol is applied uniformly to all pools managed by the system. +// +// Parameters: +// - feeProtocol0: protocol fee for token0 (must be 0 or between 4 and 10 inclusive). +// - feeProtocol1: protocol fee for token1 (must be 0 or between 4 and 10 inclusive). +// +// Returns: +// - newFee (uint8): the combined fee protocol value. +// +// Example: +// If feeProtocol0 = 4 and feeProtocol1 = 5: +// +// newFee = 4 + (5 << 4) +// // Results in: 0x54 (84 in decimal) +// // Binary: 0101 0100 +// // ^^^^ ^^^^ +// // fee1=5 fee0=4 +// +// Notes: +// - This function ensures that all pools under management are updated to use the same fee protocol. +// - Caller restrictions (e.g., admin or governance) are not enforced in this function. +// - Ensure the system is not halted before updating fees. +func setFeeProtocol(feeProtocol0, feeProtocol1 uint8) (uint8, error) { + if err := validateFeeProtocol(feeProtocol0, feeProtocol1); err != nil { + return 0, err + } + + // combine both protocol fee into a single byte: + // - feePrtocol0 occupies the lower 4 bits + // - feeProtocol1 is shifted the lower 4 positions to occupy the upper 4 bits + newFee := feeProtocol0 + (feeProtocol1 << 4) // ( << 4 ) = ( * 16 ) + + // Update slot0 for each pool + pools.Iterate("", "", func(poolPath string, iPool any) bool { + pool, ok := iPool.(*Pool) + if !ok { + panic("failed to cast pool to *Pool") + } + pool.slot0.feeProtocol = newFee + + return false + }) + + // update slot0 + slot0FeeProtocol = newFee + return newFee, nil +} + +// validateFeeProtocol validates the fee protocol values for token0 and token1. +// +// This function checks whether the provided fee protocol values (`feeProtocol0` and `feeProtocol1`) +// are valid using the `isValidFeeProtocolValue` function. If either value is invalid, it returns +// an error indicating that the protocol fee percentage is invalid. +// +// Parameters: +// - feeProtocol0: uint8, the fee protocol value for token0. +// - feeProtocol1: uint8, the fee protocol value for token1. +// +// Returns: +// - error: Returns `errInvalidProtocolFeePct` if either `feeProtocol0` or `feeProtocol1` is invalid. +// Returns `nil` if both values are valid. +func validateFeeProtocol(feeProtocol0, feeProtocol1 uint8) error { + if !isValidFeeProtocolValue(feeProtocol0) || !isValidFeeProtocolValue(feeProtocol1) { + return errInvalidProtocolFeePct + } + return nil +} + +// isValidFeeProtocolValue checks if a fee protocol value is within acceptable range. +// valid values are either 0 or between 4 and 10 inclusive. +func isValidFeeProtocolValue(value uint8) bool { + return value == 0 || (value >= 4 && value <= 10) +} diff --git a/contract/r/gnoswap/v1/pool/pool.gno b/contract/r/gnoswap/v1/pool/pool.gno new file mode 100644 index 0000000..7b33342 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/pool.gno @@ -0,0 +1,364 @@ +package pool + +import ( + "std" + + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + prabc "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoswap/access" +) + +var ( + positionAddr, _ = access.GetAddress(prabc.ROLE_POSITION.String()) + poolAddr, _ = access.GetAddress(prabc.ROLE_POOL.String()) + protocolFeeAddr, _ = access.GetAddress(prabc.ROLE_PROTOCOL_FEE.String()) + routerAddr, _ = access.GetAddress(prabc.ROLE_ROUTER.String()) +) + +// Mint adds liquidity to a pool position. +// +// Increases liquidity for a position within specified tick range. +// Calculates required token amounts based on current pool price. +// Updates tick state and transfers tokens atomically. +// +// Parameters: +// - token0Path, token1Path: Token contract paths +// - fee: Fee tier (100, 500, 3000, 10000 = 0.01%, 0.05%, 0.3%, 1%) +// - recipient: Position owner address +// - tickLower, tickUpper: Price range boundaries (must be tick-aligned) +// - liquidityAmount: Liquidity to add (Q128 format) +// - positionCaller: Original caller for token transfers +// +// Returns: +// - amount0: Token0 amount consumed (decimal string) +// - amount1: Token1 amount consumed (decimal string) +// +// Requirements: +// - Pool must exist for token pair and fee +// - Liquidity amount must be positive +// - Ticks must be valid and aligned to spacing +// +// Only callable by position contract. +func Mint( + cur realm, + token0Path string, + token1Path string, + fee uint32, + recipient std.Address, + tickLower int32, + tickUpper int32, + liquidityAmount string, + positionCaller std.Address, +) (string, string) { + halt.AssertIsNotHaltedPool() + + caller := std.PreviousRealm().Address() + access.AssertIsPosition(caller) + access.AssertIsValidAddress(positionCaller) + + liquidity := u256.MustFromDecimal(liquidityAmount) + if liquidity.IsZero() { + panic(errZeroLiquidity) + } + + pool := mustGetPoolBy(token0Path, token1Path, fee) + liquidityDelta := safeConvertToInt128(liquidity) + positionParam := newModifyPositionParams(positionCaller, tickLower, tickUpper, liquidityDelta) + _, amount0, amount1, err := pool.modifyPosition(positionParam) + if err != nil { + panic(err) + } + + if amount0.Gt(zero) { + pool.safeTransferFrom(positionCaller, poolAddr, pool.token0Path, amount0, true) + } + + if amount1.Gt(zero) { + pool.safeTransferFrom(positionCaller, poolAddr, pool.token1Path, amount1, false) + } + + return amount0.ToString(), amount1.ToString() +} + +// Burn removes liquidity from a position. +// +// Decreases liquidity and calculates tokens owed to position owner. +// Updates tick state but doesn't transfer tokens (use Collect). +// Two-step process prevents reentrancy attacks. +// +// Parameters: +// - token0Path, token1Path: Token contract paths +// - fee: Fee tier matching the pool +// - tickLower, tickUpper: Position's price range +// - liquidityAmount: Liquidity to remove (uint128) +// - positionCaller: Position owner for validation +// +// Returns: +// - amount0: Token0 owed to position (uint256) +// - amount1: Token1 owed to position (uint256) +// +// Note: Tokens remain in pool until Collect is called. +// Only callable by position contract. +func Burn( + cur realm, + token0Path string, + token1Path string, + fee uint32, + tickLower int32, + tickUpper int32, + liquidityAmount string, // uint128 + positionCaller std.Address, +) (string, string) { // uint256 x2 + halt.AssertIsNotHaltedPool() + + caller := std.PreviousRealm().Address() + access.AssertIsPosition(caller) + access.AssertIsValidAddress(positionCaller) + + liqAmount := u256.MustFromDecimal(liquidityAmount) + liqAmountInt256 := safeConvertToInt128(liqAmount) + liqDelta := i256.Zero().Neg(liqAmountInt256) + + posParams := newModifyPositionParams(positionCaller, tickLower, tickUpper, liqDelta) + pool := mustGetPoolBy(token0Path, token1Path, fee) + position, amount0, amount1, err := pool.modifyPosition(posParams) + if err != nil { + panic(err) + } + + if amount0.Gt(zero) || amount1.Gt(zero) { + amount0 = toUint128(amount0) + amount1 = toUint128(amount1) + + tokensOwed0, overflow := u256.Zero().AddOverflow(position.tokensOwed0, amount0) + if overflow { + panic(errOverFlow) + } + position.tokensOwed0 = tokensOwed0 + + tokensOwed1, overflow := u256.Zero().AddOverflow(position.tokensOwed1, amount1) + if overflow { + panic(errOverFlow) + } + position.tokensOwed1 = tokensOwed1 + } + + positionKey, err := getPositionKey(tickLower, tickUpper) + if err != nil { + panic(err) + } + + pool.setPosition(positionKey, position) + + // mustGetPosition() is called to ensure the position exists + pool.mustGetPosition(positionKey) + + // actual token transfer happens in Collect() + return amount0.ToString(), amount1.ToString() +} + +// Collect transfers owed tokens from a position to recipient. +// +// Claims tokens from burned liquidity and accumulated fees. +// Applies protocol withdrawal fee (1% default) before transfer. +// Supports partial collection via amount limits. +// +// Parameters: +// - token0Path, token1Path: Token contract paths +// - fee: Fee tier of the pool +// - recipient: Address to receive tokens +// - tickLower, tickUpper: Position's price range +// - amount0Requested, amount1Requested: Max amounts to collect (use MAX_UINT128 for all) +// +// Returns: +// - amount0: Token0 actually collected (after fees) +// - amount1: Token1 actually collected (after fees) +// +// Protocol fees: 1% on collected amounts. +// Only callable by position contract. +func Collect( + cur realm, + token0Path string, + token1Path string, + fee uint32, + recipient std.Address, + tickLower int32, + tickUpper int32, + amount0Requested string, + amount1Requested string, +) (string, string) { + halt.AssertIsNotHaltedPool() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + access.AssertIsPosition(caller) + access.AssertIsValidAddress(recipient) + + pool := mustGetPoolBy(token0Path, token1Path, fee) + // Use recipient address instead of getPrevAddr() for position key generation + // Because positions are created with the recipient's address in Mint function, + // and we need to access the same position that was originally created. + // GetPrevAddr() would return the position contract address, but the actual + // position is stored under the recipient's address key. + positionKey, err := getPositionKey(tickLower, tickUpper) + if err != nil { + panic(err) + } + + position := pool.mustGetPosition(positionKey) + + var amount0, amount1 *u256.Uint + + // Smallest of three: amount0Requested, position.tokensOwed0, pool.balances.token0 + amount0Req := u256.MustFromDecimal(amount0Requested) + amount0 = collectToken(amount0Req, position.tokensOwed0, pool.BalanceToken0()) + + amount1Req := u256.MustFromDecimal(amount1Requested) + amount1 = collectToken(amount1Req, position.tokensOwed1, pool.BalanceToken1()) + + if amount0.Gt(u256.Zero()) { + tokenOwed0, overflow := u256.Zero().SubOverflow(position.tokensOwed0, amount0) + if overflow { + panic(errOverFlow) + } + + token0Balance, overflow := u256.Zero().SubOverflow(pool.balances.token0, amount0) + if overflow { + panic(errOverFlow) + } + + position.tokensOwed0 = tokenOwed0 + pool.balances.token0 = token0Balance + if err := common.Approve(cross, pool.token0Path, positionAddr, safeConvertToInt64(amount0)); err != nil { + panic(err) + } + } + if amount1.Gt(u256.Zero()) { + position.tokensOwed1 = u256.Zero().Sub(position.tokensOwed1, amount1) + pool.balances.token1 = u256.Zero().Sub(pool.balances.token1, amount1) + + if err := common.Approve(cross, pool.token1Path, positionAddr, safeConvertToInt64(amount1)); err != nil { + panic(err) + } + } + + pool.setPosition(positionKey, position) + + return amount0.ToString(), amount1.ToString() +} + +// collectToken calculates the actual amount of tokens that can be collected. +// It returns the minimum of: requested amount, tokens owed, and pool balance. +// This ensures collection never exceeds available funds. +func collectToken( + amountReq, tokensOwed, poolBalance *u256.Uint, +) (amount *u256.Uint) { + // find smallest of three amounts + amount = u256Min(amountReq, tokensOwed) + amount = u256Min(amount, poolBalance) + return amount.Clone() +} + +// CollectProtocol collects accumulated protocol fees from swap operations. +// Only callable by admin or governance. +// Returns amount0, amount1 representing protocol fees collected. +func CollectProtocol( + cur realm, + token0Path string, + token1Path string, + fee uint32, + recipient std.Address, + amount0Requested string, // uint128 + amount1Requested string, // uint128 +) (string, string) { + halt.AssertIsNotHaltedPool() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + common.MustRegistered(token0Path) + common.MustRegistered(token1Path) + + amount0, amount1 := collectProtocol( + token0Path, + token1Path, + fee, + recipient, + amount0Requested, + amount1Requested, + ) + + previousRealm := std.PreviousRealm() + std.Emit( + "CollectProtocol", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "token0Path", token0Path, + "token1Path", token1Path, + "fee", formatUint(fee), + "recipient", recipient.String(), + "internal_amount0", amount0, + "internal_amount1", amount1, + ) + + return amount0, amount1 +} + +// collectProtocol performs the actual protocol fee collection. +// It ensures requested amounts don't exceed available protocol fees. +// Returns amount0, amount1 as strings representing collected fees. +func collectProtocol( + token0Path string, + token1Path string, + fee uint32, + recipient std.Address, + amount0Requested string, + amount1Requested string, +) (string, string) { + pool := mustGetPoolBy(token0Path, token1Path, fee) + + amount0Req := u256.MustFromDecimal(amount0Requested) + amount1Req := u256.MustFromDecimal(amount1Requested) + if amount0Req.IsZero() && amount1Req.IsZero() { + return "0", "0" + } + + amount0 := u256Min(amount0Req, pool.ProtocolFeesToken0()) + amount1 := u256Min(amount1Req, pool.ProtocolFeesToken1()) + + amount0, amount1 = pool.saveProtocolFees(amount0.Clone(), amount1.Clone()) + uAmount0 := safeConvertToInt64(amount0) + uAmount1 := safeConvertToInt64(amount1) + + checkTransferError(common.Transfer(cross, pool.token0Path, recipient, uAmount0)) + newBalanceToken0, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount0, true) + if err != nil { + panic(err) + } + pool.balances.token0 = newBalanceToken0 + + checkTransferError(common.Transfer(cross, pool.token1Path, recipient, uAmount1)) + newBalanceToken1, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount1, false) + if err != nil { + panic(err) + } + pool.balances.token1 = newBalanceToken1 + + return amount0.ToString(), amount1.ToString() +} + +// saveProtocolFees updates the protocol fee balances after collection. +// Returns amount0, amount1 representing the fees deducted from protocol reserves. +func (p *Pool) saveProtocolFees(amount0, amount1 *u256.Uint) (*u256.Uint, *u256.Uint) { + p.protocolFees.token0 = u256.Zero().Sub(p.ProtocolFeesToken0(), amount0) + p.protocolFees.token1 = u256.Zero().Sub(p.ProtocolFeesToken1(), amount1) + + return amount0, amount1 +} diff --git a/contract/r/gnoswap/v1/pool/pool_type.gno b/contract/r/gnoswap/v1/pool/pool_type.gno new file mode 100644 index 0000000..cc2a0ab --- /dev/null +++ b/contract/r/gnoswap/v1/pool/pool_type.gno @@ -0,0 +1,272 @@ +package pool + +import ( + "strconv" + "strings" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +type Balances struct { + // current balance of the pool in token0/token1 + token0 *u256.Uint + token1 *u256.Uint +} + +func newBalances() Balances { + return Balances{ + token0: u256.Zero(), + token1: u256.Zero(), + } +} + +type Slot0 struct { + sqrtPriceX96 *u256.Uint // current price of the pool as a sqrt(token1/token0) Q96 value + tick int32 // current tick of the pool, i.e according to the last tick transition that was run + feeProtocol uint8 // protocol fee for both tokens of the pool + unlocked bool // whether the pool is currently locked to reentrancy +} + +func (s *Slot0) Tick() int32 { return s.tick } +func (s *Slot0) FeeProtocol() uint8 { return s.feeProtocol } + +func newSlot0( + sqrtPriceX96 *u256.Uint, + tick int32, + feeProtocol uint8, + unlocked bool, +) Slot0 { + return Slot0{ + sqrtPriceX96: sqrtPriceX96, + tick: tick, + feeProtocol: feeProtocol, + unlocked: unlocked, + } +} + +type ProtocolFees struct { + // current protocol fees of the pool in token0/token1 + token0 *u256.Uint + token1 *u256.Uint +} + +func newProtocolFees() ProtocolFees { + return ProtocolFees{ + token0: u256.Zero(), + token1: u256.Zero(), + } +} + +// type Pool describes a single Pool's state +// A pool is identificed with a unique key (token0, token1, fee), where token0 < token1 +type Pool struct { + // token0/token1 path of the pool + token0Path string + token1Path string + fee uint32 // fee tier of the pool + tickSpacing int32 // spacing between ticks + slot0 Slot0 + balances Balances // balances of the pool + protocolFees ProtocolFees + maxLiquidityPerTick *u256.Uint // the maximum amount of liquidity that can be added per tick + feeGrowthGlobal0X128 *u256.Uint // uint256 + feeGrowthGlobal1X128 *u256.Uint // uint256 + liquidity *u256.Uint // total amount of liquidity in the pool + ticks *avl.Tree // tick(int32) -> TickInfo + tickBitmaps *avl.Tree // tick(wordPos)(int16) -> bitMap(tickWord ^ mask)(*u256.Uint) + positions *avl.Tree // maps the key (caller, lower tick, upper tick) to a unique position + + observation *Observation +} + +func (p *Pool) PoolPath() string { return GetPoolPath(p.token0Path, p.token1Path, p.fee) } +func (p *Pool) Token0Path() string { return p.token0Path } +func (p *Pool) Token1Path() string { return p.token1Path } +func (p *Pool) Fee() uint32 { return p.fee } +func (p *Pool) BalanceToken0() *u256.Uint { return p.balances.token0 } +func (p *Pool) BalanceToken1() *u256.Uint { return p.balances.token1 } +func (p *Pool) TickSpacing() int32 { return p.tickSpacing } +func (p *Pool) MaxLiquidityPerTick() *u256.Uint { return p.maxLiquidityPerTick } +func (p *Pool) Slot0() Slot0 { return p.slot0 } +func (p *Pool) Slot0SqrtPriceX96() *u256.Uint { return p.slot0.sqrtPriceX96 } +func (p *Pool) Slot0Tick() int32 { return p.slot0.tick } +func (p *Pool) Slot0FeeProtocol() uint8 { return p.slot0.feeProtocol } +func (p *Pool) Slot0Unlocked() bool { return p.slot0.unlocked } +func (p *Pool) FeeGrowthGlobal0X128() *u256.Uint { return p.feeGrowthGlobal0X128 } +func (p *Pool) FeeGrowthGlobal1X128() *u256.Uint { return p.feeGrowthGlobal1X128 } +func (p *Pool) ProtocolFeesToken0() *u256.Uint { return p.protocolFees.token0 } +func (p *Pool) ProtocolFeesToken1() *u256.Uint { return p.protocolFees.token1 } +func (p *Pool) Liquidity() *u256.Uint { return p.liquidity } +func (p *Pool) Observation() *Observation { return p.observation } + +func (p *Pool) Ticks() string { + if p.ticks == nil { + return "[]" + } + + tickInfoStrings := []string{} + + p.ticks.Iterate("", "", func(tickKey string, tickValue any) bool { + tick, err := strconv.ParseInt(tickKey, 10, 32) + if err != nil { + panic(err) + } + tickInfo, ok := tickValue.(TickInfo) + if !ok { + panic("failed to cast tick info to TickInfo") + } + + tickInfoStrings = append(tickInfoStrings, ufmt.Sprintf( + `{"tick":%d,"feeGrowthOutside0X128":"%s","feeGrowthOutside1X128":"%s"}`, + tick, + tickInfo.feeGrowthOutside0X128.ToString(), + tickInfo.feeGrowthOutside1X128.ToString(), + )) + + return false + }) + + return "[" + strings.Join(tickInfoStrings, ",") + "]" +} + +func (p *Pool) Clone() *Pool { + ticks := avl.NewTree() + tickBitmaps := avl.NewTree() + positions := avl.NewTree() + + // clone ticks + p.ticks.Iterate("", "", func(tickKey string, tickValue any) bool { + tickInfo, ok := tickValue.(TickInfo) + if !ok { + panic(ufmt.Sprintf("failed to cast tickValue to TickInfo: %T", tickValue)) + } + + ticks.Set(tickKey, TickInfo{ + feeGrowthOutside0X128: u256.Zero().Set(tickInfo.feeGrowthOutside0X128), + feeGrowthOutside1X128: u256.Zero().Set(tickInfo.feeGrowthOutside1X128), + liquidityGross: u256.Zero().Set(tickInfo.liquidityGross), + liquidityNet: i256.Zero().Set(tickInfo.liquidityNet), + tickCumulativeOutside: tickInfo.tickCumulativeOutside, + secondsPerLiquidityOutsideX128: u256.Zero().Set(tickInfo.secondsPerLiquidityOutsideX128), + secondsOutside: tickInfo.secondsOutside, + initialized: tickInfo.initialized, + }) + return false + }) + + // clone tickBitmaps + p.tickBitmaps.Iterate("", "", func(tickKey string, tickValue any) bool { + tickBitmap, ok := tickValue.(*u256.Uint) + if !ok { + panic(ufmt.Sprintf("failed to cast tickValue to *u256.Uint: %T", tickValue)) + } + tickBitmaps.Set(tickKey, u256.Zero().Set(tickBitmap)) + return false + }) + + // clone positions + p.positions.Iterate("", "", func(positionKey string, positionValue any) bool { + positionInfo, ok := positionValue.(PositionInfo) + if !ok { + panic(ufmt.Sprintf("failed to cast positionValue to PositionInfo: %T", positionValue)) + } + positions.Set(positionKey, PositionInfo{ + liquidity: u256.Zero().Set(positionInfo.liquidity), + feeGrowthInside0LastX128: u256.Zero().Set(positionInfo.feeGrowthInside0LastX128), + feeGrowthInside1LastX128: u256.Zero().Set(positionInfo.feeGrowthInside1LastX128), + tokensOwed0: u256.Zero().Set(positionInfo.tokensOwed0), + tokensOwed1: u256.Zero().Set(positionInfo.tokensOwed1), + }) + return false + }) + + return &Pool{ + token0Path: p.token0Path, + token1Path: p.token1Path, + fee: p.fee, + tickSpacing: p.tickSpacing, + slot0: Slot0{ + sqrtPriceX96: u256.Zero().Set(p.slot0.sqrtPriceX96), + tick: p.slot0.tick, + feeProtocol: p.slot0.feeProtocol, + unlocked: p.slot0.unlocked, + }, + balances: Balances{ + token0: u256.Zero().Set(p.balances.token0), + token1: u256.Zero().Set(p.balances.token1), + }, + protocolFees: ProtocolFees{ + token0: u256.Zero().Set(p.protocolFees.token0), + token1: u256.Zero().Set(p.protocolFees.token1), + }, + maxLiquidityPerTick: u256.Zero().Set(p.maxLiquidityPerTick), + feeGrowthGlobal0X128: u256.Zero().Set(p.feeGrowthGlobal0X128), + feeGrowthGlobal1X128: u256.Zero().Set(p.feeGrowthGlobal1X128), + liquidity: u256.Zero().Set(p.liquidity), + ticks: ticks, + tickBitmaps: tickBitmaps, + positions: positions, + observation: &Observation{ + lastCumulativeUpdateTime: p.observation.lastCumulativeUpdateTime, + tickCumulative: p.observation.tickCumulative, + liquidityCumulative: u256.Zero().Set(p.observation.liquidityCumulative), + }, + } +} + +func (p *Pool) calculateTickCumulative(currentTime int64) (int64, *u256.Uint, int64) { + // calculate time delta + observation := p.Observation() + timeDelta := currentTime - observation.lastCumulativeUpdateTime + if timeDelta <= 0 { + return observation.tickCumulative, observation.liquidityCumulative, observation.lastCumulativeUpdateTime + } + + // update cumulative values (only price is used) + // sqrtPriceCumulativeX96 = sqrtPriceCumulativeX96 + sqrtPriceX96 * timeDelta + tickDelta := int64(p.slot0.Tick()) * timeDelta + + // liquidityCumulative = liquidityCumulative + liquidity * timeDelta + liquidityDelta := u256.Zero().Mul(p.liquidity, u256.NewUintFromInt64(timeDelta)) + + tickCumulative := observation.tickCumulative + tickDelta + liquidityCumulative := u256.Zero().Add(observation.liquidityCumulative, liquidityDelta) + lastCumulativeUpdateTime := currentTime + + return tickCumulative, liquidityCumulative, lastCumulativeUpdateTime +} + +// updatePriceCumulatives updates cumulative price values when pool price changes +// This should be called whenever the pool's price changes (swap, mint, burn) +func (p *Pool) updatePriceCumulatives(currentTime int64) { + if currentTime < p.observation.lastCumulativeUpdateTime { + return + } + + tickCumulative, liquidityCumulative, lastCumulativeUpdateTime := p.calculateTickCumulative(currentTime) + + p.observation.liquidityCumulative = liquidityCumulative + p.observation.tickCumulative = tickCumulative + p.observation.lastCumulativeUpdateTime = lastCumulativeUpdateTime +} + +type Observation struct { + lastCumulativeUpdateTime int64 // last cumulative update time (in seconds) + tickCumulative int64 // cumulative tick + liquidityCumulative *u256.Uint // cumulative liquidity (time-weighted average calculation) +} + +func (t *Observation) LastCumulativeUpdateTime() int64 { return t.lastCumulativeUpdateTime } +func (t *Observation) TickCumulative() int64 { return t.tickCumulative } +func (t *Observation) LiquidityCumulative() string { return t.liquidityCumulative.ToString() } + +func newObservation(currentTime int64) *Observation { + return &Observation{ + lastCumulativeUpdateTime: currentTime, + tickCumulative: 0, + liquidityCumulative: u256.Zero(), + } +} diff --git a/contract/r/gnoswap/v1/pool/position.gno b/contract/r/gnoswap/v1/pool/position.gno new file mode 100644 index 0000000..5b1f330 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/position.gno @@ -0,0 +1,397 @@ +package pool + +import ( + "encoding/base64" + + "gno.land/p/nt/ufmt" + plp "gno.land/p/gnoswap/gnsmath" + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v1/common" +) + +const positionPackagePath = "gno.land/r/gnoswap/v1/position" + +var convertedQ128 = u256.MustFromDecimal(Q128) + +// getPositionKey generates a unique base64-encoded key for a liquidity position. +// +// Creates deterministic identifier for position tracking. +// Ensures unique positions per owner and price range. +// Used internally for position state management. +// +// Parameters: +// - tickLower: Lower boundary tick of position range +// - tickUpper: Upper boundary tick of position range +// +// Key Format: +// +// base64(position_package_path + tickLower_bytes + tickUpper_bytes) +// +// Requirements: +// - tickLower < tickUpper +// - Both ticks within valid range [-887272, 887272] +// +// Returns base64-encoded position key or error. +// Combines position package path, tickLower, and tickUpper. +func getPositionKey( + tickLower int32, + tickUpper int32, +) (string, error) { + if err := validateTicks(tickLower, tickUpper); err != nil { + return "", err + } + + positionKey := ufmt.Sprintf("%s__%d__%d", positionPackagePath, tickLower, tickUpper) + encodedPositionKey := base64.StdEncoding.EncodeToString([]byte(positionKey)) + return encodedPositionKey, nil +} + +// positionUpdate updates a position's liquidity and calculates fees owed. +// Returns the updated position information and any error. +func positionUpdate( + position PositionInfo, + liquidityDelta *i256.Int, + feeGrowthInside0X128 *u256.Uint, + feeGrowthInside1X128 *u256.Uint, +) (PositionInfo, error) { + position.valueOrZero() + + if position.liquidity.IsZero() && liquidityDelta.IsZero() { + return PositionInfo{}, makeErrorWithDetails( + errZeroLiquidity, + "both liquidityDelta and current position's liquidity are zero", + ) + } + + // check negative liquidity + if liquidityDelta.IsNeg() { + // absolute value of negative liquidity delta must be less than current liquidity + absDelta := i256.Zero().Set(liquidityDelta).Abs() + currentLiquidity := position.liquidity + if absDelta.Gt(currentLiquidity) { + return PositionInfo{}, makeErrorWithDetails( + errZeroLiquidity, + ufmt.Sprintf("liquidity delta(%s) is greater than current liquidity(%s)", + liquidityDelta.ToString(), position.liquidity.ToString()), + ) + } + } + + var liquidityNext *u256.Uint + if liquidityDelta.IsZero() { + liquidityNext = position.liquidity + } else { + liquidityNext = liquidityMathAddDelta(position.liquidity, liquidityDelta) + } + + // validate negative feeGrowth before calculation + diff0 := u256.Zero().Sub(feeGrowthInside0X128, position.feeGrowthInside0LastX128) + diff1 := u256.Zero().Sub(feeGrowthInside1X128, position.feeGrowthInside1LastX128) + + // calculate tokensOwed + tokensOwed0 := u256.Zero() + if !diff0.IsZero() { + tokensOwed0 = u256.MulDiv(diff0, position.liquidity, convertedQ128) + } + + tokensOwed1 := u256.Zero() + if !diff1.IsZero() { + tokensOwed1 = u256.MulDiv(diff1, position.liquidity, convertedQ128) + } + + if !liquidityDelta.IsZero() { + position.liquidity = liquidityNext + } + + position.feeGrowthInside0LastX128 = feeGrowthInside0X128 + position.feeGrowthInside1LastX128 = feeGrowthInside1X128 + + // add tokensOwed only when it's greater than 0 + if tokensOwed0.Gt(zero) || tokensOwed1.Gt(zero) { + owed0, overflow := u256.Zero().AddOverflow(position.tokensOwed0, tokensOwed0) + if overflow { + return PositionInfo{}, errOverFlow + } + owed1, overflow := u256.Zero().AddOverflow(position.tokensOwed1, tokensOwed1) + if overflow { + return PositionInfo{}, errOverFlow + } + + position.tokensOwed0 = owed0 + position.tokensOwed1 = owed1 + } + + return position, nil +} + +// calculateToken0Amount calculates the amount of token0 based on price range and liquidity delta. +func calculateToken0Amount(sqrtPriceLower, sqrtPriceUpper *u256.Uint, liquidityDelta *i256.Int) *i256.Int { + return plp.GetAmount0Delta(sqrtPriceLower, sqrtPriceUpper, liquidityDelta) +} + +// calculateToken1Amount calculates the amount of token1 based on price range and liquidity delta. +func calculateToken1Amount(sqrtPriceLower, sqrtPriceUpper *u256.Uint, liquidityDelta *i256.Int) *i256.Int { + return plp.GetAmount1Delta(sqrtPriceLower, sqrtPriceUpper, liquidityDelta) +} + +// PositionLiquidity returns the liquidity of a position. +func (p *Pool) PositionLiquidity(key string) *u256.Uint { + return p.mustGetPosition(key).liquidity +} + +// PositionFeeGrowthInside0LastX128 returns the fee growth of token0 inside a position. +func (p *Pool) PositionFeeGrowthInside0LastX128(key string) *u256.Uint { + return p.mustGetPosition(key).feeGrowthInside0LastX128 +} + +// PositionFeeGrowthInside1LastX128 returns the fee growth of token1 inside a position. +func (p *Pool) PositionFeeGrowthInside1LastX128(key string) *u256.Uint { + return p.mustGetPosition(key).feeGrowthInside1LastX128 +} + +// PositionTokensOwed0 returns the amount of token0 owed by a position. +func (p *Pool) PositionTokensOwed0(key string) *u256.Uint { + return p.mustGetPosition(key).tokensOwed0 +} + +// PositionTokensOwed1 returns the amount of token1 owed by a position. +func (p *Pool) PositionTokensOwed1(key string) *u256.Uint { + return p.mustGetPosition(key).tokensOwed1 +} + +// GetPosition returns the position info for a given key. +func (p *Pool) GetPosition(key string) (PositionInfo, bool) { + iPositionInfo, exist := p.positions.Get(key) + if !exist { + newPosition := PositionInfo{} + newPosition.valueOrZero() + return newPosition, false + } + + positionInfo, ok := iPositionInfo.(PositionInfo) + if !ok { + panic(ufmt.Sprintf("failed to cast iPositionInfo to PositionInfo: %T", iPositionInfo)) + } + return positionInfo, true +} + +// positionUpdateWithKey updates a position in the pool and returns the updated position. +func (p *Pool) positionUpdateWithKey( + positionKey string, + liquidityDelta *i256.Int, + feeGrowthInside0X128, feeGrowthInside1X128 *u256.Uint, +) (PositionInfo, error) { + // if pointer is nil, set to zero for calculation + liquidityDelta = liquidityDelta.NilToZero() + feeGrowthInside0X128 = feeGrowthInside0X128.NilToZero() + feeGrowthInside1X128 = feeGrowthInside1X128.NilToZero() + + // if position does not exist, create a new position + positionToUpdate, _ := p.GetPosition(positionKey) + positionAfterUpdate, err := positionUpdate(positionToUpdate, liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128) + if err != nil { + return PositionInfo{}, err + } + + p.setPosition(positionKey, positionAfterUpdate) + + return positionAfterUpdate, nil +} + +// setPosition sets the position info for a given key. +func (p *Pool) setPosition(posKey string, positionInfo PositionInfo) { + p.positions.Set(posKey, positionInfo) +} + +// mustGetPosition returns the position info for a given key. +func (p *Pool) mustGetPosition(positionKey string) PositionInfo { + positionInfo, exist := p.GetPosition(positionKey) + if !exist { + panic(newErrorWithDetail( + errDataNotFound, + ufmt.Sprintf("positionKey(%s) does not exist", positionKey), + )) + } + return positionInfo +} + +// modifyPosition updates a position in the pool and calculates the amount of tokens +// needed (for minting) or returned (for burning). The calculation depends on the current +// price (tick) relative to the position's price range. +// +// The function handles three cases: +// 1. Current price below range (tick < tickLower): only token0 is used/returned +// 2. Current price in range (tickLower <= tick < tickUpper): both tokens are used/returned +// 3. Current price above range (tick >= tickUpper): only token1 is used/returned +// +// Parameters: +// - params: ModifyPositionParams containing owner, tickLower, tickUpper, and liquidityDelta +// +// Returns: +// - PositionInfo: updated position information +// - *u256.Uint: amount of token0 needed/returned +// - *u256.Uint: amount of token1 needed/returned +func (p *Pool) modifyPosition(params ModifyPositionParams) (PositionInfo, *u256.Uint, *u256.Uint, error) { + if err := validateTicks(params.tickLower, params.tickUpper); err != nil { + return PositionInfo{}, zero, zero, err + } + + // get current state and price bounds + tick := p.Slot0Tick() + // update position state + position, err := p.updatePosition(params, tick) + if err != nil { + return PositionInfo{}, zero, zero, err + } + + liqDelta := params.liquidityDelta + + amount0, amount1 := i256.Zero(), i256.Zero() + + // covert ticks to sqrt price to use in amount calculations + // price = 1.0001^tick, but we use sqrtPriceX96 + sqrtRatioLower := common.TickMathGetSqrtRatioAtTick(params.tickLower) + sqrtRatioUpper := common.TickMathGetSqrtRatioAtTick(params.tickUpper) + sqrtPriceX96 := p.Slot0SqrtPriceX96() + + // calculate token amounts based on current price position relative to range + switch { + case tick < params.tickLower: + // case 1 + // full range between lower and upper tick is used for token0 + // current tick is below the passed range; liquidity can only become in range by crossing from left to + // right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it + amount0 = calculateToken0Amount(sqrtRatioLower, sqrtRatioUpper, liqDelta) + + case tick < params.tickUpper: + // case 2 + liquidityBefore := p.liquidity + // token0 used from current price to upper tick + amount0 = calculateToken0Amount(sqrtPriceX96, sqrtRatioUpper, liqDelta) + // token1 used from lower tick to current price + amount1 = calculateToken1Amount(sqrtRatioLower, sqrtPriceX96, liqDelta) + // update pool's active liquidity since price is in range + p.liquidity = liquidityMathAddDelta(liquidityBefore, liqDelta) + + default: + // case 3 + // full range between lower and upper tick is used for token1 + // current tick is above the passed range; liquidity can only become in range by crossing from right to + // left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it + amount1 = calculateToken1Amount(sqrtRatioLower, sqrtRatioUpper, liqDelta) + } + + return position, amount0.Abs(), amount1.Abs(), nil +} + +// updatePosition modifies the position's liquidity and updates the corresponding tick states. +// +// This function updates the position data based on the specified liquidity delta and tick range. +// It also manages the fee growth, tick state flipping, and cleanup of unused tick data. +// +// Parameters: +// - positionParams: ModifyPositionParams, the parameters for the position modification, which include: +// - owner: The address of the position owner. +// - tickLower: The lower tick boundary of the position. +// - tickUpper: The upper tick boundary of the position. +// - liquidityDelta: The change in liquidity (positive or negative). +// - tick: int32, the current tick position. +// +// Returns: +// - PositionInfo: The updated position information. +// +// Workflow: +// 1. Clone the global fee growth values (token 0 and token 1). +// 2. If the liquidity delta is non-zero: +// - Update the lower and upper ticks using `tickUpdate`, flipping their states if necessary. +// - If a tick's state was flipped, update the tick bitmap to reflect the new state. +// 3. Calculate the fee growth inside the tick range using `getFeeGrowthInside`. +// 4. Generate a unique position key and update the position data using `positionUpdateWithKey`. +// 5. If liquidity is being removed (negative delta), clean up unused tick data by deleting the tick entries. +// 6. Return the updated position. +// +// Notes: +// - The function flips the tick states and cleans up unused tick data when liquidity is removed. +// - It ensures fee growth and position data remain accurate after the update. +// +// Example Usage: +// +// ```gno +// +// updatedPosition := pool.updatePosition(positionParams, currentTick) +// println("Updated Position Info:", updatedPosition) +// +// ``` +func (p *Pool) updatePosition(positionParams ModifyPositionParams, tick int32) (PositionInfo, error) { + feeGrowthGlobal0X128 := p.FeeGrowthGlobal0X128().Clone() + feeGrowthGlobal1X128 := p.FeeGrowthGlobal1X128().Clone() + + var flippedLower, flippedUpper bool + + if !positionParams.liquidityDelta.IsZero() { + flippedLower = p.tickUpdate( + positionParams.tickLower, + tick, + positionParams.liquidityDelta, + feeGrowthGlobal0X128, + feeGrowthGlobal1X128, + false, + p.maxLiquidityPerTick, + ) + + flippedUpper = p.tickUpdate( + positionParams.tickUpper, + tick, + positionParams.liquidityDelta, + feeGrowthGlobal0X128, + feeGrowthGlobal1X128, + true, + p.maxLiquidityPerTick, + ) + + if flippedLower { + p.tickBitmapFlipTick(positionParams.tickLower, p.tickSpacing) + } + + if flippedUpper { + p.tickBitmapFlipTick(positionParams.tickUpper, p.tickSpacing) + } + } + + feeGrowthInside0X128, feeGrowthInside1X128 := p.getFeeGrowthInside( + positionParams.tickLower, + positionParams.tickUpper, + tick, + feeGrowthGlobal0X128, + feeGrowthGlobal1X128, + ) + + positionKey, err := getPositionKey(positionParams.tickLower, positionParams.tickUpper) + if err != nil { + return PositionInfo{}, err + } + + position, err := p.positionUpdateWithKey( + positionKey, + positionParams.liquidityDelta, + feeGrowthInside0X128.Clone(), + feeGrowthInside1X128.Clone(), + ) + if err != nil { + return PositionInfo{}, err + } + + // clear any tick data that is no longer needed + if positionParams.liquidityDelta.IsNeg() { + if flippedLower { + p.deleteTick(positionParams.tickLower) + } + if flippedUpper { + p.deleteTick(positionParams.tickUpper) + } + } + + return position, nil +} diff --git a/contract/r/gnoswap/v1/pool/protocol_fee.gno b/contract/r/gnoswap/v1/pool/protocol_fee.gno new file mode 100644 index 0000000..ce62657 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/protocol_fee.gno @@ -0,0 +1,199 @@ +package pool + +import ( + "std" + + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" + pf "gno.land/r/gnoswap/v1/protocol_fee" +) + +var ( + // poolCreationFee is the fee that is charged when a user creates a pool. + // The fee is denominated in GNS tokens. + // This parameter can be modified through governance. + poolCreationFee = int64(100_000_000) // 100_GNS + + // withdrawalFeeBPS is the fee that is charged when a user withdraws their collected fees + // The fee is denominated in BPS (Basis Points) + // Example: 100 BPS = 1% + // This parameter can be modified through governance. + withdrawalFeeBPS = uint64(100) // 1% +) + +const ( + MaxBpsValue = uint64(10000) + ZeroBps = uint64(0) +) + +// HandleWithdrawalFee withdraws the fee from the user and returns the amount after the fee +// Only position contract can call this function +// Input: +// - positionId: the id of the LP token +// - token0Path: the path of the token0 +// - amount0: the amount of token0 +// - token1Path: the path of the token1 +// - amount1: the amount of token1 +// - poolPath: the path of the pool +// - positionCaller: the original caller of the position contract +// Output: +// - the amount of token0 after the fee +// - the amount of token1 after the fee +func HandleWithdrawalFee( + cur realm, + positionId uint64, + token0Path string, + amount0 string, // uint256 + token1Path string, + amount1 string, // uint256 + poolPath string, + positionCaller std.Address, +) (string, string) { // uint256 x2 + halt.AssertIsNotHaltedPool() + halt.AssertIsNotHaltedWithdraw() + + // only position contract can call this function + caller := std.PreviousRealm().Address() + access.AssertIsPosition(caller) + + common.MustRegistered(token0Path) + common.MustRegistered(token1Path) + + fee := GetWithdrawalFee() + if fee == ZeroBps { + return amount0, amount1 + } + + feeAmount0, afterAmount0 := calculateAmountWithFee(u256.MustFromDecimal(amount0), u256.NewUint(fee)) + feeAmount1, afterAmount1 := calculateAmountWithFee(u256.MustFromDecimal(amount1), u256.NewUint(fee)) + + checkTransferError(common.TransferFrom(cross, token0Path, positionCaller, protocolFeeAddr, safeConvertToInt64(feeAmount0))) + pf.AddToProtocolFee(cross, token0Path, safeConvertToInt64(feeAmount0)) + checkTransferError(common.TransferFrom(cross, token1Path, positionCaller, protocolFeeAddr, safeConvertToInt64(feeAmount1))) + pf.AddToProtocolFee(cross, token1Path, safeConvertToInt64(feeAmount1)) + + previousRealm := std.PreviousRealm() + std.Emit( + "WithdrawalFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "lpTokenId", formatUint(positionId), + "poolPath", poolPath, + "feeAmount0", feeAmount0.ToString(), + "feeAmount1", feeAmount1.ToString(), + "amount0WithoutFee", afterAmount0.ToString(), + "amount1WithoutFee", afterAmount1.ToString(), + ) + + return afterAmount0.ToString(), afterAmount1.ToString() +} + +// GetPoolCreationFee returns the poolCreationFee +func GetPoolCreationFee() int64 { + return poolCreationFee +} + +// SetPoolCreationFee sets the poolCreationFee. +// Only admin or governance can call this function. +func SetPoolCreationFee(cur realm, fee int64) { + halt.AssertIsNotHaltedPool() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + prevPoolCreationFee := GetPoolCreationFee() + err := setPoolCreationFee(fee) + if err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "SetPoolCreationFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "prevFee", formatInt(prevPoolCreationFee), + "newFee", formatInt(fee), + ) +} + +// GetWithdrawalFee returns the withdrawal fee +func GetWithdrawalFee() uint64 { + return withdrawalFeeBPS +} + +// SetWithdrawalFee sets the withdrawal fee. +// Only admin or governance can call this function. +func SetWithdrawalFee(cur realm, fee uint64) { + halt.AssertIsNotHaltedPool() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + prevWithdrawalFee := GetWithdrawalFee() + + err := setWithdrawalFee(fee) + if err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "SetWithdrawalFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "prevFee", formatUint(prevWithdrawalFee), + "newFee", formatUint(fee), + ) +} + +// calculateAmountWithFee calculates the fee amount and the amount after the fee +// +// Inputs: +// - amount: the amount before the fee +// - fee: the fee in BPS +// +// Outputs: +// - the fee amount +// - the amount after the fee applied +func calculateAmountWithFee(amount, fee *u256.Uint) (feeAmount, afterAmount *u256.Uint) { + feeAmount = u256.Zero().Mul(amount, fee) + feeAmount = u256.Zero().Div(feeAmount, u256.NewUint(MaxBpsValue)) + afterAmount = u256.Zero().Sub(amount, feeAmount) + return +} + +// setPoolCreationFee this function is internal function called by SetPoolCreationFee +// And SetPoolCreationFee +func setPoolCreationFee(fee int64) error { + if fee < 0 { + return makeErrorWithDetails( + errInvalidInput, + "pool creation fee cannot be negative", + ) + } + + // update pool creation fee + poolCreationFee = fee + + return nil +} + +// setWithdrawalFee this function is internal function called by SetWithdrawalFee +// function and SetWithdrawalFee function +func setWithdrawalFee(fee uint64) error { + if fee > MaxBpsValue { + return makeErrorWithDetails( + errInvalidWithdrawalFeePct, + ufmt.Sprintf("fee(%d) must be in range 0 ~ 10000", fee), + ) + } + + withdrawalFeeBPS = fee + + return nil +} diff --git a/contract/r/gnoswap/v1/pool/swap.gno b/contract/r/gnoswap/v1/pool/swap.gno new file mode 100644 index 0000000..c4c706f --- /dev/null +++ b/contract/r/gnoswap/v1/pool/swap.gno @@ -0,0 +1,647 @@ +package pool + +import ( + "std" + "strconv" + "time" + + "gno.land/p/nt/ufmt" + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" + + plp "gno.land/p/gnoswap/gnsmath" + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +const MAX_INT256 string = "57896044618658097711785492504343953926634992332820282019728792003956564819967" + +// Hook functions allow external contracts to be notified of swap events. +var ( + // MUST BE IMMUTABLE. + // DO NOT USE THIS VALUE IN ANY ARITHMETIC OPERATIONS' INITIALIZATION + zero = u256.Zero() + fixedPointQ128 = u256.MustFromDecimal(Q128) + + maxInt256 = u256.MustFromDecimal(MAX_INT256) + maxInt64 = i256.MustFromDecimal(MAX_INT64) + + swapStartHook func(poolPath string, timestamp int64) + tickCrossHook func(poolPath string, tickId int32, zeroForOne bool, timestamp int64) + swapEndHook func(poolPath string) error +) + +// SetTickCrossHook sets the hook function called when a tick is crossed during swaps. +// +// Allows staker to monitor liquidity changes at price levels. +// Used for reward calculation when positions enter/exit range. +// +// Only callable by staker contract. +func SetTickCrossHook(cur realm, hook func(poolPath string, tickId int32, zeroForOne bool, timestamp int64)) { + caller := std.PreviousRealm().Address() + access.AssertIsStaker(caller) + tickCrossHook = hook +} + +// SetSwapStartHook sets the hook function called at the beginning of a swap. +// +// Enables pre-swap state tracking for reward distribution. +// Captures timestamp for time-weighted calculations. +// +// Only callable by staker contract. +func SetSwapStartHook(cur realm, hook func(poolPath string, timestamp int64)) { + caller := std.PreviousRealm().Address() + access.AssertIsStaker(caller) + swapStartHook = hook +} + +// SetSwapEndHook sets the hook function called at the end of a swap. +// +// Finalizes reward calculations after swap completion. +// Allows error propagation to revert invalid swaps. +// +// Only callable by staker contract. +func SetSwapEndHook(cur realm, hook func(poolPath string) error) { + caller := std.PreviousRealm().Address() + access.AssertIsStaker(caller) + swapEndHook = hook +} + +// SwapResult encapsulates all state changes from a swap. +// It ensures atomic state transitions that can be applied at once. +type SwapResult struct { + Amount0 *i256.Int + Amount1 *i256.Int + NewSqrtPrice *u256.Uint + NewTick int32 + NewLiquidity *u256.Uint + NewProtocolFees ProtocolFees + FeeGrowthGlobal0X128 *u256.Uint + FeeGrowthGlobal1X128 *u256.Uint +} + +// SwapComputation encapsulates the pure computation logic for swaps. +type SwapComputation struct { + AmountSpecified *i256.Int + SqrtPriceLimitX96 *u256.Uint + ZeroForOne bool + ExactInput bool + InitialState SwapState + Cache SwapCache +} + +// Swap executes a token swap in the pool. +// +// Parameters: +// - token0Path, token1Path: token contract paths +// - fee: pool fee tier +// - recipient: address receiving output tokens +// - zeroForOne: true for token0→token1, false for token1→token0 +// - amountSpecified: amount to swap (positive=exact in, negative=exact out) +// - sqrtPriceLimitX96: price limit in Q96 format +// - payer: address paying input tokens +// +// Returns amount0, amount1 as decimal strings (negative for tokens out). +// Only callable by whitelisted routers. +// Note: Uses tick-based pricing with Q96 fixed-point math. +func Swap( + cur realm, + token0Path string, + token1Path string, + fee uint32, + recipient std.Address, + zeroForOne bool, + amountSpecified string, + sqrtPriceLimitX96 string, + payer std.Address, // router +) (string, string) { + halt.AssertIsNotHaltedPool() + + caller := std.PreviousRealm().Address() + access.AssertIsSwapWhitelisted(caller) + assertPayerIsPreviousRealmOrOriginCaller(payer) + + if amountSpecified == "0" { + panic(newErrorWithDetail( + errInvalidSwapAmount, + "amountSpecified == 0", + )) + } + + pool := mustGetPoolBy(token0Path, token1Path, fee) + + slot0Start := pool.slot0 + if !slot0Start.unlocked { + panic(errLockedPool) + } + + // no liquidity -> no swap, return zero amounts + if pool.liquidity.IsZero() { + return "0", "0" + } + + pool.slot0.unlocked = false + + // Call swap start hook if set + if swapStartHook != nil { + currentTime := time.Now().Unix() + swapStartHook(pool.PoolPath(), currentTime) + } + + sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96) + validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit) + + amounts := i256.MustFromDecimal(amountSpecified) + feeGrowthGlobalX128 := getFeeGrowthGlobal(pool, zeroForOne) + feeProtocol := getFeeProtocol(slot0Start, zeroForOne) + cache := newSwapCache(feeProtocol, pool.liquidity.Clone()) + state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart.Clone(), slot0Start) + + comp := SwapComputation{ + AmountSpecified: amounts, + SqrtPriceLimitX96: sqrtPriceLimit, + ZeroForOne: zeroForOne, + ExactInput: amounts.Gt(i256.Zero()), + InitialState: state, + Cache: cache, + } + + result, err := computeSwap(pool, comp) + if err != nil { + panic(err) + } + + applySwapResult(pool, result) + + // update TWAP state + currentTime := time.Now().Unix() + pool.updatePriceCumulatives(currentTime) + + // actual swap + pool.swapTransfers(zeroForOne, payer, recipient, result.Amount0, result.Amount1) + + pool.slot0.unlocked = true + // Call swap end hook if set + if swapEndHook != nil { + err := swapEndHook(pool.PoolPath()) + if err != nil { + panic(err) + } + } + + previousRealm := std.PreviousRealm() + std.Emit( + "Swap", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "poolPath", GetPoolPath(token0Path, token1Path, fee), + "zeroForOne", formatBool(zeroForOne), + "requestAmount", amountSpecified, + "sqrtPriceLimitX96", sqrtPriceLimitX96, + "payer", payer.String(), + "recipient", recipient.String(), + "token0Amount", result.Amount0.ToString(), + "token1Amount", result.Amount1.ToString(), + "protocolFee0", pool.protocolFees.token0.ToString(), + "protocolFee1", pool.protocolFees.token1.ToString(), + "sqrtPriceX96", pool.slot0.sqrtPriceX96.ToString(), + "exactIn", strconv.FormatBool(comp.ExactInput), + "currentTick", strconv.FormatInt(int64(pool.Slot0Tick()), 10), + "liquidity", pool.Liquidity().ToString(), + "feeGrowthGlobal0X128", pool.FeeGrowthGlobal0X128().ToString(), + "feeGrowthGlobal1X128", pool.FeeGrowthGlobal1X128().ToString(), + "balanceToken0", pool.BalanceToken0().ToString(), + "balanceToken1", pool.BalanceToken1().ToString(), + "ticks", pool.Ticks(), + "tickCumulative", formatInt(pool.Observation().TickCumulative()), + "liquidityCumulative", pool.Observation().LiquidityCumulative(), + "lastCumulativeUpdateTime", formatInt(pool.Observation().LastCumulativeUpdateTime()), + ) + + return result.Amount0.ToString(), result.Amount1.ToString() +} + +// DrySwap simulates a swap without modifying pool state. +// Returns amount0, amount1 and a success boolean. +// Returns false if pool is locked, has no liquidity, or computation fails. +func DrySwap( + token0Path string, + token1Path string, + fee uint32, + zeroForOne bool, + amountSpecified string, + sqrtPriceLimitX96 string, +) (string, string, bool) { + if amountSpecified == "0" { + return "0", "0", false + } + + pool := mustGetPoolBy(token0Path, token1Path, fee) + + // no liquidity -> simulation fails + if pool.liquidity.IsZero() { + return "0", "0", false + } + + slot0Start := pool.slot0 + sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96) + validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit) + + amounts := i256.MustFromDecimal(amountSpecified) + feeGrowthGlobalX128 := getFeeGrowthGlobal(pool, zeroForOne) + feeProtocol := getFeeProtocol(slot0Start, zeroForOne) + cache := newSwapCache(feeProtocol, pool.liquidity.Clone()) + state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart, slot0Start) + + comp := SwapComputation{ + AmountSpecified: amounts, + SqrtPriceLimitX96: sqrtPriceLimit, + ZeroForOne: zeroForOne, + ExactInput: amounts.Gt(i256.Zero()), + InitialState: state, + Cache: cache, + } + + result, err := computeSwap(pool, comp) + if err != nil { + return "0", "0", false + } + + if zeroForOne { + if pool.balances.token1.Lt(result.Amount1.Abs()) { + return "0", "0", false + } + } else { + if pool.balances.token0.Lt(result.Amount0.Abs()) { + return "0", "0", false + } + } + + // Validate non-zero amounts + if result.Amount0.IsZero() || result.Amount1.IsZero() { + return "0", "0", false + } + + return result.Amount0.ToString(), result.Amount1.ToString(), true +} + +// computeSwap performs the core swap computation without modifying pool state. +// The computation continues until either: +// - The entire amount is consumed (amountSpecifiedRemaining = 0) +// - The price limit is reached (sqrtPriceX96 = sqrtPriceLimitX96) +// +// Important: This function is critical for AMM price discovery. It iterates through +// tick ranges, calculating swap amounts and fees for each liquidity segment. +// Returns an error if the computation fails at any step. +func computeSwap(pool *Pool, comp SwapComputation) (*SwapResult, error) { + state := comp.InitialState + var err error + + // Compute swap steps until completion + for shouldContinueSwap(state, comp.SqrtPriceLimitX96) { + state, err = computeSwapStep(state, pool, comp.ZeroForOne, comp.SqrtPriceLimitX96, comp.ExactInput, comp.Cache) + if err != nil { + return nil, err + } + } + + // Calculate final amounts + amount0 := state.amountCalculated + amount1 := i256.Zero().Sub(comp.AmountSpecified, state.amountSpecifiedRemaining) + if comp.ZeroForOne == comp.ExactInput { + amount0, amount1 = amount1, amount0 + } + + // Prepare result + result := &SwapResult{ + Amount0: amount0, + Amount1: amount1, + NewSqrtPrice: state.sqrtPriceX96, + NewTick: state.tick, + NewLiquidity: state.liquidity, + NewProtocolFees: ProtocolFees{ + token0: pool.protocolFees.token0, + token1: pool.protocolFees.token1, + }, + FeeGrowthGlobal0X128: pool.feeGrowthGlobal0X128, + FeeGrowthGlobal1X128: pool.feeGrowthGlobal1X128, + } + + // Update protocol fees if necessary + if comp.ZeroForOne { + if state.protocolFee.Gt(zero) { + result.NewProtocolFees.token0 = u256.Zero().Add(result.NewProtocolFees.token0, state.protocolFee) + } + result.FeeGrowthGlobal0X128 = state.feeGrowthGlobalX128.Clone() + } else { + if state.protocolFee.Gt(zero) { + result.NewProtocolFees.token1 = u256.Zero().Add(result.NewProtocolFees.token1, state.protocolFee) + } + result.FeeGrowthGlobal1X128 = state.feeGrowthGlobalX128.Clone() + } + + return result, nil +} + +// applySwapResult updates pool state with computed results. +// All state changes are applied at once to maintain consistency +func applySwapResult(pool *Pool, result *SwapResult) { + pool.slot0.sqrtPriceX96 = result.NewSqrtPrice + pool.slot0.tick = result.NewTick + pool.liquidity = result.NewLiquidity + pool.protocolFees = result.NewProtocolFees + pool.feeGrowthGlobal0X128 = result.FeeGrowthGlobal0X128 + pool.feeGrowthGlobal1X128 = result.FeeGrowthGlobal1X128 +} + +// validatePriceLimits ensures the provided price limit is valid for the swap direction +// The function enforces that: +// For zeroForOne (selling token0): +// - Price limit must be below current price +// - Price limit must be above MIN_SQRT_RATIO +// +// For !zeroForOne (selling token1): +// - Price limit must be above current price +// - Price limit must be below MAX_SQRT_RATIO +func validatePriceLimits(slot0 Slot0, zeroForOne bool, sqrtPriceLimitX96 *u256.Uint) { + if zeroForOne { + cond1 := sqrtPriceLimitX96.Lt(slot0.sqrtPriceX96) + cond2 := sqrtPriceLimitX96.Gt(minSqrtRatio) + if !(cond1 && cond2) { + panic(newErrorWithDetail( + errPriceOutOfRange, + ufmt.Sprintf("sqrtPriceLimitX96(%s) < slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) > MIN_SQRT_RATIO(%s)", + sqrtPriceLimitX96.ToString(), + slot0.sqrtPriceX96.ToString(), + sqrtPriceLimitX96.ToString(), + MIN_SQRT_RATIO), + )) + } + } else { + cond1 := sqrtPriceLimitX96.Gt(slot0.sqrtPriceX96) + cond2 := sqrtPriceLimitX96.Lt(maxSqrtRatio) + if !(cond1 && cond2) { + panic(newErrorWithDetail( + errPriceOutOfRange, + ufmt.Sprintf("sqrtPriceLimitX96(%s) > slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) < MAX_SQRT_RATIO(%s)", + sqrtPriceLimitX96.ToString(), + slot0.sqrtPriceX96.ToString(), + sqrtPriceLimitX96.ToString(), + MAX_SQRT_RATIO), + )) + } + } +} + +// getFeeProtocol returns the appropriate fee protocol based on zero for one. +// When zeroForOne is true, we want the lower 4 bits (% 16). +// Otherwise, we want the upper 4 bits (/ 16). +func getFeeProtocol(slot0 Slot0, zeroForOne bool) uint8 { + shift := uint8(0) + if !zeroForOne { + shift = 4 + } + return (slot0.feeProtocol >> shift) & uint8(0xF) +} + +// getFeeGrowthGlobal returns the appropriate fee growth global based on zero for one. +func getFeeGrowthGlobal(pool *Pool, zeroForOne bool) *u256.Uint { + if zeroForOne { + return pool.feeGrowthGlobal0X128.Clone() + } + return pool.feeGrowthGlobal1X128.Clone() +} + +// shouldContinueSwap checks if swap should continue based on remaining amount and price limit. +func shouldContinueSwap(state SwapState, sqrtPriceLimitX96 *u256.Uint) bool { + return !state.amountSpecifiedRemaining.IsZero() && !state.sqrtPriceX96.Eq(sqrtPriceLimitX96) +} + +// computeSwapStep executes a single step of swap and returns new state +func computeSwapStep( + state SwapState, + pool *Pool, + zeroForOne bool, + sqrtPriceLimitX96 *u256.Uint, + exactInput bool, + cache SwapCache, +) (SwapState, error) { + step := computeSwapStepInit(state, pool, zeroForOne) + + // determining the price target for this step + sqrtRatioTargetX96 := computeTargetSqrtRatio(step, sqrtPriceLimitX96, zeroForOne).Clone() + + // computing the amounts to be swapped at this step + var ( + newState SwapState + err error + ) + + newState, step = computeAmounts(state, sqrtRatioTargetX96, pool, step) + newState, err = updateAmounts(step, newState, exactInput) + if err != nil { + return state, err + } + + // if the protocol fee is on, calculate how much is owed, + // decrement fee amount, and increment protocol fee + if cache.feeProtocol > 0 { + newState, step, err = updateFeeProtocol(step, cache.feeProtocol, newState) + if err != nil { + return state, err + } + } + + // update global fee tracker + if newState.liquidity.Gt(u256.Zero()) { + update := u256.MulDiv(step.feeAmount, fixedPointQ128, newState.liquidity) + feeGrowthGlobalX128 := u256.Zero().Add(newState.feeGrowthGlobalX128, update) + newState.setFeeGrowthGlobalX128(feeGrowthGlobalX128) + } + + // handling tick transitions + if newState.sqrtPriceX96.Eq(step.sqrtPriceNextX96) { + newState = tickTransition(step, zeroForOne, newState, pool) + } else if newState.sqrtPriceX96.Neq(step.sqrtPriceStartX96) { + newState.setTick(common.TickMathGetTickAtSqrtRatio(newState.sqrtPriceX96)) + } + + return newState, nil +} + +// updateFeeProtocol calculates and updates protocol fees for the current step. +func updateFeeProtocol(step StepComputations, feeProtocol uint8, state SwapState) (SwapState, StepComputations, error) { + delta := u256.Zero().Div(step.feeAmount, u256.NewUint(uint64(feeProtocol))) + + newFeeAmount, overflow := u256.Zero().SubOverflow(step.feeAmount, delta) + if overflow { + return state, step, errUnderflow + } + + step.feeAmount = newFeeAmount + + newProtocolFee, overflow := u256.Zero().AddOverflow(state.protocolFee, delta) + if overflow { + return state, step, errOverFlow + } + state.protocolFee = newProtocolFee + + return state, step, nil +} + +// computeSwapStepInit initializes the computation for a single swap step. +func computeSwapStepInit(state SwapState, pool *Pool, zeroForOne bool) StepComputations { + var step StepComputations + step.sqrtPriceStartX96 = state.sqrtPriceX96 + tickNext, initialized := pool.tickBitmapNextInitializedTickWithInOneWord( + state.tick, + pool.tickSpacing, + zeroForOne, + ) + + step.tickNext = tickNext + step.initialized = initialized + + // prevent overshoot the min/max tick + step.clampTickNext() + // get the price for the next tick + sqrtPrice := common.TickMathGetSqrtRatioAtTick(step.tickNext).ToString() + step.sqrtPriceNextX96 = u256.MustFromDecimal(sqrtPrice) + return step +} + +// computeTargetSqrtRatio determines the target sqrt price for the current swap step. +func computeTargetSqrtRatio(step StepComputations, sqrtPriceLimitX96 *u256.Uint, zeroForOne bool) *u256.Uint { + if shouldUsePriceLimit(step.sqrtPriceNextX96, sqrtPriceLimitX96, zeroForOne) { + return sqrtPriceLimitX96 + } + return step.sqrtPriceNextX96 +} + +// shouldUsePriceLimit returns true if the price limit should be used instead of the next tick price +func shouldUsePriceLimit(sqrtPriceNext, sqrtPriceLimit *u256.Uint, zeroForOne bool) bool { + if zeroForOne { + return sqrtPriceNext.Lt(sqrtPriceLimit) + } + return sqrtPriceNext.Gt(sqrtPriceLimit) +} + +// computeAmounts calculates the input and output amounts for the current swap step. +func computeAmounts(state SwapState, sqrtRatioTargetX96 *u256.Uint, pool *Pool, step StepComputations) (SwapState, StepComputations) { + sqrtPriceX96, amountIn, amountOut, feeAmount := plp.SwapMathComputeSwapStep( + state.sqrtPriceX96, + sqrtRatioTargetX96, + state.liquidity, + state.amountSpecifiedRemaining, + uint64(pool.fee), + ) + + step.amountIn = amountIn + step.amountOut = amountOut + step.feeAmount = feeAmount + + state.setSqrtPriceX96(sqrtPriceX96) + + return state, step +} + +// updateAmounts calculates new remaining and calculated amounts based on the swap step. +// For exact input swaps: +// - Decrements remaining input amount by (amountIn + feeAmount) +// - Decrements calculated amount by amountOut +// +// For exact output swaps: +// - Increments remaining output amount by amountOut +// - Increments calculated amount by (amountIn + feeAmount) +func updateAmounts(step StepComputations, state SwapState, exactInput bool) (SwapState, error) { + amountInWithFeeU256 := u256.Zero().Add(step.amountIn, step.feeAmount) + if amountInWithFeeU256.Gt(maxInt256) { + return state, errOverFlow + } + + amountInWithFee := i256.FromUint256(amountInWithFeeU256) + if step.amountOut.Gt(maxInt256) { + return state, errUnderflow + } + + var ( + amountSpecifiedRemaining *i256.Int + amountCalculated *i256.Int + ) + + if exactInput { + amountSpecifiedRemaining = i256.Zero().Sub(state.amountSpecifiedRemaining, amountInWithFee) + amountCalculated = i256.Zero().Sub(state.amountCalculated, i256.FromUint256(step.amountOut)) + } else { + amountSpecifiedRemaining = i256.Zero().Add(state.amountSpecifiedRemaining, i256.FromUint256(step.amountOut)) + amountCalculated = i256.Zero().Add(state.amountCalculated, amountInWithFee) + } + + // If an overflowed value is stored in state, it may cause problems in the next step + if amountCalculated.Gt(maxInt64) { + return state, errOverFlow + } + + state.amountSpecifiedRemaining = amountSpecifiedRemaining + state.amountCalculated = amountCalculated + + return state, nil +} + +// tickTransition handles the transition between price ticks during a swap +func tickTransition(step StepComputations, zeroForOne bool, state SwapState, pool *Pool) SwapState { + // ensure existing state to keep immutability + newState := state + + if step.initialized { + fee0, fee1 := u256.Zero(), u256.Zero() + + if zeroForOne { + fee0 = state.feeGrowthGlobalX128 + fee1 = pool.feeGrowthGlobal1X128 + } else { + fee0 = pool.feeGrowthGlobal0X128 + fee1 = state.feeGrowthGlobalX128 + } + + liquidityNet := pool.tickCross(step.tickNext, fee0, fee1) + + if zeroForOne { + liquidityNet = i256.Zero().Neg(liquidityNet) + } + + newState.liquidity = liquidityMathAddDelta(state.liquidity, liquidityNet) + + if tickCrossHook != nil { + currentTime := time.Now().Unix() + tickCrossHook(pool.PoolPath(), step.tickNext, zeroForOne, currentTime) + } + } + + decrement := int32(0) + if zeroForOne { + decrement = 1 + } + newState.tick = step.tickNext - decrement + + return newState +} + +// swapTransfers handles token transfers for a swap transaction. +// For zeroForOne swaps: transfers token0 from payer to pool and token1 from pool to recipient. +// For oneForZero swaps: transfers token1 from payer to pool and token0 from pool to recipient. +func (p *Pool) swapTransfers(zeroForOne bool, payer, recipient std.Address, amount0, amount1 *i256.Int) { + if zeroForOne { + // payer > POOL + p.safeTransferFrom(payer, poolAddr, p.token0Path, amount0.Abs(), true) + + // POOL > recipient + p.safeTransfer(recipient, p.token1Path, amount1, false) + } else { + // payer > POOL + p.safeTransferFrom(payer, poolAddr, p.token1Path, amount1.Abs(), false) + // POOL > recipient + p.safeTransfer(recipient, p.token0Path, amount0, true) + } +} diff --git a/contract/r/gnoswap/v1/pool/tick.gno b/contract/r/gnoswap/v1/pool/tick.gno new file mode 100644 index 0000000..43f6967 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/tick.gno @@ -0,0 +1,468 @@ +package pool + +import ( + "strconv" + + "gno.land/p/nt/ufmt" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +const ( + MIN_TICK int32 = -887272 + MAX_TICK int32 = 887272 +) + +// GetTickLiquidityGross returns the gross liquidity for the specified tick. +func (p *Pool) GetTickLiquidityGross(tick int32) *u256.Uint { + return p.mustGetTick(tick).liquidityGross +} + +// GetTickLiquidityNet returns the net liquidity for the specified tick. +func (p *Pool) GetTickLiquidityNet(tick int32) *i256.Int { + return p.mustGetTick(tick).liquidityNet +} + +// GetTickFeeGrowthOutside0X128 returns the fee growth outside the tick for token 0. +func (p *Pool) GetTickFeeGrowthOutside0X128(tick int32) *u256.Uint { + return p.mustGetTick(tick).feeGrowthOutside0X128 +} + +// GetTickFeeGrowthOutside1X128 returns the fee growth outside the tick for token 1. +func (p *Pool) GetTickFeeGrowthOutside1X128(tick int32) *u256.Uint { + return p.mustGetTick(tick).feeGrowthOutside1X128 +} + +// GetTickCumulativeOutside returns the cumulative liquidity outside the tick. +func (p *Pool) GetTickCumulativeOutside(tick int32) int64 { + return p.mustGetTick(tick).tickCumulativeOutside +} + +// GetTickSecondsPerLiquidityOutsideX128 returns the seconds per liquidity outside the tick. +func (p *Pool) GetTickSecondsPerLiquidityOutsideX128(tick int32) *u256.Uint { + return p.mustGetTick(tick).secondsPerLiquidityOutsideX128 +} + +// GetTickSecondsOutside returns the seconds outside the tick. +func (p *Pool) GetTickSecondsOutside(tick int32) uint32 { + return p.mustGetTick(tick).secondsOutside +} + +// GetTickInitialized returns whether the tick is initialized. +func (p *Pool) GetTickInitialized(tick int32) bool { + return p.mustGetTick(tick).initialized +} + +// getFeeGrowthInside calculates the fee growth within a specified tick range. +// +// This function computes the accumulated fee growth for token 0 and token 1 inside a given tick range +// (`tickLower` to `tickUpper`) relative to the current tick position (`tickCurrent`). It isolates the fee +// growth within the range by subtracting the fee growth below the lower tick and above the upper tick +// from the global fee growth. +// +// Parameters: +// - tickLower: int32, the lower tick boundary of the range. +// - tickUpper: int32, the upper tick boundary of the range. +// - tickCurrent: int32, the current tick index. +// - feeGrowthGlobal0X128: *u256.Uint, the global fee growth for token 0 in X128 precision. +// - feeGrowthGlobal1X128: *u256.Uint, the global fee growth for token 1 in X128 precision. +// +// Returns: +// - *u256.Uint: Fee growth inside the tick range for token 0. +// - *u256.Uint: Fee growth inside the tick range for token 1. +// +// Workflow: +// 1. Retrieve the tick information (`lower` and `upper`) for the lower and upper tick boundaries +// using `p.getTick`. +// 2. Calculate the fee growth below the lower tick using `getFeeGrowthBelowX128`. +// 3. Calculate the fee growth above the upper tick using `getFeeGrowthAboveX128`. +// 4. Subtract the fee growth below and above the range from the global fee growth values: +// feeGrowthInside = feeGrowthGlobal - feeGrowthBelow - feeGrowthAbove +// 5. Return the computed fee growth values for token 0 and token 1 within the range. +// +// Behavior: +// - The fee growth is isolated within the range `[tickLower, tickUpper]`. +// - The function ensures the calculations accurately consider the tick boundaries and the current tick position. +// +// Example: +// +// ```gno +// +// feeGrowth0, feeGrowth1 := pool.getFeeGrowthInside( +// 100, 200, 150, globalFeeGrowth0, globalFeeGrowth1, +// ) +// println("Fee Growth Inside (Token 0):", feeGrowth0) +// println("Fee Growth Inside (Token 1):", feeGrowth1) +// +// ``` +func (p *Pool) getFeeGrowthInside( + tickLower int32, + tickUpper int32, + tickCurrent int32, + feeGrowthGlobal0X128 *u256.Uint, + feeGrowthGlobal1X128 *u256.Uint, +) (*u256.Uint, *u256.Uint) { + lower := p.getTick(tickLower) + upper := p.getTick(tickUpper) + + feeGrowthBelow0X128, feeGrowthBelow1X128 := getFeeGrowthBelowX128(tickLower, tickCurrent, feeGrowthGlobal0X128, feeGrowthGlobal1X128, lower) + feeGrowthAbove0X128, feeGrowthAbove1X128 := getFeeGrowthAboveX128(tickUpper, tickCurrent, feeGrowthGlobal0X128, feeGrowthGlobal1X128, upper) + + feeGrowthInside0X128 := u256.Zero().Sub(u256.Zero().Sub(feeGrowthGlobal0X128, feeGrowthBelow0X128), feeGrowthAbove0X128) + feeGrowthInside1X128 := u256.Zero().Sub(u256.Zero().Sub(feeGrowthGlobal1X128, feeGrowthBelow1X128), feeGrowthAbove1X128) + + return feeGrowthInside0X128, feeGrowthInside1X128 +} + +// tickUpdate updates the state of a specific tick. +// +// This function applies a given liquidity change (liquidityDelta) to the specified tick, updates +// the fee growth values if necessary, and adjusts the net liquidity based on whether the tick +// is an upper or lower boundary. It also verifies that the total liquidity does not exceed the +// maximum allowed value and ensures the net liquidity stays within the valid int128 range. +// +// Parameters: +// - tick: int32, the index of the tick to update. +// - tickCurrent: int32, the current active tick index. +// - liquidityDelta: *i256.Int, the amount of liquidity to add or remove. +// - feeGrowthGlobal0X128: *u256.Uint, the global fee growth value for token 0. +// - feeGrowthGlobal1X128: *u256.Uint, the global fee growth value for token 1. +// - upper: bool, indicates if this is the upper boundary (true for upper, false for lower). +// - maxLiquidity: *u256.Uint, the maximum allowed liquidity. +// +// Returns: +// - flipped: bool, indicates if the tick's initialization state has changed. +// (e.g., liquidity transitioning from zero to non-zero, or vice versa) +// +// Workflow: +// 1. Nil input values are replaced with zero. +// 2. The function retrieves the tick information for the specified tick index. +// 3. Applies the liquidityDelta to compute the new total liquidity (liquidityGross). +// - If the total liquidity exceeds the maximum allowed value, the function panics. +// 4. Checks whether the tick's initialized state has changed and sets the `flipped` flag. +// 5. If the tick was previously uninitialized and its index is less than or equal to the current tick, +// the fee growth values are initialized to the current global values. +// 6. Updates the tick's net liquidity: +// - For an upper boundary, it subtracts liquidityDelta. +// - For a lower boundary, it adds liquidityDelta. +// - Ensures the net liquidity remains within the int128 range using `checkOverFlowInt128`. +// 7. Updates the tick's state with the new values. +// 8. Returns whether the tick's initialized state has flipped. +// +// Panic Conditions: +// - The total liquidity (liquidityGross) exceeds the maximum allowed liquidity (maxLiquidity). +// - The net liquidity (liquidityNet) exceeds the int128 range. +// +// Example: +// +// ```gno +// +// flipped := pool.tickUpdate(10, 5, liquidityDelta, feeGrowth0, feeGrowth1, true, maxLiquidity) +// println("Tick flipped:", flipped) +// +// ``` +func (p *Pool) tickUpdate( + tick int32, + tickCurrent int32, + liquidityDelta *i256.Int, + feeGrowthGlobal0X128 *u256.Uint, + feeGrowthGlobal1X128 *u256.Uint, + upper bool, + maxLiquidity *u256.Uint, +) (flipped bool) { + liquidityDelta = liquidityDelta.NilToZero() + feeGrowthGlobal0X128 = feeGrowthGlobal0X128.NilToZero() + feeGrowthGlobal1X128 = feeGrowthGlobal1X128.NilToZero() + + tickInfo := p.getTick(tick) + + liquidityGrossBefore := tickInfo.liquidityGross.Clone() + liquidityGrossAfter := liquidityMathAddDelta(liquidityGrossBefore, liquidityDelta) + + if !(liquidityGrossAfter.Lte(maxLiquidity)) { + panic(newErrorWithDetail( + errLiquidityCalculation, + ufmt.Sprintf("liquidityGrossAfter(%s) overflows maxLiquidity(%s)", liquidityGrossAfter.ToString(), maxLiquidity.ToString()), + )) + } + + flipped = (liquidityGrossAfter.IsZero()) != (liquidityGrossBefore.IsZero()) + + if liquidityGrossBefore.IsZero() { + if tick <= tickCurrent { + tickInfo.feeGrowthOutside0X128 = feeGrowthGlobal0X128.Clone() + tickInfo.feeGrowthOutside1X128 = feeGrowthGlobal1X128.Clone() + } + tickInfo.initialized = true + } + + tickInfo.liquidityGross = liquidityGrossAfter.Clone() + + if upper { + tickInfo.liquidityNet = i256.Zero().Sub(tickInfo.liquidityNet, liquidityDelta) + checkOverFlowInt128(tickInfo.liquidityNet) + } else { + tickInfo.liquidityNet = i256.Zero().Add(tickInfo.liquidityNet, liquidityDelta) + checkOverFlowInt128(tickInfo.liquidityNet) + } + + p.setTick(tick, tickInfo) + + return flipped +} + +// tickCross updates a tick's state when it is crossed and returns the liquidity net. +func (p *Pool) tickCross( + tick int32, + feeGrowthGlobal0X128 *u256.Uint, + feeGrowthGlobal1X128 *u256.Uint, +) *i256.Int { + thisTick := p.getTick(tick) + + thisTick.feeGrowthOutside0X128 = u256.Zero().Sub(feeGrowthGlobal0X128, thisTick.feeGrowthOutside0X128) + thisTick.feeGrowthOutside1X128 = u256.Zero().Sub(feeGrowthGlobal1X128, thisTick.feeGrowthOutside1X128) + + p.setTick(tick, thisTick) + + return thisTick.liquidityNet.Clone() +} + +// setTick updates the tick data for the specified tick index in the pool. +func (p *Pool) setTick(tick int32, newTickInfo TickInfo) { + tickStr := strconv.Itoa(int(tick)) + p.ticks.Set(tickStr, newTickInfo) +} + +// deleteTick deletes the tick data for the specified tick index in the pool. +func (p *Pool) deleteTick(tick int32) { + tickStr := strconv.Itoa(int(tick)) + p.ticks.Remove(tickStr) +} + +// getTick retrieves the TickInfo associated with the specified tick index from the pool. +// If the TickInfo contains any nil fields, they are replaced with zero values using valueOrZero. +// +// Parameters: +// - tick: The tick index (int32) for which the TickInfo is to be retrieved. +// +// Behavior: +// - Retrieves the TickInfo for the given tick from the pool's tick map. +// - Ensures that all fields of TickInfo are non-nil by calling valueOrZero, which replaces nil values with zero. +// - Returns the updated TickInfo. +// +// Returns: +// - TickInfo: The tick data with all fields guaranteed to have valid values (nil fields are set to zero). +// +// Use Case: +// This function ensures the retrieved tick data is always valid and safe for further operations, +// such as calculations or updates, by sanitizing nil fields in the TickInfo structure. +func (p *Pool) getTick(tick int32) TickInfo { + tickStr := formatInt(tick) + iTickInfo, exist := p.ticks.Get(tickStr) + if !exist { + tickInfo := TickInfo{} + tickInfo.valueOrZero() + return tickInfo + } + + tickInfo, ok := iTickInfo.(TickInfo) + if !ok { + panic(ufmt.Sprintf("failed to cast tickInfo to TickInfo: %T", iTickInfo)) + } + return tickInfo +} + +// mustGetTick retrieves the TickInfo for a specific tick, panicking if the tick does not exist. +// +// This function ensures that the requested tick data exists in the pool's tick mapping. +// If the tick does not exist, it panics with an appropriate error message. +// +// Parameters: +// - tick: int32, the index of the tick to retrieve. +// +// Returns: +// - TickInfo: The information associated with the specified tick. +// +// Behavior: +// - Checks if the tick exists in the pool's tick mapping (`p.ticks`). +// - If the tick exists, it returns the corresponding `TickInfo`. +// - If the tick does not exist, the function panics with a descriptive error. +// +// Panic Conditions: +// - The specified tick does not exist in the pool's mapping. +// +// Example: +// +// ```gno +// +// tickInfo := pool.mustGetTick(10) +// ufmt.Println("Tick Info:", tickInfo) +// +// ``` +func (p *Pool) mustGetTick(tick int32) TickInfo { + tickStr := formatInt(tick) + iTickInfo, exist := p.ticks.Get(tickStr) + if !exist { + panic(newErrorWithDetail( + errDataNotFound, + ufmt.Sprintf("tick(%d) does not exist", tick), + )) + } + + info, ok := iTickInfo.(TickInfo) + if !ok { + panic("failed to cast tick info to TickInfo") + } + + return info +} + +// calculateMaxLiquidityPerTick calculates the maximum liquidity +// per tick for a given tick spacing. +func calculateMaxLiquidityPerTick(tickSpacing int32) *u256.Uint { + // Floor MIN_TICK and MAX_TICK to the nearest multiple of tickSpacing + // This ensures that the tick range is properly aligned with the tickSpacing + // For example, if tickSpacing is 60 and MIN_TICK is -887272: + // -887272 / 60 = -14787.866... -> -14787 * 60 = -887220 + minTick := (MIN_TICK / tickSpacing) * tickSpacing + maxTick := (MAX_TICK / tickSpacing) * tickSpacing + numTicks := uint64((maxTick-minTick)/tickSpacing) + 1 + + return u256.Zero().Div(u256.MustFromDecimal(MAX_UINT128), u256.NewUint(numTicks)) +} + +// getFeeGrowthBelowX128 calculates the fee growth below a specified tick. +// +// This function computes the fee growth for token 0 and token 1 below a given tick (`tickLower`) +// relative to the current tick (`tickCurrent`). The fee growth values are adjusted based on whether +// the `tickCurrent` is above or below the `tickLower`. +// +// Parameters: +// - tickLower: int32, the lower tick boundary for fee calculation. +// - tickCurrent: int32, the current tick index. +// - feeGrowthGlobal0X128: *u256.Uint, the global fee growth for token 0 in X128 precision. +// - feeGrowthGlobal1X128: *u256.Uint, the global fee growth for token 1 in X128 precision. +// - lowerTick: TickInfo, the fee growth and liquidity details for the lower tick. +// +// Returns: +// - *u256.Uint: Fee growth below `tickLower` for token 0. +// - *u256.Uint: Fee growth below `tickLower` for token 1. +// +// Workflow: +// 1. If `tickCurrent` is greater than or equal to `tickLower`: +// - Return the `feeGrowthOutside0X128` and `feeGrowthOutside1X128` values of the `lowerTick`. +// 2. If `tickCurrent` is below `tickLower`: +// - Compute the fee growth below the lower tick by subtracting `feeGrowthOutside` values +// from the global fee growth values (`feeGrowthGlobal0X128` and `feeGrowthGlobal1X128`). +// 3. Return the calculated fee growth values for both tokens. +// +// Behavior: +// - If `tickCurrent >= tickLower`, the fee growth outside the lower tick is returned as-is. +// - If `tickCurrent < tickLower`, the fee growth is calculated as: +// feeGrowthBelow = feeGrowthGlobal - feeGrowthOutside +// +// Example: +// +// ```gno +// +// feeGrowth0, feeGrowth1 := getFeeGrowthBelowX128( +// 100, 150, globalFeeGrowth0, globalFeeGrowth1, lowerTickInfo, +// ) +// println("Fee Growth Below:", feeGrowth0, feeGrowth1) +func getFeeGrowthBelowX128( + tickLower, tickCurrent int32, + feeGrowthGlobal0X128, feeGrowthGlobal1X128 *u256.Uint, + lowerTick TickInfo, +) (*u256.Uint, *u256.Uint) { + if tickCurrent >= tickLower { + return lowerTick.feeGrowthOutside0X128, lowerTick.feeGrowthOutside1X128 + } + + feeGrowthBelow0X128 := u256.Zero().Sub(feeGrowthGlobal0X128, lowerTick.feeGrowthOutside0X128) + feeGrowthBelow1X128 := u256.Zero().Sub(feeGrowthGlobal1X128, lowerTick.feeGrowthOutside1X128) + + return feeGrowthBelow0X128, feeGrowthBelow1X128 +} + +// getFeeGrowthAboveX128 calculates the fee growth above a specified tick. +// +// This function computes the fee growth for token 0 and token 1 above a given tick (`tickUpper`) +// relative to the current tick (`tickCurrent`). The fee growth values are adjusted based on whether +// the `tickCurrent` is above or below the `tickUpper`. +// +// Parameters: +// - tickUpper: int32, the upper tick boundary for fee calculation. +// - tickCurrent: int32, the current tick index. +// - feeGrowthGlobal0X128: *u256.Uint, the global fee growth for token 0 in X128 precision. +// - feeGrowthGlobal1X128: *u256.Uint, the global fee growth for token 1 in X128 precision. +// - upperTick: TickInfo, the fee growth and liquidity details for the upper tick. +// +// Returns: +// - *u256.Uint: Fee growth above `tickUpper` for token 0. +// - *u256.Uint: Fee growth above `tickUpper` for token 1. +// +// Workflow: +// 1. If `tickCurrent` is less than `tickUpper`: +// - Return the `feeGrowthOutside0X128` and `feeGrowthOutside1X128` values of the `upperTick`. +// 2. If `tickCurrent` is greater than or equal to `tickUpper`: +// - Compute the fee growth above the upper tick by subtracting `feeGrowthOutside` values +// from the global fee growth values (`feeGrowthGlobal0X128` and `feeGrowthGlobal1X128`). +// 3. Return the calculated fee growth values for both tokens. +// +// Behavior: +// - If `tickCurrent < tickUpper`, the fee growth outside the upper tick is returned as-is. +// - If `tickCurrent >= tickUpper`, the fee growth is calculated as: +// feeGrowthAbove = feeGrowthGlobal - feeGrowthOutside +// +// Example: +// +// feeGrowth0, feeGrowth1 := getFeeGrowthAboveX128( +// 200, 150, globalFeeGrowth0, globalFeeGrowth1, upperTickInfo, +// ) +// println("Fee Growth Above:", feeGrowth0, feeGrowth1) +// +// ``` +func getFeeGrowthAboveX128( + tickUpper, tickCurrent int32, + feeGrowthGlobal0X128, feeGrowthGlobal1X128 *u256.Uint, + upperTick TickInfo, +) (*u256.Uint, *u256.Uint) { + if tickCurrent < tickUpper { + return upperTick.feeGrowthOutside0X128, upperTick.feeGrowthOutside1X128 + } + + feeGrowthAbove0X128 := u256.Zero().Sub(feeGrowthGlobal0X128, upperTick.feeGrowthOutside0X128) + feeGrowthAbove1X128 := u256.Zero().Sub(feeGrowthGlobal1X128, upperTick.feeGrowthOutside1X128) + + return feeGrowthAbove0X128, feeGrowthAbove1X128 +} + +// validateTicks validates the tick range for a liquidity position. +// +// This function performs three essential checks to ensure the provided +// tick values are valid before creating or modifying a liquidity position. +func validateTicks(tickLower, tickUpper int32) error { + if tickLower >= tickUpper { + return makeErrorWithDetails( + errInvalidTickRange, + ufmt.Sprintf("tickLower(%d), tickUpper(%d)", tickLower, tickUpper), + ) + } + + if tickLower < MIN_TICK { + return makeErrorWithDetails( + errTickLowerInvalid, + ufmt.Sprintf("tickLower(%d) < MIN_TICK(%d)", tickLower, MIN_TICK), + ) + } + + if tickUpper > MAX_TICK { + return makeErrorWithDetails( + errTickUpperInvalid, + ufmt.Sprintf("tickUpper(%d) > MAX_TICK(%d)", tickUpper, MAX_TICK), + ) + } + + return nil +} diff --git a/contract/r/gnoswap/v1/pool/tick_bitmap.gno b/contract/r/gnoswap/v1/pool/tick_bitmap.gno new file mode 100644 index 0000000..383e4df --- /dev/null +++ b/contract/r/gnoswap/v1/pool/tick_bitmap.gno @@ -0,0 +1,167 @@ +package pool + +import ( + "strconv" + + "gno.land/p/nt/ufmt" + plp "gno.land/p/gnoswap/gnsmath" + u256 "gno.land/p/gnoswap/uint256" +) + +// bitMask8 is used for efficient modulo 256 operations +const bitMask8 = 0xff // 256 - 1 + +// tickBitmapFlipTick flips the state of a tick in the tick bitmap. +// +// This function toggles the "initialized" state of a tick in the tick bitmap. +// It ensures that the tick aligns with the specified tick spacing and then +// flips the corresponding bit in the bitmap representation. +// +// Parameters: +// - tick: int32, the tick index to toggle. +// - tickSpacing: int32, the spacing between valid ticks. +// The tick must align with this spacing. +// +// Workflow: +// 1. Validates that the `tick` aligns with `tickSpacing` using `checkTickSpacing`. +// 2. Computes the position of the bit in the tick bitmap: +// - `wordPos`: Determines which word in the bitmap contains the bit. +// - `bitPos`: Identifies the position of the bit within the word. +// 3. Creates a bitmask using `Lsh` (Left Shift) to target the bit at `bitPos`. +// 4. Toggles (flips) the bit using XOR with the current value of the tick bitmap. +// 5. Updates the tick bitmap with the modified word. +// +// Behavior: +// - If the bit is `0` (uninitialized), it will be flipped to `1` (initialized). +// - If the bit is `1` (initialized), it will be flipped to `0` (uninitialized). +// +// Example: +// +// pool.tickBitmapFlipTick(120, 60) +// // This flips the bit for tick 120 with a tick spacing of 60. +// +// Notes: +// - The `tick` must be divisible by `tickSpacing`. If not, the function will panic. +func (p *Pool) tickBitmapFlipTick( + tick int32, + tickSpacing int32, +) { + checkTickSpacing(tick, tickSpacing) + wordPos, bitPos := tickBitmapPosition(tick / tickSpacing) + + mask := u256.Zero().Lsh(u256.One(), uint(bitPos)) + current := p.getTickBitmap(wordPos) + p.setTickBitmap(wordPos, u256.Zero().Xor(current, mask)) +} + +// tickBitmapNextInitializedTickWithInOneWord finds the next initialized tick within +// one word of the bitmap. +func (p *Pool) tickBitmapNextInitializedTickWithInOneWord( + tick int32, + tickSpacing int32, + lte bool, +) (int32, bool) { + compress := tick / tickSpacing + // Round towards negative infinity for negative ticks + if tick < 0 && tick%tickSpacing != 0 { + compress-- + } + + wordPos, bitPos := getWordAndBitPos(compress, lte) + mask := getMaskBit(uint(bitPos), lte) + masked := u256.Zero().And(p.getTickBitmap(wordPos), mask) + initialized := !masked.IsZero() + + nextTick := getNextTick(lte, initialized, compress, bitPos, tickSpacing, masked) + return nextTick, initialized +} + +// getTickBitmap gets the tick bitmap for the given word position +// if the tick bitmap is not initialized, initialize it to zero +func (p *Pool) getTickBitmap(wordPos int16) *u256.Uint { + wordPosStr := strconv.Itoa(int(wordPos)) + + value, exist := p.tickBitmaps.Get(wordPosStr) + if !exist { + p.initTickBitmap(wordPos) + value, exist = p.tickBitmaps.Get(wordPosStr) + if !exist { + panic(newErrorWithDetail( + errDataNotFound, + ufmt.Sprintf("failed to initialize tickBitmap(%d)", wordPos), + )) + } + } + + bitmap, ok := value.(*u256.Uint) + if !ok { + panic(ufmt.Sprintf("failed to cast tickBitmap to *u256.Uint: %T", value)) + } + return bitmap +} + +// setTickBitmap sets the tick bitmap for the given word position +func (p *Pool) setTickBitmap(wordPos int16, tickBitmap *u256.Uint) { + wordPosStr := strconv.Itoa(int(wordPos)) + p.tickBitmaps.Set(wordPosStr, tickBitmap) +} + +// initTickBitmap initializes the tick bitmap for the given word position +func (p *Pool) initTickBitmap(wordPos int16) { + p.setTickBitmap(wordPos, u256.Zero()) +} + +// tickBitmapPosition calculates the word and bit position for a given tick +func tickBitmapPosition(tick int32) (int16, uint8) { + return int16(tick >> 8), uint8(tick) & bitMask8 +} + +// getWordAndBitPos gets tick's wordPos and bitPos depending on the swap direction +func getWordAndBitPos(tick int32, lte bool) (int16, uint8) { + if !lte { + tick++ + } + return tickBitmapPosition(tick) +} + +// getMaskBit generates a mask based on the provided bit position (bitPos) and a boolean flag (lte). +// The function constructs a bitmask with a shift depending on the bit position and the boolean value. +// It either returns the mask or its negation, based on the value of 'lte' (swap direction). +// +// NOTE: should always use a newly created `u256.One()` object. +func getMaskBit(bitPos uint, lte bool) *u256.Uint { + if lte { + if bitPos == bitMask8 { + return u256.Zero().Not(u256.Zero()) // all ones + } + return u256.Zero().Sub(u256.Zero().Lsh(u256.One(), bitPos+1), u256.One()) + } + if bitPos == 0 { + return u256.Zero().Not(u256.Zero()) // all ones + } + return u256.Zero().Not(u256.Zero().Sub(u256.Zero().Lsh(u256.One(), bitPos), u256.One())) +} + +// getNextTick gets the next tick depending on the initialized state and the swap direction +func getNextTick(lte, initialized bool, compress int32, bitPos uint8, tickSpacing int32, masked *u256.Uint) int32 { + if initialized { + return getTickIfInitialized(compress, tickSpacing, bitPos, masked, lte) + } + return getTickIfNotInitialized(compress, tickSpacing, bitPos, lte) +} + +// getTickIfInitialized gets the next tick if the tick bitmap is initialized +func getTickIfInitialized(compress, tickSpacing int32, bitPos uint8, masked *u256.Uint, lte bool) int32 { + if lte { + return (compress - int32(bitPos-plp.BitMathMostSignificantBit(masked))) * tickSpacing + } + return (compress + 1 + int32(plp.BitMathLeastSignificantBit(masked)-bitPos)) * tickSpacing +} + +// getTickIfNotInitialized gets the next tick if the tick bitmap is not initialized +func getTickIfNotInitialized(compress, tickSpacing int32, bitPos uint8, lte bool) int32 { + if lte { + return (compress - int32(bitPos)) * tickSpacing + } + return (compress + 1 + int32(bitMask8-bitPos)) * tickSpacing +} diff --git a/contract/r/gnoswap/v1/pool/transfer.gno b/contract/r/gnoswap/v1/pool/transfer.gno new file mode 100644 index 0000000..64a19f6 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/transfer.gno @@ -0,0 +1,208 @@ +package pool + +import ( + "std" + + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/v1/common" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +// safeTransfer performs a token transfer out of the pool while ensuring +// the pool has sufficient balance and updating internal accounting. +// This function is typically used during swaps and liquidity removals. +// +// Important requirements: +// - The amount must be negative (representing an outflow from the pool) +// - The pool must have sufficient balance for the transfer +// - The transfer amount must fit within uint64 range +// +// Parameters: +// - to: destination address for the transfer +// - tokenPath: path identifier of the token to transfer +// - amount: amount to transfer (must be negative) +// - isToken0: true if transferring token0, false for token1 +// +// The function will: +// 1. Validate the amount is negative +// 2. Check pool has sufficient balance +// 3. Execute the transfer +// 4. Update pool's internal balance +// +// Panics if any validation fails or if the transfer fails +func (p *Pool) safeTransfer( + to std.Address, + tokenPath string, + amount *i256.Int, + isToken0 bool, +) { + if amount.Gt(i256.Zero()) { + panic(ufmt.Sprintf( + "%v. got: %s", errMustBeNegative, amount.ToString(), + )) + } + + absAmount := amount.Abs() + + token0 := p.BalanceToken0() + token1 := p.BalanceToken1() + + if err := validatePoolBalance(token0, token1, absAmount, isToken0); err != nil { + panic(err) + } + amountInt64 := safeConvertToInt64(absAmount) + + checkTransferError(common.Transfer(cross, tokenPath, to, amountInt64)) + + newBalance, err := updatePoolBalance(token0, token1, absAmount, isToken0) + if err != nil { + panic(err) + } + + if isToken0 { + p.balances.token0 = newBalance + } else { + p.balances.token1 = newBalance + } +} + +// safeTransferFrom securely transfers tokens into the pool while ensuring balance consistency. +// +// This function performs the following steps: +// 1. Validates and converts the transfer amount to `uint64` using `safeConvertToUint64`. +// 2. Executes the token transfer using `TransferFrom` via the token teller contract. +// 3. Verifies that the destination balance reflects the correct amount after transfer. +// 4. Updates the pool's internal balances (`token0` or `token1`) and validates the updated state. +// +// Parameters: +// - from (std.Address): Source address for the token transfer. +// - to (std.Address): Destination address, typically the pool address. +// - tokenPath (string): Path identifier for the token being transferred. +// - amount (*u256.Uint): The amount of tokens to transfer (must be a positive value). +// - isToken0 (bool): A flag indicating whether the token being transferred is token0 (`true`) or token1 (`false`). +// +// Panics: +// - If the `amount` exceeds the uint64 range during conversion. +// - If the token transfer (`TransferFrom`) fails. +// - If the destination balance after the transfer does not match the expected amount. +// - If the pool's internal balances (`token0` or `token1`) overflow or become inconsistent. +// +// Notes: +// - The function assumes that the sender (`from`) has approved the pool to spend the specified tokens. +// - The balance consistency check ensures that no tokens are lost or double-counted during the transfer. +// - Pool balance updates are performed atomically to ensure internal consistency. +// +// Example: +// p.safeTransferFrom( +// +// sender, poolAddress, "path/to/token0", u256.MustFromDecimal("1000"), true +// +// ) +func (p *Pool) safeTransferFrom( + from, to std.Address, + tokenPath string, + amount *u256.Uint, + isToken0 bool, +) { + amountInt64 := safeConvertToInt64(amount) + + token := common.GetToken(tokenPath) + beforeBalance := token.BalanceOf(to) + + checkTransferError(common.TransferFrom(cross, tokenPath, from, to, amountInt64)) + + afterBalance := token.BalanceOf(to) + if (beforeBalance + amountInt64) != afterBalance { + panic(ufmt.Sprintf( + "%v. beforeBalance(%d) + amount(%d) != afterBalance(%d)", + errTransferFailed, beforeBalance, amountInt64, afterBalance, + )) + } + + // update pool balances + if isToken0 { + beforeToken0 := p.balances.token0.Clone() + p.balances.token0 = u256.Zero().Add(p.balances.token0, amount) + if p.balances.token0.Lt(beforeToken0) { + panic(ufmt.Sprintf( + "%v. token0(%s) < beforeToken0(%s)", + errBalanceUpdateFailed, p.balances.token0.ToString(), beforeToken0.ToString(), + )) + } + } else { + beforeToken1 := p.balances.token1.Clone() + p.balances.token1 = u256.Zero().Add(p.balances.token1, amount) + if p.balances.token1.Lt(beforeToken1) { + panic(ufmt.Sprintf( + "%v. token1(%s) < beforeToken1(%s)", + errBalanceUpdateFailed, p.balances.token1.ToString(), beforeToken1.ToString(), + )) + } + } +} + +// validatePoolBalance checks if the pool has sufficient balance of either token0 and token1 +// before proceeding with a transfer. This prevents the pool won't go into a negative balance. +func validatePoolBalance(token0, token1, amount *u256.Uint, isToken0 bool) error { + if token0 == nil || token1 == nil || amount == nil { + return ufmt.Errorf( + "%v. token0(%s) or token1(%s) or amount(%s) is nil", + errTransferFailed, token0.ToString(), token1.ToString(), amount.ToString(), + ) + } + + if isToken0 { + if token0.Lt(amount) { + return ufmt.Errorf( + "%v. token0(%s) >= amount(%s)", + errTransferFailed, token0.ToString(), amount.ToString(), + ) + } + return nil + } + if token1.Lt(amount) { + return ufmt.Errorf( + "%v. token1(%s) >= amount(%s)", + errTransferFailed, token1.ToString(), amount.ToString(), + ) + } + return nil +} + +// updatePoolBalance calculates the new balance after a transfer and validate. +// It ensures the resulting balance won't be negative or overflow. +func updatePoolBalance( + token0, token1, amount *u256.Uint, + isToken0 bool, +) (*u256.Uint, error) { + var overflow bool + var newBalance *u256.Uint + + if isToken0 { + newBalance, overflow = u256.Zero().SubOverflow(token0, amount) + if isBalanceOverflowOrNegative(overflow, newBalance) { + return nil, ufmt.Errorf( + "%v. cannot decrease, token0(%s) - amount(%s)", + errBalanceUpdateFailed, token0.ToString(), amount.ToString(), + ) + } + return newBalance, nil + } + + newBalance, overflow = u256.Zero().SubOverflow(token1, amount) + if isBalanceOverflowOrNegative(overflow, newBalance) { + return nil, ufmt.Errorf( + "%v. cannot decrease, token1(%s) - amount(%s)", + errBalanceUpdateFailed, token1.ToString(), amount.ToString(), + ) + } + return newBalance, nil +} + +// isBalanceOverflowOrNegative checks if the balance calculation resulted in an overflow or negative value. +func isBalanceOverflowOrNegative(overflow bool, newBalance *u256.Uint) bool { + return overflow || newBalance.Lt(zero) +} diff --git a/contract/r/gnoswap/v1/pool/type.gno b/contract/r/gnoswap/v1/pool/type.gno new file mode 100644 index 0000000..38912df --- /dev/null +++ b/contract/r/gnoswap/v1/pool/type.gno @@ -0,0 +1,291 @@ +package pool + +import ( + "std" + "time" + + "gno.land/p/nt/avl" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v1/common" +) + +type PositionInfo struct { + liquidity *u256.Uint // amount of liquidity owned by this position + + // Fee growth per unit of liquidity as of the last update + // Used to calculate uncollected fees for token0 + feeGrowthInside0LastX128 *u256.Uint + + // Fee growth per unit of liquidity as of the last update + // Used to calculate uncollected fees for token1 + feeGrowthInside1LastX128 *u256.Uint + + // accumulated fees in token0 waiting to be collected + tokensOwed0 *u256.Uint + + // accumulated fees in token1 waiting to be collected + tokensOwed1 *u256.Uint +} + +// ModifyPositionParams repersents the parameters for modifying a liquidity position. +// This structure is used internally both `Mint` and `Burn` operation to manage +// the liquidity positions. +type ModifyPositionParams struct { + // owner is the address that owns the position + owner std.Address + + // tickLower and atickUpper define the price range + // The actual price range is calculated as 1.0001^tick + // This allows for precision in price range while using integer math. + + tickLower int32 // lower tick of the position + tickUpper int32 // upper tick of the position + + // liquidityDelta represents the change in liquidity + // Positive for minting, negative for burning + liquidityDelta *i256.Int +} + +// newModifyPositionParams creates a new `ModifyPositionParams` instance. +// This is used to preare parameters for the `modifyPosition` function, +// which handles both minting and burning of liquidity positions. +// +// Parameters: +// - owner: address that will own (or owns) the position +// - tickLower: lower tick bound of the position +// - tickUpper: upper tick bound of the position +// - liquidityDelta: amount of liquidity to add (positive) or remove (negative) +// +// The tick parameters represent prices as powers of 1.0001: +// - actual_price = 1.0001^tick +// - For example, tick = 100 means price = 1.0001^100 +// +// Returns: +// - ModifyPositionParams: a new instance of ModifyPositionParams +func newModifyPositionParams( + owner std.Address, + tickLower int32, + tickUpper int32, + liquidityDelta *i256.Int, +) ModifyPositionParams { + return ModifyPositionParams{ + owner: owner, + tickLower: tickLower, + tickUpper: tickUpper, + liquidityDelta: liquidityDelta, + } +} + +// SwapCache holds data that remains constant throughout a swap. +type SwapCache struct { + feeProtocol uint8 // protocol fee for the input token + liquidityStart *u256.Uint // liquidity at the beginning of the swap +} + +func newSwapCache( + feeProtocol uint8, + liquidityStart *u256.Uint, +) SwapCache { + return SwapCache{ + feeProtocol: feeProtocol, + liquidityStart: liquidityStart, + } +} + +// SwapState tracks the changing values during a swap. +// This type helps manage the state transitions that occur as the swap progresses +// across different price ranges. +type SwapState struct { + amountSpecifiedRemaining *i256.Int // amount remaining to be swapped in/out of the input/output token + amountCalculated *i256.Int // amount already swapped out/in of the output/input token + sqrtPriceX96 *u256.Uint // current sqrt(price) + tick int32 // tick associated with the current sqrt(price) + feeGrowthGlobalX128 *u256.Uint // global fee growth of the input token + protocolFee *u256.Uint // amount of input token paid as protocol fee + liquidity *u256.Uint // current liquidity in range +} + +func newSwapState( + amountSpecifiedRemaining *i256.Int, + feeGrowthGlobalX128 *u256.Uint, + liquidity *u256.Uint, + slot0 Slot0, +) SwapState { + return SwapState{ + amountSpecifiedRemaining: amountSpecifiedRemaining, + amountCalculated: i256.Zero(), + sqrtPriceX96: slot0.sqrtPriceX96, + tick: slot0.tick, + feeGrowthGlobalX128: feeGrowthGlobalX128, + protocolFee: u256.Zero(), + liquidity: liquidity, + } +} + +func (s *SwapState) setSqrtPriceX96(sqrtPriceX96 *u256.Uint) { + s.sqrtPriceX96 = sqrtPriceX96.Clone() +} + +func (s *SwapState) setTick(tick int32) { + s.tick = tick +} + +func (s *SwapState) setFeeGrowthGlobalX128(feeGrowthGlobalX128 *u256.Uint) { + s.feeGrowthGlobalX128 = feeGrowthGlobalX128 +} + +func (s *SwapState) setProtocolFee(fee *u256.Uint) { + s.protocolFee = fee +} + +// StepComputations holds intermediate values used during a single step of a swap. +// Each step represents movement from the current tick to the next initialized tick +// or the target price, whichever comes first. +type StepComputations struct { + sqrtPriceStartX96 *u256.Uint // price at the beginning of the step + tickNext int32 // next tick to swap to from the current tick in the swap direction + initialized bool // whether tickNext is initialized + sqrtPriceNextX96 *u256.Uint // sqrt(price) for the next tick (token1/token0) Q96 + amountIn *u256.Uint // how much being swapped in this step + amountOut *u256.Uint // how much is being swapped out in this step + feeAmount *u256.Uint // how much fee is being paid in this step +} + +// init initializes the computation for a single swap step +func (step *StepComputations) initSwapStep(state SwapState, pool *Pool, zeroForOne bool) { + step.sqrtPriceStartX96 = state.sqrtPriceX96 + step.tickNext, step.initialized = pool.tickBitmapNextInitializedTickWithInOneWord( + state.tick, + pool.tickSpacing, + zeroForOne, + ) + + // prevent overshoot the min/max tick + step.clampTickNext() + + // get the price for the next tick + step.sqrtPriceNextX96 = common.TickMathGetSqrtRatioAtTick(step.tickNext) +} + +// clampTickNext ensures that `tickNext` stays within the min, max tick boundaries +// as the tick bitmap is not aware of these bounds +func (step *StepComputations) clampTickNext() { + if step.tickNext < MIN_TICK { + step.tickNext = MIN_TICK + } else if step.tickNext > MAX_TICK { + step.tickNext = MAX_TICK + } +} + +// valueOrZero initializes nil fields in PositionInfo to zero. +// +// This function ensures that all numeric fields in the PositionInfo struct are not nil. +// If a field is nil, it is replaced with a zero value, maintaining consistency and preventing +// potential null pointer issues during calculations. +// +// Fields affected: +// - liquidity: The liquidity amount associated with the position. +// - feeGrowthInside0LastX128: Fee growth for token 0 inside the tick range, last recorded value. +// - feeGrowthInside1LastX128: Fee growth for token 1 inside the tick range, last recorded value. +// - tokensOwed0: The amount of token 0 owed to the position owner. +// - tokensOwed1: The amount of token 1 owed to the position owner. +// +// Behavior: +// - If a field is nil, it is set to its equivalent zero value. +// - If a field already has a value, it remains unchanged. +// +// Example: +// +// position := &PositionInfo{} +// position.valueOrZero() +// println(position.liquidity) // Output: 0 +// +// Notes: +// - This function is useful for ensuring numeric fields are properly initialized +// before performing operations or calculations. +// - Prevents runtime errors caused by nil values. +func (p *PositionInfo) valueOrZero() { + p.liquidity = p.liquidity.NilToZero() + p.feeGrowthInside0LastX128 = p.feeGrowthInside0LastX128.NilToZero() + p.feeGrowthInside1LastX128 = p.feeGrowthInside1LastX128.NilToZero() + p.tokensOwed0 = p.tokensOwed0.NilToZero() + p.tokensOwed1 = p.tokensOwed1.NilToZero() +} + +// TickInfo stores information about a specific tick in the pool. +// TIcks represent discrete price points that can be used as boundaries for positions. +type TickInfo struct { + liquidityGross *u256.Uint // total position liquidity that references this tick + liquidityNet *i256.Int // amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) + + // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + feeGrowthOutside0X128 *u256.Uint + feeGrowthOutside1X128 *u256.Uint + + tickCumulativeOutside int64 // cumulative tick value on the other side of the tick + + // the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + secondsPerLiquidityOutsideX128 *u256.Uint + + // the seconds spent on the other side of the tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + secondsOutside uint32 + + initialized bool // whether the tick is initialized +} + +// valueOrZero ensures that all fields of TickInfo are valid by setting nil fields to zero, +// while retaining existing values if they are not nil. +// This function updates the TickInfo struct to replace any nil values in its fields +// with their respective zero values, ensuring data consistency. +// +// Behavior: +// - If a field is nil, it is replaced with its zero value. +// - If a field already has a valid value, the value remains unchanged. +// +// Fields: +// - liquidityGross: Gross liquidity for the tick, set to zero if nil, otherwise retains its value. +// - liquidityNet: Net liquidity for the tick, set to zero if nil, otherwise retains its value. +// - feeGrowthOutside0X128: Accumulated fee growth for token0 outside the tick, set to zero if nil, otherwise retains its value. +// - feeGrowthOutside1X128: Accumulated fee growth for token1 outside the tick, set to zero if nil, otherwise retains its value. +// - secondsPerLiquidityOutsideX128: Time per liquidity outside the tick, set to zero if nil, otherwise retains its value. +// +// Use Case: +// This function ensures all numeric fields in TickInfo are non-nil and have valid values, +// preventing potential runtime errors caused by nil values during operations like arithmetic or comparisons. +func (t *TickInfo) valueOrZero() { + t.liquidityGross = t.liquidityGross.NilToZero() + t.liquidityNet = t.liquidityNet.NilToZero() + t.feeGrowthOutside0X128 = t.feeGrowthOutside0X128.NilToZero() + t.feeGrowthOutside1X128 = t.feeGrowthOutside1X128.NilToZero() + t.secondsPerLiquidityOutsideX128 = t.secondsPerLiquidityOutsideX128.NilToZero() +} + +func newPool(poolInfo *poolCreateConfig) *Pool { + maxLiquidityPerTick := calculateMaxLiquidityPerTick(poolInfo.tickSpacing) + tick := common.TickMathGetTickAtSqrtRatio(poolInfo.SqrtPriceX96()) + slot0 := newSlot0(poolInfo.SqrtPriceX96(), tick, slot0FeeProtocol, true) + + return &Pool{ + token0Path: poolInfo.Token0Path(), + token1Path: poolInfo.Token1Path(), + balances: newBalances(), + fee: poolInfo.Fee(), + tickSpacing: poolInfo.TickSpacing(), + maxLiquidityPerTick: maxLiquidityPerTick, + slot0: slot0, + feeGrowthGlobal0X128: u256.Zero(), + feeGrowthGlobal1X128: u256.Zero(), + protocolFees: newProtocolFees(), + liquidity: u256.Zero(), + ticks: avl.NewTree(), + tickBitmaps: avl.NewTree(), + positions: avl.NewTree(), + observation: newObservation(time.Now().Unix()), + } +} diff --git a/contract/r/gnoswap/v1/pool/utils.gno b/contract/r/gnoswap/v1/pool/utils.gno new file mode 100644 index 0000000..c00ef20 --- /dev/null +++ b/contract/r/gnoswap/v1/pool/utils.gno @@ -0,0 +1,193 @@ +package pool + +import ( + "strconv" + + "gno.land/p/nt/ufmt" + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +const ( + MAX_UINT64 string = "18446744073709551615" + MAX_INT64 string = "9223372036854775807" + MAX_INT128 string = "170141183460469231731687303715884105727" + MAX_UINT128 string = "340282366920938463463374607431768211455" + + INT64_MAX int64 = 9223372036854775807 + + Q96_RESOLUTION uint = 96 + Q128_RESOLUTION uint = 128 + + Q64 string = "18446744073709551616" // 2 ** 64 + Q96 string = "79228162514264337593543950336" // 2 ** 96 + Q128 string = "340282366920938463463374607431768211456" // 2 ** 128 +) + +// safeConvertToUint64 safely converts a *u256.Uint value to a uint64, ensuring no overflow. +// This function attempts to convert the given *u256.Uint value to a uint64. +// If the value exceeds the maximum allowable range for uint64 (2^64 - 1), it panics. +func safeConvertToUint64(value *u256.Uint) uint64 { + res, overflow := value.Uint64WithOverflow() + if overflow { + panic(ufmt.Sprintf( + "%v: amount(%s) overflows uint64 range (max %s)", + errOutOfRange, + value.ToString(), + MAX_UINT64, + )) + } + return res +} + +// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. +// This function attempts to convert the given *u256.Uint value to an int64. +// If the value exceeds the maximum allowable range for int64 (2^63 - 1), it panics. +func safeConvertToInt64(value *u256.Uint) int64 { + res, overflow := value.Uint64WithOverflow() + if overflow || res > uint64(INT64_MAX) { + panic(ufmt.Sprintf( + "%v: amount(%s) overflows int64 range (max %s)", + errOutOfRange, + value.ToString(), + MAX_INT64, + )) + } + return int64(res) +} + +// safeConvertToInt128 safely converts a *u256.Uint value to an *i256.Int, ensuring it does not exceed the int128 range. +// This function converts an unsigned 256-bit integer to a signed 256-bit integer. +// If the value exceeds the maximum allowable int128 range (2^127 - 1), it panics. +func safeConvertToInt128(value *u256.Uint) *i256.Int { + liquidityDelta := i256.FromUint256(value) + if liquidityDelta.Gt(i256.MustFromDecimal(MAX_INT128)) { + panic(ufmt.Sprintf( + "%v: amount(%s) overflows int128 range", + errOverFlow, value.ToString())) + } + return liquidityDelta +} + +// toUint128 ensures a *u256.Uint value fits within the uint128 range. +// +// This function validates that the given `value` is properly initialized and checks whether +// it exceeds the maximum value of uint128. If the value exceeds the uint128 range, +// it applies a masking operation to truncate the value to fit within the uint128 limit. +// +// Parameters: +// - value: *u256.Uint, the value to be checked and possibly truncated. +// +// Returns: +// - *u256.Uint: A value guaranteed to fit within the uint128 range. +// +// Notes: +// - The function first checks if the value is not nil to avoid potential runtime errors. +// - The mask ensures that only the lower 128 bits of the value are retained. +// - If the input value is already within the uint128 range, it is returned unchanged. +// - If masking is required, a new instance is returned without modifying the input. +// - MAX_UINT128 is a constant representing `2^128 - 1`. +func toUint128(value *u256.Uint) *u256.Uint { + if value == nil { + panic(newErrorWithDetail( + errInvalidInput, + "value is nil", + )) + } + + if value.Gt(u256.MustFromDecimal(MAX_UINT128)) { + mask := u256.Zero().Lsh(u256.One(), Q128_RESOLUTION) + mask = u256.Zero().Sub(mask, u256.One()) + return u256.Zero().And(value, mask) + } + return value +} + +// u256Min returns the smaller of two *u256.Uint values. +// +// This function compares two unsigned 256-bit integers and returns the smaller of the two. +// If `num1` is less than `num2`, it returns `num1`; otherwise, it returns `num2`. +// +// Parameters: +// - num1 (*u256.Uint): The first unsigned 256-bit integer. +// - num2 (*u256.Uint): The second unsigned 256-bit integer. +// +// Returns: +// - *u256.Uint: The smaller of `num1` and `num2`. +// +// Notes: +// - This function uses the `Lt` (less than) method of `*u256.Uint` to perform the comparison. +// - The function assumes both input values are non-nil. If nil inputs are possible in the usage context, +// additional validation may be needed. +// +// Example: +// smaller := u256Min(u256.MustFromDecimal("10"), u256.MustFromDecimal("20")) // Returns 10 +// smaller := u256Min(u256.MustFromDecimal("30"), u256.MustFromDecimal("20")) // Returns 20 +func u256Min(num1, num2 *u256.Uint) *u256.Uint { + if num1.Lt(num2) { + return num1 + } + return num2 +} + +// checkTransferError checks transfer error. +func checkTransferError(err error) { + if err != nil { + panic(newErrorWithDetail( + errTransferFailed, + err.Error(), + )) + } +} + +// checkOverFlowInt128 checks if the value overflows the int128 range. +func checkOverFlowInt128(value *i256.Int) { + if value.Gt(i256.MustFromDecimal(MAX_INT128)) { + panic(ufmt.Sprintf( + "%v: amount(%s) overflows int128 range", + errOverFlow, value.ToString())) + } +} + +// checkTickSpacing checks if the tick is divisible by the tickSpacing. +func checkTickSpacing(tick, tickSpacing int32) { + if tick%tickSpacing != 0 { + panic(newErrorWithDetail( + errInvalidTickAndTickSpacing, + ufmt.Sprintf("tick(%d) MOD tickSpacing(%d) != 0(%d)", tick, tickSpacing, tick%tickSpacing), + )) + } +} + +// formatUint converts various unsigned integer types to string representation. +func formatUint(v any) string { + switch v := v.(type) { + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} + +// formatInt converts various signed integer types to string representation. +func formatInt(v any) string { + switch v := v.(type) { + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case int: + return strconv.Itoa(v) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} + +// formatBool converts a boolean value to string representation. +func formatBool(v bool) string { + return strconv.FormatBool(v) +} diff --git a/contract/r/gnoswap/v1/position/README.md b/contract/r/gnoswap/v1/position/README.md new file mode 100644 index 0000000..0614753 --- /dev/null +++ b/contract/r/gnoswap/v1/position/README.md @@ -0,0 +1,157 @@ +# Position + +NFT-based liquidity position management for concentrated liquidity. + +## Overview + +Each liquidity position is a unique GRC721 NFT containing pool identifier, price range, liquidity amount, accumulated fees, and token balances. + +## Configuration + +- **Withdrawal Fee**: 1% on collected fees +- **Max Position Size**: No limit +- **Transfer Restrictions**: Non-transferable NFTs + +## Core Functions + +### `Mint` + +Creates new position NFT with initial liquidity. + +- Validates tick range alignment +- Calculates optimal token ratio +- Returns actual amounts used + +### `IncreaseLiquidity` + +Adds liquidity to existing position. + +- Maintains same price range +- Pro-rata token amounts + +### `DecreaseLiquidity` + +Removes liquidity while keeping NFT. + +- Two-step: decrease then collect +- Calculates owed tokens + +### `CollectFee` + +Claims accumulated swap fees. + +- No liquidity removal required +- 1% protocol fees applied + +### `Reposition` + +Atomically moves liquidity to new range. + +- Burns old position +- Creates new position +- Mints new NFT + +## Technical Details + +### Tick Alignment + +Ticks must align with pool's tick spacing: + +``` +0.01% fee: every 1 tick +0.05% fee: every 10 ticks +0.3% fee: every 60 ticks +1% fee: every 200 ticks +``` + +### Optimal Range Width + +**Stable Pairs (USDC/USDT)**: + +- Narrow: ±0.05% (max efficiency) +- Medium: ±0.1% (balanced) +- Wide: ±0.5% (safety) + +**Correlated Pairs (WETH/stETH)**: + +- Narrow: ±0.5% +- Medium: ±1% +- Wide: ±2% + +**Volatile Pairs (WETH/USDC)**: + +- Narrow: ±5% +- Medium: ±10% +- Wide: ±25% + +### Capital Efficiency + +Concentration factor vs infinite range: + +``` +Range ±0.1% → 2000x efficient +Range ±1% → 200x efficient +Range ±10% → 20x efficient +Range ±50% → 4x efficient +``` + +### Token Calculations + +**Below range (token1 only)**: + +``` +amount1 = L * (sqrtUpper - sqrtLower) +amount0 = 0 +``` + +**Above range (token0 only)**: + +``` +amount0 = L * (sqrtUpper - sqrtLower) / (sqrtUpper * sqrtLower) +amount1 = 0 +``` + +**In range (both tokens)**: + +``` +amount0 = L * (sqrtUpper - sqrtCurrent) / (sqrtUpper * sqrtCurrent) +amount1 = L * (sqrtCurrent - sqrtLower) +``` + +## Usage + +```go +// Mint new position +tokenId := Mint( + "WETH/USDC:3000", // pool + -887220, // tickLower + 887220, // tickUpper + "1000000", // amount0Desired + "2000000000", // amount1Desired + "950000", // amount0Min + "1900000000", // amount1Min + deadline, + recipient, +) + +// Add liquidity +IncreaseLiquidity( + tokenId, + "500000", + "1000000000", + "475000", + "950000000", + deadline, +) + +// Collect fees +CollectFee(tokenId) +``` + +## Security + +- Tick range validation prevents invalid positions +- Slippage protection on all operations +- Deadline prevents stale transactions +- Position NFTs are non-transferable +- Only owner can manage their positions diff --git a/contract/r/gnoswap/v1/position/api.gno b/contract/r/gnoswap/v1/position/api.gno new file mode 100644 index 0000000..d31e1f2 --- /dev/null +++ b/contract/r/gnoswap/v1/position/api.gno @@ -0,0 +1,152 @@ +package position + +import ( + "std" + "time" + + u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/v1/common" + "gno.land/r/gnoswap/v1/gnft" + pl "gno.land/r/gnoswap/v1/pool" +) + +const MAX_UINT256 string = "115792089237316195423570985008687907853269984665640564039457584007913129639935" + +func ApiGetPosition(id uint64) string { + _, exist := GetPosition(id) + if !exist { + return "" + } + + rpcPosition := rpcMakePosition(id) + baseStat := NewResponseQueryBase(std.ChainHeight(), time.Now().Unix()) + return makeJsonResponse(&baseStat, &PositionsResponse{Positions: []RpcPosition{rpcPosition}}) +} + +func ApiGetPositionUnclaimedFeeByLpPositionId(lpPositionId uint64) string { + unclaimedFee0, unclaimedFee1 := unclaimedFee(lpPositionId) + fee := RpcUnclaimedFee{ + LpPositionId: lpPositionId, + Fee0: unclaimedFee0.ToString(), + Fee1: unclaimedFee1.ToString(), + } + + baseStat := NewResponseQueryBase(std.ChainHeight(), time.Now().Unix()) + return makeJsonResponse(&baseStat, &UnclaimedFeesResponse{ + Fees: []RpcUnclaimedFee{fee}, + }) +} + +func rpcMakePosition(positionId uint64) RpcPosition { + position := MustGetPosition(positionId) + + currentSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(position.poolKey) + lowerTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickLower) + upperTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickUpper) + + calculatedToken0Balance, calculatedToken1Balance := common.GetAmountsForLiquidity( + currentSqrtPriceX96, + lowerTickSqrtPriceX96, + upperTickSqrtPriceX96, + position.liquidity, + ) + + token0Balance, token1Balance := position.token0Balance, position.token1Balance + + unclaimedFee0 := u256.Zero() + unclaimedFee1 := u256.Zero() + burned := IsBurned(positionId) + if !burned { + unclaimedFee0, unclaimedFee1 = unclaimedFee(positionId) + } + + owner, err := gnft.OwnerOf(positionIdFrom(positionId)) + if err != nil { + owner = zeroAddress + } + + return RpcPosition{ + LpPositionId: positionId, + Burned: burned, + Owner: owner.String(), + Operator: position.operator.String(), + PoolKey: position.poolKey, + TickLower: position.tickLower, + TickUpper: position.tickUpper, + Liquidity: position.liquidity.ToString(), + FeeGrowthInside0LastX128: position.feeGrowthInside0LastX128.ToString(), + FeeGrowthInside1LastX128: position.feeGrowthInside1LastX128.ToString(), + TokensOwed0: position.tokensOwed0.ToString(), + TokensOwed1: position.tokensOwed1.ToString(), + Token0Balance: token0Balance.ToString(), + Token1Balance: token1Balance.ToString(), + CalculatedToken0Balance: calculatedToken0Balance, + CalculatedToken1Balance: calculatedToken1Balance, + FeeUnclaimed0: unclaimedFee0.ToString(), + FeeUnclaimed1: unclaimedFee1.ToString(), + } +} + +func UnclaimedFee(positionId uint64) (*u256.Uint, *u256.Uint) { + return unclaimedFee(positionId) +} + +func unclaimedFee(positionId uint64) (*u256.Uint, *u256.Uint) { + // ref: https://blog.uniswap.org/uniswap-v3-math-primer-2#calculating-uncollected-fees + + position := MustGetPosition(positionId) + + liquidity := position.liquidity + tickLower := position.tickLower + tickUpper := position.tickUpper + + poolKey := position.poolKey + + currentTick := pl.GetSlot0Tick(poolKey) + + feeGrowthGlobal0X128, feeGrowthGlobal1X128 := pl.GetFeeGrowthGlobalX128(poolKey) + tickUpperFeeGrowthOutside0X128, tickUpperFeeGrowthOutside1X128 := pl.GetTickFeeGrowthOutsideX128(poolKey, tickUpper) + tickLowerFeeGrowthOutside0X128, tickLowerFeeGrowthOutside1X128 := pl.GetTickFeeGrowthOutsideX128(poolKey, tickLower) + + feeGrowthInside0LastX128 := position.feeGrowthInside0LastX128 + feeGrowthInside1LastX128 := position.feeGrowthInside1LastX128 + + var tickLowerFeeGrowthBelow0, tickLowerFeeGrowthBelow1, tickUpperFeeGrowthAbove0, tickUpperFeeGrowthAbove1 *u256.Uint + + if currentTick >= tickUpper { + tickUpperFeeGrowthAbove0 = subUint256(feeGrowthGlobal0X128, tickUpperFeeGrowthOutside0X128) + tickUpperFeeGrowthAbove1 = subUint256(feeGrowthGlobal1X128, tickUpperFeeGrowthOutside1X128) + } else { + tickUpperFeeGrowthAbove0 = tickUpperFeeGrowthOutside0X128 + tickUpperFeeGrowthAbove1 = tickUpperFeeGrowthOutside1X128 + } + + if currentTick >= tickLower { + tickLowerFeeGrowthBelow0 = tickLowerFeeGrowthOutside0X128 + tickLowerFeeGrowthBelow1 = tickLowerFeeGrowthOutside1X128 + } else { + tickLowerFeeGrowthBelow0 = subUint256(feeGrowthGlobal0X128, tickLowerFeeGrowthOutside0X128) + tickLowerFeeGrowthBelow1 = subUint256(feeGrowthGlobal1X128, tickLowerFeeGrowthOutside1X128) + } + + feeGrowthInside0X128 := subUint256(feeGrowthGlobal0X128, tickLowerFeeGrowthBelow0) + feeGrowthInside0X128 = subUint256(feeGrowthInside0X128, tickUpperFeeGrowthAbove0) + + feeGrowthInside1X128 := subUint256(feeGrowthGlobal1X128, tickLowerFeeGrowthBelow1) + feeGrowthInside1X128 = subUint256(feeGrowthInside1X128, tickUpperFeeGrowthAbove1) + + diffGrowthInside0X128 := subUint256(feeGrowthInside0X128, feeGrowthInside0LastX128) + unclaimedFee0X128 := u256.Zero().Mul(liquidity, diffGrowthInside0X128) + unclaimedFee0 := u256.Zero().Div(unclaimedFee0X128, u256.MustFromDecimal(Q128)) + + diffGrowthInside1X128 := subUint256(feeGrowthInside1X128, feeGrowthInside1LastX128) + unclaimedFee1X128 := u256.Zero().Mul(liquidity, diffGrowthInside1X128) + unclaimedFee1 := u256.Zero().Div(unclaimedFee1X128, u256.MustFromDecimal(Q128)) + + return unclaimedFee0, unclaimedFee1 +} + +func IsBurned(positionId uint64) bool { + position := MustGetPosition(positionId) + return position.burned +} diff --git a/contract/r/gnoswap/v1/position/assert.gno b/contract/r/gnoswap/v1/position/assert.gno new file mode 100644 index 0000000..fa3fbf7 --- /dev/null +++ b/contract/r/gnoswap/v1/position/assert.gno @@ -0,0 +1,111 @@ +package position + +import ( + "std" + "time" + + ufmt "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/access" +) + +// assertIsNotExpired panics if the deadline is expired. +func assertIsNotExpired(deadline int64) { + now := time.Now().Unix() + + if now > deadline { + panic(makeErrorWithDetails( + errExpired, + ufmt.Sprintf("transaction too old, now(%d) > deadline(%d)", now, deadline), + )) + } +} + +// assertValidNumberString panics if the input string does not represent a valid integer. +func assertValidNumberString(input string) { + if len(input) == 0 { + panic(newErrorWithDetail( + errInvalidInput, + ufmt.Sprintf("input is empty"))) + } + + bytes := []byte(input) + for i, b := range bytes { + if i == 0 && b == '-' { + continue // Allow if the first character is a negative sign (-) + } + if b < '0' || b > '9' { + panic(newErrorWithDetail( + errInvalidInput, + ufmt.Sprintf("input string : %s", input))) + } + } +} + +// assertValidLiquidityAmount panics if the liquidity amount is zero. +func assertValidLiquidityAmount(liquidity string) { + if u256.MustFromDecimal(liquidity).IsZero() { + panic(newErrorWithDetail( + errZeroLiquidity, + ufmt.Sprintf("liquidity amount must be greater than 0, got %s", liquidity), + )) + } +} + +// assertExistsPosition panics if the position does not exist. +func assertExistsPosition(positionId uint64) { + if !exists(positionId) { + panic(newErrorWithDetail( + errPositionDoesNotExist, + ufmt.Sprintf("position with position ID(%d) doesn't exist", positionId), + )) + } +} + +// assertIsOwnerForToken panics if caller is not the owner of the position. +func assertIsOwnerForToken(positionId uint64, caller std.Address) { + assertExistsPosition(positionId) + + if !isOwner(positionId, caller) { + panic(newErrorWithDetail( + errNoPermission, + ufmt.Sprintf("caller(%s) is not owner of positionId(%d)", caller, positionId), + )) + } +} + +// assertIsOwnerOrOperatorForToken panics if caller is not the owner or operator of the position. +func assertIsOwnerOrOperatorForToken(positionId uint64, caller std.Address) { + assertExistsPosition(positionId) + + if !isOwnerOrOperator(positionId, caller) { + panic(newErrorWithDetail( + errNoPermission, + ufmt.Sprintf("caller(%s) is not owner or approved operator of positionId(%d)", caller, positionId), + )) + } +} + +// assertEqualsAddress panics if addresses are invalid or not equal. +func assertEqualsAddress(prevAddr, otherAddr std.Address) { + access.AssertIsValidAddress(prevAddr) + access.AssertIsValidAddress(otherAddr) + + if prevAddr != otherAddr { + panic(newErrorWithDetail( + errInvalidAddress, + ufmt.Sprintf("(%s, %s)", prevAddr, otherAddr), + )) + } +} + +// assertSlippageIsNotExceeded panics if slippage tolerance is exceeded. +func assertSlippageIsNotExceeded(amount0, amount1, amount0Min, amount1Min *u256.Uint) { + if !(amount0.Gte(amount0Min) && amount1.Gte(amount1Min)) { + panic(newErrorWithDetail( + errSlippage, + ufmt.Sprintf("amount0(%s) >= amount0Min(%s) && amount1(%s) >= amount1Min(%s)", + amount0.ToString(), amount0Min.ToString(), amount1.ToString(), amount1Min.ToString()), + )) + } +} diff --git a/contract/r/gnoswap/v1/position/burn.gno b/contract/r/gnoswap/v1/position/burn.gno new file mode 100644 index 0000000..69d743f --- /dev/null +++ b/contract/r/gnoswap/v1/position/burn.gno @@ -0,0 +1,210 @@ +package position + +import ( + "gno.land/p/nt/ufmt" + prabc "gno.land/p/gnoswap/rbac" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/v1/common" + pl "gno.land/r/gnoswap/v1/pool" +) + +// decreaseLiquidity reduces position liquidity and collects fees. +// If unwrapResult is true, unwraps WUGNOT to GNOT. +// Returns positionId, liquidity, fee0, fee1, amount0, amount1, poolPath. +func decreaseLiquidity(params DecreaseLiquidityParams) (uint64, string, string, string, string, string, string, error) { + caller := params.caller + + // before decrease liquidity, collect fee first + _, fee0Str, fee1Str, _, _, _ := collectFee(params.positionId, params.unwrapResult, params.caller) + + position := MustGetPosition(params.positionId) + positionLiquidity := position.liquidity + if positionLiquidity.IsZero() { + return params.positionId, + "", + fee0Str, + fee1Str, + "", "", + position.poolKey, + makeErrorWithDetails( + errZeroLiquidity, + ufmt.Sprintf("position(position ID:%d) has 0 liquidity", params.positionId), + ) + } + + liquidityToRemove := u256.MustFromDecimal(params.liquidity) + if liquidityToRemove.Gt(positionLiquidity) { + return params.positionId, + liquidityToRemove.ToString(), + fee0Str, + fee1Str, + "", "", + position.poolKey, + makeErrorWithDetails( + errInvalidLiquidity, + ufmt.Sprintf("Liquidity requested(%s) is greater than liquidity held(%s)", liquidityToRemove.ToString(), positionLiquidity.ToString()), + ) + } + + pToken0, pToken1, pFee := splitOf(position.poolKey) + burn0, burn1 := pl.Burn(cross, pToken0, pToken1, pFee, position.tickLower, position.tickUpper, liquidityToRemove.ToString(), caller) + + burnedAmount0 := u256.MustFromDecimal(burn0) + burnedAmount1 := u256.MustFromDecimal(burn1) + if isSlippageExceeded(burnedAmount0, burnedAmount1, params.amount0Min, params.amount1Min) { + return params.positionId, + liquidityToRemove.ToString(), + fee0Str, + fee1Str, + burn0, + burn1, + position.poolKey, + makeErrorWithDetails( + errSlippage, + ufmt.Sprintf("burnedAmount0(%s) >= amount0Min(%s) || burnedAmount1(%s) >= amount1Min(%s)", + burnedAmount0.ToString(), + params.amount0Min.ToString(), + burnedAmount1.ToString(), + params.amount1Min.ToString(), + ), + ) + } + + positionKey := computePositionKey(position.tickLower, position.tickUpper) + feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(position.poolKey, positionKey) + + currentSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(position.poolKey) + lowerTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickLower) + upperTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickUpper) + calculatedToken0BalanceStr, calculatedToken1BalanceStr := common.GetAmountsForLiquidity( + currentSqrtPriceX96, + lowerTickSqrtPriceX96, + upperTickSqrtPriceX96, + position.liquidity, + ) + calculatedToken0Balance := u256.MustFromDecimal(calculatedToken0BalanceStr) + calculatedToken1Balance := u256.MustFromDecimal(calculatedToken1BalanceStr) + + position.tokensOwed0 = updateTokensOwed( + feeGrowthInside0LastX128, + position.feeGrowthInside0LastX128, + position.liquidity, + burnedAmount0, + position.tokensOwed0, + ) + + position.tokensOwed1 = updateTokensOwed( + feeGrowthInside1LastX128, + position.feeGrowthInside1LastX128, + position.liquidity, + burnedAmount1, + position.tokensOwed1, + ) + + position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128 + position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128 + position.liquidity = u256.Zero().Sub(positionLiquidity, liquidityToRemove) + position.token0Balance = u256.Zero().Sub(calculatedToken0Balance, burnedAmount0) + position.token1Balance = u256.Zero().Sub(calculatedToken1Balance, burnedAmount1) + mustUpdatePosition(params.positionId, position) + + collect0, collect1 := pl.Collect( + cross, + pToken0, + pToken1, + pFee, + caller, + position.tickLower, + position.tickUpper, + burn0, + burn1, + ) + + collectAmount0 := u256.MustFromDecimal(collect0) + collectAmount1 := u256.MustFromDecimal(collect1) + + poolAddr, _ := access.GetAddress(prabc.ROLE_POOL.String()) + + if isWrappedToken(pToken0) && params.unwrapResult { + unwrapWithTransferFrom(poolAddr, caller, safeConvertToInt64(collectAmount0)) + } else { + common.TransferFrom(cross, pToken0, poolAddr, caller, safeConvertToInt64(collectAmount0)) + } + + if isWrappedToken(pToken1) && params.unwrapResult { + unwrapWithTransferFrom(poolAddr, caller, safeConvertToInt64(collectAmount1)) + } else { + common.TransferFrom(cross, pToken1, poolAddr, caller, safeConvertToInt64(collectAmount1)) + } + + // Check for underflow when subtracting collected amounts from tokens owed + newOwed0, underflow0 := u256.Zero().SubOverflow(position.tokensOwed0, collectAmount0) + if underflow0 { + panic(ufmt.Sprintf("[POSITION] burn.gno | collect() | tokensOwed0(%s) < collectAmount0(%s)", position.tokensOwed0.ToString(), collectAmount0.ToString())) + } + position.tokensOwed0 = newOwed0 + + newOwed1, underflow1 := u256.Zero().SubOverflow(position.tokensOwed1, collectAmount1) + if underflow1 { + panic(ufmt.Sprintf("[POSITION] burn.gno | collect() | tokensOwed1(%s) < collectAmount1(%s)", position.tokensOwed1.ToString(), collectAmount1.ToString())) + } + position.tokensOwed1 = newOwed1 + + if position.isClear() { + position.burned = true // just update flag (we don't want to burn actual position) + } + + mustUpdatePosition(params.positionId, position) + + return params.positionId, liquidityToRemove.ToString(), fee0Str, fee1Str, collect0, collect1, position.poolKey, nil +} + +func updateTokensOwed( + feeGrowthInsideLastX128 *u256.Uint, + positionFeeGrowthInsideLastX128 *u256.Uint, + positionLiquidity *u256.Uint, + burnedAmount *u256.Uint, + tokensOwed *u256.Uint, +) *u256.Uint { + additionalTokensOwed := calculateTokensOwed(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128, positionLiquidity) + add := u256.Zero().Add(burnedAmount, additionalTokensOwed) + return u256.Zero().Add(tokensOwed, add) +} + +// calculateFees calculates the fees for the current position. +func calculateFees(position Position, currentFeeGrowth FeeGrowthInside) (*u256.Uint, *u256.Uint) { + fee0 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside0LastX128, + position.feeGrowthInside0LastX128, + position.liquidity, + ) + + fee1 := calculateTokensOwed( + currentFeeGrowth.feeGrowthInside1LastX128, + position.feeGrowthInside1LastX128, + position.liquidity, + ) + + tokensOwed0, overflow0 := u256.Zero().AddOverflow(u256.Zero().Set(position.tokensOwed0), fee0) + if overflow0 { + panic(newErrorWithDetail(errOverflow, "tokensOwed0 + fee0 overflow")) + } + tokensOwed1, overflow1 := u256.Zero().AddOverflow(u256.Zero().Set(position.tokensOwed1), fee1) + if overflow1 { + panic(newErrorWithDetail(errOverflow, "tokensOwed1 + fee1 overflow")) + } + + return tokensOwed0, tokensOwed1 +} + +func calculateTokensOwed( + feeGrowthInsideLastX128 *u256.Uint, + positionFeeGrowthInsideLastX128 *u256.Uint, + positionLiquidity *u256.Uint, +) *u256.Uint { + diff := u256.Zero().Sub(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128) + // TODO: make Q128 a global variable + return u256.MulDiv(diff, positionLiquidity, u256.MustFromDecimal(Q128)) +} diff --git a/contract/r/gnoswap/v1/position/doc.gno b/contract/r/gnoswap/v1/position/doc.gno new file mode 100644 index 0000000..62eec08 --- /dev/null +++ b/contract/r/gnoswap/v1/position/doc.gno @@ -0,0 +1,5 @@ +// Package position manages liquidity positions as NFTs in GnoSwap pools. +// It provides functionality for minting, burning, and managing concentrated liquidity positions, +// handling position ownership through GNFT tokens, and managing wrapped/unwrapped native tokens. +// Each position represents liquidity provided within a specific price range in a pool. +package position diff --git a/contract/r/gnoswap/v1/position/errors.gno b/contract/r/gnoswap/v1/position/errors.gno new file mode 100644 index 0000000..bca698b --- /dev/null +++ b/contract/r/gnoswap/v1/position/errors.gno @@ -0,0 +1,40 @@ +package position + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errNoPermission = errors.New("[GNOSWAP-POSITION-001] caller has no permission") + errSlippage = errors.New("[GNOSWAP-POSITION-002] slippage failed") + errWrapUnwrap = errors.New("[GNOSWAP-POSITION-003] wrap, unwrap failed") + errZeroWrappedAmount = errors.New("[GNOSWAP-POSITION-004] zero wrapped amount") + errInvalidInput = errors.New("[GNOSWAP-POSITION-005] invalid input data") + errDataNotFound = errors.New("[GNOSWAP-POSITION-006] requested data not found") + errExpired = errors.New("[GNOSWAP-POSITION-007] transaction expired") + errWugnotMinimum = errors.New("[GNOSWAP-POSITION-008] can not wrap less than minimum amount") + errNotClear = errors.New("[GNOSWAP-POSITION-009] position is not clear") + errZeroLiquidity = errors.New("[GNOSWAP-POSITION-010] zero liquidity") + errPositionExist = errors.New("[GNOSWAP-POSITION-011] position with same positionId already exists") + errInvalidAddress = errors.New("[GNOSWAP-POSITION-012] invalid address") + errPositionDoesNotExist = errors.New("[GNOSWAP-POSITION-013] position does not exist") + errZeroUGNOT = errors.New("[GNOSWAP-POSITION-014] No UGNOTs were sent") + errInsufficientUGNOT = errors.New("[GNOSWAP-POSITION-015] Insufficient UGNOT provided") + errInvalidTokenPath = errors.New("[GNOSWAP-POSITION-016] invalid token address") + errInvalidLiquidityRatio = errors.New("[GNOSWAP-POSITION-017] invalid liquidity ratio") + errUnderflow = errors.New("[GNOSWAP-POSITION-018] underflow") + errOverflow = errors.New("[GNOSWAP-POSITION-019] overflow") + errInvalidLiquidity = errors.New("[GNOSWAP-POSITION-019] invalid liquidity") +) + +// newErrorWithDetail appends additional context or details to an existing error message. +func newErrorWithDetail(err error, detail string) string { + return ufmt.Errorf("%s || %s", err.Error(), detail).Error() +} + +// makeErrorWithDetails creates an error with additional context. +func makeErrorWithDetails(err error, details string) error { + return ufmt.Errorf("%s || %s", err.Error(), details) +} diff --git a/contract/r/gnoswap/v1/position/getter.gno b/contract/r/gnoswap/v1/position/getter.gno new file mode 100644 index 0000000..99a61aa --- /dev/null +++ b/contract/r/gnoswap/v1/position/getter.gno @@ -0,0 +1,104 @@ +package position + +import ( + "std" + + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v1/gnft" + pl "gno.land/r/gnoswap/v1/pool" +) + +func PositionGetPosition(positionId uint64) Position { + position, _ := GetPosition(positionId) + return position +} + +func PositionGetPositionNonce(positionId uint64) *u256.Uint { + return MustGetPosition(positionId).nonce +} + +func PositionGetPositionOperator(positionId uint64) std.Address { + return MustGetPosition(positionId).operator +} + +func PositionGetPositionPoolKey(positionId uint64) string { + return MustGetPosition(positionId).poolKey +} + +func PositionGetPositionTickLower(positionId uint64) int32 { + return MustGetPosition(positionId).tickLower +} + +func PositionGetPositionTickUpper(positionId uint64) int32 { + return MustGetPosition(positionId).tickUpper +} + +func PositionGetPositionLiquidity(positionId uint64) *u256.Uint { + return MustGetPosition(positionId).liquidity +} + +func PositionGetPositionFeeGrowthInside0LastX128(positionId uint64) *u256.Uint { + return MustGetPosition(positionId).feeGrowthInside0LastX128 +} + +func PositionGetPositionFeeGrowthInside1LastX128(positionId uint64) *u256.Uint { + return MustGetPosition(positionId).feeGrowthInside1LastX128 +} + +func PositionGetPositionTokensOwed0(positionId uint64) *u256.Uint { + return MustGetPosition(positionId).tokensOwed0 +} + +func PositionGetPositionTokensOwed1(positionId uint64) *u256.Uint { + return MustGetPosition(positionId).tokensOwed1 +} + +func PositionGetPositionIsBurned(positionId uint64) bool { + return MustGetPosition(positionId).burned +} + +func PositionIsInRange(positionId uint64) bool { + position := MustGetPosition(positionId) + poolPath := position.poolKey + poolCurrentTick := pl.GetSlot0Tick(poolPath) + + return position.tickLower <= poolCurrentTick && poolCurrentTick < position.tickUpper +} + +func PositionGetPositionOwner(positionId uint64) std.Address { + owner, err := gnft.OwnerOf(positionIdFrom(positionId)) + if err != nil { + panic(newErrorWithDetail( + errDataNotFound, err.Error())) + } + return owner +} + +func PositionGetPositionNonceStr(positionId uint64) string { + return PositionGetPositionNonce(positionId).ToString() +} + +func PositionGetPositionOperatorStr(positionId uint64) string { + return PositionGetPositionOperator(positionId).String() +} + +func PositionGetPositionLiquidityStr(positionId uint64) string { + return PositionGetPositionLiquidity(positionId).ToString() +} + +func PositionGetPositionFeeGrowthInside0LastX128Str(positionId uint64) string { + return PositionGetPositionFeeGrowthInside0LastX128(positionId).ToString() +} + +func PositionGetPositionFeeGrowthInside1LastX128Str(positionId uint64) string { + return PositionGetPositionFeeGrowthInside1LastX128(positionId).ToString() +} + +func PositionGetPositionTokensOwed0Str(positionId uint64) string { + return PositionGetPositionTokensOwed0(positionId).ToString() +} + +func PositionGetPositionTokensOwed1Str(positionId uint64) string { + return PositionGetPositionTokensOwed1(positionId).ToString() +} diff --git a/contract/r/gnoswap/v1/position/gnomod.toml b/contract/r/gnoswap/v1/position/gnomod.toml new file mode 100644 index 0000000..a0d7e4c --- /dev/null +++ b/contract/r/gnoswap/v1/position/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/position" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/position/json.gno b/contract/r/gnoswap/v1/position/json.gno new file mode 100644 index 0000000..eaf7911 --- /dev/null +++ b/contract/r/gnoswap/v1/position/json.gno @@ -0,0 +1,149 @@ +package position + +import ( + "std" + + "gno.land/p/onbloc/json" + "gno.land/r/gnoswap/v1/gnft" +) + +type JsonResponse interface { + JSON() *json.Node +} + +// helper function for creating JSON response +func makeJsonResponse(stat *ResponseQueryBase, response JsonResponse) string { + node := json.ObjectNode("", map[string]*json.Node{ + "stat": stat.JSON(), + "response": response.JSON(), + }) + + b, err := json.Marshal(node) + if err != nil { + panic(err.Error()) + } + + return string(b) +} + +// Type for Positions response +type PositionsResponse struct { + Positions []RpcPosition +} + +func (pr *PositionsResponse) JSON() *json.Node { + rsps := json.ArrayNode("", []*json.Node{}) + for _, position := range pr.Positions { + owner, err := gnft.OwnerOf(positionIdFrom(position.LpPositionId)) + if err != nil { + owner = zeroAddress + } + rsps.AppendArray(position.JSON(owner)) + } + return rsps +} + +// Type for UnclaimedFee response +type UnclaimedFeesResponse struct { + Fees []RpcUnclaimedFee +} + +func (ur *UnclaimedFeesResponse) JSON() *json.Node { + rsps := json.ArrayNode("", []*json.Node{}) + for _, fee := range ur.Fees { + rsps.AppendArray(fee.JSON()) + } + return rsps +} + +///////////////// RPC TYPES ///////////////// + +type RpcPosition struct { + LpPositionId uint64 `json:"lpPositionId"` + Burned bool `json:"burned"` + Owner string `json:"owner"` + Operator string `json:"operator"` + PoolKey string `json:"poolKey"` + TickLower int32 `json:"tickLower"` + TickUpper int32 `json:"tickUpper"` + Liquidity string `json:"liquidity"` + FeeGrowthInside0LastX128 string `json:"feeGrowthInside0LastX128"` + FeeGrowthInside1LastX128 string `json:"feeGrowthInside1LastX128"` + TokensOwed0 string `json:"token0Owed"` + TokensOwed1 string `json:"token1Owed"` + + Token0Balance string `json:"token0Balance"` + Token1Balance string `json:"token1Balance"` + CalculatedToken0Balance string `json:"calculatedToken0Balance"` + CalculatedToken1Balance string `json:"calculatedToken1Balance"` + FeeUnclaimed0 string `json:"fee0Unclaimed"` + FeeUnclaimed1 string `json:"fee1Unclaimed"` +} + +func (p RpcPosition) JSON(owner std.Address) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "lpPositionId": json.NumberNode("lpPositionId", float64(p.LpPositionId)), + "burned": json.BoolNode("burned", p.Burned), + "owner": json.StringNode("owner", owner.String()), + "operator": json.StringNode("operator", p.Operator), + "poolKey": json.StringNode("poolKey", p.PoolKey), + "tickLower": json.NumberNode("tickLower", float64(p.TickLower)), + "tickUpper": json.NumberNode("tickUpper", float64(p.TickUpper)), + "liquidity": json.StringNode("liquidity", p.Liquidity), + "feeGrowthInside0LastX128": json.StringNode("feeGrowthInside0LastX128", p.FeeGrowthInside0LastX128), + "feeGrowthInside1LastX128": json.StringNode("feeGrowthInside1LastX128", p.FeeGrowthInside1LastX128), + "token0Owed": json.StringNode("token0Owed", p.TokensOwed0), + "token1Owed": json.StringNode("token1Owed", p.TokensOwed1), + "token0Balance": json.StringNode("token0Balance", p.Token0Balance), + "token1Balance": json.StringNode("token1Balance", p.Token1Balance), + "calculatedToken0Balance": json.StringNode("calculatedToken0Balance", p.CalculatedToken0Balance), + "calculatedToken1Balance": json.StringNode("calculatedToken1Balance", p.CalculatedToken1Balance), + "fee0Unclaimed": json.StringNode("fee0Unclaimed", p.FeeUnclaimed0), + "fee1Unclaimed": json.StringNode("fee1Unclaimed", p.FeeUnclaimed1), + }) +} + +type RpcUnclaimedFee struct { + LpPositionId uint64 `json:"lpPositionId"` + Fee0 string `json:"fee0"` + Fee1 string `json:"fee1"` +} + +func (p RpcUnclaimedFee) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "lpPositionId": json.NumberNode("lpPositionId", float64(p.LpPositionId)), + "fee0": json.StringNode("fee0", p.Fee0), + "fee1": json.StringNode("fee1", p.Fee1), + }) +} + +type ResponseQueryBase struct { + Height int64 `json:"height"` + Timestamp int64 `json:"timestamp"` +} + +func NewResponseQueryBase(height int64, timestamp int64) ResponseQueryBase { + return ResponseQueryBase{ + Height: height, + Timestamp: timestamp, + } +} + +func (r ResponseQueryBase) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "height": json.NumberNode("height", float64(r.Height)), + "timestamp": json.NumberNode("timestamp", float64(r.Timestamp)), + }) +} + +type ResponseApiGetPositions struct { + Stat ResponseQueryBase `json:"stat"` + Response []RpcPosition `json:"response"` +} + +func NewResponseApiGetPositions(stat ResponseQueryBase, response []RpcPosition) ResponseApiGetPositions { + return ResponseApiGetPositions{ + Stat: stat, + Response: response, + } +} diff --git a/contract/r/gnoswap/v1/position/liquidity_management.gno b/contract/r/gnoswap/v1/position/liquidity_management.gno new file mode 100644 index 0000000..69a14f8 --- /dev/null +++ b/contract/r/gnoswap/v1/position/liquidity_management.gno @@ -0,0 +1,67 @@ +package position + +import ( + "std" + + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v1/common" + pl "gno.land/r/gnoswap/v1/pool" +) + +type AddLiquidityParams struct { + poolKey string // poolPath of the pool which has the position + tickLower int32 // lower end of the tick range for the position + tickUpper int32 // upper end of the tick range for the position + amount0Desired *u256.Uint // desired amount of token0 to be minted + amount1Desired *u256.Uint // desired amount of token1 to be minted + amount0Min *u256.Uint // minimum amount of token0 to be minted + amount1Min *u256.Uint // minimum amount of token1 to be minted + caller std.Address // address to call the function +} + +// addLiquidity calculates liquidity amounts and mints position tokens to a pool. +func addLiquidity(params AddLiquidityParams) (*u256.Uint, *u256.Uint, *u256.Uint) { + sqrtPriceX96 := pl.GetSlot0SqrtPriceX96(params.poolKey) + sqrtRatioAX96 := common.TickMathGetSqrtRatioAtTick(params.tickLower) + sqrtRatioBX96 := common.TickMathGetSqrtRatioAtTick(params.tickUpper) + + liquidity := common.GetLiquidityForAmounts( + sqrtPriceX96, + sqrtRatioAX96, + sqrtRatioBX96, + params.amount0Desired, + params.amount1Desired, + ) + + token0, token1, fee := splitOf(params.poolKey) + amount0Str, amount1Str := pl.Mint( + cross, + token0, + token1, + fee, + positionAddr, + params.tickLower, + params.tickUpper, + liquidity.ToString(), + params.caller, + ) + + amount0 := u256.MustFromDecimal(amount0Str) + amount1 := u256.MustFromDecimal(amount1Str) + + amount0Cond := amount0.Gte(params.amount0Min) + amount1Cond := amount1.Gte(params.amount1Min) + + if !(amount0Cond && amount1Cond) { + panic(newErrorWithDetail( + errSlippage, + ufmt.Sprintf( + "Price Slippage Check(amount0(%s) >= amount0Min(%s), amount1(%s) >= amount1Min(%s))", + amount0Str, params.amount0Min.ToString(), amount1Str, params.amount1Min.ToString()), + )) + } + + return liquidity, amount0, amount1 +} diff --git a/contract/r/gnoswap/v1/position/manager.gno b/contract/r/gnoswap/v1/position/manager.gno new file mode 100644 index 0000000..d8adb5e --- /dev/null +++ b/contract/r/gnoswap/v1/position/manager.gno @@ -0,0 +1,95 @@ +package position + +import ( + "strconv" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" +) + +var ( + positions = avl.NewTree() // positionId[uint64] -> Position + nextId = uint64(1) +) + +// GetPosition returns a position for a given position ID. +// Returns false if position doesn't exist +func GetPosition(id uint64) (Position, bool) { + idStr := strconv.FormatUint(id, 10) + iPosition, exist := positions.Get(idStr) + if !exist { + return Position{}, false + } + + pos, ok := iPosition.(Position) + if !ok { + panic("cannot cast position to Position") + } + return pos, ok +} + +// MustGetPosition returns a position for a given position ID. +// panics if position doesn't exist +func MustGetPosition(id uint64) Position { + position, exist := GetPosition(id) + if !exist { + panic(newErrorWithDetail( + errPositionDoesNotExist, + ufmt.Sprintf("position with position ID(%d) doesn't exist", id), + )) + } + return position +} + +// ExistPosition checks if a position exists for a given position ID +func ExistPosition(id uint64) bool { + _, exist := GetPosition(id) + return exist +} + +// GetNextId is the next position ID to be minted +func GetNextId() uint64 { + return nextId +} + +// createNewPosition creates a new position with the given position data. +func createNewPosition(id uint64, pos Position) uint64 { + if ExistPosition(id) { + panic(newErrorWithDetail( + errPositionExist, + ufmt.Sprintf("positionId(%d)", id), + )) + } + setPosition(id, pos) + incrementNextId() + return id +} + +// setPosition sets a position for a given position ID. +// Returns true if position is newly created, false if position already exists and just updated. +func setPosition(id uint64, position Position) bool { + posIdStr := strconv.FormatUint(id, 10) + return positions.Set(posIdStr, position) +} + +// mustUpdatePosition updates a position for a given position ID. +func mustUpdatePosition(id uint64, pos Position) { + update := setPosition(id, pos) + if !update { + panic(newErrorWithDetail( + errPositionDoesNotExist, + ufmt.Sprintf("position with position ID(%d) doesn't exist", id), + )) + } +} + +// removePosition removes a position for a given position ID +func removePosition(id uint64) { + posIdStr := strconv.FormatUint(id, 10) + positions.Remove(posIdStr) +} + +// incrementNextId increments the next position ID to be minted +func incrementNextId() { + nextId++ +} diff --git a/contract/r/gnoswap/v1/position/mint.gno b/contract/r/gnoswap/v1/position/mint.gno new file mode 100644 index 0000000..e9afeba --- /dev/null +++ b/contract/r/gnoswap/v1/position/mint.gno @@ -0,0 +1,300 @@ +package position + +import ( + "std" + + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v1/common" + "gno.land/r/gnoswap/v1/gnft" + pl "gno.land/r/gnoswap/v1/pool" +) + +const ( + WRAPPED_WUGNOT string = "gno.land/r/gnoland/wugnot" + UGNOT string = "ugnot" + GNOT string = "gnot" +) + +// mint creates a new liquidity position by adding liquidity to a pool and minting an NFT. +// Panics if position ID already exists or adding liquidity fails. +func mint(params MintParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint) { + poolKey := pl.GetPoolPath(params.token0, params.token1, params.fee) + liquidity, amount0, amount1 := addLiquidity( + AddLiquidityParams{ + poolKey: poolKey, + tickLower: params.tickLower, + tickUpper: params.tickUpper, + amount0Desired: params.amount0Desired, + amount1Desired: params.amount1Desired, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + caller: params.caller, + }, + ) + // Ensure liquidity is not zero before minting NFT + if liquidity.IsZero() { + panic(newErrorWithDetail( + errZeroLiquidity, + "Liquidity is zero, cannot mint position.", + )) + } + + id := GetNextId() + if ExistPosition(id) { + panic(newErrorWithDetail( + errPositionExist, + ufmt.Sprintf("positionId(%d)", id), + )) + } + + gnft.Mint(cross, params.mintTo, positionIdFrom(id)) + + positionKey := computePositionKey(params.tickLower, params.tickUpper) + feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(poolKey, positionKey) + + position := Position{ + nonce: u256.Zero(), + operator: zeroAddress, + poolKey: poolKey, + tickLower: params.tickLower, + tickUpper: params.tickUpper, + liquidity: liquidity, + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128, + tokensOwed0: u256.Zero(), + tokensOwed1: u256.Zero(), + token0Balance: amount0, + token1Balance: amount1, + burned: false, + } + + // The position ID should not exist at the time of minting + updated := setPosition(id, position) + if updated { + panic(newErrorWithDetail( + errPositionExist, + ufmt.Sprintf("position ID(%d) already exists", id), + )) + } + incrementNextId() + + return id, liquidity, amount0, amount1 +} + +// processMintInput processes and validates user input for minting liquidity. +// It handles token ordering, amount validation, and native token wrapping. +func processMintInput(input MintInput) (ProcessedMintInput, error) { + assertValidNumberString(input.amount0Desired) + assertValidNumberString(input.amount1Desired) + assertValidNumberString(input.amount0Min) + assertValidNumberString(input.amount1Min) + var result ProcessedMintInput + + // process tokens + token0, token1, token0IsNative, token1IsNative, wrappedAmount, err := processTokens( + input.token0, + input.token1, + input.amount0Desired, + input.amount1Desired, + input.caller, + ) + if err != nil { + return ProcessedMintInput{}, err + } + + pair := TokenPair{ + token0: token0, + token1: token1, + token0IsNative: token0IsNative, + token1IsNative: token1IsNative, + wrappedAmount: wrappedAmount, + } + + // parse amounts + amount0Desired, amount1Desired, amount0Min, amount1Min := parseAmounts(input.amount0Desired, input.amount1Desired, input.amount0Min, input.amount1Min) + + tickLower, tickUpper := input.tickLower, input.tickUpper + + // swap if token1 < token0 + if token1 < token0 { + pair.token0, pair.token1 = pair.token1, pair.token0 + amount0Desired, amount1Desired = amount1Desired, amount0Desired + amount0Min, amount1Min = amount1Min, amount0Min + tickLower, tickUpper = -tickUpper, -tickLower + pair.token0IsNative, pair.token1IsNative = pair.token1IsNative, pair.token0IsNative + } + + poolPath := computePoolPath(pair.token0, pair.token1, input.fee) + + result = ProcessedMintInput{ + tokenPair: pair, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: amount0Min, + amount1Min: amount1Min, + tickLower: tickLower, + tickUpper: tickUpper, + poolPath: poolPath, + } + + return result, nil +} + +// processTokens validates token paths and handles native token wrapping. +// Panics if validation fails or native token wrapping encounters issues. +func processTokens( + token0 string, + token1 string, + amount0Desired string, + amount1Desired string, + caller std.Address, +) (string, string, bool, bool, int64, error) { + err := validateTokenPath(token0, token1) + if err != nil { + panic(newErrorWithDetail(err, ufmt.Sprintf("token0(%s), token1(%s)", token0, token1))) + } + + token0IsNative := false + token1IsNative := false + wrappedAmount := int64(0) + + if isNative(token0) { + token0 = WRAPPED_WUGNOT + token0IsNative = true + + amount0DesiredInt := mustParseInt64(amount0Desired) + wrappedAmount, err = safeWrapNativeToken(amount0DesiredInt, caller) + if err != nil { + return "", "", false, false, 0, err + } + } else if isNative(token1) { + token1 = WRAPPED_WUGNOT + token1IsNative = true + + amount1DesiredInt := mustParseInt64(amount1Desired) + wrappedAmount, err = safeWrapNativeToken(amount1DesiredInt, caller) + if err != nil { + return "", "", false, false, 0, err + } + } + + return token0, token1, token0IsNative, token1IsNative, wrappedAmount, nil +} + +// increaseLiquidity increases the liquidity of an existing position. +func increaseLiquidity(params IncreaseLiquidityParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint, string, error) { + caller := params.caller + position, exist := GetPosition(params.positionId) + if !exist { + return 0, nil, nil, nil, "", makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("positionId(%d) doesn't exist", params.positionId), + ) + } + + liquidity, amount0, amount1 := addLiquidity( + AddLiquidityParams{ + poolKey: position.poolKey, + tickLower: position.tickLower, + tickUpper: position.tickUpper, + amount0Desired: params.amount0Desired, + amount1Desired: params.amount1Desired, + amount0Min: params.amount0Min, + amount1Min: params.amount1Min, + caller: caller, + }, + ) + + positionKey := computePositionKey(position.tickLower, position.tickUpper) + feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(position.poolKey, positionKey) + + currentSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(position.poolKey) + lowerTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickLower) + upperTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickUpper) + calculatedToken0BalanceStr, calculatedToken1BalanceStr := common.GetAmountsForLiquidity( + currentSqrtPriceX96, + lowerTickSqrtPriceX96, + upperTickSqrtPriceX96, + position.liquidity, + ) + calculatedToken0Balance := u256.MustFromDecimal(calculatedToken0BalanceStr) + calculatedToken1Balance := u256.MustFromDecimal(calculatedToken1BalanceStr) + + { + diff := u256.Zero().Sub(feeGrowthInside0LastX128, position.feeGrowthInside0LastX128) + mulDiv := u256.MulDiv(diff, u256.Zero().Set(position.liquidity), u256.MustFromDecimal(Q128)) + + position.tokensOwed0 = u256.Zero().Add(position.tokensOwed0, mulDiv) + } + + { + diff := u256.Zero().Sub(feeGrowthInside1LastX128, position.feeGrowthInside1LastX128) + mulDiv := u256.MulDiv(diff, u256.Zero().Set(position.liquidity), u256.MustFromDecimal(Q128)) + + position.tokensOwed1 = u256.Zero().Add(position.tokensOwed1, mulDiv) + } + + position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128 + position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128 + + liquidityAmount, overflow := u256.Zero().AddOverflow(u256.Zero().Set(position.liquidity), liquidity) + if overflow { + return 0, nil, nil, nil, "", errOverflow + } + token0Balance, overflow := u256.Zero().AddOverflow(u256.Zero().Set(calculatedToken0Balance), amount0) + if overflow { + return 0, nil, nil, nil, "", errOverflow + } + token1Balance, overflow := u256.Zero().AddOverflow(u256.Zero().Set(calculatedToken1Balance), amount1) + if overflow { + return 0, nil, nil, nil, "", errOverflow + } + + position.liquidity = liquidityAmount + position.token0Balance = token0Balance + position.token1Balance = token1Balance + position.burned = false + + updated := setPosition(params.positionId, position) + if !updated { + return 0, nil, nil, nil, "", makeErrorWithDetails( + errPositionDoesNotExist, + ufmt.Sprintf("can not increase liquidity for non-existent position(%d)", params.positionId), + ) + } + + return params.positionId, liquidity, amount0, amount1, position.poolKey, nil +} + +// validateTokenPath validates token paths are not identical, not conflicting, and in valid format. +func validateTokenPath(token0, token1 string) error { + if token0 == token1 { + return errInvalidTokenPath + } + if (token0 == GNOT && token1 == WRAPPED_WUGNOT) || + (token0 == WRAPPED_WUGNOT && token1 == GNOT) { + return errInvalidTokenPath + } + if (!isNative(token0) && !isValidTokenPath(token0)) || + (!isNative(token1) && !isValidTokenPath(token1)) { + return errInvalidTokenPath + } + return nil +} + +// isValidTokenPath checks if the token path is registered in the system. +func isValidTokenPath(tokenPath string) bool { + return common.IsRegistered(tokenPath) == nil +} + +// parseAmounts converts amount strings to u256.Uint values. +func parseAmounts(amount0Desired, amount1Desired, amount0Min, amount1Min string) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) { + return u256.MustFromDecimal(amount0Desired), u256.MustFromDecimal(amount1Desired), u256.MustFromDecimal(amount0Min), u256.MustFromDecimal(amount1Min) +} + +// computePoolPath returns the pool path based on token pair and fee tier. +func computePoolPath(token0, token1 string, fee uint32) string { + return pl.GetPoolPath(token0, token1, fee) +} diff --git a/contract/r/gnoswap/v1/position/native_token.gno b/contract/r/gnoswap/v1/position/native_token.gno new file mode 100644 index 0000000..cf13141 --- /dev/null +++ b/contract/r/gnoswap/v1/position/native_token.gno @@ -0,0 +1,171 @@ +package position + +import ( + "std" + + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoland/wugnot" +) + +const ( + UGNOT_MIN_DEPOSIT_TO_WRAP = int64(1000) + WUGNOT_PATH = "gno.land/r/gnoland/wugnot" + GNOT_DENOM = "ugnot" +) + +var ( + errFailedToWrapZeroUgnot = "cannot wrap 0 ugnot" + errFailedToWrapBelowMin = "amount(%d) < minimum(%d)" +) + +// wrap wraps the specified amount of the native token ugnot into the wrapped token wugnot. +// Returns an error if ugnotAmount is zero or less than the minimum deposit threshold. +func wrap(ugnotAmount int64, to std.Address) error { + if ugnotAmount == 0 || ugnotAmount < UGNOT_MIN_DEPOSIT_TO_WRAP { + return ufmt.Errorf("amount(%d) < minimum(%d)", ugnotAmount, UGNOT_MIN_DEPOSIT_TO_WRAP) + } + + wugnotAddr := std.DerivePkgAddr(WRAPPED_WUGNOT) + transferUGNOT(positionAddr, wugnotAddr, ugnotAmount) + + wugnot.Deposit(cross) // position has wugnot + wugnot.Transfer(cross, to, ugnotAmount) // send wugnot: position -> user + + return nil +} + +// unwrap converts a specified amount of WUGNOT tokens into UGNOT tokens +// and transfers the resulting UGNOT back to the specified recipient address. +func unwrap(wugnotAmount int64, to std.Address) error { + if wugnotAmount <= 0 { + return ufmt.Errorf("amount(%d) is zero or negative", wugnotAmount) + } + + wugnot.TransferFrom(cross, to, positionAddr, wugnotAmount) // send wugnot: user -> position + wugnot.Withdraw(cross, wugnotAmount) // position has ugnot + transferUGNOT(positionAddr, to, wugnotAmount) // send ugnot: position -> user + return nil +} + +// transferUGNOT transfers a specified amount of UGNOT tokens from one address to another. +// The function ensures that no transaction occurs if the transfer amount is zero. +// Panics if amount is negative or if sender has insufficient balance. +func transferUGNOT(from, to std.Address, amount int64) { + if amount < 0 { + panic(ufmt.Sprintf("amount(%d) is negative", amount)) + } + if amount == 0 { + return + } + + banker := std.NewBanker(std.BankerTypeRealmSend) + fromBalance := banker.GetCoins(from).AmountOf(UGNOT) + if fromBalance < amount { + panic(newErrorWithDetail( + errInsufficientUGNOT, + ufmt.Sprintf("from(%s) balance(%d) is less than amount(%d)", from, fromBalance, amount))) + } + banker.SendCoins(from, to, std.Coins{ + {Denom: UGNOT, Amount: amount}, + }) +} + +// isNative checks whether the given token is a native token. +func isNative(token string) bool { + return token == GNOT +} + +// isWrappedToken checks whether the tokenPath is wrapped token. +func isWrappedToken(tokenPath string) bool { + return tokenPath == WRAPPED_WUGNOT +} + +// safeWrapNativeToken safely wraps the native token ugnot into the wrapped token wugnot for a user. +// Returns the amount of ugnot that was successfully wrapped into wugnot. +// Returns an error if the sent ugnot amount is zero, less than requested, or if wrapping fails. +func safeWrapNativeToken(amount int64, toAddress std.Address) (int64, error) { + // if amount is zero, return 0 + if amount == 0 { + return 0, nil + } + + beforeWrappedBalance := wugnot.BalanceOf(toAddress) + nativeSentAmount := std.OriginSend().AmountOf(UGNOT) + + if nativeSentAmount <= 0 { + return 0, makeErrorWithDetails(errZeroUGNOT, "amount of ugnot is zero") + } + + if nativeSentAmount < amount { + return 0, makeErrorWithDetails(errInsufficientUGNOT, "amount of ugnot is less than desired amount") + } + + // If nativeSentAmount is greater than amount, refund the excess amount. + if nativeSentAmount > amount { + excessAmount := nativeSentAmount - amount + transferUGNOT(positionAddr, toAddress, excessAmount) + + nativeSentAmount = amount + } + + if err := wrapWithTransfer(nativeSentAmount, toAddress); err != nil { + return 0, err + } + + afterWrappedBalance := wugnot.BalanceOf(toAddress) + balanceDiff := afterWrappedBalance - beforeWrappedBalance + + if balanceDiff != nativeSentAmount { + return 0, makeErrorWithDetails( + errWrapUnwrap, + ufmt.Sprintf("amount of ugnot (%d) is not equal to amount of wugnot. (diff: %d)", nativeSentAmount, balanceDiff), + ) + } + + return nativeSentAmount, nil +} + +func wrapWithTransfer(ugnotAmount int64, toAddress std.Address) error { + if ugnotAmount <= 0 { + return makeErrorWithDetails(errWrapUnwrap, errFailedToWrapZeroUgnot) + } + + if ugnotAmount < UGNOT_MIN_DEPOSIT_TO_WRAP { + return makeErrorWithDetails( + errWugnotMinimum, + ufmt.Sprintf(errFailedToWrapBelowMin, ugnotAmount, UGNOT_MIN_DEPOSIT_TO_WRAP), + ) + } + + // wrap it + wugnotAddr := std.DerivePkgAddr(WUGNOT_PATH) + currentRealmAddr := std.CurrentRealm().Address() + + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(currentRealmAddr, wugnotAddr, std.Coins{{"ugnot", ugnotAmount}}) + wugnot.Deposit(cross) // Position has wugnot + + // send wugnot: position -> user + wugnot.Transfer(cross, toAddress, ugnotAmount) + + return nil +} + +func unwrapWithTransferFrom(fromAddress, toAddress std.Address, wugnotAmount int64) error { + if wugnotAmount == 0 { + return nil + } + + currentRealmAddr := std.CurrentRealm().Address() + if fromAddress != currentRealmAddr { + wugnot.TransferFrom(cross, fromAddress, currentRealmAddr, wugnotAmount) + } + + wugnot.Withdraw(cross, wugnotAmount) + + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(currentRealmAddr, toAddress, std.Coins{{"ugnot", wugnotAmount}}) + + return nil +} diff --git a/contract/r/gnoswap/v1/position/position.gno b/contract/r/gnoswap/v1/position/position.gno new file mode 100644 index 0000000..3322ac2 --- /dev/null +++ b/contract/r/gnoswap/v1/position/position.gno @@ -0,0 +1,542 @@ +package position + +import ( + "encoding/base64" + "std" + + "gno.land/p/nt/ufmt" + prabc "gno.land/p/gnoswap/rbac" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/rbac" + "gno.land/r/gnoswap/referral" + "gno.land/r/gnoswap/v1/common" + pl "gno.land/r/gnoswap/v1/pool" +) + +var ( + positionAddr = rbac.DefaultRoleAddresses[prabc.ROLE_POSITION] + stakerAddr = rbac.DefaultRoleAddresses[prabc.ROLE_STAKER] +) + +const ( + ZERO_LIQUIDITY_FOR_FEE_COLLECTION = "0" +) + +// Mint creates a new liquidity position NFT. +// +// Parameters: +// - token0, token1: token contract paths +// - fee: pool fee tier +// - tickLower, tickUpper: price range boundaries +// - amount0Desired, amount1Desired: desired token amounts +// - amount0Min, amount1Min: minimum acceptable amounts +// - deadline: transaction deadline +// - mintTo: NFT recipient address +// - caller: transaction initiator +// - referrer: referral address +// +// Returns tokenId, liquidity, amount0, amount1. +// Only callable by users or staker contract. +// Note: Slippage protection via amount0Min/amount1Min. +func Mint( + cur realm, + token0 string, + token1 string, + fee uint32, + tickLower int32, + tickUpper int32, + amount0Desired string, + amount1Desired string, + amount0Min string, + amount1Min string, + deadline int64, + mintTo std.Address, + caller std.Address, + referrer string, +) (uint64, string, string, string) { + halt.AssertIsNotHaltedPosition() + + previousRealm := std.PreviousRealm() + if !previousRealm.IsUser() { + access.AssertIsStaker(previousRealm.Address()) + } else { + assertEqualsAddress(previousRealm.Address(), mintTo) + assertEqualsAddress(previousRealm.Address(), caller) + } + + assertIsNotExpired(deadline) + + referral.TryRegister(cross, caller, referrer) + + emission.MintAndDistributeGns(cross) + + mintInput := MintInput{ + token0: token0, + token1: token1, + fee: fee, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline, + mintTo: mintTo, + caller: caller, + } + processedInput, err := processMintInput(mintInput) + if err != nil { + panic(newErrorWithDetail(errInvalidInput, err.Error())) + } + + // mint liquidity + params := newMintParams(processedInput, mintInput) + id, liquidity, amount0, amount1 := mint(params) + + // refund leftover wrapped tokens + if processedInput.tokenPair.token0IsNative && processedInput.tokenPair.wrappedAmount > safeConvertToInt64(amount0) { + err = unwrap(processedInput.tokenPair.wrappedAmount-safeConvertToInt64(amount0), caller) + if err != nil { + panic(newErrorWithDetail(errWrapUnwrap, err.Error())) + } + } + + if processedInput.tokenPair.token1IsNative && processedInput.tokenPair.wrappedAmount > safeConvertToInt64(amount1) { + err = unwrap(processedInput.tokenPair.wrappedAmount-safeConvertToInt64(amount1), caller) + if err != nil { + panic(newErrorWithDetail(errWrapUnwrap, err.Error())) + } + } + + poolSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(processedInput.poolPath) + + std.Emit( + "Mint", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "tickLower", formatInt(processedInput.tickLower), + "tickUpper", formatInt(processedInput.tickUpper), + "poolPath", processedInput.poolPath, + "mintTo", mintTo.String(), + "caller", caller.String(), + "lpPositionId", formatUint(id), + "liquidity", liquidity.ToString(), + "amount0", amount0.ToString(), + "amount1", amount1.ToString(), + "sqrtPriceX96", poolSqrtPriceX96.ToString(), + "token0Balance", pl.GetBalanceToken0(processedInput.poolPath), + "token1Balance", pl.GetBalanceToken1(processedInput.poolPath), + ) + + return id, liquidity.ToString(), amount0.ToString(), amount1.ToString() +} + +// IncreaseLiquidity increases liquidity of an existing position. +// +// Adds more liquidity to existing NFT position. +// Maintains same price range as original position. +// Calculates optimal token ratio for current price. +// +// Parameters: +// - positionId: NFT token ID to increase +// - amount0DesiredStr: Desired token0 amount +// - amount1DesiredStr: Desired token1 amount +// - amount0MinStr: Minimum token0 (slippage protection) +// - amount1MinStr: Minimum token1 (slippage protection) +// - deadline: Transaction expiration timestamp +// +// Returns: +// - positionId: Same NFT ID +// - liquidity: Total liquidity after increase +// - amount0: Token0 actually deposited +// - amount1: Token1 actually deposited +// - poolPath: Pool identifier +// +// Requirements: +// - Caller must own the position NFT +// - Position must have liquidity +// - Sufficient token balances and approvals +func IncreaseLiquidity( + cur realm, + positionId uint64, + amount0DesiredStr string, + amount1DesiredStr string, + amount0MinStr string, + amount1MinStr string, + deadline int64, +) (uint64, string, string, string, string) { + halt.AssertIsNotHaltedPosition() + + caller := std.PreviousRealm().Address() + assertIsOwnerForToken(positionId, caller) + + assertValidNumberString(amount0DesiredStr) + assertValidNumberString(amount1DesiredStr) + assertValidNumberString(amount0MinStr) + assertValidNumberString(amount1MinStr) + assertIsNotExpired(deadline) + + emission.MintAndDistributeGns(cross) + + position := MustGetPosition(positionId) + token0, token1, _ := splitOf(position.poolKey) + err := validateTokenPath(token0, token1) + if err != nil { + panic(newErrorWithDetail(err, ufmt.Sprintf("token0(%s), token1(%s)", token0, token1))) + } + + wrappedAmount := int64(0) + if isWrappedToken(token0) { + amount0DesiredInt := mustParseInt64(amount0DesiredStr) + wrappedAmount, err = safeWrapNativeToken(amount0DesiredInt, caller) + if err != nil { + panic(err) + } + } else if isWrappedToken(token1) { + amount1DesiredInt := mustParseInt64(amount1DesiredStr) + wrappedAmount, err = safeWrapNativeToken(amount1DesiredInt, caller) + if err != nil { + panic(err) + } + } + + amount0Desired, amount1Desired, amount0Min, amount1Min := parseAmounts(amount0DesiredStr, amount1DesiredStr, amount0MinStr, amount1MinStr) + increaseLiquidityParams := IncreaseLiquidityParams{ + positionId: positionId, + amount0Desired: amount0Desired, + amount1Desired: amount1Desired, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline, + caller: caller, + } + + _, liquidity, amount0, amount1, poolPath, err := increaseLiquidity(increaseLiquidityParams) + if err != nil { + panic(err) + } + + if err := unwrapLeftoverWrappedToken(token0, wrappedAmount, safeConvertToInt64(amount0), caller); err != nil { + panic(err) + } + if err := unwrapLeftoverWrappedToken(token1, wrappedAmount, safeConvertToInt64(amount1), caller); err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "IncreaseLiquidity", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "poolPath", poolPath, + "caller", caller.String(), + "lpPositionId", formatUint(positionId), + "liquidity", liquidity.ToString(), + "amount0", amount0.ToString(), + "amount1", amount1.ToString(), + "sqrtPriceX96", pl.GetSlot0SqrtPriceX96(poolPath).ToString(), + "positionLiquidity", PositionGetPositionLiquidityStr(positionId), + "token0Balance", pl.GetBalanceToken0(poolPath), + "token1Balance", pl.GetBalanceToken1(poolPath), + ) + + return positionId, liquidity.ToString(), amount0.ToString(), amount1.ToString(), poolPath +} + +// unwrapLeftoverWrappedToken unwraps leftover wrapped tokens to native tokens. +func unwrapLeftoverWrappedToken(token string, wrapped, amount int64, caller std.Address) error { + unwrappable := isWrappedToken(token) && wrapped > amount + if !unwrappable { + return nil + } + + err := unwrap(wrapped-amount, caller) + if err != nil { + return makeErrorWithDetails(errWrapUnwrap, err.Error()) + } + + return nil +} + +// DecreaseLiquidity decreases liquidity of an existing position. +// +// Removes liquidity but keeps NFT ownership. +// Calculates tokens owed based on current price. +// Two-step: decrease then collect tokens. +// +// Parameters: +// - positionId: NFT token ID +// - liquidityStr: Amount of liquidity to remove +// - amount0MinStr: Min token0 to receive (slippage) +// - amount1MinStr: Min token1 to receive (slippage) +// - deadline: Transaction expiration +// - unwrapResult: Convert WUGNOT to GNOT if true +// +// Returns: +// - positionId: Same NFT ID +// - liquidity: Remaining liquidity +// - fee0, fee1: Fees collected +// - amount0, amount1: Principal collected +// - poolPath: Pool identifier +// +// Note: Applies 1% withdrawal fee on collected amounts. +func DecreaseLiquidity( + cur realm, + positionId uint64, + liquidityStr string, + amount0MinStr string, + amount1MinStr string, + deadline int64, + unwrapResult bool, +) (uint64, string, string, string, string, string, string) { + halt.AssertIsNotHaltedPosition() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + assertIsOwnerForToken(positionId, caller) + assertIsNotExpired(deadline) + assertValidLiquidityAmount(liquidityStr) + + emission.MintAndDistributeGns(cross) + + amount0Min := u256.MustFromDecimal(amount0MinStr) + amount1Min := u256.MustFromDecimal(amount1MinStr) + decreaseLiquidityParams := DecreaseLiquidityParams{ + positionId: positionId, + liquidity: liquidityStr, + amount0Min: amount0Min, + amount1Min: amount1Min, + deadline: deadline, + unwrapResult: unwrapResult, + caller: caller, + } + + positionId, liquidity, fee0, fee1, amount0, amount1, poolPath, err := decreaseLiquidity(decreaseLiquidityParams) + if err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "DecreaseLiquidity", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "lpPositionId", formatUint(positionId), + "poolPath", poolPath, + "decreasedLiquidity", liquidity, + "feeAmount0", fee0, + "feeAmount1", fee1, + "amount0", amount0, + "amount1", amount1, + "unwrapResult", formatBool(unwrapResult), + "sqrtPriceX96", pl.GetSlot0SqrtPriceX96(poolPath).ToString(), + "positionLiquidity", PositionGetPositionLiquidityStr(positionId), + "token0Balance", pl.GetBalanceToken0(poolPath), + "token1Balance", pl.GetBalanceToken1(poolPath), + ) + + return positionId, liquidity, fee0, fee1, amount0, amount1, poolPath +} + +// CollectFee collects swap fee from the position. +// +// Claims accumulated fees without removing liquidity. +// Useful for active positions earning ongoing fees. +// Applies protocol withdrawal fee. +// +// Parameters: +// - positionId: NFT token ID +// - unwrapResult: Convert WUGNOT to GNOT if true +// +// Returns: +// - positionId: Same NFT ID +// - fee0, fee1: Fees collected (after 1% protocol fee) +// - amount0, amount1: Always "0" (no principal) +// - poolPath: Pool identifier +// +// Requirements: +// - Caller must be owner or approved operator +// - Position must have accumulated fees +func CollectFee( + cur realm, + positionId uint64, + unwrapResult bool, +) (uint64, string, string, string, string, string) { + halt.AssertIsNotHaltedPosition() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + assertIsOwnerOrOperatorForToken(positionId, caller) + + return collectFee(positionId, unwrapResult, caller) +} + +// collectFee performs fee collection and withdrawal fee calculation. +func collectFee( + positionId uint64, + unwrapResult bool, + caller std.Address, +) (uint64, string, string, string, string, string) { + emission.MintAndDistributeGns(cross) + + // verify position + position := MustGetPosition(positionId) + token0, token1, fee := splitOf(position.poolKey) + + pl.Burn( + cross, + token0, + token1, + fee, + position.tickLower, + position.tickUpper, + ZERO_LIQUIDITY_FOR_FEE_COLLECTION, // burn '0' liquidity to collect fee + caller, + ) + + currentFeeGrowth, err := getCurrentFeeGrowth(position, caller) + if err != nil { + panic(newErrorWithDetail(err, "failed to get current fee growth")) + } + + tokensOwed0, tokensOwed1 := calculateFees(position, currentFeeGrowth) + + position.feeGrowthInside0LastX128 = u256.Zero().Set(currentFeeGrowth.feeGrowthInside0LastX128) + position.feeGrowthInside1LastX128 = u256.Zero().Set(currentFeeGrowth.feeGrowthInside1LastX128) + + // collect fee + amount0, amount1 := pl.Collect( + cross, + token0, token1, fee, + caller, + position.tickLower, position.tickUpper, + tokensOwed0.ToString(), tokensOwed1.ToString(), + ) + + // sometimes there will be a few less uBase amount than expected due to rounding down in core, but we just subtract the full amount expected + // instead of the actual amount so we can burn the token + position.tokensOwed0 = u256.Zero().Sub(tokensOwed0, u256.MustFromDecimal(amount0)) + position.tokensOwed1 = u256.Zero().Sub(tokensOwed1, u256.MustFromDecimal(amount1)) + mustUpdatePosition(positionId, position) + + withdrawalFeeBps := pl.GetWithdrawalFee() + amount0WithoutFee, fee0 := calculateAmountWithWithdrawalFee(amount0, withdrawalFeeBps) + amount1WithoutFee, fee1 := calculateAmountWithWithdrawalFee(amount1, withdrawalFeeBps) + + poolAddr, ok := access.GetAddress(prabc.ROLE_POOL.String()) + if !ok { + panic("pool address not found") + } + + if isWrappedToken(token0) && unwrapResult { + unwrapWithTransferFrom(poolAddr, caller, amount0WithoutFee) + } else { + common.TransferFrom(cross, token0, poolAddr, caller, amount0WithoutFee) + } + + if isWrappedToken(token1) && unwrapResult { + unwrapWithTransferFrom(poolAddr, caller, amount1WithoutFee) + } else { + common.TransferFrom(cross, token1, poolAddr, caller, amount1WithoutFee) + } + + protocolFeeAddr, ok := access.GetAddress(prabc.ROLE_PROTOCOL_FEE.String()) + if !ok { + panic("protocol fee address not found") + } + + common.TransferFrom(cross, token0, poolAddr, protocolFeeAddr, fee0) + common.TransferFrom(cross, token1, poolAddr, protocolFeeAddr, fee1) + + amount0WithoutFeeStr := formatInt(amount0WithoutFee) + amount1WithoutFeeStr := formatInt(amount1WithoutFee) + + previousRealm := std.PreviousRealm() + std.Emit( + "CollectSwapFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "lpPositionId", formatUint(positionId), + "feeAmount0", amount0WithoutFeeStr, + "feeAmount1", amount1WithoutFeeStr, + "poolPath", position.poolKey, + "unwrapResult", formatBool(unwrapResult), + ) + + std.Emit( + "WithdrawalFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "lpTokenId", formatUint(positionId), + "poolPath", position.poolKey, + "feeAmount0", formatInt(fee0), + "feeAmount1", formatInt(fee1), + "amount0WithoutFee", amount0WithoutFeeStr, + "amount1WithoutFee", amount1WithoutFeeStr, + ) + + return positionId, amount0WithoutFeeStr, amount1WithoutFeeStr, position.poolKey, amount0, amount1 +} + +var feeAmountDivisor = u256.NewUint(10000) + +// calculateAmountWithWithdrawalFee calculates amount after deducting withdrawal fee. +func calculateAmountWithWithdrawalFee(amount string, fee uint64) (int64, int64) { + if fee == 0 { + return safeConvertToInt64(u256.MustFromDecimal(amount)), 0 + } + + amountUint := u256.MustFromDecimal(amount) + feeUint := u256.NewUint(fee) + + feeAmount := u256.Zero().Mul(amountUint, feeUint) + feeAmount = u256.Zero().Div(feeAmount, feeAmountDivisor) + amountWithoutFee := u256.Zero().Sub(amountUint, feeAmount) + + return safeConvertToInt64(amountWithoutFee), safeConvertToInt64(feeAmount) +} + +// SetPositionOperator sets an operator for a position. +// Only staker can call this function. +func SetPositionOperator( + cur realm, + id uint64, + operator std.Address, +) { + halt.AssertIsNotHaltedPosition() + + caller := std.PreviousRealm().Address() + access.AssertIsStaker(caller) + + position := MustGetPosition(id) + position.operator = operator + mustUpdatePosition(id, position) +} + +// computePositionKey generates a unique base64-encoded key for a liquidity position. +func computePositionKey( + tickLower int32, + tickUpper int32, +) string { + currentRealmPath := std.CurrentRealm().PkgPath() + key := ufmt.Sprintf("%s__%d__%d", currentRealmPath, tickLower, tickUpper) + encoded := base64.StdEncoding.EncodeToString([]byte(key)) + return encoded +} + +// getCurrentFeeGrowth retrieves current fee growth values for a position. +func getCurrentFeeGrowth(position Position, owner std.Address) (FeeGrowthInside, error) { + positionKey := computePositionKey(position.tickLower, position.tickUpper) + feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(position.poolKey, positionKey) + + feeGrowthInside := FeeGrowthInside{ + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128, + } + + return feeGrowthInside, nil +} diff --git a/contract/r/gnoswap/v1/position/reposition.gno b/contract/r/gnoswap/v1/position/reposition.gno new file mode 100644 index 0000000..49a0cda --- /dev/null +++ b/contract/r/gnoswap/v1/position/reposition.gno @@ -0,0 +1,122 @@ +package position + +import ( + "std" + + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" + pl "gno.land/r/gnoswap/v1/pool" +) + +// Reposition adjusts the price range and liquidity of an existing position. +func Reposition( + cur realm, + positionId uint64, + tickLower int32, + tickUpper int32, + amount0DesiredStr string, + amount1DesiredStr string, + amount0MinStr string, + amount1MinStr string, +) (uint64, string, int32, int32, string, string) { + halt.AssertIsNotHaltedPosition() + halt.AssertIsNotHaltedWithdraw() + + caller := std.PreviousRealm().Address() + assertIsOwnerForToken(positionId, caller) + + emission.MintAndDistributeGns(cross) + + // position should be burned to reposition + position := MustGetPosition(positionId) + oldTickLower := position.tickLower + oldTickUpper := position.tickUpper + + if !(position.isClear()) { + panic(newErrorWithDetail( + errNotClear, + ufmt.Sprintf( + "position(%d) isn't clear(liquidity:%s, tokensOwed0:%s, tokensOwed1:%s)", + positionId, + position.liquidity.ToString(), + position.tokensOwed0.ToString(), + position.tokensOwed1.ToString(), + ), + )) + } + + token0, token1, _ := splitOf(position.poolKey) + token0, token1, _, _, _, err := processTokens( + token0, + token1, + amount0DesiredStr, + amount1DesiredStr, + caller, + ) + if err != nil { + panic(err) + } + + liquidity, amount0, amount1 := addLiquidity( + AddLiquidityParams{ + poolKey: position.poolKey, + tickLower: tickLower, + tickUpper: tickUpper, + amount0Desired: u256.MustFromDecimal(amount0DesiredStr), + amount1Desired: u256.MustFromDecimal(amount1DesiredStr), + amount0Min: u256.MustFromDecimal(amount0MinStr), + amount1Min: u256.MustFromDecimal(amount1MinStr), + caller: caller, + }, + ) + + // update position tickLower, tickUpper to new value + // because getCurrentFeeGrowth() uses tickLower, tickUpper + position.tickLower = tickLower + position.tickUpper = tickUpper + + currentFeeGrowth, err := getCurrentFeeGrowth(position, caller) + if err != nil { + panic(newErrorWithDetail(err, "failed to get current fee growth")) + } + position.feeGrowthInside0LastX128 = currentFeeGrowth.feeGrowthInside0LastX128 + position.feeGrowthInside1LastX128 = currentFeeGrowth.feeGrowthInside1LastX128 + + position.liquidity = liquidity + // OBS: do not reset feeGrowthInside1LastX128 and feeGrowthInside1LastX128 to zero + // if so, ( decrease 100% -> reposition ) + // > at this point, that position will have unclaimedFee which isn't intended + position.tokensOwed0 = u256.Zero() + position.tokensOwed1 = u256.Zero() + position.burned = false + mustUpdatePosition(positionId, position) + + poolSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(position.poolKey) + token0Balance := pl.GetBalanceToken0(position.poolKey) + token1Balance := pl.GetBalanceToken1(position.poolKey) + + previousRealm := std.PreviousRealm() + std.Emit( + "Reposition", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "lpPositionId", formatUint(positionId), + "tickLower", formatInt(int64(tickLower)), + "tickUpper", formatInt(int64(tickUpper)), + "liquidity", liquidity.ToString(), + "amount0", amount0.ToString(), + "amount1", amount1.ToString(), + "prevTickLower", formatInt(int64(oldTickLower)), + "prevTickUpper", formatInt(int64(oldTickUpper)), + "poolPath", position.poolKey, + "sqrtPriceX96", poolSqrtPriceX96.ToString(), + "positionLiquidity", PositionGetPositionLiquidityStr(positionId), + "token0Balance", token0Balance, + "token1Balance", token1Balance, + ) + + return positionId, liquidity.ToString(), tickLower, tickUpper, amount0.ToString(), amount1.ToString() +} diff --git a/contract/r/gnoswap/v1/position/type.gno b/contract/r/gnoswap/v1/position/type.gno new file mode 100644 index 0000000..84885ec --- /dev/null +++ b/contract/r/gnoswap/v1/position/type.gno @@ -0,0 +1,147 @@ +package position + +import ( + "std" + + u256 "gno.land/p/gnoswap/uint256" +) + +const ( + Q128 string = "340282366920938463463374607431768211456" // 2 ** 128 +) + +// Previously, we used a different zero address ("g1000000..."), +// but we changed the value because using the address +// below appears to have become the de facto standard practice. +var zeroAddress std.Address = std.Address("") + +// Position represents a liquidity position in a pool. +// Each position tracks the amount of liquidity, fee growth, and tokens owed to the position owner. +type Position struct { + nonce *u256.Uint // nonce for permits + operator std.Address // address that is approved for spending this token + poolKey string // poolPath of the pool which this has lp token + tickLower int32 // the lower tick of the position, bounds are included + tickUpper int32 // the upper tick of the position + liquidity *u256.Uint // liquidity of the position + + // fee growth of the aggregate position as of the last action on the individual position + feeGrowthInside0LastX128 *u256.Uint + feeGrowthInside1LastX128 *u256.Uint + + // how many uncollected tokens are owed to the position, as of the last computation + tokensOwed0 *u256.Uint + tokensOwed1 *u256.Uint + + token0Balance *u256.Uint // token0 balance of the position + token1Balance *u256.Uint // token1 balance of the position + + burned bool // whether the position has been burned (we don't burn the NFT, just mark as burned) +} + +func (p Position) PoolKey() string { return p.poolKey } +func (p Position) Liquidity() *u256.Uint { return p.liquidity } +func (p Position) TickLower() int32 { return p.tickLower } +func (p Position) TickUpper() int32 { return p.tickUpper } +func (p Position) TokensOwed0() *u256.Uint { return p.tokensOwed0 } +func (p Position) TokensOwed1() *u256.Uint { return p.tokensOwed1 } +func (p Position) Token0Balance() *u256.Uint { return p.token0Balance } +func (p Position) Token1Balance() *u256.Uint { return p.token1Balance } + +// isClear reports whether the position is empty +func (p Position) isClear() bool { + return p.liquidity.IsZero() && p.tokensOwed0.IsZero() && p.tokensOwed1.IsZero() +} + +type MintParams struct { + token0 string // token0 path for a specific pool + token1 string // token1 path for a specific pool + fee uint32 // fee for a specific pool + tickLower int32 // lower end of the tick range for the position + tickUpper int32 // upper end of the tick range for the position + amount0Desired *u256.Uint // desired amount of token0 to be minted + amount1Desired *u256.Uint // desired amount of token1 to be minted + amount0Min *u256.Uint // minimum amount of token0 to be minted + amount1Min *u256.Uint // minimum amount of token1 to be minted + deadline int64 // time by which the transaction must be included to effect the change + mintTo std.Address // address to mint lpToken + caller std.Address // address to call the function +} + +// newMintParams creates `MintParams` from processed input data. +func newMintParams(input ProcessedMintInput, mintInput MintInput) MintParams { + return MintParams{ + token0: input.tokenPair.token0, + token1: input.tokenPair.token1, + fee: mintInput.fee, + tickLower: input.tickLower, + tickUpper: input.tickUpper, + amount0Desired: input.amount0Desired, + amount1Desired: input.amount1Desired, + amount0Min: input.amount0Min, + amount1Min: input.amount1Min, + deadline: mintInput.deadline, + mintTo: mintInput.mintTo, + caller: mintInput.caller, + } +} + +type IncreaseLiquidityParams struct { + positionId uint64 // positionId of the position to increase liquidity + amount0Desired *u256.Uint // desired amount of token0 to be minted + amount1Desired *u256.Uint // desired amount of token1 to be minted + amount0Min *u256.Uint // minimum amount of token0 to be minted + amount1Min *u256.Uint // minimum amount of token1 to be minted + deadline int64 // time by which the transaction must be included to effect the change + caller std.Address // address to call the function +} + +type DecreaseLiquidityParams struct { + positionId uint64 // positionId of the position to decrease liquidity + liquidity string // amount of liquidity to decrease + amount0Min *u256.Uint // minimum amount of token0 to be minted + amount1Min *u256.Uint // minimum amount of token1 to be minted + deadline int64 // time by which the transaction must be included to effect the change + unwrapResult bool // whether to unwrap the token if it's wrapped native token + caller std.Address // address to call the function +} + +type MintInput struct { + token0 string + token1 string + fee uint32 + tickLower int32 + tickUpper int32 + amount0Desired string + amount1Desired string + amount0Min string + amount1Min string + deadline int64 + mintTo std.Address + caller std.Address +} + +type TokenPair struct { + token0 string + token1 string + token0IsNative bool + token1IsNative bool + wrappedAmount int64 +} + +type ProcessedMintInput struct { + tokenPair TokenPair + amount0Desired *u256.Uint + amount1Desired *u256.Uint + amount0Min *u256.Uint + amount1Min *u256.Uint + tickLower int32 + tickUpper int32 + poolPath string +} + +// FeeGrowthInside represents fee growth inside ticks +type FeeGrowthInside struct { + feeGrowthInside0LastX128 *u256.Uint + feeGrowthInside1LastX128 *u256.Uint +} diff --git a/contract/r/gnoswap/v1/position/utils.gno b/contract/r/gnoswap/v1/position/utils.gno new file mode 100644 index 0000000..910639a --- /dev/null +++ b/contract/r/gnoswap/v1/position/utils.gno @@ -0,0 +1,225 @@ +package position + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/tokens/grc721" + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v1/gnft" +) + +// GetOrigPkgAddr returns the original package address. +// In position contract, original package address is the position address. +func GetOrigPkgAddr() std.Address { + return std.CurrentRealm().Address() +} + +// positionIdFrom converts positionId to grc721.TokenID type +// NOTE: input parameter positionId can be string, int, uint64, or grc721.TokenID +// if positionId is nil or not supported, it will panic +// if positionId is not found, it will panic +// input: positionId any +// output: grc721.TokenID +func positionIdFrom(positionId any) grc721.TokenID { + if positionId == nil { + panic(newErrorWithDetail(errInvalidInput, "positionId is nil")) + } + + switch positionId.(type) { + case string: + return grc721.TokenID(positionId.(string)) + case int: + return grc721.TokenID(strconv.Itoa(positionId.(int))) + case uint64: + return grc721.TokenID(strconv.Itoa(int(positionId.(uint64)))) + case grc721.TokenID: + return positionId.(grc721.TokenID) + default: + panic(newErrorWithDetail(errInvalidInput, "unsupported positionId type")) + } +} + +// exists checks whether positionId exists +// If positionId doesn't exist, return false, otherwise return true +// input: positionId uint64 +// output: bool +func exists(positionId uint64) bool { + return gnft.Exists(positionIdFrom(positionId)) +} + +// isOwner checks whether the caller is the owner of the positionId +// If the caller is the owner of the positionId, return true, otherwise return false +// input: positionId uint64, addr std.Address +// output: bool +func isOwner(positionId uint64, addr std.Address) bool { + owner, err := gnft.OwnerOf(positionIdFrom(positionId)) + if err == nil { + if owner == addr { + return true + } + } + + return false +} + +// isOperator checks whether the caller is the approved operator of the positionId +// If the caller is the approved operator of the positionId, return true, otherwise return false +// input: positionId uint64, addr std.Address +// output: bool +func isOperator(positionId uint64, addr std.Address) bool { + return PositionGetPositionOperator(positionId) == addr +} + +// isStaked checks whether positionId is staked +// If positionId is staked, owner of positionId is staker contract +// If positionId is staked, return true, otherwise return false +// input: positionId grc721.TokenID +// output: bool +func isStaked(positionId grc721.TokenID) bool { + exist := gnft.Exists(positionId) + if exist { + owner, err := gnft.OwnerOf(positionId) + if err == nil && owner == stakerAddr { + return true + } + } + return false +} + +// isOwnerOrOperator checks whether the caller is the owner or approved operator of the positionId +// If the caller is the owner or approved operator of the positionId, return true, otherwise return false +// input: addr std.Address, positionId uint64 +// output: bool +func isOwnerOrOperator(positionId uint64, addr std.Address) bool { + if !addr.IsValid() || !exists(positionId) { + return false + } + + if isStaked(positionIdFrom(positionId)) { + return isOperator(positionId, addr) + } + + return isOwner(positionId, addr) +} + +// splitOf divides poolKey into pToken0, pToken1, and pFee +// If poolKey is invalid, it will panic +// +// input: poolKey string +// output: +// - token0Path string +// - token1Path string +// - fee uint32 +func splitOf(poolKey string) (string, string, uint32) { + res := strings.Split(poolKey, ":") + if len(res) != 3 { + panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid poolKey(%s)", poolKey))) + } + + pToken0, pToken1, pFeeStr := res[0], res[1], res[2] + + pFee, err := strconv.Atoi(pFeeStr) + if err != nil { + panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid fee(%s)", pFeeStr))) + } + + return pToken0, pToken1, uint32(pFee) +} + +func formatUint(v any) string { + switch v := v.(type) { + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} + +func formatInt(v any) string { + switch v := v.(type) { + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case int: + return strconv.Itoa(v) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} + +func formatBool(v bool) string { + return strconv.FormatBool(v) +} + +func mustParseInt64(v string) int64 { + amountInt, err := strconv.ParseInt(v, 10, 64) + if err != nil { + panic(err) + } + + return amountInt +} + +func isTokenOwner(positionId uint64, caller std.Address) bool { + owner, err := gnft.OwnerOf(positionIdFrom(positionId)) + if err != nil { + return false + } + + return owner == caller +} + +func isSlippageExceeded(amount0, amount1, amount0Min, amount1Min *u256.Uint) bool { + return !(amount0.Gte(amount0Min) && amount1.Gte(amount1Min)) +} + +func subUint256(x, y *u256.Uint) *u256.Uint { + if x.Cmp(y) < 0 { + q256 := u256.MustFromDecimal(MAX_UINT256) + diff := u256.Zero().Sub(q256, y) + result := u256.Zero().Add(diff, x) + + // Add 1 to the result since MAX_UINT256 is 2^256 - 1 + return u256.Zero().Add(result, u256.One()) + } + + return u256.Zero().Sub(x, y) +} + +// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. +// +// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds +// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. +// +// Parameters: +// - value (*u256.Uint): The unsigned 256-bit integer to be converted. +// +// Returns: +// - int64: The converted value if it falls within the int64 range. +// +// Panics: +// - If the `value` exceeds the range of int64, the function will panic with an error indicating +// the overflow and the original value. +func safeConvertToInt64(value *u256.Uint) int64 { + const INT64_MAX = 9223372036854775807 + const MAX_INT64 = "9223372036854775807" + + res, overflow := value.Uint64WithOverflow() + if overflow || res > uint64(INT64_MAX) { + panic(ufmt.Sprintf( + "amount(%s) overflows int64 range (max %s)", + value.ToString(), + MAX_INT64, + )) + } + return int64(res) +} diff --git a/contract/r/gnoswap/v1/protocol_fee/README.md b/contract/r/gnoswap/v1/protocol_fee/README.md new file mode 100644 index 0000000..277f133 --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/README.md @@ -0,0 +1,57 @@ +# Protocol Fee + +Fee collection and distribution for protocol operations. + +## Overview + +Protocol Fee contract collects fees from various protocol operations and distributes them to xGNS holders and DevOps. + +## Configuration + +- **Router Fee**: 0.15% of swap amount +- **Pool Creation Fee**: 100 GNS +- **Withdrawal Fee**: 1% of LP fees claimed +- **Unstaking Fee**: 1% of staking rewards +- **Distribution**: 100% to xGNS holders (default) + +## Fee Sources + +1. **Swaps**: 0.15% fee on all trades +2. **Pool Creation**: 100 GNS per new pool +3. **LP Withdrawals**: 1% of collected fees +4. **Staking Claims**: 1% of rewards + +## Key Functions + +### `DistributeProtocolFee` +Distributes accumulated fees to recipients. + +### `SetDevOpsPct` +Sets DevOps funding percentage. + +### `SetGovStakerPct` +Sets xGNS holder percentage. + +### `AddToProtocolFee` +Adds fees to distribution queue. + +## Usage + +```go +// Distribute accumulated fees +tokenAmounts := DistributeProtocolFee() + +// Configure distribution +SetDevOpsPct(2000) // 20% to DevOps +SetGovStakerPct(8000) // 80% to xGNS holders + +// View accumulated fees +GetProtocolFee(tokenPath) +``` + +## Security + +- Admin-only configuration changes +- Automatic fee accumulation +- Multi-token support +- Transparent distribution tracking \ No newline at end of file diff --git a/contract/r/gnoswap/v1/protocol_fee/api.gno b/contract/r/gnoswap/v1/protocol_fee/api.gno new file mode 100644 index 0000000..ad4ebb0 --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/api.gno @@ -0,0 +1,161 @@ +package protocol_fee + +import ( + "std" + "strconv" + "time" + + "gno.land/p/nt/avl" + "gno.land/p/onbloc/json" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/v1/common" +) + +// ApiGetAccuTransferToGovStaker returns accumulated transfers to gov/staker as JSON. +func ApiGetAccuTransferToGovStaker() string { + distributedAmountList := protocolFeeState.accuToGovStaker + if distributedAmountList == nil { + return "" + } + + return marshal(buildByAvlTree(distributedAmountList)) +} + +// ApiGetAccuTransferToDevOps returns accumulated transfers to devOps as JSON. +func ApiGetAccuTransferToDevOps() string { + distributedAmountList := protocolFeeState.accuToDevOps + if distributedAmountList == nil { + return "" + } + + return marshal(buildByAvlTree(distributedAmountList)) +} + +// ApiGetHistoryTransferToGovStaker returns transfer history to gov/staker as JSON. +func ApiGetHistoryTransferToGovStaker() string { + historyTransferList := distributedToGovStakerHistory() + if historyTransferList == nil { + return "" + } + + return marshal(buildByAvlTree(historyTransferList)) +} + +// ApiGetHistoryTransferToDevOps returns transfer history to devOps as JSON. +func ApiGetHistoryTransferToDevOps() string { + historyTransferList := distributedToDevOpsHistory() + if historyTransferList == nil { + return "" + } + return marshal(buildByAvlTree(historyTransferList)) +} + +// buildByAvlTree builds a JSON node from AVL tree data. +func buildByAvlTree(tree *avl.Tree) *json.Node { + data := json.Builder(). + WriteString("height", formatInt(std.ChainHeight())). + WriteString("now", formatInt(time.Now().Unix())) + + tree.Iterate("", "", func(key string, value any) bool { + if iv, ok := value.(int64); !ok { + panic(ufmt.Sprintf("failed to cast value to int64: %T", value)) + } else { + data.WriteString(key, formatInt(iv)) + } + return false + }) + + return data.Node() +} + +// formatUint formats uint64 to string. +func formatUint(v uint64) string { + return strconv.FormatUint(v, 10) +} + +// formatInt64 formats int64 to string. +func formatInt64(v int64) string { + return strconv.FormatInt(v, 10) +} + +// formatInt formats int64 to string. +func formatInt(v int64) string { + return strconv.FormatInt(v, 10) +} + +// marshal converts JSON node to string. +func marshal(node *json.Node) string { + b, err := json.Marshal(node) + if err != nil { + panic(err.Error()) + } + + return string(b) +} + +// ApiGetActualBalance returns all tokens with their current balances (recorded + unrecorded) +func ApiGetActualBalance() string { + tokenMap := make(map[string]int64) + + // get all recorded tokens + for token := range protocolFeeState.tokenListWithAmount { + actualBalance := common.BalanceOf(token, protocolFeeAddr) + tokenMap[token] = actualBalance + } + + // only include tokens that are already recorded. + // check for any tokens that have balance will requires + // iterating through known tokens or having a registry. + data := json.Builder(). + WriteString("height", formatInt(std.ChainHeight())). + WriteString("now", formatInt(time.Now().Unix())) + + for token, balance := range tokenMap { + data.WriteString(token, formatInt64(balance)) + } + + return marshal(data.Node()) +} + +// ApiGetRecordedBalance returns the recorded tokens and their amounts +func ApiGetRecordedBalance() string { + tokenList := GetTokenListWithAmount() + if tokenList == nil { + return "" + } + + data := json.Builder(). + WriteString("height", formatInt(std.ChainHeight())). + WriteString("now", formatInt(time.Now().Unix())) + + for token, amount := range tokenList { + data.WriteString(token, formatInt64(amount)) + } + + return marshal(data.Node()) +} + +// ApiGetUnrecordedBalance returns tokens with unrecorded balances +func ApiGetUnrecordedBalance() string { + unrecordedMap := make(map[string]int64) + + // Check all recorded tokens for discrepancies + for token, recordedAmount := range protocolFeeState.tokenListWithAmount { + actualBalance := common.BalanceOf(token, protocolFeeAddr) + if actualBalance > recordedAmount { + unrecordedAmount := actualBalance - recordedAmount + unrecordedMap[token] = unrecordedAmount + } + } + + data := json.Builder(). + WriteString("height", formatInt(std.ChainHeight())). + WriteString("now", formatInt(time.Now().Unix())) + + for token, unrecordedBalance := range unrecordedMap { + data.WriteString(token, formatInt64(unrecordedBalance)) + } + + return marshal(data.Node()) +} diff --git a/contract/r/gnoswap/v1/protocol_fee/assert.gno b/contract/r/gnoswap/v1/protocol_fee/assert.gno new file mode 100644 index 0000000..37984d7 --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/assert.gno @@ -0,0 +1,46 @@ +package protocol_fee + +import ( + "std" + + "gno.land/p/nt/ufmt" + prbac "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoswap/access" +) + +// assertIsPoolOrRouterOrStaker panics if the caller is not the pool, router, or staker contract. +func assertIsPoolOrRouterOrStaker(caller std.Address) { + access.AssertHasAnyRole( + caller, + prbac.ROLE_POOL.String(), + prbac.ROLE_ROUTER.String(), + prbac.ROLE_STAKER.String(), + ) +} + +// assertIsAdminOrGovStaker panics if the caller is not admin or gov/staker. +func assertIsAdminOrGovStaker(caller std.Address) { + access.AssertHasAnyRole( + caller, + prbac.ROLE_ADMIN.String(), + prbac.ROLE_GOV_STAKER.String(), + ) +} + +// assertIsValidPercent panics if the percentage is invalid (not between 0-10000). +func assertIsValidPercent(pct int64) { + if pct > 10000 { + panic(makeErrorWithDetail( + errInvalidPct, + ufmt.Sprintf("pct(%d) should not be bigger than 10000", pct), + )) + } + + if pct < 0 { + panic(makeErrorWithDetail( + errInvalidPct, + ufmt.Sprintf("pct(%d) should not be smaller than 0", pct), + )) + } +} diff --git a/contract/r/gnoswap/v1/protocol_fee/consts.gno b/contract/r/gnoswap/v1/protocol_fee/consts.gno new file mode 100644 index 0000000..636a9e6 --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/consts.gno @@ -0,0 +1,12 @@ +package protocol_fee + +import ( + prabc "gno.land/p/gnoswap/rbac" + "gno.land/r/gnoswap/access" +) + +var ( + protocolFeeAddr, _ = access.GetAddress(prabc.ROLE_PROTOCOL_FEE.String()) + govStakerAddr, _ = access.GetAddress(prabc.ROLE_GOV_STAKER.String()) + devOpsAddr, _ = access.GetAddress(prabc.ROLE_DEVOPS.String()) +) diff --git a/contract/r/gnoswap/v1/protocol_fee/doc.gno b/contract/r/gnoswap/v1/protocol_fee/doc.gno new file mode 100644 index 0000000..5979fb1 --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/doc.gno @@ -0,0 +1,6 @@ +// Package protocol_fee collects and distributes protocol fees from swaps. +// +// This contract accumulates fees generated from trading activity and +// distributes them to devOps and governance stakers according to +// configurable percentages set through governance. +package protocol_fee diff --git a/contract/r/gnoswap/v1/protocol_fee/errors.gno b/contract/r/gnoswap/v1/protocol_fee/errors.gno new file mode 100644 index 0000000..c438fb4 --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/errors.gno @@ -0,0 +1,18 @@ +package protocol_fee + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errNoPermission = errors.New("[GNOSWAP-PROTOCOL_FEE-001] caller has no permission") + errInvalidPct = errors.New("[GNOSWAP-PROTOCOL_FEE-002] invalid percentage") + errInvalidAmount = errors.New("[GNOSWAP-PROTOCOL_FEE-003] invalid amount") +) + +// makeErrorWithDetail creates an error with additional context. +func makeErrorWithDetail(err error, detail string) error { + return ufmt.Errorf("%s || %s", err.Error(), detail) +} diff --git a/contract/r/gnoswap/v1/protocol_fee/getter.gno b/contract/r/gnoswap/v1/protocol_fee/getter.gno new file mode 100644 index 0000000..2d15e6d --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/getter.gno @@ -0,0 +1,99 @@ +package protocol_fee + +import ( + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" +) + +// GetDevOpsPct returns the percentage allocated to devOps. +func GetDevOpsPct() int64 { + return protocolFeeState.DevOpsPct() +} + +// GetGovStakerPct returns the percentage allocated to gov/staker. +func GetGovStakerPct() int64 { + return protocolFeeState.GovStakerPct() +} + +// GetTokenListWithAmount returns the token path and amount. +func GetTokenListWithAmount() map[string]int64 { + return protocolFeeState.tokenListWithAmount +} + +// GetAmountOfToken returns the amount of token. +func GetAmountOfToken(tokenPath string) int64 { + amount, exists := protocolFeeState.tokenListWithAmount[tokenPath] + if !exists { + return 0 + } + return amount +} + +// GetAccuTransfersToGovStaker returns all accumulated transfers to gov/staker. +func GetAccuTransfersToGovStaker() map[string]int64 { + accuTransfers := make(map[string]int64) + + protocolFeeState.accuToGovStaker.Iterate("", "", func(key string, value any) bool { + amount, ok := value.(int64) + if !ok { + return false + } + + accuTransfers[key] = amount + return false + }) + + return accuTransfers +} + +// GetAccuTransfersToDevOps returns all accumulated transfers to devOps. +func GetAccuTransfersToDevOps() map[string]int64 { + accuTransfers := make(map[string]int64) + + protocolFeeState.accuToDevOps.Iterate("", "", func(key string, value any) bool { + amount, ok := value.(int64) + if !ok { + return false + } + + accuTransfers[key] = amount + return false + }) + + return accuTransfers +} + +// GetAccuTransferToGovStakerByTokenPath returns the accumulated transfer to gov/staker by token path. +func GetAccuTransferToGovStakerByTokenPath(path string) int64 { + return protocolFeeState.GetAccuTransferToGovStakerByTokenPath(path) +} + +// GetAccuTransferToDevOpsByTokenPath returns the accumulated transfer to devOps by token path. +func GetAccuTransferToDevOpsByTokenPath(path string) int64 { + return protocolFeeState.GetAccuTransferToDevOpsByTokenPath(path) +} + +// GetHistoryOfDistributedToGovStakerByTokenPath returns the history of distributed to gov/staker by token path. +func GetHistoryOfDistributedToGovStakerByTokenPath(path string) int64 { + history := protocolFeeState.distributedToGovStakerHistory + return retrieveHistory(history, path) +} + +// GetHistoryOfDistributedToDevOpsByTokenPath returns the history of distributed to devOps by token path. +func GetHistoryOfDistributedToDevOpsByTokenPath(path string) int64 { + history := protocolFeeState.distributedToDevOpsHistory + return retrieveHistory(history, path) +} + +// retrieveHistory retrieves distribution history amount from AVL tree. +func retrieveHistory(tree *avl.Tree, key string) int64 { + amountI, exists := tree.Get(key) + if !exists { + return 0 + } + res, ok := amountI.(int64) + if !ok { + panic(ufmt.Sprintf("failed to cast amount to int64: %T", amountI)) + } + return res +} diff --git a/contract/r/gnoswap/v1/protocol_fee/gnomod.toml b/contract/r/gnoswap/v1/protocol_fee/gnomod.toml new file mode 100644 index 0000000..b218732 --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/protocol_fee" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/protocol_fee/protocol_fee.gno b/contract/r/gnoswap/v1/protocol_fee/protocol_fee.gno new file mode 100644 index 0000000..951282e --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/protocol_fee.gno @@ -0,0 +1,231 @@ +package protocol_fee + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" +) + +// DistributeProtocolFee distributes collected protocol fees. +// +// Splits fees between devOps and gov/staker based on configured percentages. +// This function processes all accumulated fees since last distribution. +// +// Returns: +// - map[string]int64: Token paths to amounts distributed to gov/staker +// +// Only callable by admin or gov/staker contract. +// Note: Default split is 0% devOps, 100% gov/staker. +func DistributeProtocolFee(cur realm) map[string]int64 { + halt.AssertIsNotHaltedProtocolFee() + + caller := std.PreviousRealm().Address() + assertIsAdminOrGovStaker(caller) + + sentToDevOpsForEvent := make([]string, 0) + sentToGovStakerForEvent := make([]string, 0) + toReturnDistributedToGovStaker := make(map[string]int64) + + for token, amount := range protocolFeeState.tokenListWithAmount { + balance := common.BalanceOf(token, protocolFeeAddr) + + // amount should be less than or equal to balance + if amount > balance { + panic(makeErrorWithDetail( + errInvalidAmount, + ufmt.Sprintf("amount: %d should be less than or equal to balance: %d", amount, balance), + )) + } + + if amount <= 0 { + continue + } + + // Distribute only the recorded amount, not the entire balance + distributeAmount := amount + if distributeAmount > balance { + // This should not happen due to the check above, but safeguard anyway + distributeAmount = balance + } + + toDevOpsAmount := distributeAmount * protocolFeeState.DevOpsPct() / 10000 // default 0% + toGovStakerAmount := distributeAmount - toDevOpsAmount // default 100% + + // Distribute to DevOps + if err := protocolFeeState.distributeToDevOps(token, toDevOpsAmount); err != nil { + panic(err) + } + if toDevOpsAmount > 0 { + sentToDevOpsForEvent = append(sentToDevOpsForEvent, makeEventString(token, toDevOpsAmount)) + } + + // Distribute to Gov/Staker + if err := protocolFeeState.distributeToGovStaker(token, toGovStakerAmount); err != nil { + panic(err) + } + if toGovStakerAmount > 0 { + sentToGovStakerForEvent = append(sentToGovStakerForEvent, makeEventString(token, toGovStakerAmount)) + toReturnDistributedToGovStaker[token] = toGovStakerAmount + } + } + + protocolFeeState.clearTokenListWithAmount() + + previousRealm := std.PreviousRealm() + std.Emit( + "TransferProtocolFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "toDevOps", strings.Join(sentToDevOpsForEvent, ","), + "toGovStaker", strings.Join(sentToGovStakerForEvent, ","), + ) + + return toReturnDistributedToGovStaker +} + +// SetDevOpsPct sets the devOpsPct. +// +// Parameters: +// - pct: percentage for devOps (0-10000, where 10000 = 100%) +// +// Only callable by admin or governance. +// Note: GovStaker percentage is automatically adjusted to (10000 - devOpsPct). +func SetDevOpsPct(cur realm, pct int64) { + halt.AssertIsNotHaltedProtocolFee() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsValidPercent(pct) + + prevDevOpsPct := protocolFeeState.DevOpsPct() + prevGovStakerPct := protocolFeeState.GovStakerPct() + + newDevOpsPct, err := protocolFeeState.setDevOpsPct(pct) + if err != nil { + panic(err) + } + newGovStakerPct := protocolFeeState.GovStakerPct() + + previousRealm := std.PreviousRealm() + std.Emit( + "SetDevOpsPct", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "newDevOpsPct", strconv.FormatInt(newDevOpsPct, 10), + "prevDevOpsPct", strconv.FormatInt(prevDevOpsPct, 10), + "newGovStakerPct", strconv.FormatInt(newGovStakerPct, 10), + "prevGovStakerPct", strconv.FormatInt(prevGovStakerPct, 10), + ) +} + +// SetGovStakerPct sets the stakerPct. +// +// Parameters: +// - pct: percentage for gov/staker (0-10000, where 10000 = 100%) +// +// Only callable by admin or governance. +// Note: DevOps percentage is automatically adjusted to (10000 - govStakerPct). +func SetGovStakerPct(cur realm, pct int64) { + halt.AssertIsNotHaltedProtocolFee() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsValidPercent(pct) + + prevDevOpsPct := protocolFeeState.DevOpsPct() + prevGovStakerPct := protocolFeeState.GovStakerPct() + + newGovStakerPct, err := protocolFeeState.setGovStakerPct(pct) + if err != nil { + panic(err) + } + newDevOpsPct := protocolFeeState.DevOpsPct() + + previousRealm := std.PreviousRealm() + std.Emit( + "SetGovStakerPct", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "newDevOpsPct", strconv.FormatInt(newDevOpsPct, 10), + "prevDevOpsPct", strconv.FormatInt(prevDevOpsPct, 10), + "newGovStakerPct", strconv.FormatInt(newGovStakerPct, 10), + "prevGovStakerPct", strconv.FormatInt(prevGovStakerPct, 10), + ) +} + +// AddToProtocolFee adds the amount to the tokenListWithAmount. +// +// Parameters: +// - tokenPath: token contract path +// - amount: fee amount to add +// +// Only callable by pool, router or staker contracts. +// Note: Accumulated fees are distributed when DistributeProtocolFee is called. +func AddToProtocolFee(cur realm, tokenPath string, amount int64) { + halt.AssertIsNotHaltedProtocolFee() + + caller := std.PreviousRealm().Address() + assertIsPoolOrRouterOrStaker(caller) + + if amount < 0 { + panic(makeErrorWithDetail( + errInvalidAmount, + ufmt.Sprintf("amount(%d) should not be negative", amount), + )) + } + + currentAmount := protocolFeeState.tokenListWithAmount[tokenPath] + + // Check for overflow + if amount > 0 && currentAmount > 0 && currentAmount > (9223372036854775807-amount) { + panic(makeErrorWithDetail( + errInvalidAmount, + ufmt.Sprintf("overflow detected: current(%d) + amount(%d) would exceed int64 max", currentAmount, amount), + )) + } + + protocolFeeState.tokenListWithAmount[tokenPath] += amount +} + +// ClearTokenListWithAmount clears the tokenListWithAmount. +// +// Resets all accumulated token amounts to zero. +// Only callable by gov/staker contract. +// Note: Should be called after successful distribution. +func ClearTokenListWithAmount(cur realm) { + halt.AssertIsNotHaltedProtocolFee() + + caller := std.PreviousRealm().Address() + access.AssertIsGovStaker(caller) + + protocolFeeState.clearTokenListWithAmount() +} + +// ClearAccuTransferToGovStaker clears the accuToGovStaker. +// +// Resets accumulated transfer tracking for gov/staker. +// This allows gov/staker to track distributions between calls. +// +// Only callable by gov/staker contract. +// Note: Should be called after reading accumulated amounts. +func ClearAccuTransferToGovStaker(cur realm) { + halt.AssertIsNotHaltedProtocolFee() + + caller := std.PreviousRealm().Address() + access.AssertIsGovStaker(caller) + + protocolFeeState.accuToGovStaker = avl.NewTree() +} + +func makeEventString(tokenPath string, amount int64) string { + return tokenPath + "*FEE*" + strconv.FormatInt(amount, 10) +} diff --git a/contract/r/gnoswap/v1/protocol_fee/state.gno b/contract/r/gnoswap/v1/protocol_fee/state.gno new file mode 100644 index 0000000..157f0b6 --- /dev/null +++ b/contract/r/gnoswap/v1/protocol_fee/state.gno @@ -0,0 +1,208 @@ +package protocol_fee + +import ( + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/v1/common" +) + +// ProtocolFeeState holds all the state variables for protocol fee management +type ProtocolFeeState struct { + // By default, devOps will get 0% of the protocol fee (which means gov/staker will get 100% of the protocol fee) + // This percentage can be modified through governance. + devOpsPct int64 // 0% + + // accumulated amount distributed to gov/staker by token path + accuToGovStaker *avl.Tree + accuToDevOps *avl.Tree + + // distributedToDevOpsHistory and distributedToGovStakerHistory are used to keep track of the distribution history + distributedToGovStakerHistory *avl.Tree + distributedToDevOpsHistory *avl.Tree + + tokenListWithAmount map[string]int64 // tokenPath -> amount +} + +// distributedToGovStakerHistory returns the history of distributed to gov/staker. +func distributedToGovStakerHistory() *avl.Tree { + return protocolFeeState.distributedToGovStakerHistory +} + +// distributedToDevOpsHistory returns the history of distributed to devOps. +func distributedToDevOpsHistory() *avl.Tree { + return protocolFeeState.distributedToDevOpsHistory +} + +// NewProtocolFeeState creates a new instance of ProtocolFeeState with initialized values +func NewProtocolFeeState() *ProtocolFeeState { + return &ProtocolFeeState{ + devOpsPct: 0, // 0% + accuToGovStaker: avl.NewTree(), + accuToDevOps: avl.NewTree(), + distributedToGovStakerHistory: avl.NewTree(), + distributedToDevOpsHistory: avl.NewTree(), + tokenListWithAmount: make(map[string]int64), + } +} + +// Global instance of the protocol fee state +var protocolFeeState = NewProtocolFeeState() + +// DevOpsPct returns the percentage of protocol fees allocated to DevOps. +func (pfs *ProtocolFeeState) DevOpsPct() int64 { return pfs.devOpsPct } + +// GovStakerPct returns the percentage of protocol fees allocated to Gov/Staker. +func (pfs *ProtocolFeeState) GovStakerPct() int64 { return 10000 - pfs.devOpsPct } + +// AccuToGovStaker returns the accumulated amounts distributed to Gov/Staker. +func (pfs *ProtocolFeeState) AccuToGovStaker() *avl.Tree { return pfs.accuToGovStaker } + +// AccuToDevOps returns the accumulated amounts distributed to DevOps. +func (pfs *ProtocolFeeState) AccuToDevOps() *avl.Tree { return pfs.accuToDevOps } + +// distributeToDevOps distributes tokens to DevOps and updates related state. +// Amount should be greater than 0 (already checked in DistributeProtocolFee). +func (pfs *ProtocolFeeState) distributeToDevOps(token string, amount int64) error { + pfs.addAccuToDevOps(token, amount) + pfs.updateDistributedToDevOpsHistory(token, amount) + if err := common.Transfer(cross, token, devOpsAddr, amount); err != nil { + return ufmt.Errorf("transfer failed: token(%s), amount(%d)", token, amount) + } + return nil +} + +// distributeToGovStaker distributes tokens to Gov/Staker and updates related state. +// Amount should be greater than 0 (already checked in DistributeProtocolFee). +func (pfs *ProtocolFeeState) distributeToGovStaker(token string, amount int64) error { + pfs.addAccuToGovStaker(token, amount) + pfs.updateDistributedToGovStakerHistory(token, amount) + if err := common.Transfer(cross, token, govStakerAddr, amount); err != nil { + return ufmt.Errorf("transfer failed: token(%s), amount(%d)", token, amount) + } + return nil +} + +// setDevOpsPct sets the devOpsPct. +func (pfs *ProtocolFeeState) setDevOpsPct(pct int64) (int64, error) { + if pct < 0 { + return 0, makeErrorWithDetail( + errInvalidPct, + ufmt.Sprintf("pct(%d) should not be negative", pct), + ) + } + if pct > 10000 { + return 0, makeErrorWithDetail( + errInvalidPct, + ufmt.Sprintf("pct(%d) should not be bigger than 10000", pct), + ) + } + + pfs.devOpsPct = pct + + return pct, nil +} + +// setGovStakerPct sets the govStakerPct by calculating devOpsPct. +func (pfs *ProtocolFeeState) setGovStakerPct(pct int64) (int64, error) { + if pct < 0 { + return 0, makeErrorWithDetail( + errInvalidPct, + ufmt.Sprintf("pct(%d) should not be negative", pct), + ) + } + devOpsPct := 10000 - pct + if _, err := pfs.setDevOpsPct(devOpsPct); err != nil { + return 0, err + } + + return pct, nil +} + +// addAccuToGovStaker adds the amount to the accuToGovStaker by token path. +func (pfs *ProtocolFeeState) addAccuToGovStaker(tokenPath string, amount int64) { + before := pfs.GetAccuTransferToGovStakerByTokenPath(tokenPath) + + // Check for overflow + if amount > 0 && before > 0 && before > (9223372036854775807-amount) { + panic(makeErrorWithDetail( + errInvalidAmount, + ufmt.Sprintf("overflow detected: before(%d) + amount(%d) would exceed int64 max", before, amount), + )) + } + + after := before + amount + pfs.accuToGovStaker.Set(tokenPath, after) +} + +// addAccuToDevOps adds the amount to the accuToDevOps by token path. +func (pfs *ProtocolFeeState) addAccuToDevOps(tokenPath string, amount int64) { + before := pfs.GetAccuTransferToDevOpsByTokenPath(tokenPath) + + // Check for overflow + if amount > 0 && before > 0 && before > (9223372036854775807-amount) { + panic(makeErrorWithDetail( + errInvalidAmount, + ufmt.Sprintf("overflow detected: before(%d) + amount(%d) would exceed int64 max", before, amount), + )) + } + + after := before + amount + pfs.accuToDevOps.Set(tokenPath, after) +} + +// GetAccuTransferToGovStakerByTokenPath gets the accumulated amount to gov/staker by token path. +func (pfs *ProtocolFeeState) GetAccuTransferToGovStakerByTokenPath(tokenPath string) int64 { + return retrieveAmount(pfs.accuToGovStaker, tokenPath) +} + +// GetAccuTransferToDevOpsByTokenPath gets the accumulated amount to devOps by token path. +func (pfs *ProtocolFeeState) GetAccuTransferToDevOpsByTokenPath(tokenPath string) int64 { + return retrieveAmount(pfs.accuToDevOps, tokenPath) +} + +func retrieveAmount(tree *avl.Tree, key string) int64 { + amountI, exists := tree.Get(key) + if !exists { + return 0 + } + res, ok := amountI.(int64) + if !ok { + panic(ufmt.Sprintf("failed to cast amount to int64: %T", amountI)) + } + return res +} + +// updateDistributedToGovStakerHistory updates the distributedToGovStakerHistory. +func (pfs *ProtocolFeeState) updateDistributedToGovStakerHistory(tokenPath string, amount int64) { + prevAmount := retrievePrevAmount(pfs.distributedToGovStakerHistory, tokenPath) + afterAmount := prevAmount + amount + + pfs.distributedToGovStakerHistory.Set(tokenPath, afterAmount) +} + +// updateDistributedToDevOpsHistory updates the distributedToDevOpsHistory. +func (pfs *ProtocolFeeState) updateDistributedToDevOpsHistory(tokenPath string, amount int64) { + prevAmount := retrievePrevAmount(pfs.distributedToDevOpsHistory, tokenPath) + afterAmount := prevAmount + amount + + pfs.distributedToDevOpsHistory.Set(tokenPath, afterAmount) +} + +func retrievePrevAmount(tree *avl.Tree, key string) (amount int64) { + if prevAmountI, exists := tree.Get(key); !exists { + return 0 + } else { + v, ok := prevAmountI.(int64) + if !ok { + panic(ufmt.Sprintf("failed to cast prevAmount to int64: %T", prevAmountI)) + } + amount = v + } + return +} + +// clearTokenListWithAmount clears the tokenListWithAmount. +func (pfs *ProtocolFeeState) clearTokenListWithAmount() { + pfs.tokenListWithAmount = make(map[string]int64) +} diff --git a/contract/r/gnoswap/v1/router/README.md b/contract/r/gnoswap/v1/router/README.md new file mode 100644 index 0000000..79b2ab2 --- /dev/null +++ b/contract/r/gnoswap/v1/router/README.md @@ -0,0 +1,115 @@ +# Router + +Swap routing engine for optimal trade execution across pools. + +## Overview + +Router handles swap execution across multiple pools, finding optimal paths and managing slippage protection for traders. + +## Configuration + +- **Router Fee**: 0.15% on all swaps +- **Max Hops**: 7 pools per route +- **Deadline Buffer**: 5-30 minutes recommended + +## Core Functions + +### `ExactInSwapRoute` +Swaps exact input amount for minimum output. +- Fixed input, variable output +- Reverts if output < amountOutMin +- Supports multi-hop routing + +### `ExactOutSwapRoute` +Swaps for exact output amount with maximum input. +- Fixed output, variable input +- Reverts if input > amountInMax +- Calculates path backwards + +### `DrySwapRoute` +Simulates swap without execution. +- Frontend price quotes +- Slippage calculation +- Gas estimation +- Path validation + +## Technical Details + +### Route Format +Routes encode swap path as colon-separated string: +``` +POOL_PATH,TOKEN0,TOKEN1,FEE:NEXT_POOL... +``` + +Single-hop example: +``` +gno.land/r/demo/bar:gno.land/r/demo/baz:3000,BAR,BAZ,3000 +``` + +Multi-hop example (BAR → BAZ → QUX): +``` +POOL1,BAR,BAZ,3000:POOL2,BAZ,QUX,500 +``` + +### Quote Distribution +Split large trades across routes to minimize impact: +- `quoteArr`: Percentage per route (must sum to 100) +- Example: "30,70" = 30% route1, 70% route2 + +### GNOT Handling +- Auto-wraps GNOT to WUGNOT when specified +- Auto-unwraps on output if needed +- No manual wrapping required + +### Slippage Protection +- Set `amountOutMin = expected * (1 - slippage%)` +- 0.5-1% for stable pairs +- 1-3% for volatile pairs +- Reverts if exceeded + +## Usage + +```go +// Simple exact input swap +amountIn, amountOut := ExactInSwapRoute( + "gno.land/r/demo/bar", // input token + "gno.land/r/demo/baz", // output token + "1000000", // amount (6 decimals) + "POOL,BAR,BAZ,3000", // route + "100", // 100% through route + "950000", // min output + time.Now().Unix() + 300, // deadline + "g1referrer...", // referral +) + +// Multi-hop swap +ExactInSwapRoute( + "gno.land/r/demo/bar", + "gno.land/r/demo/baz", + "1000000", + "POOL1,BAR,WUGNOT,3000:POOL2,WUGNOT,BAZ,3000", + "100", + "900000", + deadline, + "", +) + +// Split route for large trades +ExactInSwapRoute( + "gno.land/r/demo/usdc", + "gnot", + "10000000000", + "POOL1,USDC,WUGNOT,500:POOL2,USDC,WUGNOT,3000", + "60,40", // 60% through 0.05%, 40% through 0.3% + "9500000000", + deadline, + "", +) +``` + +## Security + +- Path validation prevents circular routes +- Deadline prevents stale transactions +- Slippage limits protect against MEV +- Router fees immutable per swap \ No newline at end of file diff --git a/contract/r/gnoswap/v1/router/assert.gno b/contract/r/gnoswap/v1/router/assert.gno new file mode 100644 index 0000000..2eaaf6f --- /dev/null +++ b/contract/r/gnoswap/v1/router/assert.gno @@ -0,0 +1,19 @@ +package router + +import ( + "time" + + "gno.land/p/nt/ufmt" +) + +// assertIsNotExpired ensures the transaction deadline has not passed. +func assertIsNotExpired(deadline int64) { + now := time.Now().Unix() + + if now > deadline { + panic(makeErrorWithDetails( + errExpired, + ufmt.Sprintf("transaction too old, now(%d) > deadline(%d)", now, deadline), + )) + } +} diff --git a/contract/r/gnoswap/v1/router/base.gno b/contract/r/gnoswap/v1/router/base.gno new file mode 100644 index 0000000..60cfba2 --- /dev/null +++ b/contract/r/gnoswap/v1/router/base.gno @@ -0,0 +1,321 @@ +package router + +import ( + "errors" + "std" + "strconv" + "strings" + + "gno.land/p/nt/ufmt" + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoland/wugnot" +) + +const ( + SINGLE_HOP_ROUTE int = 1 + + INITIAL_WUGNOT_BALANCE int64 = 0 +) + +// swap can be done by multiple pools +// to separate each pool, we use POOL_SEPARATOR +const ( + POOL_SEPARATOR = "*POOL*" + gnot string = "gnot" + wugnotPath string = "gno.land/r/gnoland/wugnot" +) + +type RouterOperation interface { + Validate() error + Process() (*SwapResult, error) +} + +// executeSwapOperation validates and processes a swap operation. +func executeSwapOperation(op RouterOperation) (*SwapResult, error) { + if err := op.Validate(); err != nil { + return nil, err + } + + result, err := op.Process() + if err != nil { + return nil, err + } + + return result, nil +} + +type BaseSwapParams struct { + InputToken string + OutputToken string + RouteArr string + QuoteArr string + Deadline int64 +} + +// common swap operation +type baseSwapOperation struct { + withUnwrap bool + userBeforeWugnotBalance int64 + userWrappedWugnot int64 + routes []string + quotes []string + amountSpecified *i256.Int +} + +// handleNativeTokenWrapping handles wrapping and unwrapping of native tokens. +func (op *baseSwapOperation) handleNativeTokenWrapping( + inputToken string, + outputToken string, + specifiedAmount *i256.Int, +) error { + // no native token + if inputToken != gnot && outputToken != gnot { + return nil + } + + // save current user's WGNOT amount + op.userBeforeWugnotBalance = wugnot.BalanceOf(std.PreviousRealm().Address()) + + if outputToken == gnot { + op.withUnwrap = true + return nil + } + + sent := std.OriginSend() + ugnotSentByUser := sent.AmountOf("ugnot") + amountSpecified := specifiedAmount.Int64() + + if ugnotSentByUser != amountSpecified { + return ufmt.Errorf("ugnot sent by user(%d) is not equal to amountSpecified(%d)", ugnotSentByUser, amountSpecified) + } + + // wrap user's WUGNOT + if ugnotSentByUser > 0 { + if err := wrapWithTransfer(std.PreviousRealm().Address(), ugnotSentByUser); err != nil { + return err + } + } + + op.userWrappedWugnot = ugnotSentByUser + + return nil +} + +// validateRouteQuote validates and converts a route quote to swap amount. +func (op *baseSwapOperation) validateRouteQuote(quote string, i int) (*i256.Int, error) { + qt, err := strconv.Atoi(quote) + if err != nil { + return nil, ufmt.Errorf("invalid quote(%s) at index(%d)", quote, i) + } + + // calculate amount to swap for this route + toSwap := i256.Zero().Mul(op.amountSpecified, i256.NewInt(int64(qt))) + toSwap = i256.Zero().Div(toSwap, PERCENTAGE_DENOMINATOR) + + return toSwap, nil +} + +// processRoutes processes all swap routes and returns total amounts. +func (op *baseSwapOperation) processRoutes(swapType SwapType) (*u256.Uint, *u256.Uint, error) { + zero := u256.Zero() + resultAmountIn, resultAmountOut := zero, zero + remainRequestAmount := op.amountSpecified + + for i, route := range op.routes { + toSwapAmount := i256.Zero() + + // if it's the last route, use the remaining amount + isLastRoute := i == len(op.routes)-1 + if !isLastRoute { + // calculate the amount to swap for this route + swapAmount, err := op.validateRouteQuote(op.quotes[i], i) + if err != nil { + return nil, nil, err + } + + // update the remaining amount + remainRequestAmount = i256.Zero().Sub(remainRequestAmount, swapAmount) + toSwapAmount = swapAmount + } else { + toSwapAmount = remainRequestAmount + } + + amountIn, amountOut, err := op.processRoute(route, toSwapAmount, swapType) + if err != nil { + return nil, nil, err + } + + amountIn, overflow := u256.Zero().AddOverflow(resultAmountIn, amountIn) + if overflow { + return nil, nil, errOverFlow + } + + amountOut, overflow = u256.Zero().AddOverflow(resultAmountOut, amountOut) + if overflow { + return nil, nil, errOverFlow + } + + resultAmountIn = amountIn + resultAmountOut = amountOut + } + + return resultAmountIn, resultAmountOut, nil +} + +// processRoute processes a single route with specified swap amount. +func (op *baseSwapOperation) processRoute( + route string, + toSwap *i256.Int, + swapType SwapType, +) (amountIn, amountOut *u256.Uint, err error) { + numHops := strings.Count(route, POOL_SEPARATOR) + 1 + assertHopsInRange(numHops) + + switch numHops { + case SINGLE_HOP_ROUTE: + amountIn, amountOut = handleSingleSwap(route, toSwap, op.withUnwrap) + default: + amountIn, amountOut = handleMultiSwap(swapType, route, numHops, toSwap, op.withUnwrap) + } + + if amountIn == nil || amountOut == nil { + return nil, nil, ufmt.Errorf("swap failed to process route(%s)", route) + } + + return amountIn, amountOut, nil +} + +// handleSingleSwap executes a single-hop swap with the specified amount. +func handleSingleSwap(route string, amountSpecified *i256.Int, withUnwrap bool) (*u256.Uint, *u256.Uint) { + input, output, fee := getDataForSinglePath(route) + singleParams := SingleSwapParams{ + tokenIn: input, + tokenOut: output, + fee: fee, + amountSpecified: amountSpecified, + withUnwrap: withUnwrap, + } + + return singleSwap(&singleParams) +} + +// handleMultiSwap processes multi-hop swaps across multiple pools. +func handleMultiSwap( + swapType SwapType, + route string, + numHops int, + amountSpecified *i256.Int, + withUnwrap bool, +) (*u256.Uint, *u256.Uint) { + recipient := routerAddr + + switch swapType { + case ExactIn: + input, output, fee := getDataForMultiPath(route, 0) // first data + sp := newSwapParams(input, output, fee, recipient, amountSpecified, withUnwrap) + return multiSwap(*sp, numHops, route) + case ExactOut: + input, output, fee := getDataForMultiPath(route, numHops-1) // last data + sp := newSwapParams(input, output, fee, recipient, amountSpecified, withUnwrap) + return multiSwapNegative(*sp, numHops, route) + default: + panic(errInvalidSwapType) + } +} + +// SwapRouteParams contains all parameters needed for swap route execution +type SwapRouteParams struct { + inputToken string + outputToken string + routeArr string + quoteArr string + deadline int64 + typ SwapType + exactAmount string // amountIn for ExactIn, amountOut for ExactOut + limitAmount string // amountOutMin for ExactIn, amountInMax for ExactOut +} + +// IsUnwrap checks if the swap output is native token. +func (p *SwapRouteParams) IsUnwrap() bool { + return p.outputToken == gnot +} + +// createSwapOperation creates the appropriate swap operation based on swap type. +func createSwapOperation(params SwapRouteParams) (RouterOperation, error) { + baseParams := BaseSwapParams{ + InputToken: params.inputToken, + OutputToken: params.outputToken, + RouteArr: params.routeArr, + QuoteArr: params.quoteArr, + } + + switch params.typ { + case ExactIn: + pp := NewExactInParams(baseParams, params.exactAmount, params.limitAmount) + return NewExactInSwapOperation(pp), nil + case ExactOut: + pp := NewExactOutParams(baseParams, params.exactAmount, params.limitAmount) + return NewExactOutSwapOperation(pp), nil + default: + msg := addDetailToError(errInvalidSwapType, "unknown swap type") + return nil, errors.New(msg) + } +} + +// extractSwapOperationData extracts common data from swap operation. +func extractSwapOperationData(op RouterOperation) (int64, int64, error) { + var baseOp *baseSwapOperation + switch typedOp := op.(type) { + case *ExactInSwapOperation: + baseOp = &typedOp.baseSwapOperation + case *ExactOutSwapOperation: + baseOp = &typedOp.baseSwapOperation + default: + return 0, 0, ufmt.Errorf("unexpected operation type: %T", op) + } + + return baseOp.userBeforeWugnotBalance, baseOp.userWrappedWugnot, nil +} + +// commonSwapRoute handles the common logic for both ExactIn and ExactOut swaps. +func commonSwapRoute(params SwapRouteParams) (*i256.Int, *i256.Int, error) { + op, err := createSwapOperation(params) + if err != nil { + return i256.Zero(), i256.Zero(), err + } + + result, err := executeSwapOperation(op) + if err != nil { + msg := addDetailToError( + errInvalidInput, + ufmt.Sprintf("invalid %s SwapOperation: %s", params.typ.String(), err.Error()), + ) + return i256.Zero(), i256.Zero(), errors.New(msg) + } + + userBeforeWugnotBalance, userWrappedWugnot, err := extractSwapOperationData(op) + if err != nil { + return i256.Zero(), i256.Zero(), err + } + + limitValue, err := u256.FromDecimal(params.limitAmount) + if err != nil { + return i256.Zero(), i256.Zero(), err + } + + inputAmount, outputAmount := finalizeSwap( + params.inputToken, + params.outputToken, + result.AmountIn, + result.AmountOut, + params.typ, + limitValue, + userBeforeWugnotBalance, + userWrappedWugnot, + result.AmountSpecified.Abs(), + ) + + return inputAmount, outputAmount, nil +} diff --git a/contract/r/gnoswap/v1/router/consts.gno b/contract/r/gnoswap/v1/router/consts.gno new file mode 100644 index 0000000..8df7577 --- /dev/null +++ b/contract/r/gnoswap/v1/router/consts.gno @@ -0,0 +1,16 @@ +package router + +import ( + prabc "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoswap/access" +) + +const maxApprove int64 = 9223372036854775807 + +var ( + poolAddr, _ = access.GetAddress(prabc.ROLE_POOL.String()) + routerAddr, _ = access.GetAddress(prabc.ROLE_ROUTER.String()) + positionAddr, _ = access.GetAddress(prabc.ROLE_POSITION.String()) + protocolFeeAddr, _ = access.GetAddress(prabc.ROLE_PROTOCOL_FEE.String()) +) diff --git a/contract/r/gnoswap/v1/router/doc.gno b/contract/r/gnoswap/v1/router/doc.gno new file mode 100644 index 0000000..3e1e758 --- /dev/null +++ b/contract/r/gnoswap/v1/router/doc.gno @@ -0,0 +1,9 @@ +// Package router handles token swaps through GnoSwap liquidity pools. +// +// The router provides user-facing swap functions with slippage protection, +// multi-hop routing, and automatic GNOT wrapping/unwrapping. It supports +// both exact input and exact output swap modes. +// +// All swap functions include deadline checks and minimum output validation +// to protect users from unfavorable price movements. +package router diff --git a/contract/r/gnoswap/v1/router/errors.gno b/contract/r/gnoswap/v1/router/errors.gno new file mode 100644 index 0000000..9157ca1 --- /dev/null +++ b/contract/r/gnoswap/v1/router/errors.gno @@ -0,0 +1,37 @@ +package router + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errNoPermission = errors.New("[GNOSWAP-ROUTER-001] caller has no permission") + errSlippage = errors.New("[GNOSWAP-ROUTER-002] slippage check failed") + errInvalidRoutesAndQuotes = errors.New("[GNOSWAP-ROUTER-003] invalid routes and quotes") + errExpired = errors.New("[GNOSWAP-ROUTER-004] transaction expired") + errInvalidInput = errors.New("[GNOSWAP-ROUTER-005] invalid input data") + errInvalidPoolFeeTier = errors.New("[GNOSWAP-ROUTER-006] invalid pool fee tier") + errInvalidSwapFee = errors.New("[GNOSWAP-ROUTER-007] invalid swap fee") + errInvalidSwapType = errors.New("[GNOSWAP-ROUTER-008] invalid swap type") + errInvalidPoolPath = errors.New("[GNOSWAP-ROUTER-009] invalid pool path") + errWrapUnwrap = errors.New("[GNOSWAP-ROUTER-010] wrap, unwrap failed") + errWugnotMinimum = errors.New("[GNOSWAP-ROUTER-011] less than minimum amount ") + errQuoteParser = errors.New("[GNOSWAP-ROUTER-012] quote parse failed") + errHopsOutOfRange = errors.New("[GNOSWAP-ROUTER-013] number of hops must be 1~3") + errSameTokenSwap = errors.New("[GNOSWAP-ROUTER-014] cannot swap same token") + errProtocolFeeOverflow = errors.New("[GNOSWAP-ROUTER-015] protocol fee calculation overflow") + errOverFlow = errors.New("[GNOSWAP-ROUTER-016] overflow") +) + +// addDetailToError adds detail to an error message. +func addDetailToError(err error, detail string) string { + finalErr := ufmt.Errorf("%s || %s", err.Error(), detail) + return finalErr.Error() +} + +// makeErrorWithDetails creates an error with additional context. +func makeErrorWithDetails(err error, detail string) error { + return ufmt.Errorf("%s || %s", err.Error(), detail) +} diff --git a/contract/r/gnoswap/v1/router/exact_in.gno b/contract/r/gnoswap/v1/router/exact_in.gno new file mode 100644 index 0000000..20268c1 --- /dev/null +++ b/contract/r/gnoswap/v1/router/exact_in.gno @@ -0,0 +1,175 @@ +package router + +import ( + "std" + + "gno.land/p/nt/ufmt" + + i256 "gno.land/p/gnoswap/int256" + + "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/referral" + "gno.land/r/gnoswap/v1/common" +) + +type ExactInSwapOperation struct { + baseSwapOperation + params ExactInParams +} + +func NewExactInSwapOperation(pp ExactInParams) *ExactInSwapOperation { + return &ExactInSwapOperation{ + params: pp, + baseSwapOperation: baseSwapOperation{ + userWrappedWugnot: INITIAL_WUGNOT_BALANCE, + }, + } +} + +// ExactInSwapRoute swaps an exact amount of input tokens for output tokens. +// +// Executes multi-hop swaps through specified route. +// Supports splitting across multiple paths for price optimization. +// Applies slippage protection via minimum output amount. +// +// Parameters: +// - inputToken, outputToken: Token contract paths +// - amountIn: Exact input amount to swap +// - routeArr: Swap route "TOKEN0:TOKEN1:FEE,TOKEN1:TOKEN2:FEE" (max 7 hops) +// - quoteArr: Split percentages "70,30" (must sum to 100) +// - amountOutMin: Minimum acceptable output (slippage protection) +// - deadline: Unix timestamp for expiration +// - referrer: Optional referral address +// +// Route format: +// - Single: "WETH:USDC:3000" +// - Multi-hop: "WETH:GNS:3000,GNS:USDC:500" +// - Multi-path: Use multiple routes with quotes +// +// Returns: +// - amountIn: Actual input consumed +// - amountOut: Actual output received +// +// Reverts if output < amountOutMin or deadline passed. +func ExactInSwapRoute(cur realm, + inputToken string, + outputToken string, + amountIn string, + RouteArr string, + quoteArr string, + amountOutMin string, + deadline int64, + referrer string, +) (string, string) { + halt.AssertIsNotHaltedRouter() + + assertIsNotPassedDeadline(deadline) + + emission.MintAndDistributeGns(cross) + + params := SwapRouteParams{ + inputToken: inputToken, + outputToken: outputToken, + routeArr: RouteArr, + quoteArr: quoteArr, + deadline: deadline, + typ: ExactIn, + exactAmount: amountIn, + limitAmount: amountOutMin, + } + + inputAmount, outputAmount, err := commonSwapRoute(params) + if err != nil { + panic(err) + } + + if params.IsUnwrap() { + err = unwrapWithTransfer(std.PreviousRealm().Address(), outputAmount.Int64()) + if err != nil { + panic(err) + } + } else { + common.Transfer(cross, outputToken, std.PreviousRealm().Address(), outputAmount.Int64()) + } + + // handle referral registration + previousRealm := std.PreviousRealm() + caller := previousRealm.Address() + success := referral.TryRegister(cross, caller, referrer) + actualReferrer := referrer + if !success { + actualReferrer = referral.GetReferral(caller.String()) + } + + inputAmountStr := inputAmount.ToString() + outputAmountStr := i256.Zero().Neg(outputAmount).ToString() + + std.Emit( + "ExactInSwap", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "input", inputToken, + "output", outputToken, + "exactAmount", amountIn, + "route", RouteArr, + "quote", quoteArr, + "resultInputAmount", inputAmountStr, + "resultOutputAmount", outputAmountStr, + "referrer", actualReferrer, + ) + + return inputAmountStr, outputAmountStr +} + +// Validate validates the exact-in swap operation parameters. +func (op *ExactInSwapOperation) Validate() error { + amountIn := i256.MustFromDecimal(op.params.AmountIn) + if amountIn.IsZero() || amountIn.IsNeg() { + return ufmt.Errorf("invalid amountIn(%s), must be positive", amountIn.ToString()) + } + + // when `SwapType` is `ExactIn`, assign `amountSpecified` the `amountIn` + // obtained from above. + op.amountSpecified = amountIn + + routes, quotes, err := validateRoutesAndQuotes(op.params.RouteArr, op.params.QuoteArr) + if err != nil { + return err + } + + op.routes = routes + op.quotes = quotes + + return nil +} + +// Process executes the exact-in swap operation. +func (op *ExactInSwapOperation) Process() (*SwapResult, error) { + if err := op.handleNativeTokenWrapping(); err != nil { + return nil, err + } + + resultAmountIn, resultAmountOut, err := op.processRoutes(ExactIn) + if err != nil { + return nil, err + } + + return &SwapResult{ + AmountIn: resultAmountIn, + AmountOut: resultAmountOut, + Routes: op.routes, + Quotes: op.quotes, + AmountSpecified: op.amountSpecified, + WithUnwrap: op.withUnwrap, + }, nil +} + +// handleNativeTokenWrapping handles the wrapping of native tokens if needed. +func (op *ExactInSwapOperation) handleNativeTokenWrapping() error { + return op.baseSwapOperation.handleNativeTokenWrapping( + op.params.InputToken, + op.params.OutputToken, + op.amountSpecified, + ) +} diff --git a/contract/r/gnoswap/v1/router/exact_out.gno b/contract/r/gnoswap/v1/router/exact_out.gno new file mode 100644 index 0000000..931d50a --- /dev/null +++ b/contract/r/gnoswap/v1/router/exact_out.gno @@ -0,0 +1,178 @@ +package router + +import ( + "std" + + "gno.land/p/nt/ufmt" + + i256 "gno.land/p/gnoswap/int256" + + "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/referral" + "gno.land/r/gnoswap/v1/common" +) + +// ExactOutSwapOperation handles swaps where the output amount is specified. +type ExactOutSwapOperation struct { + baseSwapOperation + params ExactOutParams +} + +// NewExactOutSwapOperation creates a new exact-out swap operation. +func NewExactOutSwapOperation(pp ExactOutParams) *ExactOutSwapOperation { + return &ExactOutSwapOperation{ + params: pp, + baseSwapOperation: baseSwapOperation{ + userWrappedWugnot: INITIAL_WUGNOT_BALANCE, + }, + } +} + +// ExactOutSwapRoute swaps tokens for an exact output amount. +// +// Executes swap to receive exact output tokens. +// Calculates required input working backwards through route. +// Useful for buying specific amounts regardless of price. +// +// Parameters: +// - inputToken, outputToken: Token contract paths +// - amountOut: Exact output amount desired +// - routeArr: Swap route "TOKEN0:TOKEN1:FEE,TOKEN1:TOKEN2:FEE" (max 7 hops) +// - quoteArr: Split percentages "70,30" (must sum to 100) +// - amountInMax: Maximum input to spend (slippage protection) +// - deadline: Unix timestamp for expiration +// - referrer: Optional referral address +// +// Route calculation: +// - Works backwards from output to input +// - Each hop increases required input +// - Multi-path aggregates total input +// +// Returns: +// - amountIn: Actual input consumed +// - amountOut: Exact output received +// +// Reverts if input > amountInMax or deadline passed. +func ExactOutSwapRoute( + cur realm, + inputToken string, + outputToken string, + amountOut string, + routeArr string, + quoteArr string, + amountInMax string, + deadline int64, + referrer string, +) (string, string) { + halt.AssertIsNotHaltedRouter() + + assertIsNotPassedDeadline(deadline) + + emission.MintAndDistributeGns(cross) + + params := SwapRouteParams{ + inputToken: inputToken, + outputToken: outputToken, + routeArr: routeArr, + quoteArr: quoteArr, + deadline: deadline, + typ: ExactOut, + exactAmount: amountOut, + limitAmount: amountInMax, + } + + inputAmount, outputAmount, err := commonSwapRoute(params) + if err != nil { + panic(err) + } + + if params.IsUnwrap() { + err = unwrapWithTransfer(std.PreviousRealm().Address(), outputAmount.Int64()) + if err != nil { + panic(err) + } + } else { + common.Transfer(cross, outputToken, std.PreviousRealm().Address(), outputAmount.Int64()) + } + + // handle referral registration + previousRealm := std.PreviousRealm() + caller := previousRealm.Address() + success := referral.TryRegister(cross, caller, referrer) + actualReferrer := referrer + if !success { + actualReferrer = referral.GetReferral(caller.String()) + } + + inputAmountStr := inputAmount.ToString() + outputAmountStr := i256.Zero().Neg(outputAmount).ToString() + + std.Emit( + "ExactOutSwap", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "input", inputToken, + "output", outputToken, + "exactAmount", amountOut, + "route", routeArr, + "quote", quoteArr, + "resultInputAmount", inputAmountStr, + "resultOutputAmount", outputAmountStr, + "referrer", actualReferrer, + ) + + return inputAmountStr, outputAmountStr +} + +// Validate ensures the exact-out swap parameters are valid. +func (op *ExactOutSwapOperation) Validate() error { + amountOut := i256.MustFromDecimal(op.params.AmountOut) + if amountOut.IsZero() || amountOut.IsNeg() { + return ufmt.Errorf("invalid amountOut(%s), must be positive", amountOut.ToString()) + } + + // assign a signed reversed `amountOut` to `amountSpecified` + // when it's an ExactOut + op.amountSpecified = i256.Zero().Neg(amountOut) + + routes, quotes, err := validateRoutesAndQuotes(op.params.RouteArr, op.params.QuoteArr) + if err != nil { + return err + } + + op.routes = routes + op.quotes = quotes + + return nil +} + +// Process executes the exact-out swap operation. +func (op *ExactOutSwapOperation) Process() (*SwapResult, error) { + if err := op.handleNativeTokenWrapping(); err != nil { + return nil, err + } + + resultAmountIn, resultAmountOut, err := op.processRoutes(ExactOut) + if err != nil { + return nil, err + } + + return &SwapResult{ + AmountIn: resultAmountIn, + AmountOut: resultAmountOut, + Routes: op.routes, + Quotes: op.quotes, + AmountSpecified: op.amountSpecified, + WithUnwrap: op.withUnwrap, + }, nil +} + +// handleNativeTokenWrapping manages native token wrapping for exact-out swaps. +func (op *ExactOutSwapOperation) handleNativeTokenWrapping() error { + return op.baseSwapOperation.handleNativeTokenWrapping( + op.params.InputToken, + op.params.OutputToken, + i256.MustFromDecimal(op.params.AmountInMax), + ) +} diff --git a/contract/r/gnoswap/v1/router/gnomod.toml b/contract/r/gnoswap/v1/router/gnomod.toml new file mode 100644 index 0000000..8bcfa66 --- /dev/null +++ b/contract/r/gnoswap/v1/router/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/router" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/router/protocol_fee_swap.gno b/contract/r/gnoswap/v1/router/protocol_fee_swap.gno new file mode 100644 index 0000000..95a806c --- /dev/null +++ b/contract/r/gnoswap/v1/router/protocol_fee_swap.gno @@ -0,0 +1,104 @@ +package router + +import ( + "std" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" + + "gno.land/p/nt/ufmt" + + u256 "gno.land/p/gnoswap/uint256" + + pf "gno.land/r/gnoswap/v1/protocol_fee" +) + +const ( + defaultSwapFeeBPS = uint64(15) // 0.15% +) + +// swapFee is the fee charged on each swap transaction. +// This parameter can be modified through governance. +var swapFee = defaultSwapFeeBPS + +// GetSwapFee returns the current swap fee rate in basis points. +func GetSwapFee() uint64 { + return swapFee +} + +// SetSwapFee sets the swap fee rate in basis points. +// Only admin or governance can call this function. +func SetSwapFee(cur realm, fee uint64) { + halt.AssertIsNotHaltedRouter() + halt.AssertIsNotHaltedProtocolFee() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + prevSwapFee := swapFee + if err := setSwapFee(fee); err != nil { + panic(err) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "SetSwapFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "newFee", formatUint(fee), + "prevFee", formatUint(prevSwapFee), + ) +} + +// setSwapFee validates and updates the swap fee rate. +func setSwapFee(fee uint64) error { + // 10000 (bps) = 100% + if fee > 10000 { + return ufmt.Errorf( + "%s: fee must be in range 0 to 10000. got %d", + errInvalidSwapFee.Error(), fee, + ) + } + + swapFee = fee + return nil +} + +// handleSwapFee deducts the protocol fee from the swap amount and transfers it to the protocol fee contract. +func handleSwapFee( + outputToken string, + amount *u256.Uint, +) *u256.Uint { + if swapFee <= 0 { + return amount + } + + feeAmount := u256.Zero().Mul(amount, u256.NewUint(swapFee)) + feeAmount = u256.Zero().Div(feeAmount, u256.NewUint(10000)) + feeAmountInt64 := safeConvertToInt64(feeAmount) + + if outputToken == gnot { + outputToken = wugnotPath + } + + common.Transfer(cross, outputToken, protocolFeeAddr, feeAmountInt64) + + pf.AddToProtocolFee(cross, outputToken, feeAmountInt64) + + previousRealm := std.PreviousRealm() + std.Emit( + "SwapRouteFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "tokenPath", outputToken, + "amount", formatInt64(feeAmountInt64), + ) + + toUserAfterProtocol, underflow := u256.Zero().SubOverflow(amount, feeAmount) + if underflow { + panic(errProtocolFeeOverflow) + } + + return toUserAfterProtocol +} diff --git a/contract/r/gnoswap/v1/router/router.gno b/contract/r/gnoswap/v1/router/router.gno new file mode 100644 index 0000000..efd7210 --- /dev/null +++ b/contract/r/gnoswap/v1/router/router.gno @@ -0,0 +1,282 @@ +package router + +import ( + "std" + "strconv" + + "gno.land/p/nt/ufmt" + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoland/wugnot" +) + +var ( + one = u256.One() + + maxInt64 = int64(^uint64(0) >> 1) +) + +// ErrorMessages define all error message templates used throughout the router +const ( + // slippage validation + errExactOutAmountExceeded = "Received more than requested in [EXACT_OUT] requested=%s, actual=%s" + + // route validation + errInvalidRouteLength = "route length(%d) must be 1~7" + + // quote validation + errRoutesQuotesMismatch = "mismatch between routes(%d) and quotes(%d) length" + errInvalidQuote = "invalid quote(%s) at index(%d)" + errInvalidQuoteValue = "quote(%s) at index(%d) must be positive value" + errQuoteExceedsMax = "quote(%s) at index(%d) must be less than or equal to %d" + errQuoteSumExceedsMax = "quote sum exceeds 100 at index(%d)" + errInvalidQuoteSum = "quote sum(%d) must be 100" + + // balance and overflow validation + errOverflowInBalance = "overflow in balance calculation: beforeBalance(%d) + wrappedAmount(%d)" + errTooMuchWugnotSpent = "too much wugnot spent (wrapped: %d, spend: %d)" + + // swap type validation + errExactInTooFewReceived = "ExactIn: too few received (min:%s, got:%s)" + errExactOutTooMuchSpent = "ExactOut: too much spent (max:%s, used:%s)" + + // route parsing validation + errEmptyRoutes = "routes cannot be empty" +) + +// GnotSwapHandler encapsulates methods for handling GNOT token swaps +type GnotSwapHandler struct { + BeforeBalance int64 + WrappedAmount int64 + NewBalance int64 +} + +// newGnotSwapHandler creates a new handler for GNOT swaps. +func newGnotSwapHandler(beforeBalance, wrappedAmount int64) *GnotSwapHandler { + return &GnotSwapHandler{ + BeforeBalance: beforeBalance, + WrappedAmount: wrappedAmount, + } +} + +// UpdateNewBalance updates the current balance after swap operations. +func (h *GnotSwapHandler) UpdateNewBalance() { + h.NewBalance = wugnot.BalanceOf(std.PreviousRealm().Address()) +} + +// HandleInputSwap manages unwrapping logic for GNOT input tokens. +func (h *GnotSwapHandler) HandleInputSwap() error { + // Check for overflow when adding balances + if h.BeforeBalance > 0 && h.WrappedAmount > 0 { + if h.BeforeBalance > (1<<63-1)-h.WrappedAmount { + return ufmt.Errorf(errOverflowInBalance, + h.BeforeBalance, h.WrappedAmount) + } + } + + totalBefore := h.BeforeBalance + h.WrappedAmount + spend := totalBefore - h.NewBalance + + if spend > h.WrappedAmount { + return ufmt.Errorf(errTooMuchWugnotSpent, + h.WrappedAmount, spend) + } + + toUnwrap := h.WrappedAmount - spend + + caller := std.PreviousRealm().Address() + return unwrapWithTransferFrom(caller, caller, toUnwrap) +} + +// SwapValidator provides validation methods for swap operations +type SwapValidator struct{} + +// exactOutAmount checks if output amount meets specified requirements. +func (v *SwapValidator) exactOutAmount(resultAmount, specifiedAmount *u256.Uint) error { + if resultAmount.Gte(specifiedAmount) { + return nil + } + + diff := u256.Zero().Sub(specifiedAmount, resultAmount) + if diff.Gt(one) { + return ufmt.Errorf(errExactOutAmountExceeded, specifiedAmount.ToString(), resultAmount.ToString()) + } + return nil +} + +// slippage ensures swap amounts meet slippage requirements. +func (v *SwapValidator) slippage(swapType SwapType, amountIn, amountOut, limit *u256.Uint) error { + switch swapType { + case ExactIn: + if amountOut.Lt(limit) { + return ufmt.Errorf(errExactInTooFewReceived, + limit.ToString(), amountOut.ToString()) + } + case ExactOut: + if amountIn.Gt(limit) { + return ufmt.Errorf(errExactOutTooMuchSpent, + limit.ToString(), amountIn.ToString()) + } + default: + return errInvalidSwapType + } + return nil +} + +// swapType ensures the swap type string is valid. +func (v *SwapValidator) swapType(swapTypeStr string) (SwapType, error) { + swapType, err := trySwapTypeFromStr(swapTypeStr) + if err != nil { + return Unknown, errInvalidSwapType + } + return swapType, nil +} + +// amount ensures the amount is properly formatted and positive. +func (v *SwapValidator) amount(amount string) (*i256.Int, error) { + parsedAmount := i256.MustFromDecimal(amount) + if parsedAmount.Lt(i256.Zero()) { + return nil, ufmt.Errorf(ErrInvalidPositiveAmount, amount) + } + return parsedAmount, nil +} + +// amountLimit ensures the amount limit is properly formatted and non-zero. +func (v *SwapValidator) amountLimit(amountLimit string) (*i256.Int, error) { + parsedLimit := i256.MustFromDecimal(amountLimit) + if parsedLimit.IsZero() { + return nil, ufmt.Errorf(ErrInvalidZeroAmountLimit, amountLimit) + } + return parsedLimit, nil +} + +// RouteParser handles parsing and validation of routes and quotes +type RouteParser struct{} + +// NewRouteParser creates a new route parser instance. +func NewRouteParser() *RouteParser { + return &RouteParser{} +} + +// ParseRoutes parses route and quote strings into slices and validates them. +func (p *RouteParser) ParseRoutes(routes, quotes string) ([]string, []string, error) { + // Check for empty routes + if routes == "" || quotes == "" { + return nil, nil, ufmt.Errorf(errEmptyRoutes) + } + + routesArr := splitSingleChar(routes, ',') + quotesArr := splitSingleChar(quotes, ',') + + if err := p.ValidateRoutesAndQuotes(routesArr, quotesArr); err != nil { + return nil, nil, err + } + + return routesArr, quotesArr, nil +} + +// ValidateRoutesAndQuotes ensures routes and quotes meet required criteria. +func (p *RouteParser) ValidateRoutesAndQuotes(routes, quotes []string) error { + rr := len(routes) + qq := len(quotes) + + if rr < 1 || rr > 7 { + return ufmt.Errorf(errInvalidRouteLength, rr) + } + + if rr != qq { + return ufmt.Errorf(errRoutesQuotesMismatch, rr, qq) + } + + return p.ValidateQuoteSum(quotes) +} + +// ValidateQuoteSum ensures all quotes add up to 100%. +func (p *RouteParser) ValidateQuoteSum(quotes []string) error { + const ( + maxQuote int8 = 100 + minQuote int8 = 0 + ) + + var sum int8 + + for i, quote := range quotes { + qt, err := strconv.ParseInt(quote, 10, 8) + if err != nil { + return ufmt.Errorf(errInvalidQuote, quote, i) + } + intQuote := int8(qt) + + // Even if quoteArr itself contains 0, there's no problem as long as the sum equals 100, + // but since quote generally won't be 0, we check if it's less than or equal to minQuote. + if intQuote <= minQuote { + return ufmt.Errorf(errInvalidQuoteValue, quote, i) + } + + if intQuote > maxQuote { + return ufmt.Errorf(errQuoteExceedsMax, quote, i, maxQuote) + } + + if sum > maxQuote-intQuote { + return ufmt.Errorf(errQuoteSumExceedsMax, i) + } + + sum += intQuote + } + + if sum != maxQuote { + return ufmt.Errorf(errInvalidQuoteSum, sum) + } + + return nil +} + +// finalizeSwap handles post-swap operations and validations. +func finalizeSwap( + inputToken, outputToken string, + resultAmountIn, resultAmountOut *u256.Uint, + swapType SwapType, + tokenAmountLimit *u256.Uint, + userBeforeWugnotBalance, userWrappedWugnot int64, + amountSpecified *u256.Uint, +) (*i256.Int, *i256.Int) { + validator := &SwapValidator{} + + // Validate exact out amount if applicable + if swapType == ExactOut { + if err := validator.exactOutAmount(resultAmountOut, amountSpecified); err != nil { + panic(addDetailToError(errSlippage, err.Error())) + } + } + + // Handle swap fee + resultAmountOutWithoutFee := handleSwapFee(outputToken, resultAmountOut) + + // Handle GNOT token swaps + handler := newGnotSwapHandler(userBeforeWugnotBalance, userWrappedWugnot) + handler.UpdateNewBalance() + + var err error + if inputToken == gnot { + err = handler.HandleInputSwap() + } + + if err != nil { + panic(addDetailToError(errSlippage, err.Error())) + } + + if err := validator.slippage(swapType, resultAmountIn, resultAmountOutWithoutFee, tokenAmountLimit); err != nil { + panic(addDetailToError(errSlippage, err.Error())) + } + + // calculate final amounts + intAmountOut := i256.FromUint256(resultAmountOutWithoutFee) + + return i256.FromUint256(resultAmountIn), intAmountOut +} + +// validateRoutesAndQuotes is a convenience function that parses and validates routes in one call. +func validateRoutesAndQuotes(routes, quotes string) ([]string, []string, error) { + return NewRouteParser().ParseRoutes(routes, quotes) +} diff --git a/contract/r/gnoswap/v1/router/router_dry.gno b/contract/r/gnoswap/v1/router/router_dry.gno new file mode 100644 index 0000000..0826820 --- /dev/null +++ b/contract/r/gnoswap/v1/router/router_dry.gno @@ -0,0 +1,250 @@ +package router + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/nt/ufmt" + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v1/common" +) + +var PERCENTAGE_DENOMINATOR = i256.NewInt(100) + +// QuoteConstraints defines the valid range for swap quote percentages +const ( + MaxQuotePercentage = 100 + MinQuotePercentage = 0 +) + +// ErrorMessages for DrySwapRoute operations +const ( + ErrOverflowResultAmountIn = "overflow in resultAmountIn" + ErrOverflowResultAmountOut = "overflow in resultAmountOut" + ErrUnknownSwapType = "unknown swapType(%s)" + ErrInvalidPositiveAmount = "invalid amount(%s), must be positive" + ErrInvalidZeroAmountLimit = "invalid amountLimit(%s), should not be zero" + ErrInvalidQuoteRange = "quote(%d) must be %d~%d" + ErrOverflowCalculateSwapAmount = "overflow in calculateSwapAmount" +) + +// SwapProcessor handles the execution of swap operations +type SwapProcessor struct{} + +// ProcessSwapAmount calculates the exact amount to swap based on quote percentage. +func (p *SwapProcessor) ProcessSwapAmount(amountSpecified *i256.Int, quote int) (*i256.Int, error) { + if quote < MinQuotePercentage || quote > MaxQuotePercentage { + return nil, ufmt.Errorf(ErrInvalidQuoteRange, quote, MinQuotePercentage, MaxQuotePercentage) + } + + toSwap := i256.Zero().Mul(amountSpecified, i256.NewInt(int64(quote))) + if toSwap.IsOverflow() { + return nil, ufmt.Errorf(ErrOverflowCalculateSwapAmount) + } + + return i256.Zero().Div(toSwap, PERCENTAGE_DENOMINATOR), nil +} + +// ProcessSingleSwap handles a single-hop swap simulation. +func (p *SwapProcessor) ProcessSingleSwap(route string, amountSpecified *i256.Int) (amountIn, amountOut *u256.Uint, err error) { + input, output, fee := getDataForSinglePath(route) + singleParams := SingleSwapParams{ + tokenIn: input, + tokenOut: output, + fee: fee, + amountSpecified: amountSpecified, + } + + amountIn, amountOut = singleDrySwap(&singleParams) + return amountIn, amountOut, nil +} + +// ProcessMultiSwap handles a multi-hop swap simulation. +func (p *SwapProcessor) ProcessMultiSwap( + swapType SwapType, + route string, + numHops int, + amountSpecified *i256.Int, +) (*u256.Uint, *u256.Uint, error) { + recipient := std.PreviousRealm().Address() + pathIndex := getPathIndex(swapType, numHops) + + input, output, fee := getDataForMultiPath(route, pathIndex) + swapParams := newSwapParams(input, output, fee, recipient, amountSpecified, false) + + switch swapType { + case ExactIn: + return multiDrySwap(*swapParams, numHops, route) + case ExactOut: + return multiDrySwapNegative(*swapParams, numHops, route) + default: + return nil, nil, ufmt.Errorf(ErrUnknownSwapType, swapType) + } +} + +// ValidateSwapResults checks if the swap results meet the required constraints. +func (p *SwapProcessor) ValidateSwapResults( + swapType SwapType, + resultAmountIn, resultAmountOut *u256.Uint, + amountSpecified, amountLimit *i256.Int, +) (amountIn, amountOut string, success bool) { + if resultAmountIn.IsZero() || resultAmountOut.IsZero() { + return "0", "0", false + } + + switch swapType { + case ExactIn: + if i256.FromUint256(resultAmountIn).Gt(amountSpecified) { + return resultAmountIn.ToString(), resultAmountOut.ToString(), false + } + if i256.FromUint256(resultAmountOut).Lt(amountLimit) { + return resultAmountIn.ToString(), resultAmountOut.ToString(), false + } + return resultAmountIn.ToString(), resultAmountOut.ToString(), true + + case ExactOut: + if i256.FromUint256(resultAmountOut).Lt(amountSpecified) { + return resultAmountIn.ToString(), resultAmountOut.ToString(), false + } + if i256.FromUint256(resultAmountIn).Gt(amountLimit) { + return resultAmountIn.ToString(), resultAmountOut.ToString(), false + } + return resultAmountIn.ToString(), resultAmountOut.ToString(), true + + default: + // This should never happen since we validate the swap type earlier + return "", "", false + } +} + +// AddSwapResults safely adds swap result amounts, checking for overflow. +func (p *SwapProcessor) AddSwapResults( + resultAmountIn, resultAmountOut, amountIn, amountOut *u256.Uint, +) (*u256.Uint, *u256.Uint, error) { + newAmountIn := u256.Zero().Add(resultAmountIn, amountIn) + if newAmountIn.IsOverflow() { + return nil, nil, ufmt.Errorf(ErrOverflowResultAmountIn) + } + + newAmountOut := u256.Zero().Add(resultAmountOut, amountOut) + if newAmountOut.IsOverflow() { + return nil, nil, ufmt.Errorf(ErrOverflowResultAmountOut) + } + + return newAmountIn, newAmountOut, nil +} + +// DrySwapRoute simulates a token swap route without executing the swap. +// It calculates the expected outcome based on the current state of liquidity pools. +func DrySwapRoute( + inputToken, outputToken string, + specifiedAmount string, + swapTypeStr string, + strRouteArr, quoteArr string, + tokenAmountLimit string, +) (string, string, bool) { + drySwapRouteWithCrossFn := func(cur realm) (string, string, bool) { + return drySwapRoute(inputToken, outputToken, specifiedAmount, swapTypeStr, strRouteArr, quoteArr, tokenAmountLimit) + } + + return drySwapRouteWithCrossFn(cross) +} + +// drySwapRoute is a function for applying cross realm. +func drySwapRoute( + inputToken, outputToken string, + specifiedAmount string, + swapTypeStr string, + strRouteArr, quoteArr string, + tokenAmountLimit string, +) (string, string, bool) { + common.MustRegistered(inputToken, outputToken) + // initialize components + validator := &SwapValidator{} + processor := &SwapProcessor{} + parser := &RouteParser{} + + // validate and parse inputs + swapType, err := validator.swapType(swapTypeStr) + if err != nil { + panic(addDetailToError(errInvalidSwapType, err.Error())) + } + + amountSpecified, err := validator.amount(specifiedAmount) + if err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + + amountLimit, err := validator.amountLimit(tokenAmountLimit) + if err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + + routes, quotes, err := parser.ParseRoutes(strRouteArr, quoteArr) + if err != nil { + panic(addDetailToError(errInvalidRoutesAndQuotes, err.Error())) + } + + // adjust amount sign for exact out swaps + if swapType == ExactOut { + amountSpecified = i256.Zero().Neg(amountSpecified) + } + + // initialize accumulators for swap results + resultAmountIn, resultAmountOut := zero, zero + + // Process each route + for i, route := range routes { + // calculate the amount to swap for this route + quoteValue, err := strconv.Atoi(quotes[i]) + if err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + + toSwap, err := processor.ProcessSwapAmount(amountSpecified, quoteValue) + if err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + + // determine the number of hops and validate + numHops := strings.Count(route, POOL_SEPARATOR) + 1 + assertHopsInRange(numHops) + + // execute the appropriate swap type + var amountIn, amountOut *u256.Uint + if numHops == 1 { + amountIn, amountOut, err = processor.ProcessSingleSwap(route, toSwap) + } else { + amountIn, amountOut, err = processor.ProcessMultiSwap(swapType, route, numHops, toSwap) + } + + if err != nil { + panic(addDetailToError(errInvalidSwapType, err.Error())) + } + + // update accumulated results + resultAmountIn, resultAmountOut, err = processor.AddSwapResults(resultAmountIn, resultAmountOut, amountIn, amountOut) + if err != nil { + panic(addDetailToError(errInvalidInput, err.Error())) + } + } + + return processor.ValidateSwapResults(swapType, resultAmountIn, resultAmountOut, amountSpecified, amountLimit) +} + +// getPathIndex returns the path index based on swap type and number of hops. +func getPathIndex(swapType SwapType, numHops int) int { + switch swapType { + case ExactIn: + // first data for exact input swaps + return 0 + case ExactOut: + // last data for exact output swaps + return numHops - 1 + default: + panic("should not happen") + } +} diff --git a/contract/r/gnoswap/v1/router/swap_inner.gno b/contract/r/gnoswap/v1/router/swap_inner.gno new file mode 100644 index 0000000..0fadfdc --- /dev/null +++ b/contract/r/gnoswap/v1/router/swap_inner.gno @@ -0,0 +1,186 @@ +package router + +import ( + "std" + + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/v1/common" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + pl "gno.land/r/gnoswap/v1/pool" +) + +const ( + MIN_SQRT_RATIO string = "4295128739" // same as TickMathGetSqrtRatioAtTick(MIN_TICK) + MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" // same as TickMathGetSqrtRatioAtTick(MAX_TICK) +) + +// swapInner executes the core swap logic by interacting with the pool contract. +// Returns poolRecv (tokens received by pool) and poolOut (tokens sent by pool). +func swapInner( + amountSpecified *i256.Int, + recipient std.Address, + sqrtPriceLimitX96 *u256.Uint, + data SwapCallbackData, +) (poolRecv, poolOut *u256.Uint) { + zeroForOne := data.tokenIn < data.tokenOut + + sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96) + + amount0Str, amount1Str := pl.Swap( + cross, + data.tokenIn, + data.tokenOut, + data.fee, + recipient, + zeroForOne, + amountSpecified.ToString(), + sqrtPriceLimitX96.ToString(), + data.payer, + ) + + amount0 := i256.MustFromDecimal(amount0Str) + amount1 := i256.MustFromDecimal(amount1Str) + + poolOut, poolRecv = i256MinMax(amount0, amount1) + if poolRecv.IsOverflow() || poolOut.IsOverflow() { + panic("overflow in swapInner") + } + + // approves pool as spender + if data.hasNext { + common.Approve(cross, data.tokenOut, poolAddr, poolOut.Int64()) + } + + return poolRecv, poolOut +} + +// RealSwapExecutor implements SwapExecutor for actual swaps. +type RealSwapExecutor struct{} + +// execute performs the actual swap execution. +func (e *RealSwapExecutor) execute(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { + caller := std.PreviousRealm().Address() + recipient := routerAddr + + return swapInner( + p.amountSpecified, + recipient, // if single swap => user will receive + zero, // sqrtPriceLimitX96 + newSwapCallbackData(p, caller, false), + ) +} + +// swapDryInner performs a dry-run of a swap operation without executing it. +func swapDryInner( + amountSpecified *i256.Int, + sqrtPriceLimitX96 *u256.Uint, + data SwapCallbackData, +) (poolRecv, poolOut *u256.Uint) { + zeroForOne := data.tokenIn < data.tokenOut + sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96) + + // check possible + amount0Str, amount1Str, ok := pl.DrySwap( + data.tokenIn, + data.tokenOut, + data.fee, + zeroForOne, + amountSpecified.ToString(), + sqrtPriceLimitX96.ToString(), + ) + if !ok { + return zero, zero + } + + amount0 := i256.MustFromDecimal(amount0Str) + amount1 := i256.MustFromDecimal(amount1Str) + + poolOut, poolRecv = i256MinMax(amount0, amount1) + if poolRecv.IsOverflow() || poolOut.IsOverflow() { + panic("overflow in swapDryInner") + } + + return poolRecv, poolOut +} + +// DrySwapExecutor implements SwapExecutor for dry swaps. +type DrySwapExecutor struct{} + +// execute performs the dry swap execution. +func (e *DrySwapExecutor) execute(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { + previousRealmAddr := std.PreviousRealm().Address() + + return swapDryInner( + p.amountSpecified, + zero, + newSwapCallbackData(p, previousRealmAddr, false), + ) +} + +// calculateSqrtPriceLimitForSwap calculates the price limit for a swap operation. +func calculateSqrtPriceLimitForSwap(zeroForOne bool, fee uint32, sqrtPriceLimitX96 *u256.Uint) *u256.Uint { + if !sqrtPriceLimitX96.IsZero() { + return sqrtPriceLimitX96 + } + + if zeroForOne { + minTick := getMinTick(fee) + 1 + sqrtPriceLimitX96 = u256.Zero().Set(common.TickMathGetSqrtRatioAtTick(minTick)) + if sqrtPriceLimitX96.IsZero() { + sqrtPriceLimitX96 = u256.MustFromDecimal(MIN_SQRT_RATIO) + } + return u256.Zero().Add(sqrtPriceLimitX96, one) + } + + maxTick := getMaxTick(fee) - 1 + sqrtPriceLimitX96 = u256.Zero().Set(common.TickMathGetSqrtRatioAtTick(maxTick)) + if sqrtPriceLimitX96.IsZero() { + sqrtPriceLimitX96 = u256.MustFromDecimal(MAX_SQRT_RATIO) + } + return u256.Zero().Sub(sqrtPriceLimitX96, one) +} + +// getMinTick returns the minimum tick value for a given fee tier. +// The implementation follows Uniswap V3's tick spacing rules where +// lower fee tiers allow for finer price granularity. +func getMinTick(fee uint32) int32 { + switch fee { + case 100: + return -887272 + case 500: + return -887270 + case 3000: + return -887220 + case 10000: + return -887200 + default: + panic(addDetailToError( + errInvalidPoolFeeTier, + ufmt.Sprintf("unknown fee(%d)", fee), + )) + } +} + +// getMaxTick returns the maximum tick value for a given fee tier. +// The max tick values are the exact negatives of min tick values. +func getMaxTick(fee uint32) int32 { + switch fee { + case 100: + return 887272 + case 500: + return 887270 + case 3000: + return 887220 + case 10000: + return 887200 + default: + panic(addDetailToError( + errInvalidPoolFeeTier, + ufmt.Sprintf("unknown fee(%d)", fee), + )) + } +} diff --git a/contract/r/gnoswap/v1/router/swap_multi.gno b/contract/r/gnoswap/v1/router/swap_multi.gno new file mode 100644 index 0000000..49ff662 --- /dev/null +++ b/contract/r/gnoswap/v1/router/swap_multi.gno @@ -0,0 +1,259 @@ +package router + +import ( + "std" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +// SwapDirection represents the direction of swap execution in multi-hop swaps. +// It determines whether swaps are processed in forward order (first to last pool) +// or backward order (last to first pool). +type SwapDirection int + +const ( + _ SwapDirection = iota + // Forward indicates a swap processing direction from the first pool to the last pool. + // Used primarily for exactIn swaps where the input amount is known. + Forward + + // Backward indicates a swap processing direction from the last pool to the first pool. + // Used primarily for exactOut swaps where the output amount is known and input amounts + // Need to be calculated in reverse order. + Backward +) + +// MultiSwapExecutor defines the interface for multi-hop swap operation execution. +type MultiSwapExecutor interface { + // Run performs the swap operation and returns pool received and pool output amounts. + Run(p SwapParams, data SwapCallbackData, recipient std.Address) (*u256.Uint, *u256.Uint) +} + +// DryMultiSwapExecutor implements MultiSwapExecutor for dry run simulations. +type DryMultiSwapExecutor struct{} + +// Run performs a dry swap operation without changing state. +func (e *DryMultiSwapExecutor) Run(p SwapParams, data SwapCallbackData, _ std.Address) (*u256.Uint, *u256.Uint) { + return swapDryInner(p.amountSpecified, zero, data) +} + +// RealMultiSwapExecutor implements MultiSwapExecutor for actual swap operations. +type RealMultiSwapExecutor struct{} + +// Run performs a real swap operation with state changes. +func (e *RealMultiSwapExecutor) Run(p SwapParams, data SwapCallbackData, recipient std.Address) (*u256.Uint, *u256.Uint) { + return swapInner(p.amountSpecified, recipient, zero, data) +} + +// MultiSwapProcessor handles the execution flow for multi-hop swaps. +type MultiSwapProcessor struct { + executor MultiSwapExecutor + direction SwapDirection + isSimulate bool +} + +var ( + _ MultiSwapExecutor = (*DryMultiSwapExecutor)(nil) + _ MultiSwapExecutor = (*RealMultiSwapExecutor)(nil) +) + +// NewMultiSwapProcessor creates a new MultiSwapProcessor with the specified configuration. +func NewMultiSwapProcessor(isSimulate bool, direction SwapDirection) *MultiSwapProcessor { + var executor MultiSwapExecutor + if isSimulate { + executor = &DryMultiSwapExecutor{} + } else { + executor = &RealMultiSwapExecutor{} + } + + return &MultiSwapProcessor{ + executor: executor, + direction: direction, + isSimulate: isSimulate, + } +} + +// processForwardSwap handles forward direction swaps (exactIn). +func (p *MultiSwapProcessor) processForwardSwap(sp SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint, error) { + payer := std.PreviousRealm().Address() // Initial payer is the user + + firstAmountIn := zero + currentPoolIndex := 0 + + for { + currentPoolIndex++ + + // Execute the swap operation + callbackData := newSwapCallbackData(sp, payer, currentPoolIndex != numPools) + amountIn, amountOut := p.executor.Run(sp, callbackData, sp.recipient) + + // Record the first hop's input amount + if currentPoolIndex == 1 { + firstAmountIn = amountIn + } + + // Check if we've processed all hops + if currentPoolIndex >= numPools { + if p.isSimulate { + return firstAmountIn, amountOut, nil + } + return firstAmountIn, amountOut, nil + } + + // Update parameters for the next hop + payer = routerAddr + nextInput, nextOutput, nextFee := getDataForMultiPath(swapPath, currentPoolIndex) + sp.tokenIn = nextInput + sp.tokenOut = nextOutput + sp.fee = nextFee + sp.amountSpecified = i256.FromUint256(amountOut) + } +} + +// processBackwardSwap handles backward direction swaps (exactOut). +func (p *MultiSwapProcessor) processBackwardSwap(sp SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint, error) { + if !p.isSimulate { + return p.processBackwardRealSwap(sp, numPools, swapPath) + } + return p.processBackwardDrySwap(sp, numPools, swapPath) +} + +// processBackwardDrySwap handles backward simulated swaps. +func (p *MultiSwapProcessor) processBackwardDrySwap(sp SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint, error) { + firstAmountIn := u256.Zero() + currentPoolIndex := numPools - 1 + payer := routerAddr + + for { + callbackData := newSwapCallbackData(sp, payer, false) + amountIn, amountOut := p.executor.Run(sp, callbackData, sp.recipient) + + if currentPoolIndex == 0 { + firstAmountIn = amountIn + } + + currentPoolIndex-- + + if currentPoolIndex == -1 { + return firstAmountIn, amountOut, nil + } + + // Update parameters for the next hop + nextInput, nextOutput, nextFee := getDataForMultiPath(swapPath, currentPoolIndex) + intAmountIn := i256.FromUint256(amountIn) + + sp.amountSpecified = i256.Zero().Neg(intAmountIn) + sp.tokenIn = nextInput + sp.tokenOut = nextOutput + sp.fee = nextFee + } +} + +// processBackwardRealSwap handles backward real swaps. +func (p *MultiSwapProcessor) processBackwardRealSwap(sp SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint, error) { + // First collect all swap information by simulating backward + swapInfo := p.collectBackwardSwapInfo(sp, numPools, swapPath) + + // Then execute swaps in forward order + return p.executeCollectedSwaps(swapInfo, sp.recipient, sp.withUnwrap) +} + +// collectBackwardSwapInfo simulates swaps backward to collect parameters. +func (p *MultiSwapProcessor) collectBackwardSwapInfo(sp SwapParams, numPools int, swapPath string) []SingleSwapParams { + swapInfo := make([]SingleSwapParams, 0, numPools-1) + currentPoolIndex := numPools - 1 + + for currentPoolIndex >= 0 { + thisSwap := SingleSwapParams{ + tokenIn: sp.tokenIn, + tokenOut: sp.tokenOut, + fee: sp.fee, + amountSpecified: sp.amountSpecified, + } + + // dry simulation to calculate input amount + amountIn, _ := singleDrySwap(&thisSwap) + swapInfo = append(swapInfo, thisSwap) + + if currentPoolIndex == 0 { + break + } + currentPoolIndex-- + + // Update parameters for the next simulation + nextInput, nextOutput, nextFee := getDataForMultiPath(swapPath, currentPoolIndex) + + sp.tokenIn = nextInput + sp.tokenOut = nextOutput + sp.fee = nextFee + sp.amountSpecified = i256.Zero().Neg(i256.FromUint256(amountIn)) + } + + return swapInfo +} + +// executeCollectedSwaps performs the collected swaps in forward order. +func (p *MultiSwapProcessor) executeCollectedSwaps(swapInfo []SingleSwapParams, recipient std.Address, withUnwrap bool) (*u256.Uint, *u256.Uint, error) { + firstAmountIn := zero + currentPoolIndex := len(swapInfo) - 1 + payer := std.PreviousRealm().Address() // Initial payer is the user + + for currentPoolIndex >= 0 { + // Execute the swap + callbackData := newSwapCallbackData( + swapInfo[currentPoolIndex], + payer, + currentPoolIndex != 0, + ) + + amountIn, amountOut := swapInner( + swapInfo[currentPoolIndex].amountSpecified, + recipient, + zero, + callbackData, + ) + + // Record the first hop's input amount + if currentPoolIndex == len(swapInfo)-1 { + firstAmountIn = amountIn + } + + if currentPoolIndex == 0 { + return firstAmountIn, amountOut, nil + } + + // Update parameters for the next swap + swapInfo[currentPoolIndex-1].amountSpecified = i256.FromUint256(amountOut) + payer = routerAddr + currentPoolIndex-- + } + + return firstAmountIn, zero, nil +} + +// multiSwap performs a multi-hop swap in forward direction. +func multiSwap(p SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint) { + result, output, _ := NewMultiSwapProcessor(false, Forward). + processForwardSwap(p, numPools, swapPath) + return result, output +} + +// multiSwapNegative performs a multi-hop swap in backward direction. +func multiSwapNegative(p SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint) { + result, output, _ := NewMultiSwapProcessor(false, Backward). + processBackwardSwap(p, numPools, swapPath) + return result, output +} + +// multiDrySwap simulates a multi-hop swap in forward direction. +func multiDrySwap(p SwapParams, numPool int, swapPath string) (*u256.Uint, *u256.Uint, error) { + return NewMultiSwapProcessor(true, Forward). + processForwardSwap(p, numPool, swapPath) +} + +// multiDrySwapNegative simulates a multi-hop swap in backward direction. +func multiDrySwapNegative(p SwapParams, numPool int, swapPath string) (*u256.Uint, *u256.Uint, error) { + return NewMultiSwapProcessor(true, Backward). + processBackwardSwap(p, numPool, swapPath) +} diff --git a/contract/r/gnoswap/v1/router/swap_single.gno b/contract/r/gnoswap/v1/router/swap_single.gno new file mode 100644 index 0000000..c9dc5f3 --- /dev/null +++ b/contract/r/gnoswap/v1/router/swap_single.gno @@ -0,0 +1,43 @@ +package router + +import ( + u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/v1/common" +) + +var zero = u256.Zero() + +// SwapExecutor defines the interface for executing swaps. +type SwapExecutor interface { + // execute performs the swap operation. + execute(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) +} + +// executeSwap is the common logic for both real and dry swaps. +func executeSwap(executor SwapExecutor, p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { + if p.tokenIn == p.tokenOut { + panic(errSameTokenSwap) + } + + common.MustRegistered(p.tokenIn, p.tokenOut) + + return executor.execute(p) +} + +var ( + _ SwapExecutor = (*RealSwapExecutor)(nil) + _ SwapExecutor = (*DrySwapExecutor)(nil) +) + +// singleSwap executes a swap within a single pool using the provided parameters. +// It processes a token swap within two assets using a specific fee tier and +// automatically sets the recipient to the caller's address. +func singleSwap(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { + return executeSwap(&RealSwapExecutor{}, p) +} + +// singleDrySwap simulates a single-token swap operation without executing it. +// It performs a dry run simulation and does not alter the state. +func singleDrySwap(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { + return executeSwap(&DrySwapExecutor{}, p) +} diff --git a/contract/r/gnoswap/v1/router/type.gno b/contract/r/gnoswap/v1/router/type.gno new file mode 100644 index 0000000..7c062f1 --- /dev/null +++ b/contract/r/gnoswap/v1/router/type.gno @@ -0,0 +1,189 @@ +package router + +import ( + "std" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/p/nt/ufmt" +) + +const ( + rawUnknown = "UNKNOWN" + rawExactIn = "EXACT_IN" + rawExactOut = "EXACT_OUT" +) + +type SwapType string + +const ( + Unknown SwapType = rawUnknown + // ExactIn represents a swap type where the input amount is exact and the output amount may vary. + // Used when a user wants to swap a specific amount of input tokens. + ExactIn SwapType = rawExactIn + + // ExactOut represents a swap type where the output amount is exact and the input amount may vary. + // Used when a user wants to swap a specific amount of output tokens. + ExactOut SwapType = rawExactOut +) + +// trySwapTypeFromStr attempts to convert a string into a SwapType. +// It validates and converts string representations of swap types into their corresponding enum values. +func trySwapTypeFromStr(swapType string) (SwapType, error) { + switch swapType { + case rawExactIn: + return ExactIn, nil + case rawExactOut: + return ExactOut, nil + default: + return "", ufmt.Errorf("unknown swapType: expected ExactIn or ExactOut, got %s", swapType) + } +} + +// String returns the string representation of SwapType. +func (s SwapType) String() string { + switch s { + case ExactIn: + return rawExactIn + case ExactOut: + return rawExactOut + default: + return "" + } +} + +// SingleSwapParams contains parameters for executing a single pool swap. +// It represents the simplest form of swap that occurs within a single liquidity pool. +type SingleSwapParams struct { + tokenIn string // token to spend + tokenOut string // token to receive + fee uint32 // fee of the pool used to swap + withUnwrap bool // whether to unwrap the token + + // Amount specified for the swap: + // - Positive: exact input amount (tokenIn) + // - Negative: exact output amount (tokenOut) + amountSpecified *i256.Int +} + +// TokenIn returns the input token address. +func (p SingleSwapParams) TokenIn() string { return p.tokenIn } + +// TokenOut returns the output token address. +func (p SingleSwapParams) TokenOut() string { return p.tokenOut } + +// Fee returns the pool fee tier. +func (p SingleSwapParams) Fee() uint32 { return p.fee } + +// SwapParams contains parameters for executing a multi-hop swap operation. +type SwapParams struct { + SingleSwapParams + recipient std.Address // address to receive the token + withUnwrap bool // whether to unwrap the token +} + +// TokenIn returns the input token address. +func (p SwapParams) TokenIn() string { return p.tokenIn } + +// TokenOut returns the output token address. +func (p SwapParams) TokenOut() string { return p.tokenOut } + +// Fee returns the pool fee tier. +func (p SwapParams) Fee() uint32 { return p.fee } + +// Recipient returns the recipient address. +func (p SwapParams) Recipient() std.Address { return p.recipient } + +// newSwapParams creates a new SwapParams instance with the provided parameters. +func newSwapParams(tokenIn, tokenOut string, fee uint32, recipient std.Address, amountSpecified *i256.Int, withUnwrap bool) *SwapParams { + return &SwapParams{ + SingleSwapParams: SingleSwapParams{ + tokenIn: tokenIn, + tokenOut: tokenOut, + fee: fee, + amountSpecified: amountSpecified, + }, + withUnwrap: withUnwrap, + recipient: recipient, + } +} + +// SwapResult encapsulates the outcome of a swap operation. +type SwapResult struct { + Routes []string + Quotes []string + AmountIn *u256.Uint + AmountOut *u256.Uint + AmountSpecified *i256.Int + WithUnwrap bool +} + +// SwapParamsI defines the common interface for swap parameters. +type SwapParamsI interface { + TokenIn() string + TokenOut() string + Fee() uint32 +} + +// SwapCallbackData contains the callback data required for swap execution. +// This type is used to pass necessary information during the swap callback process, +// ensuring proper token transfers and pool data updates. +type SwapCallbackData struct { + tokenIn string // token to spend + tokenOut string // token to receive + fee uint32 // fee of the pool used to swap + payer std.Address // address to spend the token + hasNext bool // whether there is a next swap +} + +// newSwapCallbackData creates a new SwapCallbackData from a SwapParamsI. +func newSwapCallbackData(params SwapParamsI, payer std.Address, hasNext bool) SwapCallbackData { + return SwapCallbackData{ + tokenIn: params.TokenIn(), + tokenOut: params.TokenOut(), + fee: params.Fee(), + payer: payer, + hasNext: hasNext, + } +} + +// ExactInParams contains parameters for exact input swaps. +type ExactInParams struct { + BaseSwapParams + AmountIn string + AmountOutMin string +} + +// NewExactInParams creates a new ExactInParams instance. +func NewExactInParams( + baseParams BaseSwapParams, + amountIn string, + amountOutMin string, +) ExactInParams { + return ExactInParams{ + BaseSwapParams: baseParams, + AmountIn: amountIn, + AmountOutMin: amountOutMin, + } +} + +// ExactOutParams contains parameters for exact output swaps. +type ExactOutParams struct { + BaseSwapParams + AmountOut string + AmountInMax string +} + +// NewExactOutParams creates a new ExactOutParams instance. +func NewExactOutParams( + baseParams BaseSwapParams, + amountOut string, + amountInMax string, +) ExactOutParams { + return ExactOutParams{ + BaseSwapParams: baseParams, + AmountOut: amountOut, + AmountInMax: amountInMax, + } +} diff --git a/contract/r/gnoswap/v1/router/utils.gno b/contract/r/gnoswap/v1/router/utils.gno new file mode 100644 index 0000000..20af180 --- /dev/null +++ b/contract/r/gnoswap/v1/router/utils.gno @@ -0,0 +1,167 @@ +package router + +import ( + "bytes" + "strconv" + "strings" + "time" + + "gno.land/p/nt/ufmt" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +var ( + errRouterHalted = "router contract operations are currently disabled" + errTxExpired = "transaction too old, now(%d) > deadline(%d)" +) + +// assertHopsInRange ensures the number of hops is within the valid range of 1-3. +func assertHopsInRange(hops int) { + switch hops { + case 1, 2, 3: + return + default: + panic(errHopsOutOfRange) + } +} + +// assertIsNotPassedDeadline ensures the transaction deadline has not expired. +func assertIsNotPassedDeadline(deadline int64) { + if err := checkDeadline(deadline); err != nil { + errMsg := addDetailToError(errExpired, err.Error()) + panic(errMsg) + } +} + +// getDataForSinglePath extracts token addresses and fee from a single pool path. +func getDataForSinglePath(poolPath string) (token0, token1 string, fee uint32) { + poolPathSplit := strings.Split(poolPath, ":") + if len(poolPathSplit) != 3 { + panic(addDetailToError( + errInvalidPoolPath, + ufmt.Sprintf("len(poolPathSplit) != 3, poolPath: %s", poolPath), + )) + } + + f, err := strconv.Atoi(poolPathSplit[2]) + if err != nil { + panic(ufmt.Sprintf("invalid fee: %s", poolPathSplit[2])) + } + + return poolPathSplit[0], poolPathSplit[1], uint32(f) +} + +// getDataForMultiPath extracts token addresses and fee from a multi-hop path at specified index. +func getDataForMultiPath(possiblePath string, poolIdx int) (token0, token1 string, fee uint32) { + pools := strings.Split(possiblePath, POOL_SEPARATOR) + + switch poolIdx { + case 0: + return getDataForSinglePath(pools[0]) + case 1: + return getDataForSinglePath(pools[1]) + case 2: + return getDataForSinglePath(pools[2]) + default: + return "", "", uint32(0) + } +} + +// i256MinMax returns the absolute values of x and y in min-max order. +func i256MinMax(x, y *i256.Int) (min, max *u256.Uint) { + if x.Lt(y) || x.Eq(y) { + return x.Abs(), y.Abs() + } + return y.Abs(), x.Abs() +} + +// checkDeadline verifies that the transaction deadline has not passed. +func checkDeadline(deadline int64) error { + now := time.Now().Unix() + if now <= deadline { + return nil + } + + return ufmt.Errorf(errTxExpired, now, deadline) +} + +// splitSingleChar splits a string by a single character separator. +// This function is optimized for splitting strings with a single-byte separator +// and is more memory efficient than strings.Split for this use case. +func splitSingleChar(s string, sep byte) []string { + if s == "" { + return []string{""} + } + + result := make([]string, 0, bytes.Count([]byte(s), []byte{sep})+1) + start := 0 + for i := range s { + if s[i] == sep { + result = append(result, s[start:i]) + start = i + 1 + } + } + result = append(result, s[start:]) + return result +} + +// formatUint formats an unsigned integer to string. +func formatUint(v any) string { + switch v := v.(type) { + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + default: + panic(ufmt.Sprintf("invalid type: %T", v)) + } +} + +// formatInt64 formats a signed integer to string. +func formatInt64(v any) string { + switch v := v.(type) { + case int8: + return strconv.FormatInt(int64(v), 10) + case int16: + return strconv.FormatInt(int64(v), 10) + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + default: + panic(ufmt.Sprintf("invalid type %T", v)) + } +} + +// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. +// +// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds +// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. +// +// Parameters: +// - value (*u256.Uint): The unsigned 256-bit integer to be converted. +// +// Returns: +// - int64: The converted value if it falls within the int64 range. +// +// Panics: +// - If the `value` exceeds the range of int64, the function will panic with an error indicating +// the overflow and the original value. +func safeConvertToInt64(value *u256.Uint) int64 { + const INT64_MAX = 9223372036854775807 + const MAX_INT64 = "9223372036854775807" + + res, overflow := value.Uint64WithOverflow() + if overflow || res > uint64(INT64_MAX) { + panic(ufmt.Sprintf( + "amount(%s) overflows int64 range (max %s)", + value.ToString(), + MAX_INT64, + )) + } + return int64(res) +} diff --git a/contract/r/gnoswap/v1/router/wrap_unwrap.gno b/contract/r/gnoswap/v1/router/wrap_unwrap.gno new file mode 100644 index 0000000..85fa364 --- /dev/null +++ b/contract/r/gnoswap/v1/router/wrap_unwrap.gno @@ -0,0 +1,98 @@ +package router + +import ( + "std" + + "gno.land/r/gnoland/wugnot" + + "gno.land/p/nt/ufmt" +) + +const ( + UGNOT_MIN_DEPOSIT_TO_WRAP int64 = 1000 + WUGNOT_PATH = "gno.land/r/gnoland/wugnot" + GNOT = "gnot" + GNOT_DENOM = "ugnot" +) + +var ( + errFailedToWrapZeroUgnot = "cannot wrap 0 ugnot" + errFailedToWrapBelowMin = "amount(%d) < minimum(%d)" +) + +// wrapWithTransfer wraps GNOT into WUGNOT and transfers it to the specified address. +func wrapWithTransfer(toAddress std.Address, amount int64) error { + if amount <= 0 { + return nil + } + + if amount < UGNOT_MIN_DEPOSIT_TO_WRAP { + return makeErrorWithDetails( + errWugnotMinimum, + ufmt.Sprintf("amount(%d) < minimum(%d)", amount, UGNOT_MIN_DEPOSIT_TO_WRAP), + ) + } + + // transfer ugnot from fromAddress to current realm + currentRealmAddr := std.CurrentRealm().Address() + + sentCoins := std.OriginSend() + ugnotSent := sentCoins.AmountOf(GNOT_DENOM) + if ugnotSent != amount { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("user(%s) sent ugnot(%d) amount not equal to rewardAmount(%d)", toAddress.String(), ugnotSent, amount), + ) + } + + // wrap gnot to wugnot + wugnotAddr := std.DerivePkgAddr(WUGNOT_PATH) + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(currentRealmAddr, wugnotAddr, sentCoins) + wugnot.Deposit(cross) + + // if to address is not current realm, transfer wugnot to to address + if toAddress != currentRealmAddr { + wugnot.Transfer(cross, toAddress, amount) + } + + return nil +} + +// unwrapWithTransferFrom transfers WUGNOT from a source address, unwraps it to GNOT, and sends it to the target. +func unwrapWithTransferFrom(fromAddress, toAddress std.Address, wugnotAmount int64) error { + if wugnotAmount == 0 { + return nil + } + + currentRealmAddr := std.CurrentRealm().Address() + if fromAddress != currentRealmAddr { + wugnot.TransferFrom(cross, fromAddress, currentRealmAddr, wugnotAmount) + } + + wugnot.Withdraw(cross, wugnotAmount) + + sendCoins := std.Coins{{Denom: GNOT_DENOM, Amount: wugnotAmount}} + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(currentRealmAddr, toAddress, sendCoins) + + return nil +} + +// unwrapWithTransfer unwraps WUGNOT to GNOT and sends it to the specified address. +func unwrapWithTransfer(toAddress std.Address, amount int64) error { + if amount <= 0 { + return nil + } + + // unwrap wugnot to gnot + wugnot.Withdraw(cross, amount) + + // send gnot to user + sendCoins := std.Coins{{Denom: GNOT_DENOM, Amount: amount}} + banker := std.NewBanker(std.BankerTypeRealmSend) + currentRealmAddr := std.CurrentRealm().Address() + banker.SendCoins(currentRealmAddr, toAddress, sendCoins) + + return nil +} diff --git a/contract/r/gnoswap/v1/staker/README.md b/contract/r/gnoswap/v1/staker/README.md new file mode 100644 index 0000000..8935f93 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/README.md @@ -0,0 +1,182 @@ +# Staker + +Liquidity mining and reward distribution for LP positions. + +## Overview + +Staker manages distribution of internal (GNS emission) and external (user-provided) rewards to staked LP positions, with time-weighted rewards and warmup periods. + +## Configuration + +- **Deposit GNS Amount**: 1,000 GNS for external incentives (default) +- **Minimum Reward Amount**: 1,000 tokens (default) +- **Unstaking Fee**: 1% (default) +- **Pool Tiers**: 1, 2, or 3 (assigned per pool) +- **Warmup Schedule**: 30/50/70/100% over 30/60/90 days +- **External Token Whitelist**: Approved reward tokens + +## Core Features + +### Internal Rewards (GNS Emission) +- Allocated to tiered pools (tiers 1, 2, 3) +- Split across tiers by TierRatio +- Distributed proportionally to in-range liquidity +- Unclaimed rewards go to community pool + +### External Rewards (User Incentives) +- Created for specific pools +- Constant reward per block +- Proportional to staked liquidity +- Unclaimed rewards returned to creator + +### Warmup Periods +Every staked position progresses through warmup periods: +- 0-30 days: 30% rewards (70% to community/creator) +- 30-60 days: 50% rewards (50% to community/creator) +- 60-90 days: 70% rewards (30% to community/creator) +- 90+ days: 100% rewards + +## Key Functions + +### `StakeToken` +Stakes LP position NFT to earn rewards. + +### `UnStakeToken` +Unstakes position and collects all rewards. + +### `CollectReward` +Collects accumulated rewards without unstaking. + +### `MintAndStake` +Mints new position and stakes in single transaction. + +### `CreateExternalIncentive` +Creates external reward program for specific pool. + +### `EndExternalIncentive` +Ends incentive program and returns unused rewards. + +## Reward Calculation Logic + +### Tier Ratio Distribution + +Emission split across tiers based on active pools: + +``` +If only tier 1 has pools: [100%, 0%, 0%] +If tiers 1 & 3 have pools: [80%, 0%, 20%] +If tiers 1 & 2 have pools: [70%, 30%, 0%] +If all tiers have pools: [50%, 30%, 20%] +``` + +Mathematical representation: +```math +TierRatio(t) = + [1, 0, 0] if Count(2) = 0 ∧ Count(3) = 0 + [0.8, 0, 0.2] if Count(2) = 0 + [0.7, 0.3, 0] if Count(3) = 0 + [0.5, 0.3, 0.2] otherwise +``` + +### Pool Reward Formula + +```math +poolReward(pool) = (emission × TierRatio[tier(pool)]) / Count(tier(pool)) +``` + +Where emission is calculated as: +```math +emission = GNSEmissionPerSecond × (avgMsPerBlock/1000) × StakerEmissionRatio +``` + +### Position Reward Calculation + +The reward for each position is calculated through: + +1. **Cache pool rewards** up to current block +2. **Retrieve position state** from deposit records +3. **Calculate internal rewards** if pool is tiered +4. **Calculate external rewards** for active incentives +5. **Apply warmup penalties** based on stake duration + +Mathematical formula for total reward ratio: +```math +TotalRewardRatio(s,e) = Σ[i=0 to m-1] ΔRaw(αᵢ, βᵢ) × rᵢ + +where: + αᵢ = max(s, Hᵢ₋₁) + βᵢ = min(e, Hᵢ) + +ΔRaw(a, b) = CalcRaw(b) - CalcRaw(a) + +CalcRaw(h) = + L(h) - U(h) if tick(h) < ℓ + U(h) - L(h) if tick(h) ≥ u + G(h) - (L(h) + U(h)) otherwise + +where: + L(h) = tickLower.OutsideAccumulation(h) + U(h) = tickUpper.OutsideAccumulation(h) + G(h) = globalRewardRatioAccumulation(h) + ℓ = tickLower.id + u = tickUpper.id +``` + +Final position reward: +```math +finalReward = TotalRewardRatio × poolReward × positionLiquidity + = ∫[s to e] (poolReward × positionLiquidity) / TotalStakedLiquidity(h) dh +``` + +### Tick Cross Hook + +When price crosses an initialized tick with staked positions: + +1. **Updates staked liquidity** - Adjusts total staked liquidity +2. **Updates reward accumulation** - Recalculates `globalRewardRatioAccumulation` +3. **Manages unclaimable periods** - Starts/ends periods with no in-range liquidity +4. **Updates tick accumulation** - Adjusts `CurrentOutsideAccumulation` + +The `globalRewardRatioAccumulation` tracks the integral: +```math +globalRewardRatioAccumulation = ∫ 1/TotalStakedLiquidity(h) dh +``` + +This integral is only computed when `TotalStakedLiquidity(h) ≠ 0`, enabling precise reward calculation even as liquidity changes. + +### Reward State Tracking + +The system maintains: +- **Global accumulation**: Tracks reward ratio across all positions +- **Tick accumulation**: Tracks rewards "outside" each tick +- **Position state**: Individual reward calculation parameters + +## Usage + +```go +// Stake existing position +StakeToken(123, "g1referrer...") + +// Create external incentive +CreateExternalIncentive( + "gno.land/r/demo/bar:gno.land/r/demo/baz:3000", + "gno.land/r/demo/reward", + "1000000000", // 1000 tokens + startTime, + endTime, +) + +// Collect rewards without unstaking +CollectReward(123) + +// Unstake and collect all rewards +UnStakeToken(123) +``` + +## Security + +- Positions locked during staking +- External incentives require GNS deposit +- Warmup periods prevent gaming +- Unclaimed rewards properly redirected +- Hook integration ensures accurate tracking \ No newline at end of file diff --git a/contract/r/gnoswap/v1/staker/api.gno b/contract/r/gnoswap/v1/staker/api.gno new file mode 100644 index 0000000..232d1f5 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/api.gno @@ -0,0 +1,310 @@ +package staker + +import ( + "std" + "time" + + "gno.land/p/onbloc/json" + "gno.land/p/nt/ufmt" +) + +// ApiGetRewardTokensByPoolPath returns all reward tokens for a specific pool. +func ApiGetRewardTokensByPoolPath(targetPoolPath string) string { + rewardTokens := []RewardToken{} + + pool, ok := pools.Get(targetPoolPath) + if !ok { + return "" + } + + thisPoolRewardTokens := []string{} + + // HANDLE INTERNAL + if poolTier.IsInternallyIncentivizedPool(pool.poolPath) { + thisPoolRewardTokens = append(thisPoolRewardTokens, GNS_PATH) + } + + // HANDLE EXTERNAL + if pool.IsExternallyIncentivizedPool() { + pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { + ictv := value.(*ExternalIncentive) + if ictv.RewardToken() == "" { + return false + } + thisPoolRewardTokens = append(thisPoolRewardTokens, ictv.RewardToken()) + return false + }) + } + + rt := newRewardToken(pool.poolPath, thisPoolRewardTokens) + rewardTokens = append(rewardTokens, rt) + + rsps := make([]JsonResponse, len(rewardTokens)) + for i := range rewardTokens { + rsps[i] = rewardTokens[i] + } + + return makeApiResponse(rsps) +} + +// ApiGetExternalIncentives returns all external incentives across all pools. +func ApiGetExternalIncentives() string { + apiExternalIncentives := []ApiExternalIncentive{} + + pools.tree.Iterate("", "", func(key string, value any) bool { + pool := value.(*Pool) + pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { + ictv := value.(*ExternalIncentive) + externalIctv := newApiExternalIncentive(ictv) + apiExternalIncentives = append(apiExternalIncentives, externalIctv) + return false + }) + return false + }) + + rsps := make([]JsonResponse, len(apiExternalIncentives)) + for i := range apiExternalIncentives { + rsps[i] = apiExternalIncentives[i] + } + + return makeApiResponse(rsps) +} + +// ApiGetExternalIncentiveById returns a specific external incentive by pool path and incentive ID. +func ApiGetExternalIncentiveById(poolPath, incentiveId string) string { + apiExternalIncentives := []ApiExternalIncentive{} + + pool, ok := pools.Get(poolPath) + if !ok { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("pool(%s) not found", poolPath), + )) + } + + incentive, exist := pool.incentives.GetByIncentiveId(incentiveId) + if !exist { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("incentive(%s) not found", incentiveId), + )) + } + + externalictv := newApiExternalIncentive(incentive) + apiExternalIncentives = append(apiExternalIncentives, externalictv) + + rsps := make([]JsonResponse, len(apiExternalIncentives)) + for i := range apiExternalIncentives { + rsps[i] = apiExternalIncentives[i] + } + + return makeApiResponse(rsps) +} + +// ApiGetExternalIncentivesByPoolPath returns all external incentives for a specific pool. +func ApiGetExternalIncentivesByPoolPath(targetPoolPath string) string { + apiExternalIncentives := []ApiExternalIncentive{} + + pool, ok := pools.Get(targetPoolPath) + if !ok { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("pool(%s) not found", targetPoolPath), + )) + } + + pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { + incentive, ok := value.(*ExternalIncentive) + if !ok { + panic("failed to cast value to *ExternalIncentive") + } + if incentive.targetPoolPath != targetPoolPath { + return false + } + + externalIctv := newApiExternalIncentive(incentive) + apiExternalIncentives = append(apiExternalIncentives, externalIctv) + + return false + }) + + rsps := make([]JsonResponse, len(apiExternalIncentives)) + for i := range apiExternalIncentives { + rsps[i] = apiExternalIncentives[i] + } + + return makeApiResponse(rsps) +} + +// ApiGetInternalIncentives returns all internal incentives across all pools. +func ApiGetInternalIncentives() string { + apiInternalIncentives := []ApiInternalIncentive{} + + poolTier.membership.Iterate("", "", func(key string, value any) bool { + poolPath := key + internalTier, ok := value.(uint64) + if !ok { + panic(ufmt.Sprintf("failed to cast value to uint64: %T", value)) + } + internalIctv := newApiInternalIncentive(poolPath, internalTier) + apiInternalIncentives = append(apiInternalIncentives, internalIctv) + return false + }) + + rsps := make([]JsonResponse, len(apiInternalIncentives)) + for i := range apiInternalIncentives { + rsps[i] = apiInternalIncentives[i] + } + + return makeApiResponse(rsps) +} + +// ApiGetInternalIncentivesByPoolPath returns internal incentives for a specific pool. +func ApiGetInternalIncentivesByPoolPath(targetPoolPath string) string { + apiInternalIncentives := []ApiInternalIncentive{} + + tier := poolTier.CurrentTier(targetPoolPath) + if tier == 0 { + return "" + } + + internalIctv := newApiInternalIncentive(targetPoolPath, tier) + apiInternalIncentives = append(apiInternalIncentives, internalIctv) + + rsps := make([]JsonResponse, len(apiInternalIncentives)) + for i := range apiInternalIncentives { + rsps[i] = apiInternalIncentives[i] + } + + return makeApiResponse(rsps) +} + +// ApiGetInternalIncentivesByTiers returns all internal incentives for a specific tier. +func ApiGetInternalIncentivesByTiers(targetTier uint64) string { + apiInternalIncentives := []ApiInternalIncentive{} + + poolTier.membership.Iterate("", "", func(key string, value any) bool { + poolPath := key + internalTier := value.(uint64) + if internalTier != targetTier { + return false + } + + internalIctv := newApiInternalIncentive(poolPath, internalTier) + apiInternalIncentives = append(apiInternalIncentives, internalIctv) + + return false + }) + + rsps := make([]JsonResponse, len(apiInternalIncentives)) + for i := range apiInternalIncentives { + rsps[i] = apiInternalIncentives[i] + } + + return makeApiResponse(rsps) +} + +// makeRewardTokensArray creates a JSON array of reward tokens. +func makeRewardTokensArray(rewardsTokenList []string) []*json.Node { + rewardsTokenArray := make([]*json.Node, len(rewardsTokenList)) + for i, rewardToken := range rewardsTokenList { + rewardsTokenArray[i] = json.StringNode("", rewardToken) + } + return rewardsTokenArray +} + +// calculateInternalRewardPerSecondByPoolPath calculates the internal reward per second for a pool. +func calculateInternalRewardPerSecondByPoolPath(poolPath string) string { + reward := poolTier.CurrentRewardPerPool(poolPath) + return ufmt.Sprintf("%d", reward) +} + +// ResponseQueryBase contains basic information about a query response. +type ResponseQueryBase struct { + Height int64 `json:"height"` // The block height at the time of the query + Timestamp int64 `json:"timestamp"` // The timestamp at the time of the query +} + +// ResponseApiGetRewards represents the API response for getting rewards. +type ResponseApiGetRewards struct { + Stat ResponseQueryBase `json:"stat"` // Basic query information + Response []LpTokenReward `json:"response"` // A slice of LpTokenReward structs +} + +// ResponseApiGetRewardByLpTokenId represents the API response for getting rewards for a specific LP token. +type ResponseApiGetRewardByLpTokenId struct { + Stat ResponseQueryBase `json:"stat"` // Basic query information + Response LpTokenReward `json:"response"` // The LpTokenReward for the specified LP token +} + +// ApiGetRewardsByLpTokenId returns all rewards for a specific LP token ID. +func ApiGetRewardsByLpTokenId(targetLpTokenId uint64) string { + deposit := deposits.get(targetLpTokenId) + + reward := calcPositionReward(std.ChainHeight(), time.Now().Unix(), targetLpTokenId) + + rewards := []ApiReward{} + + if reward.Internal > 0 { + rewards = append(rewards, ApiReward{ + IncentiveType: "INTERNAL", + IncentiveId: "", + TargetPoolPath: deposit.targetPoolPath, + RewardTokenPath: GNS_PATH, + RewardTokenAmount: reward.Internal, + StakeTimestamp: deposit.stakeTimestamp, + StakeTime: deposit.stakeTime, + IncentiveStart: deposit.stakeTimestamp, + }) + } + + for incentiveId, externalReward := range reward.External { + if externalReward == 0 { + continue + } + incentive := externalIncentives.get(incentiveId) + rewards = append(rewards, ApiReward{ + IncentiveType: "EXTERNAL", + IncentiveId: incentiveId, + TargetPoolPath: incentive.targetPoolPath, + RewardTokenPath: incentive.rewardToken, + RewardTokenAmount: externalReward, + StakeTimestamp: deposit.stakeTimestamp, + StakeTime: deposit.stakeTime, + IncentiveStart: incentive.startTimestamp, + }) + } + + rsps := make([]JsonResponse, len(rewards)) + for i := range rewards { + rsps[i] = rewards[i] + } + + return makeApiResponse(rsps) +} + +// ApiGetStakesByLpTokenId returns stake information for a specific LP token ID. +func ApiGetStakesByLpTokenId(targetLpTokenId uint64) string { + stakes := []ApiStake{} + + deposit := deposits.get(targetLpTokenId) + stk := newApiStake(targetLpTokenId, deposit) + stakes = append(stakes, stk) + + rsps := make([]JsonResponse, len(stakes)) + for i := range stakes { + rsps[i] = stakes[i] + } + + return makeApiResponse(rsps) +} + +// IsStaked checks if a position ID is currently staked. +func IsStaked(positionId uint64) bool { + return deposits.Has(positionId) +} + +// formatInt formats an int64 value to string. +func formatInt(value int64) string { + return ufmt.Sprintf("%d", value) +} diff --git a/contract/r/gnoswap/v1/staker/assert.gno b/contract/r/gnoswap/v1/staker/assert.gno new file mode 100644 index 0000000..57188ee --- /dev/null +++ b/contract/r/gnoswap/v1/staker/assert.gno @@ -0,0 +1,196 @@ +package staker + +import ( + "std" + "strconv" + "strings" + "time" + + "gno.land/p/nt/ufmt" + "gno.land/r/gnoswap/v1/gnft" + pl "gno.land/r/gnoswap/v1/pool" +) + +// assertIsValidAmount ensures the amount is non-negative. +func assertIsValidAmount(amount int64) { + if amount < 0 { + panic(makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("amount(%d) must be positive", amount), + )) + } +} + +// assertIsValidRewardAmountFormat ensures the reward amount string is formatted as "tokenPath:amount". +func assertIsValidRewardAmountFormat(rewardAmountStr string) { + parts := strings.SplitN(rewardAmountStr, ":", 2) + if len(parts) != 2 { + panic(makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("invalid format for SetTokenMinimumRewardAmount params: expected 'tokenPath:amount', got '%s'", rewardAmountStr), + )) + } +} + +// assertIsDepositor ensures the caller is the owner of the deposit. +func assertIsDepositor(caller std.Address, positionId uint64) { + deposit := deposits.get(positionId) + if deposit == nil { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("positionId(%d) not found", positionId), + )) + } + + if caller != deposit.owner { + panic(makeErrorWithDetails( + errNoPermission, + ufmt.Sprintf("caller(%s) is not depositor(%s)", caller.String(), deposit.owner.String()), + )) + } +} + +// assertIsNotStaked ensures the position is not already staked. +func assertIsNotStaked(positionId uint64) { + if deposits.Has(positionId) { + panic(makeErrorWithDetails( + errAlreadyStaked, + ufmt.Sprintf("positionId(%d) already staked", positionId), + )) + } +} + +// assertIsPositionOwner ensures the caller owns the position NFT. +func assertIsPositionOwner(positionId uint64, caller std.Address) { + owner := gnft.MustOwnerOf(positionIdFrom(positionId)) + if owner != caller { + panic(makeErrorWithDetails( + errNoPermission, + ufmt.Sprintf("caller(%s) is not owner(%s)", caller.String(), owner.String()), + )) + } +} + +// assertIsPoolExists ensures the pool exists. +func assertIsPoolExists(poolPath string) { + if !pl.ExistsPoolPath(poolPath) { + panic(makeErrorWithDetails( + errInvalidPoolPath, + ufmt.Sprintf("pool(%s) does not exist", poolPath), + )) + } +} + +// assertIsValidPoolTier ensures the tier is within valid range. +func assertIsValidPoolTier(tier uint64) { + if tier >= AllTierCount { + panic(makeErrorWithDetails( + errInvalidPoolTier, + ufmt.Sprintf("tier(%d) must be less than %d", tier, AllTierCount), + )) + } +} + +// assertIsGreaterThanMinimumRewardAmount ensures the reward amount meets minimum requirements. +func assertIsGreaterThanMinimumRewardAmount(rewardToken string, rewardAmount int64) { + minReward := GetMinimumRewardAmountForToken(rewardToken) + if rewardAmount < minReward { + panic(makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("rewardAmount(%d) is less than minimum required amount(%d)", rewardAmount, minReward), + )) + } +} + +// assertIsAllowedForExternalReward ensures the token is allowed for external rewards. +func assertIsAllowedForExternalReward(poolPath, tokenPath string) { + token0, token1, _ := poolPathDivide(poolPath) + + if tokenPath == token0 || tokenPath == token1 { + return + } + + allowed := contains(allowedTokens, tokenPath) + if allowed { + return + } + + panic(makeErrorWithDetails( + errNotAllowedForExternalReward, + ufmt.Sprintf("tokenPath(%s) is not allowed for external reward for poolPath(%s)", tokenPath, poolPath), + )) +} + +// assertIsValidFeeRate ensures the fee rate is within valid range (0-10000 basis points). +func assertIsValidFeeRate(fee int64) { + if fee < 0 || fee > 10000 { + panic(makeErrorWithDetails( + errInvalidUnstakingFee, + ufmt.Sprintf("fee(%d) must be in range 0 ~ 10000", fee), + )) + } +} + +// assertIsValidIncentiveStartTime ensures the incentive starts at midnight of a future date. +func assertIsValidIncentiveStartTime(startTimestamp int64) { + // must be in seconds format, not milliseconds + // REF: https://stackoverflow.com/a/23982005 + numStr := strconv.Itoa(int(startTimestamp)) + + if len(numStr) >= 13 { + panic(makeErrorWithDetails( + errInvalidIncentiveStartTime, + ufmt.Sprintf("startTimestamp(%d) must be in seconds format, not milliseconds", startTimestamp), + )) + } + + // must be at least +1 day midnight + tomorrowMidnight := time.Now().AddDate(0, 0, 1).Truncate(24 * time.Hour).Unix() + if startTimestamp < tomorrowMidnight { + panic(makeErrorWithDetails( + errInvalidIncentiveStartTime, + ufmt.Sprintf("startTimestamp(%d) must be at least +1 day midnight(%d)", startTimestamp, tomorrowMidnight), + )) + } + + // must be midnight of the day + startTime := time.Unix(startTimestamp, 0) + if !isMidnight(startTime) { + panic(makeErrorWithDetails( + errInvalidIncentiveStartTime, + ufmt.Sprintf("startTime(%d = %s) must be midnight of the day", startTimestamp, startTime.String()), + )) + } +} + +// assertIsValidIncentiveEndTime ensures the end timestamp is within valid epoch range. +func assertIsValidIncentiveEndTime(endTimestamp int64) { + if endTimestamp >= MAX_UNIX_EPOCH_TIME { + panic(makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("endTimestamp(%d) cannot be later than 253402300799 (9999-12-31 23:59:59)", endTimestamp), + )) + } +} + +// assertIsValidIncentiveDuration ensures the duration is 90, 180, or 365 days. +func assertIsValidIncentiveDuration(externalDuration int64) { + switch externalDuration { + case TIMESTAMP_90DAYS, TIMESTAMP_180DAYS, TIMESTAMP_365DAYS: + return + } + + panic(makeErrorWithDetails( + errInvalidIncentiveDuration, + ufmt.Sprintf("externalDuration(%d) must be 90, 180, 365 days", externalDuration), + )) +} + +// isMidnight checks if a time represents midnight (00:00:00). +func isMidnight(startTime time.Time) bool { + hour := startTime.Hour() + minute := startTime.Minute() + second := startTime.Second() + + return hour == 0 && minute == 0 && second == 0 +} diff --git a/contract/r/gnoswap/v1/staker/calculate_pool_position_reward.gno b/contract/r/gnoswap/v1/staker/calculate_pool_position_reward.gno new file mode 100644 index 0000000..dc10f54 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/calculate_pool_position_reward.gno @@ -0,0 +1,153 @@ +package staker + +import ( + u256 "gno.land/p/gnoswap/uint256" +) + +// Q96 +var _q96 = u256.MustFromDecimal("79228162514264337593543950336") + +func isAbleToCalculateEmissionReward(prev int64, current int64) bool { + if prev >= current { + return false + } + return true +} + +// Reward is a struct for storing reward for a position. +// Internal reward is the GNS reward, external reward is the reward for other incentives. +// Penalties are the amount that is deducted from the reward due to the position's warmup. +type Reward struct { + Internal int64 + InternalPenalty int64 + External map[string]int64 // Incentive ID -> TokenAmount + ExternalPenalty map[string]int64 // Incentive ID -> TokenAmount +} + +// calculate total position rewards and penalties +func calcPositionReward(currentHeight, currentTimestamp int64, positionId uint64) Reward { + rewards := calculatePositionReward(CalcPositionRewardParam{ + CurrentHeight: currentHeight, + CurrentTime: currentTimestamp, + Deposits: deposits, + Pools: pools, + PoolTier: poolTier, + PositionId: positionId, + }) + + internal := int64(0) + for _, reward := range rewards { + internal += reward.Internal + } + + internalPenalty := int64(0) + for _, reward := range rewards { + internalPenalty += reward.InternalPenalty + } + + externalReward := make(map[string]int64) + for _, reward := range rewards { + if reward.External != nil { + for incentive, reward := range reward.External { + externalReward[incentive] += reward + } + } + } + + externalPenalty := make(map[string]int64) + for _, reward := range rewards { + if reward.ExternalPenalty != nil { + for incentive, penalty := range reward.ExternalPenalty { + externalPenalty[incentive] += penalty + } + } + } + + return Reward{ + Internal: internal, + InternalPenalty: internalPenalty, + External: externalReward, + ExternalPenalty: externalPenalty, + } +} + +// CalcPositionRewardParam is a struct for calculating position reward +type CalcPositionRewardParam struct { + // Environmental variables + CurrentHeight int64 + CurrentTime int64 + Deposits *Deposits + Pools *Pools + PoolTier *PoolTier + + // Position variables + PositionId uint64 +} + +func calculatePositionReward(param CalcPositionRewardParam) []Reward { + // cache per-pool rewards in the internal incentive(tiers) + param.PoolTier.cacheReward(param.CurrentHeight, param.CurrentTime, param.Pools) + + deposit := param.Deposits.get(param.PositionId) + poolPath := deposit.targetPoolPath + + pool, ok := param.Pools.Get(poolPath) + if !ok { + pool = NewPool(poolPath, param.CurrentTime) + param.Pools.set(poolPath, pool) + } + + lastCollectTime := deposit.lastCollectTime + + // Initializes reward/penalty arrays for rewards and penalties for each warmup + internalRewards := make([]int64, len(deposit.warmups)) + internalPenalties := make([]int64, len(deposit.warmups)) + externalRewards := make([]map[string]int64, len(deposit.warmups)) + externalPenalties := make([]map[string]int64, len(deposit.warmups)) + + if param.PoolTier.CurrentTier(poolPath) != 0 { + // Internal incentivized pool. + // Calculate reward for each warmup + internalRewards, internalPenalties = pool.RewardStateOf(deposit).calculateInternalReward(lastCollectTime, param.CurrentTime) + } + + // All active incentives + allIncentives := pool.incentives.GetAllInTimestamps(lastCollectTime, param.CurrentTime) + + for i := range externalRewards { + externalRewards[i] = make(map[string]int64) + externalPenalties[i] = make(map[string]int64) + } + + for incentiveId, incentive := range allIncentives { + // External incentivized pool. + // Calculate reward for each warmup + externalReward, externalPenalty := pool.RewardStateOf(deposit).calculateExternalReward(lastCollectTime, param.CurrentTime, incentive) + + for i := range externalReward { + externalRewards[i][incentiveId] = externalReward[i] + externalPenalties[i][incentiveId] = externalPenalty[i] + } + } + + rewards := make([]Reward, len(internalRewards)) + for i := range internalRewards { + rewards[i] = Reward{ + Internal: internalRewards[i], + InternalPenalty: internalPenalties[i], + External: externalRewards[i], + ExternalPenalty: externalPenalties[i], + } + } + + return rewards +} + +// calculates internal unclaimable reward for the pool +func processUnClaimableReward(poolPath string, endTimestamp int64) int64 { + pool, ok := pools.Get(poolPath) + if !ok { + return 0 + } + return pool.processUnclaimableReward(poolTier, endTimestamp) +} diff --git a/contract/r/gnoswap/v1/staker/consts.gno b/contract/r/gnoswap/v1/staker/consts.gno new file mode 100644 index 0000000..1c31cef --- /dev/null +++ b/contract/r/gnoswap/v1/staker/consts.gno @@ -0,0 +1,13 @@ +package staker + +// WRAP & UNWRAP +const ( + GNOT string = "gnot" + GNOT_DENOM string = "ugnot" + + // ref: https://github.com/gnolang/gno/blob/81a88a2976ba9f2f9127ebbe7fb7d1e1f7fa4bd4/examples/gno.land/r/gnoland/wugnot/wugnot.gno#L19 + UGNOT_MIN_DEPOSIT_TO_WRAP int64 = 1000 + + GNS_PATH string = "gno.land/r/gnoswap/gns" + WUGNOT_PATH string = "gno.land/r/gnoland/wugnot" +) diff --git a/contract/r/gnoswap/v1/staker/counter.gno b/contract/r/gnoswap/v1/staker/counter.gno new file mode 100644 index 0000000..998d7e1 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/counter.gno @@ -0,0 +1,21 @@ +package staker + +type Counter struct { + id int64 +} + +func NewCounter() *Counter { + return &Counter{ + id: 0, + } +} + +func (c *Counter) next() int64 { + c.id++ + + return c.id +} + +func (c *Counter) Get() int64 { + return c.id +} diff --git a/contract/r/gnoswap/v1/staker/doc.gno b/contract/r/gnoswap/v1/staker/doc.gno new file mode 100644 index 0000000..15fe6bd --- /dev/null +++ b/contract/r/gnoswap/v1/staker/doc.gno @@ -0,0 +1,9 @@ +// Package staker manages liquidity mining rewards for GnoSwap positions. +// +// The staker distributes GNS emissions and external incentives to liquidity +// providers based on their position size, price range, and staking duration. +// It supports both internal GNS rewards and external token incentives. +// +// Rewards are calculated per-tick and accumulate over time, with automatic +// compounding and fee collection integration. +package staker diff --git a/contract/r/gnoswap/v1/staker/errors.gno b/contract/r/gnoswap/v1/staker/errors.gno new file mode 100644 index 0000000..ae05b89 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/errors.gno @@ -0,0 +1,48 @@ +package staker + +import ( + "errors" + + "gno.land/p/nt/ufmt" +) + +var ( + errNoPermission = errors.New("[GNOSWAP-STAKER-001] caller has no permission") + errPoolNotFound = errors.New("[GNOSWAP-STAKER-002] pool not found") + errAlreadyRegistered = errors.New("[GNOSWAP-STAKER-003] already registered token") + errInsufficientReward = errors.New("[GNOSWAP-STAKER-004] insufficient reward") + errWrapUnwrap = errors.New("[GNOSWAP-STAKER-005] wrap, unwrap failed") + errWugnotMinimum = errors.New("[GNOSWAP-STAKER-006] can not wrapless than minimum amount") + errInvalidInput = errors.New("[GNOSWAP-STAKER-007] invalid input data") + errInvalidUnstakingFee = errors.New("[GNOSWAP-STAKER-008] invalid unstaking fee") + errAlreadyStaked = errors.New("[GNOSWAP-STAKER-009] already staked position") + errNonIncentivizedPool = errors.New("[GNOSWAP-STAKER-010] pool is not incentivized") + errOutOfRange = errors.New("[GNOSWAP-STAKER-011] out of range") + errCannotEndIncentive = errors.New("[GNOSWAP-STAKER-012] can not end incentive") + errInvalidIncentiveStartTime = errors.New("[GNOSWAP-STAKER-013] invalid incentive start time") + errInvalidIncentiveEndTime = errors.New("[GNOSWAP-STAKER-014] invalid incentive end time") + errCannotUseForExternalReward = errors.New("[GNOSWAP-STAKER-015] can not use for external reward") + errMinTier = errors.New("[GNOSWAP-STAKER-016] emission minimum tier is 1") + errDefaultPoolTier1 = errors.New("[GNOSWAP-STAKER-017] can not delete default pool tier 1") + errDefaultExternalToken = errors.New("[GNOSWAP-STAKER-018] can not delete default external token") + errInvalidPoolPath = errors.New("[GNOSWAP-STAKER-019] invalid pool path") + errInvalidPoolTier = errors.New("[GNOSWAP-STAKER-020] invalid pool tier") + errAlreadyHasTier = errors.New("[GNOSWAP-STAKER-021] pool already has emission target") + errDataNotFound = errors.New("[GNOSWAP-STAKER-022] requested data not found") + errCalculationError = errors.New("[GNOSWAP-STAKER-023] unexpected calculation error") + errZeroLiquidity = errors.New("[GNOSWAP-STAKER-024] zero liquidity") + errInvalidIncentiveDuration = errors.New("[GNOSWAP-STAKER-025] invalid incentive duration") + errNotAllowedForExternalReward = errors.New("[GNOSWAP-STAKER-026] not allowed for external reward") + errInvalidWarmUpPercent = errors.New("[GNOSWAP-STAKER-027] invalid warm-up duration") + errInvalidTickCross = errors.New("[GNOSWAP-STAKER-028] invalid tick cross") + errIncentiveAlreadyExists = errors.New("[GNOSWAP-STAKER-029] incentive already exists") + errIncentiveNotFound = errors.New("[GNOSWAP-STAKER-030] incentive not found") + errWarmUpAmountNotFound = errors.New("[GNOSWAP-STAKER-031] warm-up amount not found") + errOverflow = errors.New("[GNOSWAP-STAKER-032] overflow") + errUnauthorized = errors.New("[GNOSWAP-STAKER-033] unauthorized access") + errAddExistingToken = errors.New("[GNOSWAP-STAKER-034] can not add existing token") +) + +func makeErrorWithDetails(err error, details string) error { + return ufmt.Errorf("%s || %s", err.Error(), details) +} diff --git a/contract/r/gnoswap/v1/staker/external_deposit_fee.gno b/contract/r/gnoswap/v1/staker/external_deposit_fee.gno new file mode 100644 index 0000000..9550634 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/external_deposit_fee.gno @@ -0,0 +1,173 @@ +package staker + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" +) + +var ( + // depositGnsAmount is the amount of GNS required to create an external incentive. + // This parameter can be modified through governance. + depositGnsAmount = int64(1_000_000_000) // 1_000 GNS + + // minimumRewardAmount is the default minimum reward amount for external incentives. + // This parameter can be modified through governance. + minimumRewardAmount = int64(1_000_000_000) // Default 1000 (ugnot equivalent for GNS) +) + +// tokenSpecificMinimumRewards stores minimum reward amounts for specific tokens. +// Key: tokenPath (string), Value: minimumAmount (int64) +var tokenSpecificMinimumRewards = avl.NewTree() + +// GetDepositGnsAmount returns the current deposit amount in GNS. +func GetDepositGnsAmount() int64 { + return depositGnsAmount +} + +// GetMinimumRewardAmount returns the default minimum reward amount required for external incentives. +func GetMinimumRewardAmount() int64 { + return minimumRewardAmount +} + +// GetMinimumRewardAmountForToken returns the minimum reward amount for a specific token. +func GetMinimumRewardAmountForToken(tokenPath string) int64 { + amountI, found := tokenSpecificMinimumRewards.Get(tokenPath) + if found { + return amountI.(int64) + } + // Fallback to default if not found + return GetMinimumRewardAmount() +} + +// GetSpecificTokenMinimumRewardAmount returns the explicitly set minimum reward amount for a token. +func GetSpecificTokenMinimumRewardAmount(tokenPath string) (int64, bool) { + amountI, found := tokenSpecificMinimumRewards.Get(tokenPath) + if !found { + return 0, false + } + v, ok := amountI.(int64) + if !ok { + panic("failed to cast amount to int64") + } + return v, true +} + +// SetDepositGnsAmount sets the GNS deposit amount required for creating external incentives. +// Only admin or governance can call this function. +func SetDepositGnsAmount(cur realm, amount int64) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsValidAmount(amount) + + prevDepositGnsAmount := getDepositGnsAmount() + setDepositGnsAmount(amount) + + previousRealm := std.PreviousRealm() + std.Emit( + "SetDepositGnsAmount", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "prevAmount", formatInt(prevDepositGnsAmount), + "newAmount", formatInt(amount), + ) +} + +// SetMinimumRewardAmount sets the default minimum reward amount for external incentives. +// Only admin or governance can call this function. +func SetMinimumRewardAmount(cur realm, amount int64) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsValidAmount(amount) + + prevMinimumRewardAmount := getMinimumRewardAmount() + setMinimumRewardAmount(amount) + + previousRealm := std.PreviousRealm() + std.Emit( + "SetMinimumRewardAmount", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "prevAmount", formatInt(prevMinimumRewardAmount), + "newAmount", formatInt(amount), + ) +} + +// SetTokenMinimumRewardAmount sets the minimum reward amount for a specific token. +// Only admin or governance can call this function. +func SetTokenMinimumRewardAmount(cur realm, paramsStr string) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsValidRewardAmountFormat(paramsStr) + + // Parse the paramsStr + parts := strings.SplitN(paramsStr, ":", 2) + tokenPath := parts[0] + amountStr := parts[1] + amount64, err := strconv.ParseInt(amountStr, 10, 64) + if err != nil { + panic(makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("invalid amount format in params '%s': %v", paramsStr, err), + )) + } + + prevAmount, found := GetSpecificTokenMinimumRewardAmount(tokenPath) + + // If amount is 0, remove the entry; otherwise, set it. + if amount64 == 0 { + // Only attempt removal if an entry actually existed + if found { + tokenSpecificMinimumRewards.Remove(tokenPath) + } + } else { + tokenSpecificMinimumRewards.Set(tokenPath, amount64) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "SetTokenMinimumRewardAmount", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "paramsStr", paramsStr, // Log the raw input string + "tokenPath", tokenPath, + "prevAmountFound", formatBool(found), + "prevAmount", formatInt(prevAmount), // Will be 0 if !found + "newAmount", formatInt(amount64), + ) +} + +// setDepositGnsAmount internally updates the deposit GNS amount. +func setDepositGnsAmount(amount int64) { + depositGnsAmount = amount +} + +// setMinimumRewardAmount internally updates the minimum reward amount. +func setMinimumRewardAmount(amount int64) { + minimumRewardAmount = amount +} + +// getDepositGnsAmount internally retrieves the deposit GNS amount. +func getDepositGnsAmount() int64 { + return depositGnsAmount +} + +// getMinimumRewardAmount internally retrieves the minimum reward amount. +func getMinimumRewardAmount() int64 { + return minimumRewardAmount +} diff --git a/contract/r/gnoswap/v1/staker/external_incentive.gno b/contract/r/gnoswap/v1/staker/external_incentive.gno new file mode 100644 index 0000000..00df855 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/external_incentive.gno @@ -0,0 +1,224 @@ +package staker + +import ( + "std" + "time" + + "gno.land/p/nt/ufmt" + prbac "gno.land/p/gnoswap/rbac" + + "gno.land/r/gnoswap/access" + en "gno.land/r/gnoswap/emission" + "gno.land/r/gnoswap/gns" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" +) + +// CreateExternalIncentive creates an external incentive program for a pool. +// +// Parameters: +// - targetPoolPath: pool to incentivize +// - rewardToken: reward token path +// - rewardAmount: total reward amount +// - startTimestamp, endTimestamp: incentive period +// +// Only callable by users. +func CreateExternalIncentive( + cur realm, + targetPoolPath string, + rewardToken string, // token path should be registered + rewardAmount int64, + startTimestamp int64, + endTimestamp int64, +) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsUser(std.PreviousRealm()) + + assertIsPoolExists(targetPoolPath) + assertIsGreaterThanMinimumRewardAmount(rewardToken, rewardAmount) + assertIsAllowedForExternalReward(targetPoolPath, rewardToken) + assertIsValidIncentiveStartTime(startTimestamp) + assertIsValidIncentiveEndTime(endTimestamp) + assertIsValidIncentiveDuration(endTimestamp - startTimestamp) + + en.MintAndDistributeGns(cross) + + // transfer reward token from user to staker + if rewardToken == GNOT { + rewardToken = WUGNOT_PATH + err := wrapWithTransfer(stakerAddr, rewardAmount) + if err != nil { + panic(err) + } + } else { + err := common.TransferFrom(cross, rewardToken, caller, stakerAddr, rewardAmount) + if err != nil { + panic(err) + } + } + + // deposit gns amount + gns.TransferFrom(cross, caller, stakerAddr, depositGnsAmount) + + currentTime := time.Now().Unix() + currentHeight := std.ChainHeight() + incentiveId := nextIncentiveID(caller, currentTime) + pool := pools.GetOrCreate(targetPoolPath) + + incentive := NewExternalIncentive( + incentiveId, + targetPoolPath, + rewardToken, + rewardAmount, + startTimestamp, + endTimestamp, + caller, + currentHeight, + depositGnsAmount, + currentTime, + ) + + if externalIncentives.Has(incentiveId) { + panic(makeErrorWithDetails( + errIncentiveAlreadyExists, + ufmt.Sprintf("incentiveId(%s)", incentiveId), + )) + } + // store external incentive information for each incentiveId + externalIncentives.set(incentiveId, incentive) + + pool.incentives.create(caller, incentive) + + previousRealm := std.PreviousRealm() + std.Emit( + "CreateExternalIncentive", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "incentiveId", incentiveId, + "targetPoolPath", targetPoolPath, + "rewardToken", rewardToken, + "rewardAmount", formatInt(rewardAmount), + "startTimestamp", formatInt(startTimestamp), + "endTimestamp", formatInt(endTimestamp), + "depositGnsAmount", formatInt(depositGnsAmount), + "currentHeight", formatInt(currentHeight), + "currentTime", formatInt(currentTime), + ) +} + +// EndExternalIncentive ends an external incentive and refunds remaining rewards. +// +// Finalizes incentive program after end timestamp. +// Returns unallocated rewards and GNS deposit. +// Calculates unclaimable rewards for refund. +// +// Parameters: +// - targetPoolPath: Pool with the incentive +// - incentiveId: Unique incentive identifier +// +// Process: +// 1. Validates incentive end time reached +// 2. Calculates remaining and unclaimable rewards +// 3. Refunds rewards to original creator +// 4. Returns 100 GNS deposit +// 5. Removes incentive from active list +// +// Only callable by Refundee or Admin. +func EndExternalIncentive(cur realm, targetPoolPath, incentiveId string) { + halt.AssertIsNotHaltedStaker() + halt.AssertIsNotHaltedWithdraw() + + assertIsPoolExists(targetPoolPath) + + pool, exists := pools.Get(targetPoolPath) + if !exists { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("targetPoolPath(%s) does not exist", targetPoolPath), + )) + } + + caller := std.PreviousRealm().Address() + currentTime := time.Now().Unix() + incentive, refund, err := endExternalIncentive(pool, incentiveId, caller, currentTime) + if err != nil { + panic(err) + } + + poolLeftExternalRewardAmount := common.BalanceOf(incentive.rewardToken, stakerAddr) + if poolLeftExternalRewardAmount < refund { + refund = poolLeftExternalRewardAmount + } + + // unwrap if wugnot + isUnwrap := incentive.rewardToken == WUGNOT_PATH + if isUnwrap { + err = unwrapWithTransfer(incentive.refundee, refund) + } else { + err = common.Transfer(cross, incentive.rewardToken, incentive.refundee, refund) + } + + if err != nil { + panic(err) + } + + // also refund deposit gns amount + gns.Transfer(cross, incentive.refundee, incentive.depositGnsAmount) + + pool.incentives.update(incentive.refundee, incentive) + + previousRealm := std.PreviousRealm() + std.Emit( + "EndExternalIncentive", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "incentiveId", incentiveId, + "targetPoolPath", targetPoolPath, + "refundee", incentive.refundee.String(), + "refundToken", incentive.rewardToken, + "refundAmount", formatInt(refund), + "refundGnsAmount", formatInt(incentive.depositGnsAmount), + "isRequestUnwrap", formatBool(incentive.rewardToken == WUGNOT_PATH), + "externalIncentiveEndBy", previousRealm.Address().String(), + ) +} + +// endExternalIncentive processes the end of an external incentive program. +func endExternalIncentive(pool *Pool, incentiveId string, caller std.Address, currentTime int64) (*ExternalIncentive, int64, error) { + incentive, exists := pool.incentives.Get(incentiveId) + if !exists { + return nil, 0, makeErrorWithDetails( + errCannotEndIncentive, + ufmt.Sprintf("cannot end non existent incentive(%s)", incentiveId), + ) + } + + if currentTime < incentive.endTimestamp { + return nil, 0, makeErrorWithDetails( + errCannotEndIncentive, + ufmt.Sprintf("cannot end incentive before endTime(%d), current(%d)", incentive.endTimestamp, currentTime), + ) + } + + // only refundee or admin can end incentive + if !access.IsAuthorized(prbac.ROLE_ADMIN.String(), caller) && caller != incentive.refundee { + return nil, 0, makeErrorWithDetails( + errNoPermission, + ufmt.Sprintf( + "only refundee(%s) or admin(%s) can end incentive, but called from %s", + incentive.refundee, adminAddr.String(), caller, + ), + ) + } + + refund := int64(incentive.rewardLeft) + + if !incentive.unclaimableRefunded { + refund += int64(pool.incentives.calculateUnclaimableReward(incentive.incentiveId)) + incentive.setUnClaimableRefunded(true) + } + + return incentive, refund, nil +} diff --git a/contract/r/gnoswap/v1/staker/external_token_list.gno b/contract/r/gnoswap/v1/staker/external_token_list.gno new file mode 100644 index 0000000..efdbd5d --- /dev/null +++ b/contract/r/gnoswap/v1/staker/external_token_list.gno @@ -0,0 +1,132 @@ +package staker + +import ( + "std" + + "gno.land/p/nt/ufmt" + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" +) + +// defaultAllowed is the list of default allowed tokens to create external incentive +var defaultAllowed = []string{GNS_PATH, GNOT} + +// allowedTokens is a slice of all allowed token paths, including the default and added tokens. +var allowedTokens = make([]string, 0, len(defaultAllowed)) + +func init() { + allowedTokens = defaultAllowed +} + +// AddToken adds a new token path to the list of allowed tokens +// Only the admin can add a new token. +// +// Parameters: +// - tokenPath (string): The path of the token to add +// +// Panics: +// - If the caller is not the admin +func AddToken(cur realm, tokenPath string) { + caller := std.PreviousRealm().Address() + halt.AssertIsNotHaltedStaker() + access.AssertIsAdminOrGovernance(caller) + + if err := modifyTokenList(tokenPath, addTokenValidator, addTokenExecutor); err != nil { + panic(err.Error()) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "AddToken", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "tokenPath", tokenPath, + ) +} + +// RemoveToken removes a token path from the list of allowed tokens. +// Only the admin can remove a token. +// +// Default tokens can not be removed. +// +// Parameters: +// - tokenPath (string): The path of the token to remove +// +// Panics: +// - If the caller is not the admin +func RemoveToken(cur realm, tokenPath string) { + caller := std.PreviousRealm().Address() + halt.AssertIsNotHaltedStaker() + access.AssertIsAdminOrGovernance(caller) + + if err := modifyTokenList(tokenPath, removeTokenValidator, removeTokenExecutor); err != nil { + panic(err.Error()) + } + + previousRealm := std.PreviousRealm() + std.Emit( + "RemoveToken", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "tokenPath", tokenPath, + ) +} + +// TokenValidator is a function type that validates a token +type TokenValidator func(tokenPath string) error + +// TokenExecutor is a token manipulation function type +type TokenExecutor func(tokenPath string, tokens []string) []string + +// modifyTokenList handles common token modification logics, such as admin check, validation, and execution. +func modifyTokenList(tokenPath string, validator TokenValidator, executor TokenExecutor) error { + // validate token operation if validator is provided + if validator != nil { + if err := validator(tokenPath); err != nil { + return err + } + } + + allowedTokens = executor(tokenPath, allowedTokens) + return nil +} + +// addTokenExecutor executes token append operation +func addTokenExecutor(tokenPath string, tokens []string) []string { + if contains(tokens, tokenPath) { + return tokens + } + + return append(tokens, tokenPath) +} + +// addTokenValidator validates token addition operation +func addTokenValidator(tokenPath string) error { + if contains(defaultAllowed, tokenPath) { + return ufmt.Errorf("%v: can not add existing token(%s)", errAddExistingToken, tokenPath) + } + + return nil +} + +// removeTokenExecutor executes token removal operation +func removeTokenExecutor(tokenPath string, tokens []string) []string { + // find and remove token + for i, t := range tokens { + if t == tokenPath { + return append(tokens[:i], tokens[i+1:]...) + } + } + + // if token not found, return the original list + return tokens +} + +// removeTokenValidator validates token removal operation +func removeTokenValidator(tokenPath string) error { + if contains(defaultAllowed, tokenPath) { + return ufmt.Errorf("%v: can not remove default token(%s)", errDefaultExternalToken, tokenPath) + } + + return nil +} diff --git a/contract/r/gnoswap/v1/staker/getter.gno b/contract/r/gnoswap/v1/staker/getter.gno new file mode 100644 index 0000000..7b56e2f --- /dev/null +++ b/contract/r/gnoswap/v1/staker/getter.gno @@ -0,0 +1,410 @@ +package staker + +import ( + "std" + "strconv" + "time" + + "gno.land/p/onbloc/json" + "gno.land/p/nt/ufmt" + + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/gns" +) + +// getPoolByPoolPath retrieves the pool by its path. +func getPoolByPoolPath(poolPath string) *Pool { + pool, ok := pools.Get(poolPath) + if !ok { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("poolPath(%s) pool does not exist", poolPath)), + ) + } + + return pool +} + +// GetPoolIncentiveIdList returns all incentive IDs for a pool. +func GetPoolIncentiveIdList(poolPath string) []string { + pool := getPoolByPoolPath(poolPath) + + ids := []string{} + pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { + ids = append(ids, key) + return true + }) + + return ids +} + +// getIncentive retrieves an external incentive by ID. +func getIncentive(poolPath string, incentiveId string) *ExternalIncentive { + pool := getPoolByPoolPath(poolPath) + + incentive, exist := pool.incentives.incentives.Get(incentiveId) + if !exist { + panic(ufmt.Sprintf("incentiveId(%s) incentive does not exist", incentiveId)) + } + + ictv, ok := incentive.(*ExternalIncentive) + if !ok { + panic(ufmt.Sprintf("failed to cast incentive to *ExternalIncentive: %T", incentive)) + } + return ictv +} + +// GetIncentiveStartTimestamp returns the start timestamp of an incentive. +func GetIncentiveStartTimestamp(poolPath string, incentiveId string) int64 { + incentive := getIncentive(poolPath, incentiveId) + + return incentive.startTimestamp +} + +// GetIncentiveEndTimestamp returns the end timestamp of an incentive. +func GetIncentiveEndTimestamp(poolPath string, incentiveId string) int64 { + incentive := getIncentive(poolPath, incentiveId) + + return incentive.endTimestamp +} + +// GetTargetPoolPathByIncentiveId returns the target pool path of an incentive. +func GetTargetPoolPathByIncentiveId(poolPath string, incentiveId string) string { + incentive := getIncentive(poolPath, incentiveId) + + return incentive.targetPoolPath +} + +// GetCreatedHeightOfIncentive returns the creation height of an incentive. +func GetCreatedHeightOfIncentive(poolPath string, incentiveId string) int64 { + incentive := getIncentive(poolPath, incentiveId) + + return incentive.createdHeight +} + +// GetIncentiveRewardToken returns the reward token of an incentive. +func GetIncentiveRewardToken(poolPath string, incentiveId string) string { + incentive := getIncentive(poolPath, incentiveId) + + return incentive.rewardToken +} + +// GetIncentiveRewardAmount returns the reward amount of an incentive. +func GetIncentiveRewardAmount(poolPath string, incentiveId string) *u256.Uint { + incentive := getIncentive(poolPath, incentiveId) + + return u256.NewUintFromInt64(incentive.rewardAmount) +} + +// GetIncentiveRewardAmountAsString returns the reward amount of an incentive as string. +func GetIncentiveRewardAmountAsString(poolPath string, incentiveId string) string { + rewardAmount := GetIncentiveRewardAmount(poolPath, incentiveId) + + return rewardAmount.ToString() +} + +// GetIncentiveRewardPerSecond returns the reward per second of an incentive. +func GetIncentiveRewardPerSecond(poolPath string, incentiveId string) int64 { + incentive := getIncentive(poolPath, incentiveId) + + return incentive.rewardPerSecond +} + +// GetIncentiveRefundee returns the refundee address of an incentive. +func GetIncentiveRefundee(poolPath string, incentiveId string) std.Address { + incentive := getIncentive(poolPath, incentiveId) + + return incentive.refundee +} + +// getDeposit retrieves a deposit by LP token ID. +func getDeposit(lpTokenId uint64) *Deposit { + deposit := deposits.get(lpTokenId) + if deposit == nil { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("lpTokenId(%d) deposit does not exist", lpTokenId)), + ) + } + + return deposit +} + +// GetDepositOwner returns the owner of a deposit. +func GetDepositOwner(lpTokenId uint64) std.Address { + deposit := getDeposit(lpTokenId) + + return deposit.owner +} + +// GetDepositStakeTimestamp returns the stake timestamp of a deposit. +func GetDepositStakeTimestamp(lpTokenId uint64) int64 { + deposit := getDeposit(lpTokenId) + + return deposit.stakeTimestamp +} + +// GetDepositStakeTime returns the stake time of a deposit. +func GetDepositStakeTime(lpTokenId uint64) int64 { + deposit := getDeposit(lpTokenId) + + return deposit.stakeTime +} + +// GetDepositTargetPoolPath returns the target pool path of a deposit. +func GetDepositTargetPoolPath(lpTokenId uint64) string { + deposit := getDeposit(lpTokenId) + + return deposit.targetPoolPath +} + +// GetDepositTickLower returns the lower tick of a deposit. +func GetDepositTickLower(lpTokenId uint64) int32 { + deposit := getDeposit(lpTokenId) + + return deposit.tickLower +} + +// GetDepositTickUpper returns the upper tick of a deposit. +func GetDepositTickUpper(lpTokenId uint64) int32 { + deposit := getDeposit(lpTokenId) + + return deposit.tickUpper +} + +// GetDepositLiquidity returns the liquidity of a deposit. +func GetDepositLiquidity(lpTokenId uint64) *u256.Uint { + deposit := getDeposit(lpTokenId) + + return deposit.liquidity.Clone() +} + +// GetDepositLiquidityAsString returns the liquidity of a deposit as string. +func GetDepositLiquidityAsString(lpTokenId uint64) string { + liquidity := GetDepositLiquidity(lpTokenId) + + return liquidity.ToString() +} + +// GetDepositLastCollectTimestamp returns the last collect timestamp of a deposit. +func GetDepositLastCollectTimestamp(lpTokenId uint64) int64 { + deposit := getDeposit(lpTokenId) + + return deposit.lastCollectTime +} + +// GetDepositWarmUp returns the warm-up records of a deposit. +func GetDepositWarmUp(lpTokenId uint64) []Warmup { + deposit := getDeposit(lpTokenId) + + return deposit.warmups +} + +// GetPoolTier returns the tier of a pool. +func GetPoolTier(poolPath string) uint64 { + return poolTier.CurrentTier(poolPath) +} + +// GetPoolTierRatio returns the current reward ratio for a pool's tier. +func GetPoolTierRatio(poolPath string) uint64 { + tier := GetPoolTier(poolPath) + return poolTier.tierRatio.Get(tier) +} + +// GetPoolTierCount returns the number of pools in a tier. +func GetPoolTierCount(tier uint64) uint64 { + if tier == 0 { + return 0 + } + return uint64(poolTier.CurrentCount(tier)) +} + +// GetPoolReward returns the current reward amount for a tier. +func GetPoolReward(tier uint64) int64 { + return poolTier.CurrentReward(tier) +} + +// GetExternalIncentiveByPoolPath returns all external incentives for a pool. +func GetExternalIncentiveByPoolPath(poolPath string) []ExternalIncentive { + incentives := []ExternalIncentive{} + externalIncentives.tree.Iterate("", "", func(key string, value any) bool { + incentive := value.(*ExternalIncentive) + if incentive.targetPoolPath == poolPath { + incentives = append(incentives, *incentive) + } + return false + }) + + return incentives +} + +// GetPrintExternalInfo returns a JSON representation of external incentive debug information. +func GetPrintExternalInfo() string { + externalDebug := ApiExternalDebugInfo{ + Height: std.ChainHeight(), + Time: time.Now().Unix(), + } + + externalPositions := []ApiExternalDebugPosition{} + deposits.Iterate(uint64(0), uint64(deposits.Size()), func(positionId uint64, deposit *Deposit) bool { + externalPosition := ApiExternalDebugPosition{ + PositionId: positionId, + StakedTime: deposit.stakeTime, + StakedTimestamp: deposit.stakeTimestamp, + } + + externalIncentivesList := []ApiExternalDebugIncentive{} + externalIncentives.tree.Iterate("", "", func(key string, value any) bool { + incentive, ok := value.(*ExternalIncentive) + if !ok { + panic("failed to cast value to *ExternalIncentive") + } + if incentive.targetPoolPath == deposit.targetPoolPath { + externalIncentive := ApiExternalDebugIncentive{ + PoolPath: incentive.targetPoolPath, + IncentiveId: key, + RewardToken: incentive.rewardToken, + RewardAmount: strconv.FormatInt(incentive.rewardAmount, 10), + RewardLeft: strconv.FormatInt(incentive.rewardLeft, 10), + StartTimestamp: incentive.startTimestamp, + EndTimestamp: incentive.endTimestamp, + RewardPerSecond: strconv.FormatInt(incentive.rewardPerSecond, 10), + Refundee: incentive.refundee, + TokenAmountFull: incentive.depositGnsAmount, + TokenAmountToGive: incentive.RewardSpent(time.Now().Unix()), + } + + externalIncentivesList = append(externalIncentivesList, externalIncentive) + } + return false + }) + + externalPosition.Incentive = externalIncentivesList + externalPositions = append(externalPositions, externalPosition) + return false + }) + + externalDebug.Position = externalPositions + + // JSON Serialization + node := json.ObjectNode("", map[string]*json.Node{ + "height": json.NumberNode("", float64(externalDebug.Height)), + "time": json.NumberNode("", float64(externalDebug.Time)), + "position": json.ArrayNode("", makeExternalPositionsNode(externalDebug.Position)), + }) + + b, err := json.Marshal(node) + if err != nil { + return "JSON MARSHAL ERROR" + } + + return string(b) +} + +// makeExternalPositionsNode creates JSON nodes for external position data. +func makeExternalPositionsNode(positions []ApiExternalDebugPosition) []*json.Node { + externalPositions := make([]*json.Node, 0) + + for _, externalPosition := range positions { + incentives := make([]*json.Node, 0) + for _, incentive := range externalPosition.Incentive { + stakedOrExternalDuration := std.ChainHeight() - max(incentive.StartHeight, externalPosition.StakedTime) + + incentives = append(incentives, json.ObjectNode("", map[string]*json.Node{ + "poolPath": json.StringNode("poolPath", incentive.PoolPath), + "rewardToken": json.StringNode("rewardToken", incentive.RewardToken), + "rewardAmount": json.StringNode("rewardAmount", incentive.RewardAmount), + "rewardLeft": json.StringNode("rewardLeft", incentive.RewardLeft), + "startTimestamp": json.NumberNode("startTimestamp", float64(incentive.StartTimestamp)), + "endTimestamp": json.NumberNode("endTimestamp", float64(incentive.EndTimestamp)), + "rewardPerSecond": json.StringNode("rewardPerSecond", incentive.RewardPerSecond), + "stakedOrExternalDuration": json.NumberNode("stakedOrExternalDuration", float64(stakedOrExternalDuration)), + "tokenAmountFull": json.NumberNode("tokenAmountFull", float64(incentive.TokenAmountFull)), + "tokenAmountToGive": json.NumberNode("tokenAmountToGive", float64(incentive.TokenAmountToGive)), + })) + } + + externalPositions = append(externalPositions, json.ObjectNode("", map[string]*json.Node{ + "lpTokenId": json.NumberNode("lpTokenId", float64(externalPosition.PositionId)), + "stakedTime": json.NumberNode("stakedTime", float64(externalPosition.StakedTime)), + "stakedTimestamp": json.NumberNode("stakedTimestamp", float64(externalPosition.StakedTimestamp)), + "incentive": json.ArrayNode("", incentives), + })) + } + + return externalPositions +} + +type currentExternalInfo struct { + height int64 + time int64 + externalIncentives []ExternalIncentive +} + +type ApiExternalDebugInfo struct { + Height int64 `json:"height"` + Time int64 `json:"time"` + Position []ApiExternalDebugPosition `json:"pool"` +} + +type ApiExternalDebugPosition struct { + PositionId uint64 `json:"positionId"` + StakedTime int64 `json:"stakedTime"` + StakedTimestamp int64 `json:"stakedTimestamp"` + Incentive []ApiExternalDebugIncentive `json:"incentive"` +} + +type ApiExternalDebugIncentive struct { + PoolPath string `json:"poolPath"` + IncentiveId string `json:"incentiveId"` + RewardToken string `json:"rewardToken"` + RewardAmount string `json:"rewardAmount"` + RewardLeft string `json:"rewardLeft"` + StartTimestamp int64 `json:"startTimestamp"` + EndTimestamp int64 `json:"endTimestamp"` + RewardPerSecondX96 string `json:"rewardPerSecondX96"` + RewardPerSecond string `json:"rewardPerSecond"` + Refundee std.Address `json:"refundee"` + StartHeight int64 `json:"startHeight"` + EndHeight int64 `json:"endHeight"` + // FROM positionExternal -> externalRewards + TokenAmountX96 *u256.Uint `json:"tokenAmountX96"` + TokenAmount int64 `json:"tokenAmount"` + TokenAmountFull int64 `json:"tokenAmountFull"` + TokenAmountToGive int64 `json:"tokenAmountToGive"` + // FROM externalWarmUpAmount + Full30 int64 `json:"full30"` + Give30 int64 `json:"give30"` + Full50 int64 `json:"full50"` + Give50 int64 `json:"give50"` + Full70 int64 `json:"full70"` + Give70 int64 `json:"give70"` + Full100 int64 `json:"full100"` +} + +// DEBUG INTERNAL (GNS EMISSION) +type currentInfo struct { + height int64 + time int64 + gnsStaker int64 + gnsDevOps int64 + gnsCommunityPool int64 + gnsGovStaker int64 + gnsProtocolFee int64 + gnsADMIN int64 +} + +// getCurrentInfo returns current GNS balance information for system addresses. +func getCurrentInfo() currentInfo { + return currentInfo{ + height: std.ChainHeight(), + time: time.Now().Unix(), + gnsStaker: gns.BalanceOf(stakerAddr), + gnsDevOps: gns.BalanceOf(devOpsAddr), + gnsCommunityPool: gns.BalanceOf(communityPoolAddr), + gnsGovStaker: gns.BalanceOf(govStakerAddr), + gnsProtocolFee: gns.BalanceOf(protocolFeeAddr), + gnsADMIN: gns.BalanceOf(adminAddr), + } +} diff --git a/contract/r/gnoswap/v1/staker/gnomod.toml b/contract/r/gnoswap/v1/staker/gnomod.toml new file mode 100644 index 0000000..5c38b8f --- /dev/null +++ b/contract/r/gnoswap/v1/staker/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/gnoswap/v1/staker" +gno = "0.9" diff --git a/contract/r/gnoswap/v1/staker/incentive_id.gno b/contract/r/gnoswap/v1/staker/incentive_id.gno new file mode 100644 index 0000000..cb9f381 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/incentive_id.gno @@ -0,0 +1,21 @@ +package staker + +import ( + "std" + + "gno.land/p/nt/ufmt" +) + +// Counter for generating unique incentive IDs +var incentiveCounter = NewCounter() + +// nextIncentiveID generates a new unique incentive ID using creator address, timestamp and counter +func nextIncentiveID(creator std.Address, timestamp int64) string { + return makeIncentiveID(creator, timestamp, incentiveCounter.next()) +} + +// makeIncentiveID formats an incentive ID string from the given components +// incentive id format: creator:timestamp:index +func makeIncentiveID(creator std.Address, timestamp int64, index int64) string { + return ufmt.Sprintf("%s:%d:%d", creator.String(), timestamp, index) +} diff --git a/contract/r/gnoswap/v1/staker/json.gno b/contract/r/gnoswap/v1/staker/json.gno new file mode 100644 index 0000000..c403b20 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/json.gno @@ -0,0 +1,276 @@ +package staker + +import ( + "std" + "strconv" + "time" + + "gno.land/p/onbloc/json" +) + +// JsonResponse is an interface that all JSON response types must implement. +type JsonResponse interface { + JSON() *json.Node +} + +type RewardToken struct { + PoolPath string `json:"poolPath"` + RewardsTokenList []string `json:"rewardsTokenList"` +} + +func newRewardToken(poolPath string, tokens []string) RewardToken { + return RewardToken{ + PoolPath: poolPath, + RewardsTokenList: tokens, + } +} + +func (r RewardToken) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "poolPath": json.StringNode("poolPath", r.PoolPath), + "tokens": json.ArrayNode("tokens", makeRewardTokensArray(r.RewardsTokenList)), + }) +} + +type ApiExternalIncentive struct { + IncentiveId string `json:"incentiveId"` + PoolPath string `json:"poolPath"` + RewardToken string `json:"rewardToken"` + RewardAmount int64 `json:"rewardAmount"` + RewardLeft int64 `json:"rewardLeft"` + StartTimestamp int64 `json:"startTimestamp"` + EndTimestamp int64 `json:"endTimestamp"` + Active bool `json:"active"` + RewardPerSecond int64 `json:"rewardPerSecond"` + Refundee string `json:"refundee"` + CreatedHeight int64 `json:"createdHeight"` + DepositGnsAmount int64 `json:"depositGnsAmount"` + UnClaimableRefunded bool `json:"unclaimableRefunded"` +} + +func newApiExternalIncentive(ictv *ExternalIncentive) ApiExternalIncentive { + now := time.Now().Unix() + isActive := false + if now >= ictv.startTimestamp && now <= ictv.endTimestamp { + isActive = true + } + return ApiExternalIncentive{ + IncentiveId: ictv.incentiveId, + PoolPath: ictv.targetPoolPath, + RewardToken: ictv.rewardToken, + RewardAmount: ictv.rewardAmount, + RewardLeft: ictv.RewardLeft(now), + StartTimestamp: ictv.startTimestamp, + EndTimestamp: ictv.endTimestamp, + Active: isActive, + RewardPerSecond: ictv.rewardPerSecond, + Refundee: ictv.refundee.String(), + CreatedHeight: ictv.createdHeight, + DepositGnsAmount: ictv.depositGnsAmount, + UnClaimableRefunded: ictv.unclaimableRefunded, + } +} + +func (r ApiExternalIncentive) JSON() *json.Node { + active := false + if time.Now().Unix() >= r.StartTimestamp && time.Now().Unix() <= r.EndTimestamp { + active = true + } + + return json.ObjectNode("", map[string]*json.Node{ + "incentiveId": json.StringNode("incentiveId", r.IncentiveId), + "poolPath": json.StringNode("poolPath", r.PoolPath), + "rewardToken": json.StringNode("rewardToken", r.RewardToken), + "rewardAmount": json.StringNode("rewardAmount", strconv.FormatInt(r.RewardAmount, 10)), + "rewardLeft": json.StringNode("rewardLeft", strconv.FormatInt(r.RewardLeft, 10)), + "startTimestamp": json.NumberNode("startTimestamp", float64(r.StartTimestamp)), + "endTimestamp": json.NumberNode("endTimestamp", float64(r.EndTimestamp)), + "active": json.BoolNode("active", active), + "rewardPerSecond": json.StringNode("rewardPerSecond", strconv.FormatInt(r.RewardPerSecond, 10)), + "refundee": json.StringNode("refundee", r.Refundee), + "createdHeight": json.NumberNode("createdHeight", float64(r.CreatedHeight)), + "depositGnsAmount": json.NumberNode("depositGnsAmount", float64(r.DepositGnsAmount)), + "unclaimableRefunded": json.BoolNode("unclaimableRefunded", r.UnClaimableRefunded), + }) +} + +type ApiInternalIncentive struct { + PoolPath string `json:"poolPath"` + Tier uint64 `json:"tier"` + StartTimestamp int64 `json:"startTimestamp"` + RewardPerSecond string `json:"rewardPerSecond"` +} + +func newApiInternalIncentive(poolPath string, tier uint64) ApiInternalIncentive { + perSecond := calculateInternalRewardPerSecondByPoolPath(poolPath) + return ApiInternalIncentive{ + PoolPath: poolPath, + Tier: tier, + RewardPerSecond: perSecond, + } +} + +func (r ApiInternalIncentive) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "poolPath": json.StringNode("poolPath", r.PoolPath), + "rewardToken": json.StringNode("rewardToken", GNS_PATH), + "tier": json.NumberNode("tier", float64(r.Tier)), + "rewardPerSecond": json.StringNode("rewardPerSecond", r.RewardPerSecond), + }) +} + +// LpTokenReward represents the rewards associated with a specific LP token +type LpTokenReward struct { + LpTokenId uint64 `json:"lpTokenId"` // The ID of the LP token + Address string `json:"address"` // The address associated with the LP token + Rewards []ApiReward `json:"rewards"` +} + +func newLpTokenReward(lpTokenId uint64, address string, rewards []ApiReward) LpTokenReward { + return LpTokenReward{ + LpTokenId: lpTokenId, + Address: address, + Rewards: rewards, + } +} + +func (r LpTokenReward) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "lpTokenId": json.NumberNode("lpTokenId", float64(r.LpTokenId)), + "address": json.StringNode("address", r.Address), + "rewards": json.ArrayNode("rewards", makeRewardsArray(r.Rewards)), + }) +} + +// Stake represents a single stake +type ApiStake struct { + PositionId uint64 `json:"positionId"` // The ID of the staked LP token + Owner std.Address `json:"owner"` // The address of the owner of the staked LP token + NumberOfStakes uint64 `json:"numberOfStakes"` // The number of times this LP token has been staked + StakeTimestamp int64 `json:"stakeTimestamp"` // The timestamp when the LP token was staked + StakeTime int64 `json:"stakeTime"` // The time when the LP token was staked + TargetPoolPath string `json:"targetPoolPath"` // The path of the target pool for the stake + StakeDuration uint64 `json:"stakeDuration"` // The duration of the stake +} + +func newApiStake(tokenId uint64, deposit *Deposit) ApiStake { + stakeDuration := uint64(0) + if std.ChainHeight() > deposit.stakeTime { + stakeDuration = uint64(std.ChainHeight() - deposit.stakeTime) // TODO: should use time difference + } + + return ApiStake{ + PositionId: tokenId, + Owner: deposit.owner, + StakeTimestamp: deposit.stakeTimestamp, + StakeTime: deposit.stakeTime, + TargetPoolPath: deposit.targetPoolPath, + StakeDuration: stakeDuration, + } +} + +func (s ApiStake) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "positionId": json.NumberNode("positionId", float64(s.PositionId)), + "owner": json.StringNode("owner", s.Owner.String()), + "stakeTimestamp": json.NumberNode("stakeTimestamp", float64(s.StakeTimestamp)), + "stakeTime": json.NumberNode("stakeTime", float64(s.StakeTime)), + "targetPoolPath": json.StringNode("targetPoolPath", s.TargetPoolPath), + "stakeDuration": json.NumberNode("stakeDuration", float64(s.StakeDuration)), + }) +} + +type statNode struct { + height int64 + timestamp int64 +} + +func newStatNode() statNode { + return statNode{ + height: std.ChainHeight(), + timestamp: time.Now().Unix(), + } +} + +func (s statNode) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "height": json.NumberNode("height", float64(s.height)), + "timestamp": json.NumberNode("timestamp", float64(s.timestamp)), + }) +} + +// Reward represents a single reward for a staked LP token +type ApiReward struct { + IncentiveType string `json:"incentiveType"` // The type of incentive (INTERNAL or EXTERNAL) + IncentiveId string `json:"incentiveId"` // The unique identifier of the incentive + TargetPoolPath string `json:"targetPoolPath"` // The path of the target pool for the reward + RewardTokenPath string `json:"rewardTokenPath"` // The pathe of the reward token + RewardTokenAmount int64 `json:"rewardTokenAmount"` // The amount of the reward token + StakeTimestamp int64 `json:"stakeTimestamp"` // The timestamp when the LP token was staked + StakeTime int64 `json:"stakeTime"` // The time when the LP token was staked + IncentiveStart int64 `json:"incentiveStart"` // The timestamp when the incentive started +} + +func (r ApiReward) JSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "incentiveType": json.StringNode("incentiveType", r.IncentiveType), + "incentiveId": json.StringNode("incentiveId", r.IncentiveId), + "targetPoolPath": json.StringNode("targetPoolPath", r.TargetPoolPath), + "rewardTokenPath": json.StringNode("rewardTokenPath", r.RewardTokenPath), + "rewardTokenAmount": json.NumberNode("rewardTokenAmount", float64(r.RewardTokenAmount)), + "stakeTimestamp": json.NumberNode("stakeTimestamp", float64(r.StakeTimestamp)), + "stakeTime": json.NumberNode("stakeTime", float64(r.StakeTime)), + "incentiveStart": json.NumberNode("incentiveStart", float64(r.IncentiveStart)), + }) +} + +///////////////////// Response ///////////////////// + +type ApiResponse struct { + Stat statNode `json:"stat"` + Response []JsonResponse `json:"response"` +} + +func (r ApiResponse) JSON() *json.Node { + rspsNodes := make([]*json.Node, len(r.Response)) + for i, item := range r.Response { + rspsNodes[i] = item.JSON() + } + + return json.ObjectNode("", map[string]*json.Node{ + "stat": r.Stat.JSON(), + "response": json.ArrayNode("response", rspsNodes), + }) +} + +func makeApiResponse(rs []JsonResponse) string { + resp := ApiResponse{ + Stat: newStatNode(), + Response: rs, + } + + b, err := json.Marshal(resp.JSON()) + if err != nil { + panic(err.Error()) + } + + return string(b) +} + +func makeRewardsArray(rewards []ApiReward) []*json.Node { + rewardsArray := make([]*json.Node, len(rewards)) + + for i, reward := range rewards { + rewardsArray[i] = json.ObjectNode("", map[string]*json.Node{ + "incentiveType": json.StringNode("incentiveType", reward.IncentiveType), + "incentiveId": json.StringNode("incentiveId", reward.IncentiveId), + "targetPoolPath": json.StringNode("targetPoolPath", reward.TargetPoolPath), + "rewardTokenPath": json.StringNode("rewardTokenPath", reward.RewardTokenPath), + "rewardTokenAmount": json.NumberNode("rewardTokenAmount", float64(reward.RewardTokenAmount)), + "stakeTimestamp": json.NumberNode("stakeTimestamp", float64(reward.StakeTimestamp)), + "stakeTime": json.NumberNode("stakeTime", float64(reward.StakeTime)), + "incentiveStart": json.NumberNode("incentiveStart", float64(reward.IncentiveStart)), + }) + } + return rewardsArray +} diff --git a/contract/r/gnoswap/v1/staker/manage_pool_tier_and_warmup.gno b/contract/r/gnoswap/v1/staker/manage_pool_tier_and_warmup.gno new file mode 100644 index 0000000..56e1042 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/manage_pool_tier_and_warmup.gno @@ -0,0 +1,173 @@ +package staker + +import ( + "std" + "time" + + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + + en "gno.land/r/gnoswap/emission" + pl "gno.land/r/gnoswap/v1/pool" +) + +const ( + NOT_EMISSION_TARGET_TIER uint64 = 0 +) + +// SetPoolTier assigns a tier level to a pool for internal GNS emission rewards. +// Only admin or governance can call this function. +func SetPoolTier(cur realm, poolPath string, tier uint64) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsPoolExists(poolPath) + assertIsValidPoolTier(tier) + + currentTime := time.Now().Unix() + setPoolTier(poolPath, tier, currentTime) + + previousRealm := std.PreviousRealm() + std.Emit( + "SetPoolTier", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "poolPath", poolPath, + "tier", formatUint(tier), + "currentTime", formatInt(currentTime), + "currentHeight", formatInt(std.ChainHeight()), + ) +} + +// ChangePoolTier modifies the tier level of an existing pool. +// Only admin or governance can call this function. +func ChangePoolTier(cur realm, poolPath string, tier uint64) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsPoolExists(poolPath) + assertIsValidPoolTier(tier) + + currentTime := time.Now().Unix() + previousTier, newTier := changePoolTier(poolPath, tier, currentTime) + + previousRealm := std.PreviousRealm() + std.Emit( + "ChangePoolTier", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "poolPath", poolPath, + "prevTier", formatUint(previousTier), + "newTier", formatUint(newTier), + "currentTime", formatInt(currentTime), + "currentHeight", formatInt(std.ChainHeight()), + ) +} + +// RemovePoolTier removes a pool from internal GNS emission rewards. +// Only admin or governance can call this function. +func RemovePoolTier(cur realm, poolPath string) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsPoolExists(poolPath) + + currentTime := time.Now().Unix() + removePoolTier(poolPath, currentTime) + + previousRealm := std.PreviousRealm() + std.Emit( + "RemovePoolTier", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "poolPath", poolPath, + "currentTime", formatInt(currentTime), + "currentHeight", formatInt(std.ChainHeight()), + ) +} + +// SetWarmUp configures the warm-up percentage and duration for rewards. +// Only admin or governance can call this function. +func SetWarmUp(cur realm, pct, timeDuration int64) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + setWarmUp(pct, timeDuration) + + previousRealm := std.PreviousRealm() + std.Emit( + "SetWarmUp", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "pct", formatInt(pct), + "timeDuration", formatInt(timeDuration), + ) +} + +// setPoolTier internally sets the pool tier. +func setPoolTier(poolPath string, tier uint64, currentTime int64) { + en.MintAndDistributeGns(cross) + + pools.GetOrCreate(poolPath) + poolTier.changeTier(std.ChainHeight(), currentTime, pools, poolPath, tier) +} + +// changePoolTier internally changes the pool tier and returns old and new tiers. +func changePoolTier(poolPath string, tier uint64, currentTime int64) (uint64, uint64) { + en.MintAndDistributeGns(cross) + previousTier := poolTier.CurrentTier(poolPath) + + poolTier.changeTier(std.ChainHeight(), currentTime, pools, poolPath, tier) + + return previousTier, tier +} + +// removePoolTier internally removes the pool from tier system. +func removePoolTier(poolPath string, currentTime int64) { + en.MintAndDistributeGns(cross) + + poolTier.changeTier(std.ChainHeight(), currentTime, pools, poolPath, NOT_EMISSION_TARGET_TIER) +} + +// setWarmUp internally sets the warm-up parameters. +func setWarmUp(pct, timeDuration int64) { + en.MintAndDistributeGns(cross) + + modifyWarmup(pctToIndex(pct), timeDuration) +} + +// pctToIndex converts percentage to warmup index. +func pctToIndex(pct int64) int { + switch pct { + case 30: + return 0 + case 50: + return 1 + case 70: + return 2 + case 100: + return 3 + default: + panic("staker.gno__pctToIndex() || pct is not valid") + } +} + +// assertPoolMustExist panics if the pool doesn't exist. +func assertPoolMustExist(poolPath string) { + if !pl.ExistsPoolPath(poolPath) { + panic(makeErrorWithDetails( + errInvalidPoolPath, + ufmt.Sprintf("pool(%s) does not exist", poolPath), + )) + } +} diff --git a/contract/r/gnoswap/v1/staker/mint_stake.gno b/contract/r/gnoswap/v1/staker/mint_stake.gno new file mode 100644 index 0000000..cb9f92c --- /dev/null +++ b/contract/r/gnoswap/v1/staker/mint_stake.gno @@ -0,0 +1,93 @@ +package staker + +import ( + "std" + + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/halt" + + pn "gno.land/r/gnoswap/v1/position" +) + +// MintAndStake mints a new liquidity position and immediately stakes it. +// +// Atomic operation combining position creation and staking. +// Saves gas by avoiding separate mint and stake transactions. +// Position NFT transferred directly to staker contract. +// +// Parameters: +// - token0, token1: Token contract paths +// - fee: Pool fee tier (500=0.05%, 3000=0.3%, 10000=1%) +// - tickLower, tickUpper: Price range boundaries +// - amount0Desired, amount1Desired: Target token amounts +// - amount0Min, amount1Min: Minimum amounts (slippage protection) +// - deadline: Transaction expiration timestamp +// - referrer: Optional referral address +// +// Native token support: +// - Accepts GNOT via std.OriginSend() +// - Auto-wraps to WUGNOT for liquidity +// - Minimum 1 GNOT required +// +// Returns: +// - positionId: New NFT token ID +// - liquidity: Amount of liquidity minted +// - amount0, amount1: Actual tokens deposited +// - poolPath: Pool identifier +func MintAndStake( + cur realm, + token0 string, + token1 string, + fee uint32, + tickLower int32, + tickUpper int32, + amount0Desired string, + amount1Desired string, + amount0Min string, + amount1Min string, + deadline int64, + referrer string, +) (uint64, string, string, string, string) { + halt.AssertIsNotHaltedStaker() + + // if one click native + if token0 == GNOT || token1 == GNOT { + // check sent ugnot + sent := std.OriginSend() + ugnotSent := sent.AmountOf("ugnot") + + // not enough ugnot sent + if ugnotSent < UGNOT_MIN_DEPOSIT_TO_WRAP { + panic(ufmt.Errorf( + "%v: too less ugnot sent(%d), minimum:%d", + errWugnotMinimum, ugnotSent, UGNOT_MIN_DEPOSIT_TO_WRAP, + )) + } + + // send it over to position to wrap + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(stakerAddr, positionAddr, std.Coins{{Denom: "ugnot", Amount: ugnotSent}}) + } + + positionId, liquidity, amount0, amount1 := pn.Mint( + cross, + token0, + token1, + fee, + tickLower, + tickUpper, + amount0Desired, + amount1Desired, + amount0Min, + amount1Min, + deadline, + stakerAddr, + std.PreviousRealm().Address(), + referrer, + ) + + poolPath, _, _ := StakeToken(cur, positionId, "") + + return positionId, liquidity, amount0, amount1, poolPath +} diff --git a/contract/r/gnoswap/v1/staker/protocol_fee_unstaking.gno b/contract/r/gnoswap/v1/staker/protocol_fee_unstaking.gno new file mode 100644 index 0000000..647d7bb --- /dev/null +++ b/contract/r/gnoswap/v1/staker/protocol_fee_unstaking.gno @@ -0,0 +1,84 @@ +package staker + +import ( + "std" + + "gno.land/p/nt/ufmt" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" + + pf "gno.land/r/gnoswap/v1/protocol_fee" +) + +const FEE_PRECISION = 10000 + +// unstakingFee is the fee charged when unstaking positions. +// This parameter can be modified through governance. +var unstakingFee = int64(100) // 1% + +// GetUnstakingFee returns the current unstaking fee rate in basis points. +func GetUnstakingFee() int64 { return unstakingFee } + +// handleUnStakingFee calculates and applies the unstaking fee. +func handleUnStakingFee( + tokenPath string, + amount int64, + internal bool, + positionId uint64, + poolPath string, +) (int64, int64, error) { + if unstakingFee == 0 { + return amount, 0, nil + } + + // Do not change the order of the operation. + feeAmount := (amount * unstakingFee) / FEE_PRECISION + if feeAmount < 0 { + return 0, 0, ufmt.Errorf("fee amount cannot be negative") + } + + if feeAmount == 0 { + return amount, 0, nil + } + + if internal { + tokenPath = GNS_PATH + } + + // external contract has fee + common.Transfer(cross, tokenPath, protocolFeeAddr, feeAmount) + pf.AddToProtocolFee(cross, tokenPath, feeAmount) + + return amount - feeAmount, feeAmount, nil +} + +// SetUnStakingFee sets the unstaking fee rate in basis points. +// Only admin or governance can call this function. +func SetUnStakingFee(cur realm, fee int64) { + halt.AssertIsNotHaltedStaker() + + caller := std.PreviousRealm().Address() + access.AssertIsAdminOrGovernance(caller) + + assertIsValidFeeRate(fee) + + prevUnStakingFee := GetUnstakingFee() + + setUnStakingFee(fee) + + previousRealm := std.PreviousRealm() + std.Emit( + "SetUnStakingFee", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "prevFee", formatAnyInt(prevUnStakingFee), + "newFee", formatAnyInt(fee), + ) +} + +// setUnStakingFee internally updates the unstaking fee. +func setUnStakingFee(fee int64) { + unstakingFee = fee +} diff --git a/contract/r/gnoswap/v1/staker/query.gno b/contract/r/gnoswap/v1/staker/query.gno new file mode 100644 index 0000000..7436681 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/query.gno @@ -0,0 +1,127 @@ +package staker + +import ( + "std" + "time" + + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" +) + +type PoolData struct { + PoolPath string + Tier uint64 + ActiveIncentives []string + StakedLiquidity *u256.Uint +} + +type IncentiveData struct { + IncentiveID string + StartTimestamp int64 + EndTimestamp int64 + RewardToken string + RewardAmount *u256.Uint + Refundee std.Address + PoolPath string +} + +type DepositData struct { + TokenID uint64 + Owner std.Address + TargetPoolPath string + StakeTimestamp int64 + Liquidity *u256.Uint + WarmupCount int +} + +// QueryPoolData returns combined pool data including tier, incentives and current staked liquidity +func QueryPoolData(poolPath string) (*PoolData, error) { + pool, exist := pools.Get(poolPath) + if !exist { + return nil, ufmt.Errorf("pool %s not found", poolPath) + } + + currentTimestamp := time.Now().Unix() + tier := poolTier.CurrentTier(poolPath) + + ictvIds := filterActiveIncentives(pool, currentTimestamp) + + return &PoolData{ + PoolPath: poolPath, + Tier: tier, + ActiveIncentives: ictvIds, + StakedLiquidity: pool.CurrentStakedLiquidity(currentTimestamp), + }, nil +} + +// QueryIncentiveData returns detailed information about a specific incentive +func QueryIncentiveData(incentiveId string) (*IncentiveData, error) { + var found bool + var data IncentiveData + + pools.tree.Iterate("", "", func(key string, value any) bool { + pool := value.(*Pool) + + pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { + if key == incentiveId { + ictv := value.(*ExternalIncentive) + data = IncentiveData{ + IncentiveID: incentiveId, + StartTimestamp: ictv.startTimestamp, + EndTimestamp: ictv.endTimestamp, + RewardToken: ictv.rewardToken, + RewardAmount: u256.NewUintFromInt64(ictv.rewardAmount), + Refundee: ictv.refundee, + PoolPath: pool.poolPath, + } + found = true + return true + } + return false + }) + + return found + }) + + if !found { + return nil, ufmt.Errorf("incentiveId(%s) incentive does not exist", incentiveId) + } + + return &data, nil +} + +// QueryDepositData returns detailed information about a specific deposit +func QueryDepositData(lpTokenId uint64) (*DepositData, error) { + deposit := deposits.get(lpTokenId) + if deposit == nil { + return nil, ufmt.Errorf("positionId(%d) deposit does not exist", lpTokenId) + } + + return &DepositData{ + TokenID: lpTokenId, + Owner: deposit.owner, + TargetPoolPath: deposit.targetPoolPath, + StakeTimestamp: deposit.stakeTimestamp, + Liquidity: deposit.liquidity, + WarmupCount: len(deposit.warmups), + }, nil +} + +func filterActiveIncentives(pool *Pool, currentTimestamp int64) []string { + ictvIds := make([]string, 0) + + pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { + ictv, ok := value.(*ExternalIncentive) + if !ok { + return false + } + + if ictv.startTimestamp <= currentTimestamp && currentTimestamp < ictv.endTimestamp { + ictvIds = append(ictvIds, ictv.incentiveId) + } + + return false + }) + + return ictvIds +} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation.gno b/contract/r/gnoswap/v1/staker/reward_calculation.gno new file mode 100644 index 0000000..9c49a3a --- /dev/null +++ b/contract/r/gnoswap/v1/staker/reward_calculation.gno @@ -0,0 +1,61 @@ +package staker + +import ( + "gno.land/p/nt/ufmt" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +// liquidityMathAddDelta calculates the new liquidity by applying the delta liquidity to the current liquidity. +// If delta liquidity is negative, it subtracts the absolute value of delta liquidity from the current liquidity. +// If delta liquidity is positive, it adds the absolute value of delta liquidity to the current liquidity. +// +// Parameters: +// - x: The current liquidity as a uint256 value. +// - y: The delta liquidity as a signed int256 value. +// +// Returns: +// - The new liquidity as a uint256 value. +// +// Notes: +// - If `x` or `y` is nil, the function panics with an appropriate error message. +// - If `y` is negative, its absolute value is subtracted from `x`. +// - The result must be less than `x`. Otherwise, the function panics to prevent underflow. +// +// - If `y` is positive, it is added to `x`. +// - The result must be greater than or equal to `x`. Otherwise, the function panics to prevent overflow. +// +// - The function ensures correctness by validating the results of the arithmetic operations. +func liquidityMathAddDelta(x *u256.Uint, y *i256.Int) *u256.Uint { + if x == nil || y == nil { + panic(makeErrorWithDetails( + errInvalidInput, + "x or y is nil", + )) + } + + var z *u256.Uint + + // Subtract or add based on the sign of y + if y.Lt(i256.Zero()) { + absDelta := y.Abs() + z = u256.Zero().Sub(x, absDelta) + if z.Gte(x) { + panic(makeErrorWithDetails( + errCalculationError, + ufmt.Sprintf("Condition failed: (z must be < x) (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString()), + )) + } + } else { + z = u256.Zero().Add(x, y.Abs()) + if z.Lt(x) { + panic(makeErrorWithDetails( + errCalculationError, + ufmt.Sprintf("Condition failed: (z must be >= x) (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString()), + )) + } + } + + return z +} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_incentives.gno b/contract/r/gnoswap/v1/staker/reward_calculation_incentives.gno new file mode 100644 index 0000000..8cf0aca --- /dev/null +++ b/contract/r/gnoswap/v1/staker/reward_calculation_incentives.gno @@ -0,0 +1,223 @@ +package staker + +import ( + "std" + "time" + + "gno.land/p/nt/avl" +) + +// Incentives represents a collection of external incentives for a specific pool. +// +// Fields: +// +// - incentives: AVL tree storing ExternalIncentive objects indexed by incentiveId +// The incentiveId serves as the key to efficiently lookup incentive details +// +// - targetPoolPath: String identifier for the pool this incentive collection belongs to +// Used to associate incentives with their corresponding liquidity pool +// +// - unclaimablePeriods: Tree storing periods when rewards cannot be claimed +// Maps start timestamp (key) to end timestamp (value) +// An end timestamp of 0 indicates an ongoing unclaimable period +// Used to track intervals when staking rewards are not claimable +type Incentives struct { + incentives *avl.Tree // (incentiveId) => ExternalIncentive + + targetPoolPath string // The target pool path for this incentive collection + + unclaimablePeriods *UintTree // startTimestamp => endTimestamp +} + +// NewIncentives creates a new Incentives instance. +func NewIncentives(targetPoolPath string) Incentives { + result := Incentives{ + targetPoolPath: targetPoolPath, + unclaimablePeriods: NewUintTree(), + incentives: avl.NewTree(), + } + + // initial unclaimable period starts, as there cannot be any staked positions yet. + currentTimestamp := time.Now().Unix() + result.unclaimablePeriods.set(currentTimestamp, int64(0)) + return result +} + +// Get incentive by incentiveId +func (self *Incentives) Get(incentiveId string) (*ExternalIncentive, bool) { + return retrieveIncentive(self.incentives, incentiveId) +} + +// Get incentive by full incentiveId(by time) +func (self *Incentives) GetByIncentiveId(incentiveId string) (*ExternalIncentive, bool) { + return retrieveIncentive(self.incentives, incentiveId) +} + +func retrieveIncentive(tree *avl.Tree, id string) (*ExternalIncentive, bool) { + value, ok := tree.Get(id) + if !ok { + return nil, false + } + v, ok := value.(*ExternalIncentive) + if !ok { + panic("failed to cast value to *ExternalIncentive") + } + return v, true +} + +// Get all incentives that is active in given [startTimestamp, endTimestamp) +func (self *Incentives) GetAllInTimestamps(startTimestamp, endTimestamp int64) map[string]*ExternalIncentive { + incentives := make(map[string]*ExternalIncentive) + + // Iterate all incentives that has start timestamp less than endTimestamp + self.incentives.Iterate("", "", func(key string, value any) bool { + incentive, ok := value.(*ExternalIncentive) + if !ok { + return false + } + + // incentive is not active + if incentive.startTimestamp > endTimestamp || incentive.endTimestamp < startTimestamp { + return false + } + + incentives[incentive.incentiveId] = incentive + + return false + }) + + return incentives +} + +// Create a new external incentive +// Panics if the incentive already exists. +func (self *Incentives) create( + creator std.Address, + incentive *ExternalIncentive, +) { + self.incentives.Set(incentive.incentiveId, incentive) +} + +// starts incentive unclaimable period for this pool +func (self *Incentives) update( + creator std.Address, + incentive *ExternalIncentive, +) { + self.incentives.Set(incentive.incentiveId, incentive) +} + +// starts incentive unclaimable period for this pool +func (self *Incentives) startUnclaimablePeriod(startTimestamp int64) { + self.unclaimablePeriods.set(startTimestamp, int64(0)) +} + +// ends incentive unclaimable period for this pool +// ignores if currently not in unclaimable period +func (self *Incentives) endUnclaimablePeriod(endTimestamp int64) { + startTimestamp := int64(0) + self.unclaimablePeriods.ReverseIterate(0, endTimestamp, func(key int64, value any) bool { + if value.(int64) != 0 { + // Already ended, no need to update + // keeping startTimestamp as 0 to indicate this + return true + } + startTimestamp = key + return true + }) + + if startTimestamp == 0 { + // No ongoing unclaimable period found + return + } + + if startTimestamp == endTimestamp { + self.unclaimablePeriods.remove(startTimestamp) + } else { + self.unclaimablePeriods.set(startTimestamp, endTimestamp) + } +} + +// calculate unclaimable reward by checking unclaimable periods +func (self *Incentives) calculateUnclaimableReward(incentiveId string) int64 { + incentive, ok := self.GetByIncentiveId(incentiveId) + if !ok { + return 0 + } + + timeDiff := int64(0) + + // Find unclaimable periods that end before or at incentive start + self.unclaimablePeriods.ReverseIterate(0, incentive.startTimestamp, func(key int64, value any) bool { + startTimestamp := key + endTimestamp := value.(int64) + if endTimestamp == 0 { + endTimestamp = incentive.endTimestamp + } + + if endTimestamp <= incentive.startTimestamp { + return true + } + + // Calculate duration of unclaimable period that overlaps with incentive period + duration := calculateUnClaimableDuration( + startTimestamp, + endTimestamp, + incentive.startTimestamp, + incentive.endTimestamp, + ) + + timeDiff += duration + + return true + }) + + // Find unclaimable periods that start within incentive period + self.unclaimablePeriods.Iterate(incentive.startTimestamp, incentive.endTimestamp, func(key int64, value any) bool { + startTimestamp := key + endTimestamp, ok := value.(int64) + if !ok { + panic("failed to cast value to int64") + } + + if endTimestamp == 0 { + endTimestamp = incentive.endTimestamp + } + + // Calculate duration of unclaimable period that overlaps with incentive period + duration := calculateUnClaimableDuration( + startTimestamp, + endTimestamp, + incentive.startTimestamp, + incentive.endTimestamp, + ) + + timeDiff += duration + + return false + }) + + return timeDiff * incentive.rewardPerSecond +} + +// calculateUnClaimableDuration calculates the duration of overlap between an unclaimable period and incentive period +func calculateUnClaimableDuration(unclaimableStart, unclaimableEnd, incentiveStartTimestamp, incentiveEndTimestamp int64) int64 { + // Use later timestamp between unclaimable start and incentive start + startTime := unclaimableStart + if startTime < incentiveStartTimestamp { + startTime = incentiveStartTimestamp + } + + // Use earlier timestamp between unclaimable end and incentive end + endTime := unclaimableEnd + if endTime > incentiveEndTimestamp { + endTime = incentiveEndTimestamp + } + + // Return 0 if no overlap + if endTime < startTime { + return 0 + } + + // Calculate overlap duration + return endTime - startTime +} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_pool.gno b/contract/r/gnoswap/v1/staker/reward_calculation_pool.gno new file mode 100644 index 0000000..3046673 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/reward_calculation_pool.gno @@ -0,0 +1,523 @@ +package staker + +import ( + "time" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" +) + +var ( + // Q128 is 2^128 + q128 = u256.MustFromDecimal("340282366920938463463374607431768211456") + // Q192 is 2^192 + q192 = u256.MustFromDecimal("6277101735386680763835789423207666416102355444464034512896") + + // pools is the global pool storage + pools *Pools +) + +func init() { + pools = NewPools() +} + +// Pools represents the global pool storage +type Pools struct { + tree *avl.Tree // string poolPath -> pool +} + +func NewPools() *Pools { + return &Pools{ + tree: avl.NewTree(), + } +} + +// Get returns the pool for the given poolPath +func (self *Pools) Get(poolPath string) (*Pool, bool) { + v, ok := self.tree.Get(poolPath) + if !ok { + return nil, false + } + p, ok := v.(*Pool) + if !ok { + panic(ufmt.Sprintf("failed to cast v to *Pool: %T", v)) + } + return p, true +} + +// GetOrCreate returns the pool for the given poolPath, or creates a new pool if it does not exist +func (self *Pools) GetOrCreate(poolPath string) *Pool { + pool, ok := self.Get(poolPath) + if !ok { + pool = NewPool(poolPath, time.Now().Unix()) + self.set(poolPath, pool) + } + return pool +} + +// set sets the pool for the given poolPath +func (self *Pools) set(poolPath string, pool *Pool) { + self.tree.Set(poolPath, pool) +} + +// Has returns true if the pool exists for the given poolPath +func (self *Pools) Has(poolPath string) bool { + return self.tree.Has(poolPath) +} + +func (self *Pools) IterateAll(fn func(key string, pool *Pool) bool) { + self.tree.Iterate("", "", func(key string, value any) bool { + return fn(key, value.(*Pool)) + }) +} + +// Pool is a struct for storing an incentivized pool information +// Each pool stores Incentives and Ticks associated with it. +// +// Fields: +// - poolPath: The path of the pool. +// +// - currentStakedLiquidity: +// The current total staked liquidity of the in-range positions for the pool. +// Updated when tick cross happens or stake/unstake happens. +// Used to calculate the global reward ratio accumulation or +// decide whether to enter/exit unclaimable period. +// +// - lastUnclaimableTime: +// The time at which the unclaimable period started. +// Set to 0 when the pool is not in an unclaimable period. +// +// - unclaimableAcc: +// The accumulated undisributed unclaimable reward. +// Reset to 0 when processUnclaimableReward is called and sent to community pool. +// +// - rewardCache: +// The cached per-second reward emitted for this pool. +// Stores new entry only when the reward is changed. +// PoolTier.cacheReward() updates this. +// +// - incentives: The external incentives associated with the pool. +// +// - ticks: The Ticks associated with the pool. +// +// - globalRewardRatioAccumulation: +// Global ratio of Time / TotalStake accumulation(since the pool creation) +// Stores new entry only when tick cross or stake/unstake happens. +// It is used to calculate the reward for a staked position at certain time. +// +// - historicalTick: +// The historical tick for the pool at a given time. +// It does not reflect the exact tick at the timestamp, +// but it provides correct ordering for the staked position's ticks. +// Therefore, you should not compare it for equality, only for ordering. +// Set when tick cross happens or a new position is created. +type Pool struct { + poolPath string + + stakedLiquidity *UintTree // uint64 timestamp -> *u256.Uint(Q128) + + lastUnclaimableTime int64 + unclaimableAcc int64 + + rewardCache *UintTree // uint64 timestamp -> int64 gnsReward + + incentives Incentives + + ticks Ticks // int32 tickId -> Tick tick + + globalRewardRatioAccumulation *UintTree // uint64 timestamp -> *u256.Uint(Q128) rewardRatioAccumulation + + historicalTick *UintTree // uint64 timestamp -> int32 tickId +} + +// NewPool creates a new pool with the given poolPath and currentHeight. +func NewPool(poolPath string, currentTime int64) *Pool { + pool := &Pool{ + poolPath: poolPath, + stakedLiquidity: NewUintTree(), + lastUnclaimableTime: currentTime, + unclaimableAcc: 0, + rewardCache: NewUintTree(), + incentives: NewIncentives(poolPath), + ticks: NewTicks(), + globalRewardRatioAccumulation: NewUintTree(), + historicalTick: NewUintTree(), + } + + pool.globalRewardRatioAccumulation.set(currentTime, u256.Zero()) + pool.rewardCache.set(currentTime, int64(0)) + pool.stakedLiquidity.set(currentTime, u256.Zero()) + + return pool +} + +// Get the latest global reward ratio accumulation in [0, currentTime] range. +// Returns the time and the accumulation. +func (self *Pool) CurrentGlobalRewardRatioAccumulation(currentTime int64) (time int64, acc *u256.Uint) { + self.globalRewardRatioAccumulation.ReverseIterate(0, currentTime, func(key int64, value any) bool { + time = key + v, ok := value.(*u256.Uint) + if !ok { + panic(ufmt.Sprintf("failed to cast value to *u256.Uint: %T", value)) + } + acc = v + return true + }) + if acc == nil { + panic("should not happen, globalRewardRatioAccumulation must be set when pool is created") + } + return +} + +// Get the latest tick in [0, currentTime] range. +// Returns the tick. +func (self *Pool) CurrentTick(currentTime int64) (tick int32) { + self.historicalTick.ReverseIterate(0, currentTime, func(key int64, value any) bool { + res, ok := value.(int32) + if !ok { + panic(ufmt.Sprintf("failed to cast value to int32: %T", value)) + } + tick = res + return true + }) + return +} + +func (self *Pool) CurrentStakedLiquidity(currentTime int64) (liquidity *u256.Uint) { + self.stakedLiquidity.ReverseIterate(0, currentTime, func(key int64, value any) bool { + res, ok := value.(*u256.Uint) + if !ok { + panic(ufmt.Sprintf("failed to cast value to *u256.Uint: %T", value)) + } + liquidity = res + return true + }) + return +} + +// IsExternallyIncentivizedPool returns true if the pool has any external incentives. +func (self *Pool) IsExternallyIncentivizedPool() bool { + return self.incentives.incentives.Size() > 0 +} + +// Get the latest reward in [0, currentTime] range. +// Returns the reward. +func (self *Pool) CurrentReward(currentTime int64) (reward int64) { + self.rewardCache.ReverseIterate(0, currentTime, func(key int64, value any) bool { + res, ok := value.(int64) + if !ok { + panic(ufmt.Sprintf("failed to cast value to int64: %T", value)) + } + reward = res + return true + }) + return +} + +// cacheReward sets the current reward for the pool +// If the pool is in unclaimable period, it will end the unclaimable period, updates the reward, and start the unclaimable period again. +func (self *Pool) cacheReward(currentTime int64, currentTierReward int64) { + oldTierReward := self.CurrentReward(currentTime) + if oldTierReward == currentTierReward { + return + } + + isInUnclaimable := self.CurrentStakedLiquidity(currentTime).IsZero() + if isInUnclaimable { + self.endUnclaimablePeriod(currentTime) + } + + self.rewardCache.set(currentTime, currentTierReward) + + if isInUnclaimable { + self.startUnclaimablePeriod(currentTime) + } +} + +// cacheInternalReward caches the current emission and updates the global reward ratio accumulation. +func (self *Pool) cacheInternalReward(currentTime int64, currentEmission int64) { + self.cacheReward(currentTime, currentEmission) + + currentStakedLiquidity := self.CurrentStakedLiquidity(currentTime) + if currentStakedLiquidity.IsZero() { + self.endUnclaimablePeriod(currentTime) + self.startUnclaimablePeriod(currentTime) + } + + self.updateGlobalRewardRatioAccumulation(currentTime, currentStakedLiquidity) +} + +func (self *Pool) calculateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint { + oldAccTime, oldAcc := self.CurrentGlobalRewardRatioAccumulation(currentTime) + timeDiff := currentTime - oldAccTime + if timeDiff == 0 { + return oldAcc.Clone() + } + if timeDiff < 0 { + panic("time cannot go backwards") + } + + if currentStakedLiquidity.IsZero() { + return oldAcc.Clone() + } + + acc := u256.MulDiv( + u256.NewUintFromInt64(timeDiff), + q128, + currentStakedLiquidity, + ) + return u256.Zero().Add(oldAcc, acc) +} + +// updateGlobalRewardRatioAccumulation updates the global reward ratio accumulation and returns the new accumulation. +func (self *Pool) updateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint { + newAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, currentStakedLiquidity) + + self.globalRewardRatioAccumulation.set(currentTime, newAcc) + return newAcc +} + +// RewardState is a struct for storing the intermediate state for reward calculation. +type RewardState struct { + pool *Pool + deposit *Deposit + + // accumulated rewards for each warmup + rewards []int64 + penalties []int64 +} + +// RewardStateOf initializes a new RewardState for the given deposit. +func (self *Pool) RewardStateOf(deposit *Deposit) *RewardState { + result := &RewardState{ + pool: self, + deposit: deposit, + rewards: make([]int64, len(deposit.warmups)), + penalties: make([]int64, len(deposit.warmups)), + } + + for i := range result.rewards { + result.rewards[i] = 0 + result.penalties[i] = 0 + } + + return result +} + +// calculateInternalReward calculates the internal reward for the deposit. +// It calls rewardPerWarmup for each rewardCache interval, applies warmup, and returns the rewards and penalties. +func (self *RewardState) calculateInternalReward(startTime, endTime int64) ([]int64, []int64) { + currentReward := self.pool.CurrentReward(startTime) + self.pool.rewardCache.Iterate(startTime, endTime, func(key int64, value any) bool { + // we calculate per-position reward + err := self.rewardPerWarmup(startTime, key, currentReward) + if err != nil { + panic(err) + } + + reward, ok := value.(int64) + if !ok { + panic(ufmt.Sprintf("failed to cast value to int64: %T", value)) + } + startTime = key + currentReward = reward + return false + }) + + if startTime < endTime { + err := self.rewardPerWarmup(startTime, endTime, currentReward) + if err != nil { + panic(err) + } + } + + self.applyWarmup() + + return self.rewards, self.penalties +} + +// calculateExternalReward calculates the external reward for the deposit. +// It calls rewardPerWarmup for startTime to endTime(clamped to the incentive period), applies warmup and returns the rewards and penalties. +func (self *RewardState) calculateExternalReward(startTime, endTime int64, incentive *ExternalIncentive) ([]int64, []int64) { + if startTime < self.deposit.lastCollectTime { + // This must not happen, but adding some guards just in case. + startTime = self.deposit.lastCollectTime + } + + if endTime < incentive.startTimestamp { + return nil, nil // Not started yet + } + + if startTime < incentive.startTimestamp { + startTime = incentive.startTimestamp + } + + if endTime > incentive.endTimestamp { + endTime = incentive.endTimestamp + } + + if startTime > incentive.endTimestamp { + return nil, nil // Already ended + } + + err := self.rewardPerWarmup(startTime, endTime, incentive.rewardPerSecond) + if err != nil { + panic(err) + } + + self.applyWarmup() + + return self.rewards, self.penalties +} + +// applyWarmup applies the warmup to the rewards and calculate penalties. +func (self *RewardState) applyWarmup() { + for i, warmup := range self.deposit.warmups { + refactorReward := self.rewards[i] + self.rewards[i] = safeMulInt64(refactorReward, int64(warmup.WarmupRatio)) / 100 + self.penalties[i] = safeSubInt64(refactorReward, self.rewards[i]) + } +} + +// rewardPerWarmup calculates the reward for each warmup, adds to the RewardState's rewards array. +func (self *RewardState) rewardPerWarmup(startTime, endTime int64, rewardPerSecond int64) error { + for i, warmup := range self.deposit.warmups { + if startTime >= warmup.NextWarmupTime { + // passed the warmup + continue + } + + if endTime < warmup.NextWarmupTime { + rewardAcc := self.pool.CalculateRewardForPosition( + startTime, + self.pool.CurrentTick(startTime), + endTime, + self.pool.CurrentTick(endTime), + self.deposit, + ) + + rewardAcc = u256.Zero().Mul(rewardAcc, self.deposit.liquidity) + rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128) + self.rewards[i] += safeConvertToInt64(rewardAcc) + + break + } + + rewardAcc := self.pool.CalculateRewardForPosition( + startTime, + self.pool.CurrentTick(startTime), + warmup.NextWarmupTime, + self.pool.CurrentTick(warmup.NextWarmupTime), + self.deposit, + ) + + rewardAcc = u256.Zero().Mul(rewardAcc, self.deposit.liquidity) + rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128) + self.rewards[i] += safeConvertToInt64(rewardAcc) + + startTime = warmup.NextWarmupTime + } + + return nil +} + +// modifyDeposit updates the pool's staked liquidity and returns the new staked liquidity. +// updates when there is a change in the staked liquidity(tick cross, stake, unstake) +func (self *Pool) modifyDeposit(delta *i256.Int, currentTime int64, nextTick int32) *u256.Uint { + // update staker side pool info + lastStakedLiquidity := self.CurrentStakedLiquidity(currentTime) + deltaApplied := liquidityMathAddDelta(lastStakedLiquidity, delta) + result := self.updateGlobalRewardRatioAccumulation(currentTime, lastStakedLiquidity) + + // historical tick does NOT actually reflect the tick at the timestamp, but it provides correct ordering for the staked positions + // because TickCrossHook is assured to be called for the staked-initialized ticks + self.historicalTick.set(currentTime, nextTick) + + switch deltaApplied.Sign() { + case -1: + panic("stakedLiquidity is less than 0, should not happen") + case 0: + if lastStakedLiquidity.Sign() == 1 { + // StakedLiquidity moved from positive to zero, start unclaimable period + self.startUnclaimablePeriod(currentTime) + self.incentives.startUnclaimablePeriod(currentTime) + } + case 1: + if lastStakedLiquidity.Sign() == 0 { + // StakedLiquidity moved from zero to positive, end unclaimable period + self.endUnclaimablePeriod(currentTime) + self.incentives.endUnclaimablePeriod(currentTime) + } + } + + self.stakedLiquidity.set(currentTime, deltaApplied) + + return result +} + +// startUnclaimablePeriod starts the unclaimable period. +func (self *Pool) startUnclaimablePeriod(currentTime int64) { + if self.lastUnclaimableTime == 0 { + // We set only if it's the first time entering(0 indicates not set yet) + self.lastUnclaimableTime = currentTime + } +} + +// endUnclaimablePeriod ends the unclaimable period. +// Accumulates to unclaimableAcc and resets lastUnclaimableTime to 0. +func (self *Pool) endUnclaimablePeriod(currentTime int64) { + if self.lastUnclaimableTime == 0 { + // This should not happen, but guarding just in case + return + } + unclaimableDuration := currentTime - self.lastUnclaimableTime + self.unclaimableAcc += unclaimableDuration * self.CurrentReward(self.lastUnclaimableTime) + self.lastUnclaimableTime = 0 +} + +// processUnclaimableReward processes the unclaimable reward and returns the accumulated reward. +// It resets unclaimableAcc to 0 and updates lastUnclaimableTime to endTime. +func (self *Pool) processUnclaimableReward(poolTier *PoolTier, endTime int64) int64 { + internalUnClaimable := self.unclaimableAcc + self.unclaimableAcc = 0 + self.lastUnclaimableTime = endTime + return internalUnClaimable +} + +// Calculates reward for a position *without* considering debt or warmup +// It calculates the theoretical total reward for the position if it has been staked since the pool creation +func (self *Pool) CalculateRawRewardForPosition(currentTime int64, currentTick int32, deposit *Deposit) *u256.Uint { + var rewardAcc *u256.Uint + + globalAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, self.CurrentStakedLiquidity(currentTime)) + lowerAcc := self.ticks.Get(deposit.tickLower).CurrentOutsideAccumulation(currentTime) + upperAcc := self.ticks.Get(deposit.tickUpper).CurrentOutsideAccumulation(currentTime) + if currentTick < deposit.tickLower { + rewardAcc = u256.Zero().Sub(lowerAcc, upperAcc) + } else if currentTick >= deposit.tickUpper { + rewardAcc = u256.Zero().Sub(upperAcc, lowerAcc) + } else { + rewardAcc = u256.Zero().Sub(globalAcc, lowerAcc) + rewardAcc = rewardAcc.Sub(rewardAcc, upperAcc) + } + + return rewardAcc +} + +// Calculate actual reward in [startTime, endTime) for a position by +// subtracting the startTime's raw reward from the endTime's raw reward +func (self *Pool) CalculateRewardForPosition( + startTime int64, + startTick int32, + endTime int64, + endTick int32, + deposit *Deposit, +) *u256.Uint { + rewardAcc := self.CalculateRawRewardForPosition(endTime, endTick, deposit) + debtAcc := self.CalculateRawRewardForPosition(startTime, startTick, deposit) + + return u256.Zero().Sub(rewardAcc, debtAcc) +} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_pool_tier.gno b/contract/r/gnoswap/v1/staker/reward_calculation_pool_tier.gno new file mode 100644 index 0000000..e531590 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/reward_calculation_pool_tier.gno @@ -0,0 +1,326 @@ +package staker + +import ( + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" +) + +const ( + AllTierCount = 4 // 0, 1, 2, 3 + Tier1 = 1 + Tier2 = 2 + Tier3 = 3 +) + +// 100%, 0%, 0% if no tier2 and tier3 +// 80%, 0%, 20% if no tier2 +// 70%, 30%, 0% if no tier3 +// 50%, 30%, 20% if has tier2 and tier3 +type TierRatio struct { + Tier1 uint64 + Tier2 uint64 + Tier3 uint64 +} + +// TierRatioFromCounts calculates the ratio distribution for each tier based on pool counts. +// +// Parameters: +// - tier1Count (uint64): Number of pools in tier 1. +// - tier2Count (uint64): Number of pools in tier 2. +// - tier3Count (uint64): Number of pools in tier 3. +// +// Returns: +// - TierRatio: The ratio distribution across tier 1, 2, and 3, scaled up by 100. +func TierRatioFromCounts(tier1Count, tier2Count, tier3Count uint64) TierRatio { + // tier1 always exists + if tier2Count == 0 && tier3Count == 0 { + return TierRatio{ + Tier1: 100, + Tier2: 0, + Tier3: 0, + } + } + if tier2Count == 0 { + return TierRatio{ + Tier1: 80, + Tier2: 0, + Tier3: 20, + } + } + if tier3Count == 0 { + return TierRatio{ + Tier1: 70, + Tier2: 30, + Tier3: 0, + } + } + return TierRatio{ + Tier1: 50, + Tier2: 30, + Tier3: 20, + } +} + +// Get returns the ratio(scaled up by 100) for the given tier. +func (self *TierRatio) Get(tier uint64) uint64 { + switch tier { + case Tier1: + return self.Tier1 + case Tier2: + return self.Tier2 + case Tier3: + return self.Tier3 + default: + panic(makeErrorWithDetails( + errInvalidPoolTier, ufmt.Sprintf("unsupported tier(%d)", tier))) + } +} + +// PoolTier manages pool counts, ratios, and rewards for different tiers. +// +// Fields: +// - membership: Tracks which tier a pool belongs to (poolPath -> blockNumber -> tier). +// +// Methods: +// - CurrentCount: Returns the current count of pools in a tier at a specific timestamp. +// - CurrentRatio: Returns the current ratio for a tier at a specific timestamp. +// - CurrentTier: Returns the tier of a specific pool at a given timestamp. +// - CurrentReward: Retrieves the reward for a tier at a specific timestamp. +// - changeTier: Updates the tier of a pool and recalculates ratios. +type PoolTier struct { + membership *avl.Tree // poolPath -> tier(1, 2, 3) + + tierRatio TierRatio + + counts [AllTierCount]uint64 + + lastRewardCacheTimestamp int64 + lastRewardCacheHeight int64 + + currentEmission int64 + + // returns current emission. + getEmission func() int64 + // Returns a list of halving timestamps and their emission amounts within the interval [start, end) in ascending order. + // The first return value is a list of timestamps where halving occurs. + // The second return value is a list of emission amounts corresponding to each halving timestamp. + getHalvingBlocksInRange func(start, end int64) ([]int64, []int64) +} + +// NewPoolTier creates a new PoolTier instance with single initial 1 tier pool. +// +// Parameters: +// - pools: The pool collection. +// - currentHeight: The current block height. +// - initialPoolPath: The path of the initial pool. +// - getEmission: A function that returns the current emission to the staker contract. +// - getHalvingBlocksInRange: A function that returns a list of halving blocks within the interval [start, end) in ascending order. +// +// Returns: +// - *PoolTier: The new PoolTier instance. +func NewPoolTier(pools *Pools, currentHeight int64, currentTime int64, initialPoolPath string, getEmission func() int64, getHalvingBlocksInRange func(start, end int64) ([]int64, []int64)) *PoolTier { + result := &PoolTier{ + membership: avl.NewTree(), + tierRatio: TierRatioFromCounts(1, 0, 0), + lastRewardCacheTimestamp: currentTime + 1, + lastRewardCacheHeight: currentHeight + 1, + getEmission: getEmission, + getHalvingBlocksInRange: getHalvingBlocksInRange, + currentEmission: getEmission(), + } + + pools.set(initialPoolPath, NewPool(initialPoolPath, currentTime+1)) + result.changeTier(currentHeight+1, currentTime+1, pools, initialPoolPath, 1) + return result +} + +// CurrentReward returns the current per-pool reward for the given tier. +func (self *PoolTier) CurrentReward(tier uint64) int64 { + currentEmission := self.getEmission() + tierRatio := int64(self.tierRatio.Get(tier)) + count := int64(self.CurrentCount(tier)) + + // Check for zero count to prevent division by zero + if count == 0 { + return 0 + } + + return currentEmission * tierRatio / count / 100 +} + +// CurrentCount returns the current count of pools in the given tier. +func (self *PoolTier) CurrentCount(tier uint64) int { + if tier >= AllTierCount { + return 0 + } + return int(self.counts[tier]) +} + +// CurrentAllTierCounts returns the current count of pools in each tier. +func (self *PoolTier) CurrentAllTierCounts() []uint64 { + out := make([]uint64, AllTierCount) + copy(out, self.counts[:]) + return out // returning snapshot +} + +// CurrentTier returns the tier of the given pool. +func (self *PoolTier) CurrentTier(poolPath string) (tier uint64) { + if tierI, ok := self.membership.Get(poolPath); !ok { + return 0 + } else { + tier, ok = tierI.(uint64) + if !ok { + panic("failed to cast tier to uint64") + } + return + } +} + +// changeTier updates the tier of a pool, recalculates ratios, and applies +// updated per-pool reward to each of the pools. +func (self *PoolTier) changeTier(currentHeight int64, currentTime int64, pools *Pools, poolPath string, nextTier uint64) { + self.cacheReward(currentHeight, currentTime, pools) + // same as prev. no need to update + currentTier := self.CurrentTier(poolPath) + if currentTier == nextTier { + // no change, return + return + } + + // decrement count from current tier if it exists + if currentTier > 0 { + if self.counts[currentTier] == 0 { + panic("counts underflow: removing from empty tier") + } + self.counts[currentTier]-- + } + + if nextTier == 0 { + // removed from the tier + self.membership.Remove(poolPath) + pool, ok := pools.Get(poolPath) + if !ok { + panic("changeTier: pool not found") + } + pool.cacheReward(currentTime, int64(0)) + } else { + // handle all move/add operations + self.membership.Set(poolPath, nextTier) + self.counts[nextTier]++ + } + + self.tierRatio = TierRatioFromCounts(self.counts[Tier1], self.counts[Tier2], self.counts[Tier3]) + currentEmission := self.getEmission() + + // Cache updated reward for each tiered pool + self.membership.Iterate("", "", func(key string, value any) bool { + pool, ok := pools.Get(key) + if !ok { + panic("changeTier: pool not found") + } + tier, ok := value.(uint64) + if !ok { + panic("failed to cast value to uint64") + } + + tierRatio := int64(self.tierRatio.Get(tier)) + tierCount := int64(self.counts[tier]) + if tierCount == 0 { + return false // Skip if no pools in tier + } + + poolReward := currentEmission * tierRatio / tierCount / 100 + pool.cacheReward(currentTime, poolReward) + return false + }) + + self.currentEmission = currentEmission +} + +// cacheReward MUST be called before calculating any position reward +// cacheReward updates the reward cache for each pools, accounting for any halving event in between the last cached height and the current height. +func (self *PoolTier) cacheReward(currentHeight int64, currentTimestamp int64, pools *Pools) { + lastTimestamp := self.lastRewardCacheTimestamp + + if currentTimestamp <= lastTimestamp { + // no need to check + return + } + + // find halving blocks in range + halvingTimestamps, halvingEmissions := self.getHalvingBlocksInRange(lastTimestamp, currentTimestamp) + + if len(halvingTimestamps) == 0 { + self.applyCacheToAllPools(pools, currentTimestamp, self.currentEmission) + self.lastRewardCacheTimestamp = currentTimestamp + return + } + + for i, hvTimestamp := range halvingTimestamps { + emission := halvingEmissions[i] + // caching: [lastTimestamp, hvTimestamp) + self.applyCacheToAllPools(pools, hvTimestamp, emission) + + // halve emissions when halvingBlock is reached + self.currentEmission = emission + } + + // remaining range [lastTimestamp, currentTimestamp) + self.applyCacheToAllPools(pools, currentTimestamp, self.currentEmission) + + // update lastRewardCacheHeight and currentEmission + self.lastRewardCacheTimestamp = currentTimestamp + self.lastRewardCacheHeight = currentHeight +} + +// applyCacheToAllPools applies the cached reward to all tiered pools. +func (self *PoolTier) applyCacheToAllPools(pools *Pools, currentTimestamp, emissionInThisInterval int64) { + // calculate denominator and number of pools in each tier + counts := self.CurrentAllTierCounts() + + // apply cache to all pools + self.membership.Iterate("", "", func(key string, value any) bool { + tierNum := value.(uint64) + pool, ok := pools.Get(key) + if !ok { + return false + } + + // Calculate real reward with overflow check + tierRatio := int64(self.tierRatio.Get(tierNum)) + tierCount := int64(counts[tierNum]) + + if tierCount == 0 { + return false // Skip if no pools in tier + } + + reward := emissionInThisInterval * tierRatio / 100 / tierCount + // accumulate the reward for the interval (startBlock to endBlock) in the Pool + pool.cacheInternalReward(currentTimestamp, reward) + return false + }) +} + +// IsInternallyIncentivizedPool returns true if the pool is in a tier. +func (self *PoolTier) IsInternallyIncentivizedPool(poolPath string) bool { + return self.CurrentTier(poolPath) > 0 +} + +func (self *PoolTier) CurrentRewardPerPool(poolPath string) int64 { + emission := self.getEmission() + counts := self.CurrentAllTierCounts() + tierNum := self.CurrentTier(poolPath) + + if tierNum == 0 { + return 0 // Pool not in any tier + } + + tierRatio := int64(self.tierRatio.Get(tierNum)) + tierCount := int64(counts[tierNum]) + + if tierCount == 0 { + return 0 // No pools in tier + } + + return emission * tierRatio / 100 / tierCount +} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_tick.gno b/contract/r/gnoswap/v1/staker/reward_calculation_tick.gno new file mode 100644 index 0000000..9797a10 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/reward_calculation_tick.gno @@ -0,0 +1,364 @@ +package staker + +import ( + "strconv" + "strings" + + "gno.land/p/nt/avl" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + pl "gno.land/r/gnoswap/v1/pool" +) + +var ( + zeroUint256 = u256.Zero() + zeroInt256 = i256.Zero() +) + +// Global batch processor for current swap +var currentSwapBatch *SwapBatchProcessor + +// EncodeInt takes an int32 and returns a zero-padded decimal string +// with up to 10 digits for the absolute value. +// If the number is negative, the '-' sign comes first, followed by zeros, then digits. +func EncodeInt(num int32) string { + // Convert the absolute value to a decimal string. + absValue := int64(num) + isNegative := false + if num < 0 { + isNegative = true + absValue = -absValue // Safely negate into int64 to avoid overflow. + } + + s := strconv.FormatInt(absValue, 10) + + // Zero-pad to a total of 10 digits for the absolute value. + // (The '-' sign will be added later if needed.) + zerosNeeded := 10 - len(s) + if zerosNeeded < 0 { + zerosNeeded = 0 + } + + padded := strings.Repeat("0", zerosNeeded) + s + + // If the original number was negative, prepend '-'. + if isNegative { + return "-" + padded + } + return padded +} + +// Tick mapping for each pool +type Ticks struct { + tree *avl.Tree // int32 tickId -> tick +} + +func NewTicks() Ticks { + return Ticks{ + tree: avl.NewTree(), + } +} + +func (self *Ticks) Get(tickId int32) *Tick { + v, ok := self.tree.Get(EncodeInt(tickId)) + if !ok { + tick := &Tick{ + id: tickId, + stakedLiquidityGross: u256.Zero(), + stakedLiquidityDelta: i256.Zero(), + outsideAccumulation: NewUintTree(), + } + self.tree.Set(EncodeInt(tickId), tick) + return tick + } + + tick, ok := v.(*Tick) + if !ok { + panic("failed to cast value to *Tick") + } + return tick +} + +func (self *Ticks) set(tickId int32, tick *Tick) { + if tick.stakedLiquidityGross.IsZero() { + self.tree.Remove(EncodeInt(tickId)) + return + } + self.tree.Set(EncodeInt(tickId), tick) +} + +func (self *Ticks) Has(tickId int32) bool { + return self.tree.Has(EncodeInt(tickId)) +} + +// Tick represents the state of a specific tick in a pool. +// +// Fields: +// - id (int32): The ID of the tick. +// - stakedLiquidityGross (*u256.Uint): Total gross staked liquidity at this tick. +// - stakedLiquidityDelta (*i256.Int): Net change in staked liquidity at this tick. +// - outsideAccumulation (*UintTree): RewardRatioAccumulation outside the tick. +type Tick struct { + id int32 + + // conceptually equal with Pool.liquidityGross but only for the staked positions + stakedLiquidityGross *u256.Uint + + // conceptually equal with Pool.liquidityNet but only for the staked positions + stakedLiquidityDelta *i256.Int + + // currentOutsideAccumulation is the accumulation of the time / TotalStake outside the tick. + // It is calculated by subtracting the current tick's currentOutsideAccumulation from the global reward ratio accumulation. + outsideAccumulation *UintTree // timestamp -> *u256.Uint +} + +// CurrentOutsideAccumulation returns the latest outside accumulation for the tick +func (self *Tick) CurrentOutsideAccumulation(timestamp int64) *u256.Uint { + acc := u256.Zero() + self.outsideAccumulation.ReverseIterate(0, timestamp, func(key int64, value any) bool { + acc = value.(*u256.Uint) + return true + }) + if acc == nil { + acc = u256.Zero() + } + return acc +} + +// modifyDepositLower updates the tick's liquidity info by treating the deposit as a lower tick +func (self *Tick) modifyDepositLower(currentTime int64, liquidity *i256.Int) { + // update staker side tick info + self.stakedLiquidityGross = liquidityMathAddDelta(self.stakedLiquidityGross, liquidity) + if self.stakedLiquidityGross.Lt(zeroUint256) { + panic("stakedLiquidityGross is negative") + } + self.stakedLiquidityDelta = i256.Zero().Add(self.stakedLiquidityDelta, liquidity) +} + +// modifyDepositUpper updates the tick's liquidity info by treating the deposit as an upper tick +func (self *Tick) modifyDepositUpper(currentTime int64, liquidity *i256.Int) { + self.stakedLiquidityGross = liquidityMathAddDelta(self.stakedLiquidityGross, liquidity) + if self.stakedLiquidityGross.Lt(zeroUint256) { + panic("stakedLiquidityGross is negative") + } + self.stakedLiquidityDelta = i256.Zero().Sub(self.stakedLiquidityDelta, liquidity) +} + +// updateCurrentOutsideAccumulation updates the tick's outside accumulation +// It "flips" the accumulation's inside/outside by subtracting the current outside accumulation from the global accumulation +func (self *Tick) updateCurrentOutsideAccumulation(timestamp int64, acc *u256.Uint) { + currentOutsideAccumulation := self.CurrentOutsideAccumulation(timestamp) + newOutsideAccumulation := u256.Zero().Sub(acc, currentOutsideAccumulation) + self.outsideAccumulation.set(timestamp, newOutsideAccumulation) +} + +// SwapTickCross stores information about a tick cross during a swap +// This struct is used to accumulate tick cross events during a single swap transaction +// for batch processing to optimize gas usage and computational efficiency +type SwapTickCross struct { + tickId int32 // The tick index that was crossed + zeroForOne bool // Direction of the swap (true: token0->token1, false: token1->token0) + delta *i256.Int // Pre-calculated liquidity delta for this tick cross +} + +// SwapBatchProcessor processes tick crosses in batch for a swap +// This processor accumulates all tick crosses that occur during a single swap +// and processes them together at the end, reducing redundant calculations +// and state updates that would occur with individual tick processing +type SwapBatchProcessor struct { + poolPath string // The pool path identifier for this swap + pool *Pool // Reference to the pool being swapped in + crosses []SwapTickCross // Accumulated tick crosses during the swap + timestamp int64 // Timestamp when the swap started + isActive bool // Flag to prevent accumulation after swap ends +} + +// swapStartHook is called when a swap starts +// This hook initializes the batch processor for accumulating tick crosses +func swapStartHook(pools *Pools) func(poolPath string, timestamp int64) { + return func(poolPath string, timestamp int64) { + func(cur realm) { + pool, ok := pools.Get(poolPath) + if !ok { + return + } + + // Initialize batch processor for this swap + // This will accumulate all tick crosses until swap completion + currentSwapBatch = &SwapBatchProcessor{ + poolPath: poolPath, + pool: pool, + crosses: make([]SwapTickCross, 0), // Pre-allocate slice for tick crosses + timestamp: timestamp, + isActive: true, // Enable accumulation mode + } + }(cross) + } +} + +// swapEndHook is called when a swap ends +// This hook processes all accumulated tick crosses in a single batch operation +// and cleans up the batch processor. The batch processing approach provides: +// 1. O(1) pool state updates instead of O(n) where n = number of tick crosses +// 2. Reduced computational overhead for reward calculations +// 3. Atomic processing ensuring consistency across all tick updates +func swapEndHook(pools *Pools) func(poolPath string) error { + return func(poolPath string) error { + return func(cur realm) error { + // Validate batch processor state + if currentSwapBatch == nil || !currentSwapBatch.isActive || currentSwapBatch.poolPath != poolPath { + return nil + } + + // Disable further accumulation + currentSwapBatch.isActive = false + + // Process all accumulated tick crosses in a single batch + // This is where the optimization happens - instead of processing + // each tick cross individually, we calculate cumulative effects + err := processBatchedTickCrosses() + if err != nil { + return err + } + + // Clean up batch processor + currentSwapBatch = nil + + return nil + }(cross) + } +} + +// tickCrossHook is called when a tick is crossed +// This hook implements intelligent routing between batch processing and immediate processing: +// - During swaps: accumulates tick crosses for batch processing at swap end +// - Outside swaps: processes tick crosses immediately for real-time updates +// The hybrid approach optimizes for both swap performance and non-swap responsiveness +func tickCrossHook(pools *Pools) func(poolPath string, tickId int32, zeroForOne bool, timestamp int64) { + return func(poolPath string, tickId int32, zeroForOne bool, timestamp int64) { + func(cur realm) { + pool, ok := pools.Get(poolPath) + if !ok { + return + } + + tick := pool.ticks.Get(tickId) + // Skip ticks with zero staked liquidity (no reward impact) + if tick.stakedLiquidityDelta.Sign() == 0 { + return + } + + // Batch processing path: accumulate tick crosses during active swap + if currentSwapBatch != nil && currentSwapBatch.isActive && currentSwapBatch.poolPath == poolPath { + // Pre-calculate liquidity delta with direction consideration + // zeroForOne swap: liquidity delta is negated (liquidity being removed from current tick) + liquidityDelta := tick.stakedLiquidityDelta + if zeroForOne { + liquidityDelta = i256.Zero().Neg(liquidityDelta) + } + + // Accumulate this tick cross for batch processing + currentSwapBatch.crosses = append(currentSwapBatch.crosses, SwapTickCross{ + tickId: tickId, + zeroForOne: zeroForOne, + delta: liquidityDelta, // Store pre-calculated delta for efficiency + }) + return + } + + // Immediate processing path: handle tick crosses outside of swap context + // This ensures real-time updates for non-swap operations (e.g., position modifications) + processTickCrossImmediate(pool, tick, tickId, zeroForOne, timestamp) + }(cross) + } +} + +// processTickCrossImmediate processes a single tick cross immediately +// This function handles individual tick crosses for non-swap operations +// where batch processing is not applicable (e.g., position modifications, liquidations) +func processTickCrossImmediate(pool *Pool, tick *Tick, tickId int32, zeroForOne bool, timestamp int64) { + // Calculate the effective tick position after crossing + // For zeroForOne swaps, liquidity becomes effective one tick lower + nextTick := tickId + if zeroForOne { + nextTick-- // Move to the lower tick where liquidity becomes active + } + + // Calculate liquidity delta with direction consideration + liquidityDelta := tick.stakedLiquidityDelta + if zeroForOne { + // Negate delta for zeroForOne direction (liquidity being removed from current range) + liquidityDelta = i256.Zero().Neg(liquidityDelta) + } + + // Update pool's cumulative deposit with the liquidity change + newAcc := pool.modifyDeposit(liquidityDelta, timestamp, nextTick) + + // Update the tick's outside accumulation for reward calculations + // This ensures proper reward distribution tracking across tick boundaries + tick.updateCurrentOutsideAccumulation(timestamp, newAcc) +} + +// processBatchedTickCrosses processes all accumulated tick crosses at once +// This is the core optimization function that processes multiple tick crosses in a single operation. +// Instead of updating pool state for each tick cross individually (O(n) operations), +// it calculates the cumulative effect and applies it once (O(1) pool updates + O(n) tick updates). +func processBatchedTickCrosses() error { + // Early exit for empty batches + if currentSwapBatch == nil || len(currentSwapBatch.crosses) == 0 { + return nil + } + + // Validate pool reference + if currentSwapBatch.pool == nil { + return errPoolNotFound + } + + batch := currentSwapBatch + timestamp := batch.timestamp + + // Phase 1: Calculate cumulative liquidity delta across all tick crosses + // This replaces multiple individual pool updates with a single cumulative update + cumulativeDelta := i256.Zero() + for _, tickCross := range batch.crosses { + newDelta := cumulativeDelta.Add(cumulativeDelta, tickCross.delta) + cumulativeDelta = newDelta + } + + // Phase 2: Determine the effective tick position for pool state update + // Use the last crossed tick as the reference point for cumulative changes + lastCross := batch.crosses[len(batch.crosses)-1] + lastTick := lastCross.tickId + if lastCross.zeroForOne { + lastTick-- // Adjust for zeroForOne direction + } + + // Phase 3: Apply cumulative changes to pool state in a single operation + // This is the key optimization - one pool update instead of many + newAcc := batch.pool.modifyDeposit(cumulativeDelta, timestamp, lastTick) + + // Phase 4: Update individual tick outside accumulations for reward tracking + // While we optimize pool updates, each tick still needs its accumulation updated + // for proper reward distribution calculations + for _, tickCross := range batch.crosses { + tick := batch.pool.ticks.Get(tickCross.tickId) + tick.updateCurrentOutsideAccumulation(timestamp, newAcc) + } + + return nil +} + +func setHooks() { + // Set tick cross hook for pool contract + pl.SetTickCrossHook(cross, tickCrossHook(pools)) + + // Set swap start/end hooks for batch processing + pl.SetSwapStartHook(cross, swapStartHook(pools)) + pl.SetSwapEndHook(cross, swapEndHook(pools)) +} + +func init() { + setHooks() +} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_types.gno b/contract/r/gnoswap/v1/staker/reward_calculation_types.gno new file mode 100644 index 0000000..103c700 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/reward_calculation_types.gno @@ -0,0 +1,117 @@ +package staker + +import ( + "strconv" + "strings" + + "gno.land/p/nt/avl" +) + +// EncodeUint converts a uint64 number into a zero-padded 20-character string. +// +// Parameters: +// - num (uint64): The number to encode. +// +// Returns: +// - string: A zero-padded string representation of the number. +// +// Example: +// Input: 12345 +// Output: "00000000000000012345" +func EncodeUint(num uint64) string { + // Convert the value to a decimal string. + s := strconv.FormatUint(num, 10) + + // Zero-pad to a total length of 20 characters. + zerosNeeded := 20 - len(s) + return strings.Repeat("0", zerosNeeded) + s +} + +func EncodeInt64(num int64) string { + s := strconv.FormatInt(num, 10) + zerosNeeded := 20 - len(s) + return strings.Repeat("0", zerosNeeded) + s +} + +// DecodeUint converts a zero-padded string back into a uint64 number. +// +// Parameters: +// - s (string): The zero-padded string. +// +// Returns: +// - uint64: The decoded number. +// +// Panics: +// - If the string cannot be parsed into a uint64. +// +// Example: +// Input: "00000000000000012345" +// Output: 12345 +func DecodeUint(s string) uint64 { + num, err := strconv.ParseUint(s, 10, 64) + if err != nil { + panic(err) + } + return num +} + +func DecodeInt64(s string) int64 { + num, err := strconv.ParseInt(s, 10, 64) + if err != nil { + panic(err) + } + return num +} + +// UintTree is a wrapper around an AVL tree for storing block timestamps as strings. +// Since block timestamps are defined as int64, we take int64 and convert it to uint64 for the tree. +// +// Methods: +// - Get: Retrieves a value associated with a uint64 key. +// - set: Stores a value with a uint64 key. +// - Has: Checks if a uint64 key exists in the tree. +// - remove: Removes a uint64 key and its associated value. +// - Iterate: Iterates over keys and values in a range. +// - ReverseIterate: Iterates in reverse order over keys and values in a range. +type UintTree struct { + tree *avl.Tree // blockTimestamp -> any +} + +// NewUintTree creates a new UintTree instance. +func NewUintTree() *UintTree { + return &UintTree{ + tree: avl.NewTree(), + } +} + +func (self *UintTree) Get(key int64) (any, bool) { + v, ok := self.tree.Get(EncodeInt64(key)) + if !ok { + return nil, false + } + return v, true +} + +func (self *UintTree) set(key int64, value any) { + self.tree.Set(EncodeInt64(key), value) +} + +func (self *UintTree) Has(key int64) bool { + return self.tree.Has(EncodeInt64(key)) +} + +func (self *UintTree) remove(key int64) { + self.tree.Remove(EncodeInt64(key)) +} + +func (self *UintTree) Iterate(start, end int64, fn func(key int64, value any) bool) { + self.tree.Iterate(EncodeInt64(start), EncodeInt64(end), func(key string, value any) bool { + return fn(DecodeInt64(key), value) + }) +} + +func (self *UintTree) ReverseIterate(start, end int64, fn func(key int64, value any) bool) { + self.tree.ReverseIterate(EncodeInt64(start), EncodeInt64(end), func(key string, value any) bool { + return fn(DecodeInt64(key), value) + }) +} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_warmup.gno b/contract/r/gnoswap/v1/staker/reward_calculation_warmup.gno new file mode 100644 index 0000000..efbabb9 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/reward_calculation_warmup.gno @@ -0,0 +1,118 @@ +package staker + +import ( + "math" + + "gno.land/p/nt/ufmt" + u256 "gno.land/p/gnoswap/uint256" +) + +type Warmup struct { + Index int + TimeDuration int64 + NextWarmupTime int64 // time when this warmup period ends + WarmupRatio uint64 +} + +// warmupTemplate defines the warmup periods for staking rewards. +// These parameters can be modified through governance via SetWarmUp. +var warmupTemplate []Warmup = DefaultWarmupTemplate() + +func DefaultWarmupTemplate() []Warmup { + secondsInDay := int64(86400) + secondsIn5Days := int64(5 * secondsInDay) + secondsIn10Days := int64(10 * secondsInDay) + secondsIn30Days := int64(30 * secondsInDay) + + // NextWarmupTime is set to 0 for template. + // They will be set by InstantiateWarmup() + return []Warmup{ + { + Index: 0, + TimeDuration: secondsIn5Days, + // NextWarmupTime will be set based on currentTime + // NextWarmupTime: currentTime + secondsIn5Days, + WarmupRatio: 30, + }, + { + Index: 1, + TimeDuration: secondsIn10Days, + // NextWarmupTime will be set based on currentTime + // NextWarmupTime: currentTime + secondsIn10Days, + WarmupRatio: 50, + }, + { + Index: 2, + TimeDuration: secondsIn30Days, + // NextWarmupTime will be set based on currentTime + // NextWarmupTime: currentTime + secondsIn30Days, + WarmupRatio: 70, + }, + { + Index: 3, + TimeDuration: math.MaxInt64, + // NextWarmupTime will be set to math.MaxInt64 + // NextWarmupTime: math.MaxInt64, + WarmupRatio: 100, + }, + } +} + +// expected to be called by governance +func modifyWarmup(index int, timeDuration int64) { + if index >= len(warmupTemplate) { + panic(ufmt.Sprintf("index(%d) is out of range", index)) + } + + warmupTemplate[index].TimeDuration = timeDuration +} + +func instantiateWarmup(currentTime int64) []Warmup { + warmups := make([]Warmup, 0) + for _, warmup := range warmupTemplate { + nextWarmupTime := currentTime + warmup.TimeDuration + if nextWarmupTime < 0 { + nextWarmupTime = math.MaxInt64 + } + + warmups = append(warmups, Warmup{ + Index: warmup.Index, + TimeDuration: warmup.TimeDuration, + NextWarmupTime: nextWarmupTime, + WarmupRatio: warmup.WarmupRatio, + }) + currentTime += warmup.TimeDuration + } + return warmups +} + +func (warmup *Warmup) apply(poolReward int64, positionLiquidity, stakedLiquidity *u256.Uint) (int64, int64) { + if stakedLiquidity.IsZero() { + return 0, 0 + } + + divisor := u256.NewUint(100) + poolRewardUint := u256.NewUintFromInt64(poolReward) + perPositionReward := u256.Zero().Mul(poolRewardUint, positionLiquidity) + perPositionReward = u256.Zero().Div(perPositionReward, stakedLiquidity) + rewardRatio := u256.NewUint(warmup.WarmupRatio) + penaltyRatio := u256.NewUint(100 - warmup.WarmupRatio) + totalReward := u256.Zero().Mul(perPositionReward, rewardRatio) + totalReward = u256.Zero().Div(totalReward, divisor) + totalPenalty := u256.Zero().Mul(perPositionReward, penaltyRatio) + totalPenalty = u256.Zero().Div(totalPenalty, divisor) + return safeConvertToInt64(totalReward), safeConvertToInt64(totalPenalty) +} + +func (self *Deposit) FindWarmup(currentTime int64) int { + for i, warmup := range self.warmups { + if currentTime < warmup.NextWarmupTime { + return i + } + } + return len(self.warmups) - 1 +} + +func (self *Deposit) GetWarmup(index int) Warmup { + return self.warmups[index] +} diff --git a/contract/r/gnoswap/v1/staker/staker.gno b/contract/r/gnoswap/v1/staker/staker.gno new file mode 100644 index 0000000..8ace704 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/staker.gno @@ -0,0 +1,789 @@ +package staker + +import ( + "std" + "time" + + "gno.land/p/nt/avl" + "gno.land/p/nt/ufmt" + + prbac "gno.land/p/gnoswap/rbac" + "gno.land/r/gnoswap/halt" + "gno.land/r/gnoswap/v1/common" + + "gno.land/r/gnoswap/gns" + "gno.land/r/gnoswap/v1/gnft" + + en "gno.land/r/gnoswap/emission" + pl "gno.land/r/gnoswap/v1/pool" + pn "gno.land/r/gnoswap/v1/position" + + i256 "gno.land/p/gnoswap/int256" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/referral" +) + +const ZERO_ADDRESS = std.Address("") + +var ( + stakerAddr = getRoleAddress(prbac.ROLE_STAKER) + devOpsAddr = getRoleAddress(prbac.ROLE_DEVOPS) + communityPoolAddr = getRoleAddress(prbac.ROLE_COMMUNITY_POOL) + govStakerAddr = getRoleAddress(prbac.ROLE_GOV_STAKER) + protocolFeeAddr = getRoleAddress(prbac.ROLE_PROTOCOL_FEE) + adminAddr = getRoleAddress(prbac.ROLE_ADMIN) + positionAddr = getRoleAddress(prbac.ROLE_POSITION) +) + +// Deposits manages all staked positions. +type Deposits struct { + tree *avl.Tree +} + +// NewDeposits creates a new Deposits instance. +func NewDeposits() *Deposits { + return &Deposits{ + tree: avl.NewTree(), // positionId -> *Deposit + } +} + +// Has checks if a position ID exists in deposits. +func (self *Deposits) Has(positionId uint64) bool { + return self.tree.Has(EncodeUint(positionId)) +} + +// Iterate traverses deposits within the specified range. +func (self *Deposits) Iterate(start uint64, end uint64, fn func(positionId uint64, deposit *Deposit) bool) { + self.tree.Iterate(EncodeUint(start), EncodeUint(end), func(positionId string, depositI any) bool { + dpst := retrieveDeposit(depositI) + return fn(DecodeUint(positionId), dpst) + }) +} + +// Size returns the number of deposits. +func (self *Deposits) Size() int { + return self.tree.Size() +} + +// get retrieves a deposit by position ID. +func (self *Deposits) get(positionId uint64) *Deposit { + depositI, ok := self.tree.Get(EncodeUint(positionId)) + if !ok { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("positionId(%d) not found", positionId), + )) + } + return retrieveDeposit(depositI) +} + +// retrieveDeposit safely casts data to Deposit type. +func retrieveDeposit(data any) *Deposit { + deposit, ok := data.(*Deposit) + if !ok { + panic("failed to cast value to *Deposit") + } + return deposit +} + +// set stores a deposit for a position ID. +func (self *Deposits) set(positionId uint64, deposit *Deposit) { + self.tree.Set(EncodeUint(positionId), deposit) +} + +// remove deletes a deposit by position ID. +func (self *Deposits) remove(positionId uint64) { + self.tree.Remove(EncodeUint(positionId)) +} + +// ExternalIncentives manages external incentive programs. +type ExternalIncentives struct { + tree *avl.Tree +} + +// NewExternalIncentives creates a new ExternalIncentives instance. +func NewExternalIncentives() *ExternalIncentives { + return &ExternalIncentives{ + tree: avl.NewTree(), + } +} + +// Has checks if an incentive ID exists. +func (self *ExternalIncentives) Has(incentiveId string) bool { return self.tree.Has(incentiveId) } + +// Size returns the number of external incentives. +func (self *ExternalIncentives) Size() int { return self.tree.Size() } + +// get retrieves an external incentive by ID. +func (self *ExternalIncentives) get(incentiveId string) *ExternalIncentive { + incentiveI, ok := self.tree.Get(incentiveId) + if !ok { + panic(makeErrorWithDetails( + errDataNotFound, + ufmt.Sprintf("incentiveId(%s) not found", incentiveId), + )) + } + + incentive, ok := incentiveI.(*ExternalIncentive) + if !ok { + panic("failed to cast value to *ExternalIncentive") + } + return incentive +} + +// set stores an external incentive. +func (self *ExternalIncentives) set(incentiveId string, incentive *ExternalIncentive) { + self.tree.Set(incentiveId, incentive) +} + +// remove deletes an external incentive by ID. +func (self *ExternalIncentives) remove(incentiveId string) { + self.tree.Remove(incentiveId) +} + +// Stakers manages deposits by staker address. +type Stakers struct { + tree *avl.Tree // address -> depositId -> *Deposit +} + +// NewStakers creates a new Stakers instance. +func NewStakers() *Stakers { + return &Stakers{ + tree: avl.NewTree(), + } +} + +// IterateAll traverses all deposits for a specific address. +func (self *Stakers) IterateAll(address std.Address, fn func(depositId uint64, deposit *Deposit) bool) { + depositTreeI, ok := self.tree.Get(address.String()) + if !ok { + return + } + depositTree := retrieveDepositTree(depositTreeI) + depositTree.Iterate("", "", func(depositId string, depositI any) bool { + deposit, ok := depositI.(*Deposit) + if !ok { + panic("failed to cast value to *Deposit") + } + return fn(DecodeUint(depositId), deposit) + }) +} + +// addDeposit adds a deposit for a staker address. +func (self *Stakers) addDeposit(address std.Address, depositId uint64, deposit *Deposit) { + depositTreeI, ok := self.tree.Get(address.String()) + if !ok { + depositTree := avl.NewTree() + self.tree.Set(address.String(), depositTree) + depositTreeI = depositTree + } + + depositTree := retrieveDepositTree(depositTreeI) + depositTree.Set(EncodeUint(depositId), deposit) +} + +// removeDeposit removes a deposit for a staker address. +func (self *Stakers) removeDeposit(address std.Address, depositId uint64) { + depositTreeI, ok := self.tree.Get(address.String()) + if !ok { + return + } + + depositTree := retrieveDepositTree(depositTreeI) + depositTree.Remove(EncodeUint(depositId)) +} + +// retrieveDepositTree safely casts data to AVL tree type. +func retrieveDepositTree(data any) *avl.Tree { + depositTree, ok := data.(*avl.Tree) + if !ok { + panic("failed to cast depositTree to *avl.Tree") + } + return depositTree +} + +var ( + // deposits stores deposit information for each positionId + deposits *Deposits = NewDeposits() + + // externalIncentives stores external incentive information for each incentiveId + externalIncentives *ExternalIncentives = NewExternalIncentives() + + // stakers stores staker information for each address + stakers *Stakers = NewStakers() + + // poolTier stores pool tier information + poolTier *PoolTier + + // totalEmissionSent is the total amount of GNS emission sent from staker to user(and community pool if penalty exists) + // which includes following + // 1. reward sent to user (which also includes protocol_fee) + // 2. penalty sent to community pool + // 3. unclaimable reward + totalEmissionSent int64 +) + +const ( + TIMESTAMP_90DAYS = int64(7776000) + TIMESTAMP_180DAYS = int64(15552000) + TIMESTAMP_365DAYS = int64(31536000) + + MAX_UNIX_EPOCH_TIME = 253402300799 // 9999-12-31 23:59:59 + + MUST_EXISTS_IN_TIER_1 = "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000" + + INTERNAL = true + EXTERNAL = false +) + +// init initializes the staker contract with tier 1 pool. +func init() { + // Initialize tier 1 with GNOT:GNS 0.3% pool + + pools.GetOrCreate(MUST_EXISTS_IN_TIER_1) + + poolTier = NewPoolTier( + pools, + std.ChainHeight(), + time.Now().Unix(), + MUST_EXISTS_IN_TIER_1, + en.GetStakerEmissionAmountPerSecond, + en.GetStakerEmissionAmountPerSecondInRange, + ) +} + +// StakeToken stakes an LP position NFT to earn rewards. +// +// Transfers position NFT to staker and begins reward accumulation. +// Eligible for internal incentives (GNS emission) and external rewards. +// Position must have liquidity and be in eligible pool tier. +// +// Parameters: +// - positionId: LP position NFT token ID to stake +// - referrer: Optional referral address for tracking +// +// Returns: +// - poolPath: Pool identifier (token0:token1:fee) +// - token0Amount: Current token0 balance in position +// - token1Amount: Current token1 balance in position +// +// Requirements: +// - Caller must own the position NFT +// - Position must have active liquidity +// - Pool must be in tier 1, 2, or 3 +// - Position not already staked +// +// Note: Out-of-range positions earn no rewards but can be staked. +func StakeToken(cur realm, positionId uint64, referrer string) (string, string, string) { + halt.AssertIsNotHaltedStaker() + + assertIsNotStaked(positionId) + + en.MintAndDistributeGns(cross) + + previousRealm := std.PreviousRealm() + caller := previousRealm.Address() + owner := gnft.MustOwnerOf(positionIdFrom(positionId)) + currentTime := time.Now().Unix() + + success := referral.TryRegister(cross, caller, referrer) + actualReferrer := referrer + if !success { + actualReferrer = referral.GetReferral(caller.String()) + } + + token0Amount, token1Amount, err := getPositionStakeTokenAmount(positionId, owner, caller) + if err != nil { + panic(err.Error()) + } + + // check pool path from positionId + poolPath := pn.PositionGetPositionPoolKey(positionId) + pool, ok := pools.Get(poolPath) + if !ok { + panic(makeErrorWithDetails( + errNonIncentivizedPool, + ufmt.Sprintf("can not stake position to non existing pool(%s)", poolPath), + )) + } + liquidity := getLiquidity(positionId) + + tickLower, tickUpper := getTickOf(positionId) + + // staked status + deposit := &Deposit{ + owner: caller, + stakeTimestamp: currentTime, + stakeTime: currentTime, + targetPoolPath: poolPath, + tickLower: tickLower, + tickUpper: tickUpper, + liquidity: liquidity, + lastCollectTime: currentTime, + warmups: instantiateWarmup(currentTime), + } + + currentTick := pl.GetSlot0Tick(poolPath) + + deposits.set(positionId, deposit) + stakers.addDeposit(caller, positionId, deposit) + + // transfer NFT ownership to staker contract + if err := transferDeposit(positionId, owner, caller, stakerAddr); err != nil { + panic(err.Error()) + } + + // after transfer, set caller(user) as position operator (to collect fee and reward) + pn.SetPositionOperator(cross, positionId, caller) + + signedLiquidity := i256.FromUint256(liquidity) + isInRange := false + poolTier.cacheReward(currentTime, currentTime, pools) + + if pn.PositionIsInRange(positionId) { + isInRange = true + pool.modifyDeposit(signedLiquidity, currentTime, currentTick) + } + // historical tick must be set regardless of the deposit's range + pool.historicalTick.set(currentTime, currentTick) + + // This could happen because of how position stores the ticks. + // Ticks are negated if the token1 < token0. + upperTick := pool.ticks.Get(tickUpper) + lowerTick := pool.ticks.Get(tickLower) + + upperTick.modifyDepositUpper(currentTime, signedLiquidity) + lowerTick.modifyDepositLower(currentTime, signedLiquidity) + + std.Emit( + "StakeToken", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "positionId", formatUint(positionId), + "poolPath", poolPath, + "amount0", token0Amount, + "amount1", token1Amount, + "liquidity", liquidity.ToString(), + "positionUpperTick", formatAnyInt(tickUpper), + "positionLowerTick", formatAnyInt(tickLower), + "currentTick", formatAnyInt(currentTick), + "isInRange", formatBool(isInRange), + "referrer", actualReferrer, + ) + + return poolPath, token0Amount, token1Amount +} + +// getPositionStakeTokenAmount validates staking requirements and returns token amounts. +func getPositionStakeTokenAmount(positionId uint64, owner, caller std.Address) (string, string, error) { + exist := deposits.Has(positionId) + if exist { + return "", "", errAlreadyStaked + } + + if err := hasTokenOwnership(owner, caller); err != nil { + return "", "", err + } + + if err := tokenHasLiquidity(positionId); err != nil { + return "", "", err + } + + poolPath := pn.PositionGetPositionPoolKey(positionId) + if err := poolHasIncentives(poolPath); err != nil { + return "", "", err + } + + token0Amount, token1Amount := getTokenPairBalanceFromPosition(poolPath, positionId) + + return token0Amount, token1Amount, nil +} + +// transferDeposit transfers deposit ownership to a new address. +// +// Manages NFT custody during staking operations. +// Transfers ownership to staker contract for reward eligibility. +// Handles special cases for mint-and-stake operations. +// +// Parameters: +// - positionId: The ID of the position NFT to transfer +// - owner: The current owner of the position +// - caller: The entity initiating the transfer +// - to: The recipient address (usually staker contract) +// +// Security Features: +// - Prevents self-transfer exploits +// - Validates ownership before transfer +// - Atomic operation with staking +// - No transfer if owner == to (mint & stake case) +// +// Returns: +// - nil: If owner and recipient are same (mint-and-stake) +// - error: If caller unauthorized or transfer fails +// +// NFT remains locked in staker until unstaking. +// Otherwise delegates the transfer to `gnft.TransferFrom`. +func transferDeposit(positionId uint64, owner, caller, to std.Address) error { + // if owner is the same as to, when mint and stake, it will be the same address + if owner == to { + return nil + } + + if caller == to { + return ufmt.Errorf( + "%v: only owner(%s) can transfer positionId(%d), called from %s", + errNoPermission, owner, positionId, caller, + ) + } + + // transfer NFT ownership + return gnft.TransferFrom(cross, owner, to, positionIdFrom(positionId)) +} + +// CollectReward harvests accumulated rewards for a staked position. This includes both +// internal GNS emission and external incentive rewards. +// +// State Transition: +// 1. Warm-up amounts are clears for both internal and external rewards +// 2. Reward tokens are transferred to the owner +// 3. Penalty fees are transferred to protocol/community addresses +// 4. GNS balance is recalculated +// +// Requirements: +// - Contract must not be halted +// - Caller must be the position owner +// - Position must be staked (have a deposit record) +// +// Parameters: +// CollectReward claims accumulated rewards without unstaking. +// +// Parameters: +// - positionId: LP position NFT token ID +// - unwrapResult: if true, unwraps WUGNOT to GNOT +// +// Returns poolPath, gnsAmount, externalRewards map, externalPenalties map. +func CollectReward(cur realm, positionId uint64, unwrapResult bool) (string, string, map[string]int64, map[string]int64) { + caller := std.PreviousRealm().Address() + halt.AssertIsNotHaltedStaker() + halt.AssertIsNotHaltedWithdraw() + + assertIsDepositor(caller, positionId) + + deposit := deposits.get(positionId) + + en.MintAndDistributeGns(cross) + + currentTime := time.Now().Unix() + blockHeight := std.ChainHeight() + previousRealm := std.PreviousRealm() + // get all internal and external rewards + reward := calcPositionReward(blockHeight, currentTime, positionId) + + // update lastCollectTime to current time + deposit.lastCollectTime = currentTime + + // transfer external rewards to user + externalReward := reward.External + toUserExternalReward := make(map[string]int64) + toUserExternalPenalty := make(map[string]int64) + for incentiveId, rewardAmount := range externalReward { + incentive := externalIncentives.get(incentiveId).Clone() + if !incentive.IsStarted(currentTime) { + continue + } + + if incentive.rewardAmount < rewardAmount { + // Emit event for insufficient reward and skip this incentive + std.Emit( + "InsufficientExternalReward", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "positionId", formatUint(positionId), + "incentiveId", incentiveId, + "requiredAmount", formatInt(rewardAmount), + "availableAmount", formatInt(incentive.rewardAmount), + "currentTime", formatInt(currentTime), + "currentHeight", formatInt(blockHeight), + ) + continue + } + + // process external reward to user + incentive.rewardAmount = safeSubInt64(incentive.rewardAmount, rewardAmount) + rewardToken := incentive.rewardToken + toUserExternalReward[rewardToken] = safeAddInt64(toUserExternalReward[rewardToken], rewardAmount) + toUser, feeAmount, err := handleUnStakingFee(rewardToken, rewardAmount, false, positionId, incentive.targetPoolPath) + if err != nil { + panic(err.Error()) + } + + std.Emit( + "ProtocolFeeExternalReward", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "fromPositionId", formatUint(positionId), + "fromPoolPath", incentive.targetPoolPath, + "feeTokenPath", rewardToken, + "feeAmount", formatInt(feeAmount), + "currentTime", formatInt(currentTime), + "currentHeight", formatInt(blockHeight), + ) + if toUser > 0 { + if unwrapResult { + tErr := unwrapWithTransfer(deposit.owner, toUser) + if tErr != nil { + panic(tErr) + } + } else { + common.Transfer(cross, rewardToken, deposit.owner, toUser) + } + } + + // process external penalty + externalPenalty := reward.ExternalPenalty[incentiveId] + incentive.rewardAmount = safeSubInt64(incentive.rewardAmount, externalPenalty) + incentive.rewardLeft = safeAddInt64(incentive.rewardLeft, externalPenalty) + toUserExternalPenalty[rewardToken] = safeAddInt64(toUserExternalPenalty[rewardToken], externalPenalty) + + // update + externalIncentives.set(incentiveId, incentive) + + std.Emit( + "CollectReward", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "positionId", formatUint(positionId), + "poolPath", deposit.targetPoolPath, + "recipient", deposit.owner.String(), + "incentiveId", incentiveId, + "rewardToken", rewardToken, + "rewardAmount", formatInt(rewardAmount), + "rewardToUser", formatInt(toUser), + "rewardToFee", formatInt(rewardAmount-toUser), + "rewardPenalty", formatInt(externalPenalty), + "isRequestUnwrap", formatBool(unwrapResult), + "currentTime", formatInt(currentTime), + "currentHeight", formatInt(blockHeight), + ) + } + + communityPoolAddr := getRoleAddress(prbac.ROLE_COMMUNITY_POOL) + + // internal reward to user + toUser, feeAmount, err := handleUnStakingFee(GNS_PATH, reward.Internal, true, positionId, deposit.targetPoolPath) + if err != nil { + panic(err.Error()) + } + + std.Emit( + "ProtocolFeeInternalReward", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "fromPositionId", formatUint(positionId), + "fromPoolPath", deposit.targetPoolPath, + "feeTokenPath", GNS_PATH, + "feeAmount", formatInt(feeAmount), + ) + + if toUser > 0 { + // internal reward to user + totalEmissionSent = safeAddInt64(totalEmissionSent, toUser) + gns.Transfer(cross, deposit.owner, toUser) + + // internal penalty to community pool + totalEmissionSent = safeAddInt64(totalEmissionSent, reward.InternalPenalty) + gns.Transfer(cross, communityPoolAddr, reward.InternalPenalty) + } + + unClaimableInternal := processUnClaimableReward(deposit.targetPoolPath, currentTime) + if unClaimableInternal > 0 { + // internal unClaimable to community pool + totalEmissionSent = safeAddInt64(totalEmissionSent, unClaimableInternal) + gns.Transfer(cross, communityPoolAddr, unClaimableInternal) + } + + rewardToUser := formatInt(toUser) + rewardPenalty := formatInt(reward.InternalPenalty) + + std.Emit( + "CollectReward", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "positionId", formatUint(positionId), + "poolPath", deposit.targetPoolPath, + "recipient", deposit.owner.String(), + "rewardToken", GNS_PATH, + "rewardAmount", formatInt(reward.Internal), + "rewardToUser", rewardToUser, + "rewardToFee", formatInt(reward.Internal-toUser), + "rewardPenalty", rewardPenalty, + "rewardUnClaimableAmount", formatInt(unClaimableInternal), + "currentTime", formatInt(currentTime), + ) + + return rewardToUser, rewardPenalty, toUserExternalReward, toUserExternalPenalty +} + +// UnStakeToken withdraws an LP token from staking, collecting all pending rewards +// and returning the token to its original owner. +// +// Parameters: +// - positionId: LP position NFT token ID to unstake +// - unwrapResult: Convert WUGNOT to GNOT if true +// +// Process: +// 1. Collects all pending rewards (GNS + external) +// 2. Transfers NFT ownership back to original owner +// 3. Clears position operator rights +// 4. Removes from reward tracking systems +// 5. Cleans up all staking metadata +// +// Returns: +// - poolPath: Pool identifier where position was staked +// - token0Amount: Current token0 balance in position +// - token1Amount: Current token1 balance in position +// +// Requirements: +// - Caller must be the depositor +// - Position must be currently staked +func UnStakeToken(cur realm, positionId uint64, unwrapResult bool) (string, string, string) { // poolPath, token0Amount, token1Amount + caller := std.PreviousRealm().Address() + halt.AssertIsNotHaltedStaker() + halt.AssertIsNotHaltedWithdraw() + assertIsDepositor(caller, positionId) + + deposit := deposits.get(positionId) + + // unStaked status + poolPath := deposit.targetPoolPath + + // claim All Rewards + CollectReward(cur, positionId, unwrapResult) + token0Amount, token1Amount := getTokenPairBalanceFromPosition(poolPath, positionId) + + if err := applyUnStake(positionId); err != nil { + panic(err) + } + + // transfer NFT ownership to origin owner + gnft.TransferFrom(cross, stakerAddr, deposit.owner, positionIdFrom(positionId)) + pn.SetPositionOperator(cross, positionId, ZERO_ADDRESS) + + previousRealm := std.PreviousRealm() + std.Emit( + "UnStakeToken", + "prevAddr", previousRealm.Address().String(), + "prevRealm", previousRealm.PkgPath(), + "positionId", formatUint(positionId), + "poolPath", poolPath, + "isRequestUnwrap", formatBool(unwrapResult), + "from", stakerAddr.String(), + "to", deposit.owner.String(), + "amount0", token0Amount, + "amount1", token1Amount, + ) + + return poolPath, token0Amount, token1Amount +} + +func applyUnStake(positionId uint64) error { + deposit := deposits.get(positionId) + pool, ok := pools.Get(deposit.targetPoolPath) + if !ok { + return ufmt.Errorf( + "%v: pool(%s) does not exist", + errDataNotFound, deposit.targetPoolPath, + ) + } + + currentTime := time.Now().Unix() + currentTick := pl.GetSlot0Tick(deposit.targetPoolPath) + signedLiquidity := i256.Zero().Neg(i256.FromUint256(deposit.liquidity)) + if pn.PositionIsInRange(positionId) { + pool.modifyDeposit(signedLiquidity, currentTime, currentTick) + } + + upperTick := pool.ticks.Get(deposit.tickUpper) + lowerTick := pool.ticks.Get(deposit.tickLower) + upperTick.modifyDepositUpper(currentTime, signedLiquidity) + lowerTick.modifyDepositLower(currentTime, signedLiquidity) + + deposits.remove(positionId) + stakers.removeDeposit(deposit.owner, positionId) + + owner := gnft.MustOwnerOf(positionIdFrom(positionId)) + caller := std.PreviousRealm().Address() + + _, _, err := getPositionStakeTokenAmount(positionId, owner, caller) + if err != nil { + return err + } + + return nil +} + +// hasTokenOwnership validates that the caller has permission to operate the token. +func hasTokenOwnership(owner, caller std.Address) error { + isCallerOwner := owner == caller + isStakerOwner := owner == stakerAddr + + if !isCallerOwner && !isStakerOwner { + return errNoPermission + } + + return nil +} + +// poolHasIncentives checks if the pool has any active incentives (internal or external). +func poolHasIncentives(poolPath string) error { + pool, ok := pools.Get(poolPath) + if !ok { + return ufmt.Errorf( + "%v: can not stake position to non existent pool(%s)", + errNonIncentivizedPool, poolPath, + ) + } + hasInternal := poolTier.IsInternallyIncentivizedPool(poolPath) + hasExternal := pool.IsExternallyIncentivizedPool() + if hasInternal == false && hasExternal == false { + return ufmt.Errorf( + "%v: can not stake position to non incentivized pool(%s)", + errNonIncentivizedPool, poolPath, + ) + } + return nil +} + +// tokenHasLiquidity checks if the target positionId has non-zero liquidity +func tokenHasLiquidity(positionId uint64) error { + liquidity := getLiquidity(positionId) + + if liquidity.Lte(u256.Zero()) { + return ufmt.Errorf( + "%v: positionId(%d) has no liquidity", + errZeroLiquidity, positionId, + ) + } + return nil +} + +func getLiquidity(positionId uint64) *u256.Uint { + liq := pn.PositionGetPositionLiquidityStr(positionId) + return u256.MustFromDecimal(liq) +} + +func getTokenPairBalanceFromPosition(poolPath string, positionId uint64) (string, string) { + position := pn.MustGetPosition(positionId) + + return position.Token0Balance().ToString(), position.Token1Balance().ToString() +} + +func getTickOf(positionId uint64) (int32, int32) { + tickLower := pn.PositionGetPositionTickLower(positionId) + tickUpper := pn.PositionGetPositionTickUpper(positionId) + if tickUpper < tickLower { + panic(ufmt.Sprintf("tickUpper(%d) is less than tickLower(%d)", tickUpper, tickLower)) + } + return tickLower, tickUpper +} diff --git a/contract/r/gnoswap/v1/staker/type.gno b/contract/r/gnoswap/v1/staker/type.gno new file mode 100644 index 0000000..6732b66 --- /dev/null +++ b/contract/r/gnoswap/v1/staker/type.gno @@ -0,0 +1,204 @@ +package staker + +import ( + "math" + "std" + + u256 "gno.land/p/gnoswap/uint256" +) + +// ExternalIncentive is a struct for storing external incentive information. +type ExternalIncentive struct { + incentiveId string // incentive id + startTimestamp int64 // start time for external reward + endTimestamp int64 // end time for external reward + createdHeight int64 // block height when the incentive was created + depositGnsAmount int64 // deposited gns amount + targetPoolPath string // external reward target pool path + rewardToken string // external reward token path + rewardAmount int64 // total reward amount + rewardLeft int64 // remaining reward amount + rewardPerSecond int64 // reward per second + refundee std.Address // refundee address + + unclaimableRefunded bool // whether unclaimable reward is refunded +} + +func (e ExternalIncentive) IsStarted(currentTimestamp int64) bool { + return currentTimestamp >= e.startTimestamp +} + +// safeMulInt64 performs safe multiplication of int64 values, panicking on overflow +func safeMulInt64(a, b int64) int64 { + if a == 0 || b == 0 { + return 0 + } + if a > 0 && b > 0 { + if a > math.MaxInt64/b { + panic("int64 multiplication overflow") + } + } else if a < 0 && b < 0 { + if a < math.MaxInt64/b { + panic("int64 multiplication overflow") + } + } else if a > 0 && b < 0 { + if b < math.MinInt64/a { + panic("int64 multiplication underflow") + } + } else { // a < 0 && b > 0 + if a < math.MinInt64/b { + panic("int64 multiplication underflow") + } + } + return a * b +} + +// safeAddInt64 performs safe addition of int64 values, panicking on overflow +func safeAddInt64(a, b int64) int64 { + if a > 0 && b > math.MaxInt64-a { + panic("int64 addition overflow") + } + if a < 0 && b < math.MinInt64-a { + panic("int64 addition underflow") + } + return a + b +} + +// safeSubInt64 performs safe subtraction of int64 values, panicking on underflow +func safeSubInt64(a, b int64) int64 { + if b > 0 && a < math.MinInt64+b { + panic("int64 subtraction underflow") + } + if b < 0 && a > math.MaxInt64+b { + panic("int64 subtraction overflow") + } + return a - b +} + +func (e ExternalIncentive) StartTimestamp() int64 { + return e.startTimestamp +} + +func (e ExternalIncentive) EndTimestamp() int64 { + return e.endTimestamp +} + +func (e ExternalIncentive) RewardToken() string { + return e.rewardToken +} + +func (e ExternalIncentive) RewardAmount() int64 { + return e.rewardAmount +} + +func (self *ExternalIncentive) RewardSpent(currentTimestamp int64) int64 { + // Still check timestamps for state validation + if currentTimestamp < self.startTimestamp { + return 0 + } + + if currentTimestamp > self.endTimestamp { + return int64(self.rewardAmount) + } + + // But use time for calculation + if currentTimestamp < self.startTimestamp { + return 0 + } + + if currentTimestamp > self.endTimestamp { + return int64(self.rewardAmount) + } + + timeDuration := currentTimestamp - self.startTimestamp + rewardSpent := safeMulInt64(timeDuration, self.rewardPerSecond) + return rewardSpent +} + +func (self *ExternalIncentive) RewardLeft(currentTimestamp int64) int64 { + // Still check timestamps for state validation + if currentTimestamp <= self.startTimestamp { + return int64(self.rewardAmount) + } + + if currentTimestamp > self.endTimestamp { + return 0 + } + + // But use time for calculation + if currentTimestamp <= self.startTimestamp { + return int64(self.rewardAmount) + } + + if currentTimestamp > self.endTimestamp { + return 0 + } + + timeDuration := self.endTimestamp - currentTimestamp + rewardLeft := safeMulInt64(timeDuration, self.rewardPerSecond) + return rewardLeft +} + +func (self *ExternalIncentive) Clone() *ExternalIncentive { + return &ExternalIncentive{ + incentiveId: self.incentiveId, + startTimestamp: self.startTimestamp, + endTimestamp: self.endTimestamp, + createdHeight: self.createdHeight, + depositGnsAmount: self.depositGnsAmount, + targetPoolPath: self.targetPoolPath, + rewardToken: self.rewardToken, + rewardAmount: self.rewardAmount, + rewardLeft: self.rewardLeft, + rewardPerSecond: self.rewardPerSecond, + refundee: self.refundee, + unclaimableRefunded: self.unclaimableRefunded, + } +} + +func (self *ExternalIncentive) setUnClaimableRefunded(unClaimableRefunded bool) { + self.unclaimableRefunded = unClaimableRefunded +} + +// NewExternalIncentive creates a new external incentive +func NewExternalIncentive( + incentiveId string, + targetPoolPath string, + rewardToken string, + rewardAmount int64, + startTimestamp int64, // timestamp is in unix time(seconds) + endTimestamp int64, + refundee std.Address, + createdHeight int64, + depositGnsAmount int64, + currentTime int64, // current time in unix time(seconds) +) *ExternalIncentive { + incentiveDuration := endTimestamp - startTimestamp + rewardPerSecond := rewardAmount / incentiveDuration + + return &ExternalIncentive{ + incentiveId: incentiveId, + targetPoolPath: targetPoolPath, + rewardToken: rewardToken, + rewardAmount: rewardAmount, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + rewardPerSecond: rewardPerSecond, + refundee: refundee, + createdHeight: createdHeight, + depositGnsAmount: depositGnsAmount, + unclaimableRefunded: false, + } +} + +type Deposit struct { + owner std.Address // owner address + stakeTimestamp int64 // staked time + stakeTime int64 // staked time (same as stakeTimestamp) + targetPoolPath string // staked position's pool path + tickLower int32 // tick lower + tickUpper int32 // tick upper + liquidity *u256.Uint // liquidity + lastCollectTime int64 // last collect time + warmups []Warmup // warmup information +} diff --git a/contract/r/gnoswap/v1/staker/utils.gno b/contract/r/gnoswap/v1/staker/utils.gno new file mode 100644 index 0000000..c98f48e --- /dev/null +++ b/contract/r/gnoswap/v1/staker/utils.gno @@ -0,0 +1,168 @@ +package staker + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/tokens/grc721" + "gno.land/p/nt/ufmt" + prbac "gno.land/p/gnoswap/rbac" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/access" + "gno.land/r/gnoswap/rbac" +) + +// GetOrigPkgAddr returns the original package address. +func GetOrigPkgAddr() std.Address { + return stakerAddr +} + +// poolPathAlign ensures pool path tokens are in lexicographical order. +func poolPathAlign(poolPath string) string { + pToken0, pToken1, fee := poolPathDivide(poolPath) + + if pToken0 < pToken1 { + return ufmt.Sprintf("%s:%s:%s", pToken0, pToken1, fee) + } + + return ufmt.Sprintf("%s:%s:%s", pToken1, pToken0, fee) +} + +// poolPathDivide splits a pool path into token addresses and fee tier. +func poolPathDivide(poolPath string) (string, string, string) { + res := strings.Split(poolPath, ":") + if len(res) != 3 { + panic(errInvalidPoolPath) + } + + pToken0, pToken1, fee := res[0], res[1], res[2] + return pToken0, pToken1, fee +} + +// positionIdFrom converts various types to grc721.TokenID. +func positionIdFrom(positionId any) grc721.TokenID { + if positionId == nil { + panic(makeErrorWithDetails( + errDataNotFound, + "positionId is nil", + )) + } + + switch positionId.(type) { + case string: + return grc721.TokenID(positionId.(string)) + case int: + return grc721.TokenID(strconv.Itoa(positionId.(int))) + case uint64: + return grc721.TokenID(strconv.Itoa(int(positionId.(uint64)))) + case grc721.TokenID: + return positionId.(grc721.TokenID) + default: + panic(makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("unsupported positionId type(%T)", positionId), + )) + } +} + +// max returns the larger of two int64 values. +func max(x, y int64) int64 { + if x > y { + return x + } + return y +} + +// min returns the smaller of two uint64 values. +func min(x, y uint64) uint64 { + if x < y { + return x + } + return y +} + +// contains checks if a string exists in a slice. +func contains(slice []string, item string) bool { + // We can use strings.EqualFold here, but this function should be case-sensitive. + // So, it is better to compare strings directly. + for _, element := range slice { + if element == item { + return true + } + } + return false +} + +// formatUint formats an unsigned integer to string. +func formatUint(v any) string { + switch v := v.(type) { + case uint8: + return strconv.FormatUint(uint64(v), 10) + case uint32: + return strconv.FormatUint(uint64(v), 10) + case uint64: + return strconv.FormatUint(v, 10) + default: + panic(ufmt.Sprintf("invalid type for Unsigned: %T", v)) + } +} + +// formatAnyInt formats a signed integer to string. +func formatAnyInt(v any) string { + switch v := v.(type) { + case int32: + return strconv.FormatInt(int64(v), 10) + case int64: + return strconv.FormatInt(v, 10) + case int: + return strconv.Itoa(v) + default: + panic(ufmt.Sprintf("invalid type for Signed: %T", v)) + } +} + +// formatBool formats a boolean to string. +func formatBool(v bool) string { + return strconv.FormatBool(v) +} + +// getRoleAddress retrieves the address for a system role. +func getRoleAddress(role prbac.SystemRole) std.Address { + addr, exists := access.GetAddress(role.String()) + if !exists { + return rbac.DefaultRoleAddresses[role] + } + + return addr +} + +// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. +// +// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds +// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. +// +// Parameters: +// - value (*u256.Uint): The unsigned 256-bit integer to be converted. +// +// Returns: +// - int64: The converted value if it falls within the int64 range. +// +// Panics: +// - If the `value` exceeds the range of int64, the function will panic with an error indicating +// the overflow and the original value. +func safeConvertToInt64(value *u256.Uint) int64 { + const INT64_MAX = 9223372036854775807 + const MAX_INT64 = "9223372036854775807" + + res, overflow := value.Uint64WithOverflow() + if overflow || res > uint64(INT64_MAX) { + panic(ufmt.Sprintf( + "amount(%s) overflows int64 range (max %s)", + value.ToString(), + MAX_INT64, + )) + } + return int64(res) +} diff --git a/contract/r/gnoswap/v1/staker/wrap_unwrap.gno b/contract/r/gnoswap/v1/staker/wrap_unwrap.gno new file mode 100644 index 0000000..ec582fd --- /dev/null +++ b/contract/r/gnoswap/v1/staker/wrap_unwrap.gno @@ -0,0 +1,82 @@ +package staker + +import ( + "std" + + "gno.land/r/gnoland/wugnot" + + "gno.land/p/nt/ufmt" +) + +// wrapWithTransfer wraps GNOT into WUGNOT and transfers it to the specified address. +func wrapWithTransfer(toAddress std.Address, amount int64) error { + if amount <= 0 { + return nil + } + + if amount < UGNOT_MIN_DEPOSIT_TO_WRAP { + return makeErrorWithDetails( + errWugnotMinimum, + ufmt.Sprintf("amount(%d) < minimum(%d)", amount, UGNOT_MIN_DEPOSIT_TO_WRAP), + ) + } + + // transfer ugnot from fromAddress to current realm + currentRealmAddr := std.CurrentRealm().Address() + + sentCoins := std.OriginSend() + ugnotSent := sentCoins.AmountOf(GNOT_DENOM) + if ugnotSent != amount { + return makeErrorWithDetails( + errInvalidInput, + ufmt.Sprintf("user(%s) sent ugnot(%d) amount not equal to rewardAmount(%d)", toAddress.String(), ugnotSent, amount), + ) + } + + // wrap gnot to wugnot + wugnotAddr := std.DerivePkgAddr(WUGNOT_PATH) + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(currentRealmAddr, wugnotAddr, sentCoins) + wugnot.Deposit(cross) + + // if to address is not current realm, transfer wugnot to to address + if toAddress != currentRealmAddr { + wugnot.Transfer(cross, toAddress, amount) + } + + return nil +} + +// unwrapWithTransfer unwraps WUGNOT to GNOT and sends it to the specified address. +func unwrapWithTransfer(toAddress std.Address, wugnotAmount int64) error { + if wugnotAmount == 0 { + return nil + } + + wugnot.Withdraw(cross, wugnotAmount) + + currentRealmAddr := std.CurrentRealm().Address() + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(currentRealmAddr, toAddress, std.Coins{{"ugnot", int64(wugnotAmount)}}) + + return nil +} + +// unwrapWithTransferFrom transfers WUGNOT from a source address, unwraps it to GNOT, and sends it to the target. +func unwrapWithTransferFrom(fromAddress, toAddress std.Address, wugnotAmount int64) error { + if wugnotAmount == 0 { + return nil + } + + currentRealmAddr := std.CurrentRealm().Address() + if fromAddress != currentRealmAddr { + wugnot.TransferFrom(cross, fromAddress, currentRealmAddr, wugnotAmount) + } + + wugnot.Withdraw(cross, wugnotAmount) + + banker := std.NewBanker(std.BankerTypeRealmSend) + banker.SendCoins(currentRealmAddr, toAddress, std.Coins{{"ugnot", int64(wugnotAmount)}}) + + return nil +} diff --git a/contract/r/volos/README.md b/contract/r/volos/README.md new file mode 100644 index 0000000..47d2af1 --- /dev/null +++ b/contract/r/volos/README.md @@ -0,0 +1,37 @@ +# Volos (ex GnoLend) + +Volos is the first lending protocol built using Gnolang, implementing financial primitives for decentralized lending and borrowing. The protocol features lending markets with configurable parameters, variable interest rate models, collateralized borrowing with health monitoring, and liquidation mechanisms for undercollateralized positions. It employs a shares-based accounting system to track user positions, calculates interest based on utilization metrics, and maintains system solvency through real-time risk assessment. + +For price determination, Volos integrates with [Gnoswap](https://github.com/gnoswap-labs/gnoswap)'s liquidity pools, using them as price oracles in the absence of dedicated oracle infrastructure. This approach enables the protocol to obtain reliable price data directly from on-chain sources without requiring external oracle networks, demonstrating how essential financial primitives can be implemented within the current gno.land ecosystem. + +The system calculates borrowing capacity based on collateral values derived from Gnoswap pool prices. This integration creates a self-contained lending solution that maintains the security guarantees of the underlying blockchain while providing the necessary infrastructure for expanding DeFi capabilities on gno.land. + +> **Warning:** This project is work in progress. The protocol is under active development and contains incomplete features and known issues. + +## Prerequisites + +- GNU Make 3.81 or higher +- Latest version of [gno.land](https://github.com/gnolang/gno) +- Go 1.21 or higher + +## Setup + +1. First, follow the setup instructions from [Gnoswap](https://github.com/gnoswap-labs/gnoswap) to set up your development environment. + +2. Add Volos realms to your gno repository: + + ```bash + cp -r volos/* $WORKDIR/gno.land/examples/r/volos/ + ``` + +3. Configure admin addresses: + + For proper testing and development, you'll need to update the admin addresses in several locations: + + - In Volos test files: Update the admin addresses to match your test accounts + - In Gnoswap test files: Ensure the admin addresses align with your test environment + - To avoid manual token minting, you can aloso modify the admin address in `p/gnoswap/consts.gno` to match your preferred test account + +4. Run Make file tests: + + Before running Volos tests, you'll need to first initialize a WUGNOT-GNS pool in Gnoswap and mint at least one liquidity position. This provides the price oracle that Volos depends on. After that's done, you can proceed with testing the Volos functions. diff --git a/contract/r/volos/core/api.gno b/contract/r/volos/core/api.gno index 4262f10..5eba2c6 100644 --- a/contract/r/volos/core/api.gno +++ b/contract/r/volos/core/api.gno @@ -1,7 +1,7 @@ package core import ( - "gno.land/p/demo/json" + "gno.land/p/onbloc/json" u256 "gno.land/p/gnoswap/uint256" ) diff --git a/contract/r/volos/core/events.gno b/contract/r/volos/core/events.gno index 0db1f51..f468a35 100644 --- a/contract/r/volos/core/events.gno +++ b/contract/r/volos/core/events.gno @@ -57,8 +57,11 @@ const ( EventCollateralAmtKey = "collateral_amount" // Interest keys - EventBorrowRateKey = "borrow_rate" - EventInterestKey = "interest" + EventBorrowRateKey = "borrow_rate" + EventInterestKey = "interest" + EventTotalSupplyAssetsKey = "total_supply_assets" + EventTotalBorrowAssetsKey = "total_borrow_assets" + EventUtilizationKey = "utilization" // Event keys EventMarketIdKey = "market_id" @@ -74,23 +77,64 @@ const ( EventTimestampKey = "currentTimestamp" EventSupplyAPRKey = "supplyAPR" EventBorrowAPRKey = "borrowAPR" + EventBadDebtAssetsKey = "badDebtAssets" + EventBadDebtSharesKey = "badDebtShares" + // MarketInfo keys + EventIsToken0LoanKey = "isToken0Loan" + EventLoanTokenNameKey = "loanTokenName" + EventLoanTokenSymbolKey = "loanTokenSymbol" + EventLoanTokenDecimalsKey = "loanTokenDecimals" + EventCollateralTokenNameKey = "collateralTokenName" + EventCollateralTokenSymbolKey = "collateralTokenSymbol" + EventCollateralTokenDecimalsKey = "collateralTokenDecimals" + EventLLTVKey = "lltv" + EventPoolPathKey = "poolPath" ) // Event emission helper functions func emitCreateMarket(marketId string, loanToken string, collateralToken string) { + _, params := GetMarket(marketId) + var loanTokenName, loanTokenSymbol string + var loanTokenDecimals uint + var collateralTokenName, collateralTokenSymbol string + var collateralTokenDecimals uint + + loanTokenInfo := GetToken(loanToken) + if loanTokenInfo != nil { + loanTokenName = loanTokenInfo.GetName() + loanTokenSymbol = loanTokenInfo.GetSymbol() + loanTokenDecimals = uint(loanTokenInfo.GetDecimals()) + } + + collateralTokenInfo := GetToken(collateralToken) + if collateralTokenInfo != nil { + collateralTokenName = collateralTokenInfo.GetName() + collateralTokenSymbol = collateralTokenInfo.GetSymbol() + collateralTokenDecimals = uint(collateralTokenInfo.GetDecimals()) + } + std.Emit( CreateMarketEvent, EventMarketIDKey, marketId, EventLoanTokenKey, loanToken, EventCollateralTokenKey, collateralToken, + EventLoanTokenNameKey, loanTokenName, + EventLoanTokenSymbolKey, loanTokenSymbol, + EventLoanTokenDecimalsKey, strconv.FormatUint(uint64(loanTokenDecimals), 10), + EventCollateralTokenNameKey, collateralTokenName, + EventCollateralTokenSymbolKey, collateralTokenSymbol, + EventCollateralTokenDecimalsKey, strconv.FormatUint(uint64(collateralTokenDecimals), 10), + EventLLTVKey, params.LLTV.ToString(), EventTimestampKey, strconv.FormatInt(time.Now().Unix(), 10), + EventPoolPathKey, params.PoolPath, ) } func emitSupply(marketId string, caller std.Address, onBehalf std.Address, assets, shares *u256.Uint) { - // Calculate APRs after supply operation + // Calculate APRs and utilization after supply operation supplyAPR := CalculateSupplyAPR(marketId) borrowAPR := CalculateBorrowAPR(marketId) + utilization := calculateUtilization(marketId) std.Emit( SupplyEvent, @@ -102,13 +146,15 @@ func emitSupply(marketId string, caller std.Address, onBehalf std.Address, asset EventTimestampKey, strconv.FormatInt(time.Now().Unix(), 10), EventSupplyAPRKey, supplyAPR.ToString(), EventBorrowAPRKey, borrowAPR.ToString(), + EventUtilizationKey, utilization.ToString(), ) } func emitWithdraw(marketId string, caller std.Address, onBehalf std.Address, receiver std.Address, assets, shares *u256.Uint) { - // Calculate APRs after withdraw operation + // Calculate APRs and utilization after withdraw operation supplyAPR := CalculateSupplyAPR(marketId) borrowAPR := CalculateBorrowAPR(marketId) + utilization := calculateUtilization(marketId) std.Emit( WithdrawEvent, @@ -121,13 +167,15 @@ func emitWithdraw(marketId string, caller std.Address, onBehalf std.Address, rec EventTimestampKey, strconv.FormatInt(time.Now().Unix(), 10), EventSupplyAPRKey, supplyAPR.ToString(), EventBorrowAPRKey, borrowAPR.ToString(), + EventUtilizationKey, utilization.ToString(), ) } func emitBorrow(marketId string, caller std.Address, onBehalf std.Address, receiver std.Address, assets, shares *u256.Uint) { - // Calculate APRs after borrow operation + // Calculate APRs and utilization after borrow operation supplyAPR := CalculateSupplyAPR(marketId) borrowAPR := CalculateBorrowAPR(marketId) + utilization := calculateUtilization(marketId) std.Emit( BorrowEvent, @@ -140,6 +188,7 @@ func emitBorrow(marketId string, caller std.Address, onBehalf std.Address, recei EventTimestampKey, strconv.FormatInt(time.Now().Unix(), 10), EventSupplyAPRKey, supplyAPR.ToString(), EventBorrowAPRKey, borrowAPR.ToString(), + EventUtilizationKey, utilization.ToString(), ) } @@ -147,6 +196,8 @@ func emitRepay(marketId string, caller std.Address, onBehalf std.Address, assets // Calculate APRs after repay operation supplyAPR := CalculateSupplyAPR(marketId) borrowAPR := CalculateBorrowAPR(marketId) + utilization := calculateUtilization(marketId) + std.Emit( RepayEvent, @@ -158,13 +209,15 @@ func emitRepay(marketId string, caller std.Address, onBehalf std.Address, assets EventTimestampKey, strconv.FormatInt(time.Now().Unix(), 10), EventSupplyAPRKey, supplyAPR.ToString(), EventBorrowAPRKey, borrowAPR.ToString(), + EventUtilizationKey, utilization.ToString(), ) } -func emitLiquidate(marketId string, caller std.Address, borrower std.Address, repaidAssets, repaidShares, seizedAssets *u256.Uint) { - // Calculate APRs after liquidation operation +func emitLiquidate(marketId string, caller std.Address, borrower std.Address, repaidAssets, repaidShares, seizedAssets, badDebtAssets, badDebtShares *u256.Uint) { + // Calculate APRs and utilization after liquidation operation supplyAPR := CalculateSupplyAPR(marketId) borrowAPR := CalculateBorrowAPR(marketId) + utilization := calculateUtilization(marketId) std.Emit( EventLiquidate, @@ -174,18 +227,23 @@ func emitLiquidate(marketId string, caller std.Address, borrower std.Address, re EventAmountKey, repaidAssets.ToString(), EventSharesKey, repaidShares.ToString(), EventSeizedKey, seizedAssets.ToString(), + EventBadDebtAssetsKey, badDebtAssets.ToString(), + EventBadDebtSharesKey, badDebtShares.ToString(), EventTimestampKey, strconv.FormatInt(time.Now().Unix(), 10), EventSupplyAPRKey, supplyAPR.ToString(), EventBorrowAPRKey, borrowAPR.ToString(), + EventUtilizationKey, utilization.ToString(), ) } -func emitAccrueInterest(marketId string, borrowRate, interest *u256.Uint) { +func emitAccrueInterest(marketId string, borrowRate, interest, totalSupplyAssets, totalBorrowAssets *u256.Uint) { std.Emit( AccrueInterestEvent, EventMarketIDKey, marketId, EventBorrowRateKey, borrowRate.ToString(), EventInterestKey, interest.ToString(), + EventTotalSupplyAssetsKey, totalSupplyAssets.ToString(), + EventTotalBorrowAssetsKey, totalBorrowAssets.ToString(), EventTimestampKey, strconv.FormatInt(time.Now().Unix(), 10), ) } diff --git a/contract/r/volos/core/getter.gno b/contract/r/volos/core/getter.gno index ee9cd5b..8d140b9 100644 --- a/contract/r/volos/core/getter.gno +++ b/contract/r/volos/core/getter.gno @@ -3,7 +3,7 @@ package core import ( "std" - "gno.land/p/demo/avl" + "gno.land/p/nt/avl" u256 "gno.land/p/gnoswap/uint256" ) @@ -237,21 +237,6 @@ func GetMarketPrice(marketId string) string { // Other getters -func GetSupplyShares(marketId string, user std.Address) string { - position := GetPosition(marketId, user.String()) - return position.SupplyShares.ToString() -} - -func GetBorrowShares(marketId string, user std.Address) string { - position := GetPosition(marketId, user.String()) - return position.BorrowShares.ToString() -} - -func GetCollateral(marketId string, user std.Address) string { - position := GetPosition(marketId, user.String()) - return position.Collateral.ToString() -} - func GetTotalSupplyAssets(marketId string) string { market, _ := GetMarket(marketId) return market.TotalSupplyAssets.ToString() @@ -319,12 +304,6 @@ func GetExpectedBorrowAssets(marketId string, user string) string { return ExpectedBorrowAssets(marketId, user).ToString() } -// GetBorrowRate returns the current borrow rate per second as a string (WAD-scaled) -func GetBorrowRate(marketId string) string { - borrowRate := CalculateBorrowRate(marketId) - return borrowRate.ToString() -} - // GetSupplyRate returns the current supply rate per second as a string (WAD-scaled) func GetSupplyRate(marketId string) string { supplyRate := CalculateSupplyRate(marketId) @@ -387,3 +366,23 @@ func GetAddress() string { func GetIsAuthorized(authorizer string, authorized string) bool { return IsAuthorized(std.Address(authorizer), std.Address(authorized)) } + +// getBorrowRate returns the current borrow rate per second +// The returned value is WAD-scaled (1e18) +func GetBorrowRate(marketId string) *u256.Uint { + // Check if market exists + if _, exists := markets.Get(marketId); !exists { + panic(ErrMarketNotCreated) + } + + market, params := GetMarket(marketId) + + // If no borrows or no IRM, rate is zero + if market.TotalBorrowAssets.IsZero() || params.IRM == "" { + return u256.Zero() + } + + // Get IRM and calculate current borrow rate + irm := GetIRM(params.IRM) + return irm.BorrowRate(market.TotalSupplyAssets, market.TotalBorrowAssets) +} diff --git a/contract/r/volos/core/json.gno b/contract/r/volos/core/json.gno index 5757c8f..aaadec3 100644 --- a/contract/r/volos/core/json.gno +++ b/contract/r/volos/core/json.gno @@ -1,7 +1,7 @@ package core import ( - "gno.land/p/demo/json" + "gno.land/p/onbloc/json" u256 "gno.land/p/gnoswap/uint256" "gno.land/p/volos/math" ) diff --git a/contract/r/volos/core/oracle.gno b/contract/r/volos/core/oracle.gno index aa9fe03..933e1c5 100644 --- a/contract/r/volos/core/oracle.gno +++ b/contract/r/volos/core/oracle.gno @@ -1,6 +1,8 @@ package core import ( + "strings" + u256 "gno.land/p/gnoswap/uint256" "gno.land/p/volos/consts" "gno.land/p/volos/math" @@ -14,25 +16,25 @@ func GetPrice(marketId string) *u256.Uint { _, params := GetMarket(marketId) // Get the sqrt price from the pool - sqrtPriceX96Str := pool.PoolGetSlot0SqrtPriceX96(params.PoolPath) - if sqrtPriceX96Str == "" { + sqrtPriceX96 := pool.GetSlot0SqrtPriceX96(params.PoolPath) + if sqrtPriceX96.IsZero() { panic(ErrPriceNotAvailable) } - // Convert string to uint256 - sqrtPriceX96 := u256.MustFromDecimal(sqrtPriceX96Str) - // Square the price to get the actual price in Q192 priceQ192 := new(u256.Uint).Mul(sqrtPriceX96, sqrtPriceX96) - // Finally divide by Q192 to get the actual price ratio with ORACLE_PRICE_SCALE precision - price := math.MulDivDown(priceQ192, consts.ORACLE_PRICE_SCALE, consts.Q192) + // Calculate decimal-adjusted scale factor: 10^(36 + loanDecimals - collateralDecimals) + scaleFactor := u256.MustFromDecimal("1" + strings.Repeat("0", 36 + int(GetToken(params.GetLoanToken()).GetDecimals()) - int(GetToken(params.GetCollateralToken()).GetDecimals()))) + + // Finally divide by Q192 to get the actual price ratio with adjusted precision + price := math.MulDivDown(priceQ192, scaleFactor, consts.Q192) // If token0 is the loan token, we need to invert the price // because Gnoswap's price is always token1/token0 if params.IsToken0Loan { - // Invert price: ORACLE_PRICE_SCALE² / price - price = math.MulDivDown(consts.ORACLE_PRICE_SCALE, consts.ORACLE_PRICE_SCALE, price) + // Invert price: scaleFactor² / price + price = math.MulDivDown(scaleFactor, scaleFactor, price) } return price diff --git a/contract/r/volos/core/periphery.gno b/contract/r/volos/core/periphery.gno index 527f9c9..1845c30 100644 --- a/contract/r/volos/core/periphery.gno +++ b/contract/r/volos/core/periphery.gno @@ -1,7 +1,7 @@ package core import ( - "gno.land/p/demo/avl" + "gno.land/p/nt/avl" u256 "gno.land/p/gnoswap/uint256" "gno.land/p/volos/consts" "gno.land/p/volos/math" @@ -70,26 +70,6 @@ func ExpectedBorrowAssets(marketId string, user string) *u256.Uint { return math.ToAssetsUp(position.BorrowShares, market.TotalBorrowAssets, market.TotalBorrowShares) } -// CalculateBorrowRate returns the current borrow rate per second -// The returned value is WAD-scaled (1e18) -func CalculateBorrowRate(marketId string) *u256.Uint { - // Check if market exists - if _, exists := markets.Get(marketId); !exists { - panic(ErrMarketNotCreated) - } - - market, params := GetMarket(marketId) - - // If no borrows or no IRM, rate is zero - if market.TotalBorrowAssets.IsZero() || params.IRM == "" { - return u256.Zero() - } - - // Get IRM and calculate current borrow rate - irm := GetIRM(params.IRM) - return irm.BorrowRate(market.TotalSupplyAssets, market.TotalBorrowAssets) -} - // CalculateSupplyRate returns the current supply rate per second // The returned value is WAD-scaled (1e18) func CalculateSupplyRate(marketId string) *u256.Uint { @@ -109,24 +89,19 @@ func CalculateSupplyRate(marketId string) *u256.Uint { irm := GetIRM(params.IRM) borrowRate := irm.BorrowRate(market.TotalSupplyAssets, market.TotalBorrowAssets) - // Calculate utilization rate: totalBorrow / totalSupply - utilizationRate := math.WDivDown(market.TotalBorrowAssets, market.TotalSupplyAssets) - // Calculate fee-adjusted borrow rate feeFactor := new(u256.Uint).Sub(consts.WAD, market.Fee) // (1 - fee) - // Supply rate = borrow rate * utilization rate * (1 - fee) - // This formula accounts for: - // 1. Interest is only earned on the portion of funds being borrowed (utilization) - // 2. A portion of interest (the fee) goes to the fee recipient - return math.WMulDown(math.WMulDown(borrowRate, utilizationRate), feeFactor) + // Supply rate = borrow rate * (1 - fee) + // This is the base supply rate before utilization adjustment + return math.WMulDown(borrowRate, feeFactor) } // CalculateBorrowAPR returns the current borrow APR (scaled by WAD) // This converts the per-second rate to an annual rate func CalculateBorrowAPR(marketId string) *u256.Uint { // Get the per-second borrow rate - borrowRatePerSecond := CalculateBorrowRate(marketId) + borrowRatePerSecond := GetBorrowRate(marketId) if borrowRatePerSecond.IsZero() { return u256.Zero() } diff --git a/contract/r/volos/core/types.gno b/contract/r/volos/core/types.gno index f5989e5..9d82f44 100644 --- a/contract/r/volos/core/types.gno +++ b/contract/r/volos/core/types.gno @@ -42,17 +42,17 @@ func (mp *MarketParams) ID() string { // GetLoanToken returns the loan token path from the pool path func (mp *MarketParams) GetLoanToken() string { if mp.IsToken0Loan { - return pl.PoolGetToken0Path(mp.PoolPath) + return pl.GetToken0Path(mp.PoolPath) } - return pl.PoolGetToken1Path(mp.PoolPath) + return pl.GetToken1Path(mp.PoolPath) } // GetCollateralToken returns the collateral token path from the pool path func (mp *MarketParams) GetCollateralToken() string { if mp.IsToken0Loan { - return pl.PoolGetToken1Path(mp.PoolPath) + return pl.GetToken1Path(mp.PoolPath) } - return pl.PoolGetToken0Path(mp.PoolPath) + return pl.GetToken0Path(mp.PoolPath) } // IRM is the interface that all interest rate models must implement diff --git a/contract/r/volos/core/utils.gno b/contract/r/volos/core/utils.gno index 1f5e3dc..5683059 100644 --- a/contract/r/volos/core/utils.gno +++ b/contract/r/volos/core/utils.gno @@ -3,11 +3,11 @@ package core import ( "std" - "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/tokens/grc20" u256 "gno.land/p/gnoswap/uint256" + "gno.land/p/volos/math" - "gno.land/r/demo/grc20reg" - "gno.land/r/gnoswap/v1/pool" + "gno.land/r/demo/defi/grc20reg" ) // GetToken returns a GRC20 token instance @@ -108,22 +108,6 @@ func safeTransferTo(tokenPath string, to std.Address, amount int64) { } } -// Helper function to check if two tokens exist together in a Gnoswap pool -func areTokensPairedInGnoswap(token0, token1 string) bool { - poolPaths := pool.PoolGetPoolList() - for _, poolPath := range poolPaths { - poolToken0 := pool.PoolGetToken0Path(poolPath) - poolToken1 := pool.PoolGetToken1Path(poolPath) - - // Check if both tokens are in this pool (in either order) - if (poolToken0 == token0 && poolToken1 == token1) || - (poolToken0 == token1 && poolToken1 == token0) { - return true - } - } - return false -} - // Min returns the minimum of two uint256 numbers func Min(a, b *u256.Uint) *u256.Uint { if a.Lt(b) { @@ -149,3 +133,15 @@ func IsIRMEnabled(irm string) { panic(ErrIRMNotEnabled) } } + +// calculateUtilization calculates the utilization rate for a market +// Returns utilization as a WAD-scaled value (totalBorrow / totalSupply) +func calculateUtilization(marketId string) *u256.Uint { + market, _ := GetMarket(marketId) + + if market.TotalSupplyAssets.IsZero() { + return u256.Zero() + } + + return math.WDivDown(market.TotalBorrowAssets, market.TotalSupplyAssets) +} diff --git a/contract/r/volos/core/volos.gno b/contract/r/volos/core/volos.gno index 9f5f58d..e358e57 100644 --- a/contract/r/volos/core/volos.gno +++ b/contract/r/volos/core/volos.gno @@ -4,8 +4,8 @@ import ( "std" "time" - "gno.land/p/demo/avl" - "gno.land/p/demo/ownable" + "gno.land/p/nt/avl" + "gno.land/p/nt/ownable" u256 "gno.land/p/gnoswap/uint256" "gno.land/p/volos/consts" @@ -49,7 +49,7 @@ func init() { authorizers = avl.NewTree() // Set initial owner - Ownable = ownable.NewWithAddress(std.Address("g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42")) + Ownable = ownable.NewWithOrigin() } // RegisterIRM registers a new interest rate model @@ -119,7 +119,7 @@ func SetFeeRecipient(cur realm, newFeeRecipient std.Address) { } // setFee sets the fee for a specific market -func setFee(cur realm, marketId string, newFee *u256.Uint) { +func SetFee(cur realm, marketId string, newFee *u256.Uint) { Ownable.AssertOwnedByPrevious() // Get market (will panic if not found) @@ -154,7 +154,7 @@ func CreateMarket(cur realm, poolPath string, isToken0Loan bool, irm string, llt } // Verify pool exists in Gnoswap - if !pl.DoesPoolPathExist(poolPath) { + if !pl.ExistsPoolPath(poolPath) { panic(ErrTokenPairNotInGnoswap) } @@ -710,9 +710,10 @@ func Liquidate(cur realm, marketId string, borrower std.Address, seizedAssets, r markets.Set(marketId, market) // Handle bad debt if all collateral is seized + var badDebtShares, badDebtAssets *u256.Uint if borrowerPos.Collateral.IsZero() { - badDebtShares := borrowerPos.BorrowShares - badDebtAssets := math.ToAssetsUp( + badDebtShares = borrowerPos.BorrowShares + badDebtAssets = math.ToAssetsUp( badDebtShares, market.TotalBorrowAssets, market.TotalBorrowShares, @@ -725,6 +726,9 @@ func Liquidate(cur realm, marketId string, borrower std.Address, seizedAssets, r market.TotalSupplyAssets = new(u256.Uint).Sub(market.TotalSupplyAssets, badDebtAssets) market.TotalBorrowShares = new(u256.Uint).Sub(market.TotalBorrowShares, badDebtShares) borrowerPos.BorrowShares = u256.Zero() + } else { + badDebtShares = u256.Zero() + badDebtAssets = u256.Zero() } // Transfer seized collateral to liquidator @@ -734,8 +738,8 @@ func Liquidate(cur realm, marketId string, borrower std.Address, seizedAssets, r // Transfer repaid assets from liquidator to contract safeTransferFrom(params.GetLoanToken(), caller, repaidAssets.Int64()) - // Emit liquidate event - emitLiquidate(marketId, caller, borrower, repaidAssets, repaidSharesU256, seizedAssetsU256) + // Emit liquidate event with bad debt information + emitLiquidate(marketId, caller, borrower, repaidAssets, repaidSharesU256, seizedAssetsU256, badDebtAssets, badDebtShares) return seizedAssetsU256.Uint64(), repaidAssets.Uint64() } @@ -857,7 +861,7 @@ func accrueInterest(marketId string) { market.LastUpdate = now markets.Set(marketId, market) - emitAccrueInterest(marketId, borrowRate, interest) + emitAccrueInterest(marketId, borrowRate, interest, market.TotalSupplyAssets, market.TotalBorrowAssets) } /* HEALTH CALCULATIONS */ diff --git a/contract/r/volos/gov/doc.gno b/contract/r/volos/gov/doc.gno new file mode 100644 index 0000000..6344844 --- /dev/null +++ b/contract/r/volos/gov/doc.gno @@ -0,0 +1,20 @@ +// Package gov provides the entry point and coordination for the Volos protocol's +// on-chain governance system. +// +// The Volos governance system is composed of several tightly integrated +// contracts. vls.gno implements the VLS governance token, which is +// freely transferable and used for staking. The staker.gno contract manages +// staking and delegation: users stake VLS to mint xVLS to themselves or a +// delegatee, and unstaking triggers a cooldown before VLS can be withdrawn. +// Only the staker contract can mint or burn xVLS. +// +// xvls.gno defines the xVLS token, a non-transferable representation of staked +// VLS. xVLS determines voting power in governance. The governance contract +// contains the DAO logic, built on commondao.gno, where only xVLS holders are +// members and can propose or vote. Membership is updated automatically by +// staking actions. +// +// Together, these contracts enable secure, weighted, and flexible on-chain +// governance for the Volos protocol, with clear separation of concerns between +// token logic, staking and delegation, and proposal and voting mechanisms. +package gov diff --git a/contract/r/volos/gov/gnomod.toml b/contract/r/volos/gov/gnomod.toml new file mode 100644 index 0000000..e5b56ea --- /dev/null +++ b/contract/r/volos/gov/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/volos/gov" +gno = "0.9" diff --git a/contract/r/volos/gov/governance/api.gno b/contract/r/volos/gov/governance/api.gno index 1c7961a..1348d0f 100644 --- a/contract/r/volos/gov/governance/api.gno +++ b/contract/r/volos/gov/governance/api.gno @@ -4,7 +4,7 @@ import ( "std" "strconv" - "gno.land/p/demo/json" + "gno.land/p/onbloc/json" ) // ApiGetProposal returns a proposal as JSON object diff --git a/contract/r/volos/gov/governance/errors.gno b/contract/r/volos/gov/governance/errors.gno index e75a6c8..73d1349 100644 --- a/contract/r/volos/gov/governance/errors.gno +++ b/contract/r/volos/gov/governance/errors.gno @@ -8,4 +8,5 @@ var ( ErrNoXVLS = errors.New("cannot add member: address does not hold any xVLS") ErrVotingDeadlineNotMet = errors.New("voting deadline not met") ErrNotStaker = errors.New("only staker can call this function") + ErrVotingPeriodExceedsMaximum = errors.New("voting period exceeds maximum allowed duration") ) diff --git a/contract/r/volos/gov/governance/events.gno b/contract/r/volos/gov/governance/events.gno index f1f5546..6e890c5 100644 --- a/contract/r/volos/gov/governance/events.gno +++ b/contract/r/volos/gov/governance/events.gno @@ -44,6 +44,7 @@ const ( EventNewThresholdKey = "new_threshold" EventQuorumKey = "quorum" EventXvlsAmountKey = "xvls_amount" + EventStatusKey = "status" ) func emitProposalCreated(caller std.Address, proposalID uint64, title, body string, deadline, quorum int64) { @@ -59,11 +60,12 @@ func emitProposalCreated(caller std.Address, proposalID uint64, title, body stri ) } -func emitProposalExecuted(caller std.Address, proposalID uint64) { +func emitProposalExecuted(caller std.Address, proposalID uint64, status string) { std.Emit( EventProposalExecuted, EventCallerKey, caller.String(), EventProposalIDKey, strconv.FormatUint(proposalID, 10), + EventStatusKey, status, EventTimestampKey, strconv.FormatInt(time.Now().Unix(), 10), ) } diff --git a/contract/r/volos/gov/governance/governance.gno b/contract/r/volos/gov/governance/governance.gno index d7b27fc..13185af 100644 --- a/contract/r/volos/gov/governance/governance.gno +++ b/contract/r/volos/gov/governance/governance.gno @@ -10,6 +10,7 @@ package governance import ( "std" + "time" "gno.land/p/moul/authz" "gno.land/p/nt/commondao" @@ -19,6 +20,7 @@ var ( volosGovernance *commondao.CommonDAO proposalThreshold int64 = 1000 //min xVLS required to create a proposal votingPowerQuorum int64 = 1000 //minimum xVLS voting power required for a proposal to pass (0 = disabled) + maximumProposalDuration time.Duration = 14 * 24 * time.Hour //maximum duration of a proposal authorizer = authz.NewWithAuthority(authz.NewMemberAuthority( std.DerivePkgAddr("gno.land/r/volos/gov/staker"), @@ -55,6 +57,14 @@ func SetVotingPowerQuorum(newQuorum int64) { }) } +// SetMaximumProposalDuration allows governance to change the maximum proposal duration. +func SetMaximumProposalDuration(newMax time.Duration) { + authorizer.DoByCurrent("set_maximum_proposal_duration", func() error { + maximumProposalDuration = newMax + return nil + }) +} + // MemberSet returns a read-only view of the current member set for analytics and governance logic. func MemberSet() commondao.MemberSet { return commondao.NewMemberSet(volosGovernance.Members()) @@ -75,6 +85,11 @@ func VotingPowerQuorumEnabled() bool { return votingPowerQuorum > 0 } +// MaximumProposalDuration returns the current maximum allowed proposal duration. +func MaximumProposalDuration() time.Duration { + return maximumProposalDuration +} + // Render function for explorer integration. func Render(path string) string { return "Volos Governance - On-chain governance for the Volos protocol" diff --git a/contract/r/volos/gov/governance/governance_test.gno b/contract/r/volos/gov/governance/governance_test.gno index 911c13d..f583a23 100644 --- a/contract/r/volos/gov/governance/governance_test.gno +++ b/contract/r/volos/gov/governance/governance_test.gno @@ -5,8 +5,8 @@ import ( "testing" "time" - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" + "gno.land/p/nt/uassert" + "gno.land/p/nt/urequire" "gno.land/p/nt/commondao" "gno.land/r/volos/gov/xvls" ) @@ -446,3 +446,34 @@ func TestProposal_QuorumChangesDontAffectActiveProposals(cur realm, t *testing.T SetVotingPowerQuorum(1000) }) } + +func TestProposal_MaximumDurationEnforced(cur realm, t *testing.T) { + staker := "gno.land/r/volos/gov/staker" + gov := "gno.land/r/volos/gov/governance" + alice := std.DerivePkgAddr("gno.land/r/volos/gov/alice") + + crossThrough(std.NewCodeRealm(staker), func() { + xvls.Mint(cross, alice, 10_000) + AddMember(cross, alice) + }) + + max := MaximumProposalDuration() + urequire.True(t, max > 0) + + crossThrough(std.NewUserRealm(alice), func() { + uassert.AbortsWithMessage(t, "voting period exceeds maximum allowed duration", func() { + CreateProposal(cross, "Too long", "Body", max+time.Second, func() {}) + }) + }) + + crossThrough(std.NewCodeRealm(gov), func() { + SetMaximumProposalDuration(max + time.Hour) + }) + + urequire.Equal(t, int64(max+time.Hour), int64(MaximumProposalDuration())) + + crossThrough(std.NewUserRealm(alice), func() { + p := CreateProposal(cross, "Within new max", "Body", max+time.Second, func() {}) + urequire.NotEqual(t, nil, p) + }) +} diff --git a/contract/r/volos/gov/governance/json.gno b/contract/r/volos/gov/governance/json.gno index 3e767e5..4a13f34 100644 --- a/contract/r/volos/gov/governance/json.gno +++ b/contract/r/volos/gov/governance/json.gno @@ -3,7 +3,7 @@ package governance import ( "std" - "gno.land/p/demo/json" + "gno.land/p/onbloc/json" "gno.land/p/nt/commondao" "gno.land/r/volos/gov/vls" "gno.land/r/volos/gov/xvls" diff --git a/contract/r/volos/gov/governance/members_test.gno b/contract/r/volos/gov/governance/members_test.gno index c453283..d68bdf5 100644 --- a/contract/r/volos/gov/governance/members_test.gno +++ b/contract/r/volos/gov/governance/members_test.gno @@ -4,8 +4,8 @@ import ( "std" "testing" - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" + "gno.land/p/nt/uassert" + "gno.land/p/nt/urequire" "gno.land/r/volos/gov/xvls" ) diff --git a/contract/r/volos/gov/governance/proposal.gno b/contract/r/volos/gov/governance/proposal.gno index 82d5b28..b7c6915 100644 --- a/contract/r/volos/gov/governance/proposal.gno +++ b/contract/r/volos/gov/governance/proposal.gno @@ -8,7 +8,7 @@ import ( "std" "time" - "gno.land/p/demo/avl" + "gno.land/p/nt/avl" "gno.land/p/nt/commondao" "gno.land/r/volos/gov/xvls" ) @@ -90,6 +90,10 @@ func CreateProposal(cur realm, title, body string, votingPeriod time.Duration, a panic(ErrInsufficientXVLS) } + if votingPeriod > maximumProposalDuration { + panic(ErrVotingPeriodExceedsMaximum) + } + def := VolosProposalDefinition{ TitleField: title, BodyField: body, @@ -118,7 +122,7 @@ func Execute(cur realm, proposalID uint64) { panic(err) } - emitProposalExecuted(caller, proposalID) + emitProposalExecuted(caller, proposalID, string(volosGovernance.GetProposal(proposalID).Status())) } // GetUserActiveProposals returns all active proposals that the given user has voted on. diff --git a/contract/r/volos/gov/governance/proposal_test.gno b/contract/r/volos/gov/governance/proposal_test.gno index c2ba358..d388da7 100644 --- a/contract/r/volos/gov/governance/proposal_test.gno +++ b/contract/r/volos/gov/governance/proposal_test.gno @@ -5,8 +5,8 @@ import ( "testing" "time" - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" + "gno.land/p/nt/uassert" + "gno.land/p/nt/urequire" "gno.land/p/nt/commondao" "gno.land/r/volos/gov/xvls" ) diff --git a/contract/r/volos/gov/governance/vote_test.gno b/contract/r/volos/gov/governance/vote_test.gno index c8feccd..7359cb0 100644 --- a/contract/r/volos/gov/governance/vote_test.gno +++ b/contract/r/volos/gov/governance/vote_test.gno @@ -5,8 +5,8 @@ import ( "testing" "time" - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" + "gno.land/p/nt/uassert" + "gno.land/p/nt/urequire" "gno.land/p/nt/commondao" "gno.land/r/volos/gov/xvls" ) diff --git a/contract/r/volos/gov/staker/events.gno b/contract/r/volos/gov/staker/events.gno index f17fffa..6b91bde 100644 --- a/contract/r/volos/gov/staker/events.gno +++ b/contract/r/volos/gov/staker/events.gno @@ -6,7 +6,7 @@ import ( "strings" "time" - "gno.land/p/demo/seqid" + "gno.land/p/nt/seqid" ) // Events diff --git a/contract/r/volos/gov/staker/staker.gno b/contract/r/volos/gov/staker/staker.gno index acf3db3..ee2c68f 100644 --- a/contract/r/volos/gov/staker/staker.gno +++ b/contract/r/volos/gov/staker/staker.gno @@ -14,9 +14,9 @@ import ( "std" "time" - "gno.land/p/demo/avl" - "gno.land/p/demo/avl/rotree" - "gno.land/p/demo/seqid" + "gno.land/p/nt/avl" + "gno.land/p/nt/avl/rotree" + "gno.land/p/nt/seqid" "gno.land/p/moul/authz" "gno.land/r/volos/gov/governance" "gno.land/r/volos/gov/vls" @@ -159,7 +159,7 @@ func WithdrawUnstaked(cur realm) { } if totalToWithdraw == 0 { - panic(ErrCooldownNotFinished) + panic(ErrNoReadyUnstake) } vls.Transfer(cross, caller, totalToWithdraw) diff --git a/contract/r/volos/gov/staker/staker_test.gno b/contract/r/volos/gov/staker/staker_test.gno index 7f80f2f..990c351 100644 --- a/contract/r/volos/gov/staker/staker_test.gno +++ b/contract/r/volos/gov/staker/staker_test.gno @@ -4,8 +4,8 @@ import ( "std" "testing" - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" + "gno.land/p/nt/uassert" + "gno.land/p/nt/urequire" "gno.land/r/volos/gov/governance" "gno.land/r/volos/gov/vls" "gno.land/r/volos/gov/xvls" diff --git a/contract/r/volos/gov/staker/utils.gno b/contract/r/volos/gov/staker/utils.gno index 5e0ae5c..5d259ea 100644 --- a/contract/r/volos/gov/staker/utils.gno +++ b/contract/r/volos/gov/staker/utils.gno @@ -3,7 +3,7 @@ package staker import ( "std" - "gno.land/p/demo/avl" + "gno.land/p/nt/avl" "gno.land/r/volos/gov/governance" ) diff --git a/contract/r/volos/gov/vls/api.gno b/contract/r/volos/gov/vls/api.gno index 3cc69f7..817d296 100644 --- a/contract/r/volos/gov/vls/api.gno +++ b/contract/r/volos/gov/vls/api.gno @@ -3,7 +3,7 @@ package vls import ( "std" - "gno.land/p/demo/json" + "gno.land/p/onbloc/json" ) // ApiGetTokenInfo returns VLS token information as JSON object diff --git a/contract/r/volos/gov/vls/json.gno b/contract/r/volos/gov/vls/json.gno index 7ea501e..031b635 100644 --- a/contract/r/volos/gov/vls/json.gno +++ b/contract/r/volos/gov/vls/json.gno @@ -3,7 +3,7 @@ package vls import ( "std" - "gno.land/p/demo/json" + "gno.land/p/onbloc/json" ) type RpcTokenInfo struct { diff --git a/contract/r/volos/gov/vls/vls.gno b/contract/r/volos/gov/vls/vls.gno index 8ff9add..b6808d0 100644 --- a/contract/r/volos/gov/vls/vls.gno +++ b/contract/r/volos/gov/vls/vls.gno @@ -15,9 +15,9 @@ package vls import ( "std" - "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/tokens/grc20" "gno.land/p/moul/authz" - "gno.land/r/demo/grc20reg" + "gno.land/r/demo/defi/grc20reg" ) var ( diff --git a/contract/r/volos/gov/vls/vls_test.gno b/contract/r/volos/gov/vls/vls_test.gno index f829514..7674a0b 100644 --- a/contract/r/volos/gov/vls/vls_test.gno +++ b/contract/r/volos/gov/vls/vls_test.gno @@ -4,7 +4,7 @@ import ( "std" "testing" - "gno.land/p/demo/urequire" + "gno.land/p/nt/urequire" ) func crossThrough(rlm std.Realm, cr func()) { diff --git a/contract/r/volos/gov/xvls/api.gno b/contract/r/volos/gov/xvls/api.gno index 7a23bcd..fc270c4 100644 --- a/contract/r/volos/gov/xvls/api.gno +++ b/contract/r/volos/gov/xvls/api.gno @@ -3,7 +3,7 @@ package xvls import ( "std" - "gno.land/p/demo/json" + "gno.land/p/onbloc/json" ) // ApiGetTokenInfo returns xVLS token information as JSON object diff --git a/contract/r/volos/gov/xvls/json.gno b/contract/r/volos/gov/xvls/json.gno index 57ce18e..9197a1f 100644 --- a/contract/r/volos/gov/xvls/json.gno +++ b/contract/r/volos/gov/xvls/json.gno @@ -3,7 +3,7 @@ package xvls import ( "std" - "gno.land/p/demo/json" + "gno.land/p/onbloc/json" ) type RpcTokenInfo struct { diff --git a/contract/r/volos/gov/xvls/xvls.gno b/contract/r/volos/gov/xvls/xvls.gno index a0160a1..980be89 100644 --- a/contract/r/volos/gov/xvls/xvls.gno +++ b/contract/r/volos/gov/xvls/xvls.gno @@ -12,9 +12,9 @@ package xvls import ( "std" - "gno.land/p/demo/grc/grc20" + "gno.land/p/demo/tokens/grc20" "gno.land/p/moul/authz" - "gno.land/r/demo/grc20reg" + "gno.land/r/demo/defi/grc20reg" ) var ( diff --git a/contract/r/volos/gov/xvls/xvls_test.gno b/contract/r/volos/gov/xvls/xvls_test.gno index ba77123..a64eca2 100644 --- a/contract/r/volos/gov/xvls/xvls_test.gno +++ b/contract/r/volos/gov/xvls/xvls_test.gno @@ -4,8 +4,8 @@ import ( "std" "testing" - "gno.land/p/demo/uassert" - "gno.land/p/demo/urequire" + "gno.land/p/nt/uassert" + "gno.land/p/nt/urequire" ) func crossThrough(rlm std.Realm, cr func()) { diff --git a/tests/volos/create_proposals.gno b/contract/r/volos/mocks/create_proposals.gno similarity index 60% rename from tests/volos/create_proposals.gno rename to contract/r/volos/mocks/create_proposals.gno index c05af70..a6f22ff 100644 --- a/tests/volos/create_proposals.gno +++ b/contract/r/volos/mocks/create_proposals.gno @@ -1,8 +1,11 @@ -package main +package mocks import ( "time" + "std" "gno.land/r/volos/gov/governance" + "gno.land/r/volos/gov/vls" + "gno.land/r/volos/gov/staker" ) // callback function that will be executed if the proposal passes @@ -11,26 +14,30 @@ func callback() { // In a real scenario, this could update governance parameters, mint tokens, etc. } -func main() { +func CreateProposals(cur realm) { + vls.Faucet(cross, 100000000) + vls.Approve(cross, std.DerivePkgAddr("gno.land/r/volos/gov/staker"), 100000000) + staker.Stake(cross, 100000, std.DerivePkgAddr("gno.land/r/volos/mocks")) + governance.CreateProposal( cross, - "Test Proposal", + "Test Proposal 1", "This is a test proposal to verify governance functionality", - time.Duration(5*time.Hour), + time.Duration(1*time.Minute), callback, ) governance.CreateProposal( cross, - "Test Proposal", + "Test Proposal 2", "This is a test proposal to verify governance functionality", - time.Duration(5*time.Hour), + time.Duration(1*time.Minute), callback, ) governance.CreateProposal( cross, - "Test Proposal", + "Test Proposal 3", "This is a test proposal to verify governance functionality", time.Duration(5*time.Hour), callback, diff --git a/contract/r/volos/mocks/flash_borrower.gno b/contract/r/volos/mocks/flash_borrower.gno index 1a4970b..46c0197 100644 --- a/contract/r/volos/mocks/flash_borrower.gno +++ b/contract/r/volos/mocks/flash_borrower.gno @@ -3,7 +3,7 @@ package mocks import ( "std" - "gno.land/r/demo/grc20reg" + "gno.land/r/demo/defi/grc20reg" volos "gno.land/r/volos/core" ) diff --git a/contract/r/volos/render/home.gno b/contract/r/volos/render/home.gno index 2159e1a..1328847 100644 --- a/contract/r/volos/render/home.gno +++ b/contract/r/volos/render/home.gno @@ -1,12 +1,12 @@ package render import ( - "gno.land/p/demo/avl" - "gno.land/p/demo/avl/pager" - "gno.land/p/demo/avl/rotree" + "gno.land/p/nt/avl" + "gno.land/p/nt/avl/pager" + "gno.land/p/nt/avl/rotree" "gno.land/p/moul/md" "gno.land/p/moul/mdtable" - "gno.land/r/demo/grc20reg" + "gno.land/r/demo/defi/grc20reg" volos "gno.land/r/volos/core" ) diff --git a/contract/r/volos/render/market.gno b/contract/r/volos/render/market.gno index a84fe52..42992f5 100644 --- a/contract/r/volos/render/market.gno +++ b/contract/r/volos/render/market.gno @@ -6,12 +6,12 @@ import ( "strconv" "strings" - "gno.land/p/demo/avl" - "gno.land/p/demo/avl/pager" - "gno.land/p/demo/avl/rotree" + "gno.land/p/nt/avl" + "gno.land/p/nt/avl/pager" + "gno.land/p/nt/avl/rotree" "gno.land/p/moul/md" "gno.land/p/moul/mdtable" - "gno.land/r/demo/grc20reg" + "gno.land/r/demo/defi/grc20reg" "gno.land/r/sys/users" volos "gno.land/r/volos/core" ) diff --git a/contract/r/volos/render/user.gno b/contract/r/volos/render/user.gno index b0684de..ee7977a 100644 --- a/contract/r/volos/render/user.gno +++ b/contract/r/volos/render/user.gno @@ -8,7 +8,7 @@ import ( "gno.land/p/matijamarjanovic/charts" "gno.land/p/moul/md" "gno.land/p/moul/mdtable" - "gno.land/r/demo/grc20reg" + "gno.land/r/demo/defi/grc20reg" "gno.land/r/sys/users" volos "gno.land/r/volos/core" ) diff --git a/contract/r/volos/render/utils.gno b/contract/r/volos/render/utils.gno index 99134cb..4b52d4e 100644 --- a/contract/r/volos/render/utils.gno +++ b/contract/r/volos/render/utils.gno @@ -5,7 +5,7 @@ import ( "strings" "time" - "gno.land/p/demo/ufmt" + "gno.land/p/nt/ufmt" volos "gno.land/r/volos/core" ) diff --git a/tests/gnoswap/_info.mk b/tests/gnoswap/_info.mk index a6c4dcb..4739083 100644 --- a/tests/gnoswap/_info.mk +++ b/tests/gnoswap/_info.mk @@ -1,5 +1,5 @@ -# r/demo/wugnot from gno -ADDR_WUGNOT := g1pf6dv9fjk3rn0m4jjcne306ga4he3mzmupfjl6 +# r/gnoland/wugnot from gno +ADDR_WUGNOT := g15vj5q08amlvyd0nx6zjgcvwq2d0gt9fcchrvum # based on v1 ADDR_POOL := g148tjamj80yyrm309z7rk690an22thd2l3z8ank @@ -10,7 +10,7 @@ ADDR_PROTOCOL_FEE := g1f7wpek7q67tkns27sw495u5yuu3a5wwjxw5l6l ADDR_GOV_STAKER := g17e3ykyqk9jmqe2y9wxe9zhep3p7cw56davjqwa ADR_GOV_GOV := g17s8w2ve7k85fwfnrk59lmlhthkjdted8whvqxd ADDR_LAUNCHPAD := g122mau2lp2rc0scs8d27pkkuys4w54mdy2tuer3 -ADDR_GNS := g1jgqwaa2le3yr63d533fj785qkjspumzv22ys5m +ADDR_GNS := g13ffa5r3mqfxu3s7ejl02scq9536wt6c2t789dm ADDR_GNFT := g1wxv2rdfn53qc84nt3nn646f9yh3nly8lm7j89t # username address diff --git a/tests/gnoswap/test.mk b/tests/gnoswap/test.mk index 9e6b985..28789ee 100644 --- a/tests/gnoswap/test.mk +++ b/tests/gnoswap/test.mk @@ -1,18 +1,18 @@ include _info.mk -GNS_PATH := gno.land/r/gnoswap/v1/gns -USDC_PATH := gno.land/r/gnoswap/v1/test_token/usdc -BAZ_PATH := gno.land/r/gnoswap/v1/test_token/baz -BAR_PATH := gno.land/r/gnoswap/v1/test_token/bar -OBL_PATH := gno.land/r/gnoswap/v1/test_token/obl -QUX_PATH := gno.land/r/gnoswap/v1/test_token/qux -FOO_PATH := gno.land/r/gnoswap/v1/test_token/foo +GNS_PATH := gno.land/r/gnoswap/gns +USDC_PATH := gno.land/r/gnoswap/test_token/usdc +BAZ_PATH := gno.land/r/gnoswap/test_token/baz +BAR_PATH := gno.land/r/gnoswap/test_token/bar +OBL_PATH := gno.land/r/gnoswap/test_token/obl +QUX_PATH := gno.land/r/gnoswap/test_token/qux +FOO_PATH := gno.land/r/gnoswap/test_token/foo ADDR_TEST_ADMIN := g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42 -ADDR_USER_1 := g149gwxltsfg6dghhq0txu8kam7chfpt9amnx4yx -ADDR_USER_2 := g170akkhclm6dzv67sxpandkjyvtv92kvyzyd2q9 -ADDR_USER_3 := g135ut9uh8l3f092r46z52qwa3wgfa5q8qcjlehy -ADDR_USER_4 := g1upryqylqstmy9wfpq2jz6ep0udj9mau8st8hf2 +ADDR_USER_1 := g1yupktql2l6jd8tkxu8c4rv4uc5l7cv3y70d7ln +ADDR_USER_2 := g194zxz7ahvkhj3p3hlxkc5axe4exe9ra0yzmhqd +ADDR_USER_3 := g1uf7c8tsuhsp5la6hepp5p6yjy390k6rcee8hg5 +ADDR_USER_4 := g133mzul2d7c43vr87s58797pkj6l7vpx30jcn9p .PHONY: transfer-base-token transfer-base-token: transfer-ugnot transfer-gns transfer-usdc transfer-baz transfer-bar transfer-obl transfer-qux transfer-foo @@ -29,65 +29,65 @@ transfer-ugnot: transfer-gns: $(info ************ transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP) ************) - @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 + @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(GNS_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin @echo transfer-usdc: $(info ************ transfer 1_000_000_000 USDC to $(ADDR_GNOSWAP) ************) - @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 + @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(USDC_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin @echo transfer-baz: $(info ************ transfer 1_000_000_000 BAZ to $(ADDR_GNOSWAP) ************) - @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 + @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(BAZ_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin @echo transfer-bar: $(info ************ transfer 1_000_000_000 BAR to $(ADDR_GNOSWAP) ************) - @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 + @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(BAR_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin @echo transfer-obl: $(info ************ transfer 1_000_000_000 OBL to $(ADDR_GNOSWAP) ************) - @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 + @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(OBL_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin @echo transfer-qux: $(info ************ transfer 1_000_000_000 QUX to $(ADDR_GNOSWAP) ************) - @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 + @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(QUX_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin @echo transfer-foo: $(info ************ transfer 1_000_000_000 FOO to $(ADDR_GNOSWAP) ************) - @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 - @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" test1 + @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_TEST_ADMIN) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_USER_1) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_USER_2) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_USER_3) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath $(FOO_PATH) -func Transfer -args $(ADDR_USER_4) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "transfer 1_000_000_000 GNS to $(ADDR_GNOSWAP)" gnoswap_admin @echo faucet-ugnot: @@ -98,68 +98,63 @@ faucet-ugnot: # pool create pool-create-gns-wugnot-default: $(info ************ create default pool (GNS:WUGNOT:0.03%) ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/pool -func CreatePool -args "gno.land/r/demo/wugnot" -args "gno.land/r/gnoswap/v1/gns" -args 3000 -args 79228162514264337593543950337 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/pool -func CreatePool -args "gno.land/r/gnoland/wugnot" -args "gno.land/r/gnoswap/gns" -args 3000 -args 79228162514264337593543950337 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # pool create 2 pool-create-bar-wugnot-default: $(info ************ create default pool (BAR:WUGNOT:0.03%) ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/test_token/bar -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/pool -func CreatePool -args "gno.land/r/demo/wugnot" -args "gno.land/r/gnoswap/v1/test_token/bar" -args 3000 -args 79228162514264337593543950337 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/test_token/bar -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/pool -func CreatePool -args "gno.land/r/gnoland/wugnot" -args "gno.land/r/gnoswap/test_token/bar" -args 3000 -args 79228162514264337593543950337 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo + # mint new position mint-gns-gnot: $(info ************ mint position(1) to gns:wugnot ************) # APPROVE FISRT - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # APPROVE WUGNOT TO POSITION, to get refund wugnot left after wrap -> mint - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args $(ADDR_POSITION) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_POSITION) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN MINT - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func Mint -send "20000000ugnot" -args "gno.land/r/gnoswap/v1/gns" -args "gnot" -args 3000 -args "-49980" -args "49980" -args 20000000 -args 20000000 -args 1 -args 1 -args $(TX_EXPIRE) -args $(ADDR_ADMIN) -args $(ADDR_ADMIN) -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func Mint -send "20000000ugnot" -args "gno.land/r/gnoswap/gns" -args "gnot" -args 3000 -args "-49980" -args "49980" -args 20000000 -args 20000000 -args 1 -args 1 -args $(TX_EXPIRE) -args $(ADDR_ADMIN) -args $(ADDR_ADMIN) -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo - # SetTokenURI - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gnft -func SetTokenURIByImageURI -args "1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo # mint new position mint-bar-wugnot: $(info ************ mint position(1) to bar:wugnot ************) # APPROVE FISRT - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/test_token/bar -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/test_token/bar -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # APPROVE WUGNOT TO POSITION, to get refund wugnot left after wrap -> mint - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args $(ADDR_POSITION) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_POSITION) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN MINT - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func Mint -send "20000000ugnot" -args "gno.land/r/gnoswap/v1/test_token/bar" -args "gnot" -args 3000 -args "-49980" -args "49980" -args 20000000 -args 20000000 -args 1 -args 1 -args $(TX_EXPIRE) -args $(ADDR_GNOSWAP) -args $(ADDR_GNOSWAP) -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func Mint -send "20000000ugnot" -args "gno.land/r/gnoswap/test_token/bar" -args "gnot" -args 3000 -args "-49980" -args "49980" -args 20000000 -args 20000000 -args 1 -args 1 -args $(TX_EXPIRE) -args $(ADDR_GNOSWAP) -args $(ADDR_GNOSWAP) -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo - # SetTokenURI - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gnft -func SetTokenURIByImageURI -args "2" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo # increase liquidity increase-liquidity-position-01: $(info ************ increase position(1) liquidity gnot:gns:3000 ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func IncreaseLiquidity -send "20000000ugnot" -args 1 -args 20000000 -args 20000000 -args 1 -args 1 -args $(TX_EXPIRE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func IncreaseLiquidity -send "20000000ugnot" -args 1 -args 20000000 -args 20000000 -args 1 -args 1 -args $(TX_EXPIRE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # decrease liquidity decrease-liquidity-position-01: $(info ************ decrease position(1) liquidity gnot:gns:3000 ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func DecreaseLiquidity -args 1 -args 12345678 -args 0 -args 0 -args $(TX_EXPIRE) -args "false" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func DecreaseLiquidity -args 1 -args 12345678 -args 0 -args 0 -args $(TX_EXPIRE) -args "false" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo @@ -167,33 +162,33 @@ decrease-liquidity-position-01: create-external-incentive: $(info ************ create external incentive [gns] => gnot:gns:3000 ************) # APPROVE REWARD (+ DepositGNS) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_STAKER) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_STAKER) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN CREATE EXTERNAL INCENTIVE - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/staker -func CreateExternalIncentive -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000" -args "gno.land/r/gnoswap/v1/gns" -args 1000000000 -args $(TOMORROW_MIDNIGHT) -args $(INCENTIVE_END) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/staker -func CreateExternalIncentive -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000" -args "gno.land/r/gnoswap/gns" -args 1000000000 -args $(TOMORROW_MIDNIGHT) -args $(INCENTIVE_END) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # stake token stake-token-1: $(info ************ stake token 1 ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gnft -func Approve -args $(ADDR_STAKER) -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/staker -func StakeToken -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gnft -func Approve -args $(ADDR_STAKER) -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/staker -func StakeToken -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # collect staking reward collect-staking-reward-1: $(info ************ collect reward 1 ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/staker -func CollectReward -args 1 -args false -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/staker -func CollectReward -args 1 -args false -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # unstake token unstake-token-1: $(info ************ unstake token 1 ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/staker -func UnStakeToken -args 1 -args true -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/staker -func UnStakeToken -args 1 -args true -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo @@ -201,12 +196,12 @@ unstake-token-1: swap-exact-in-gns-wugnot: $(info ************ swap gns -> wgnot, exact_in ************) # approve INPUT TOKEN to POOL - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin # approve OUTPUT TOKEN to ROUTER ( as 0.15% fee ) - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args $(ADDR_ROUTER) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_ROUTER) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/router -func ExactInSwapRoute -args "gno.land/r/gnoswap/v1/gns" -args "gno.land/r/demo/wugnot" -args 50000 -args "gno.land/r/gnoswap/v1/gns:gno.land/r/demo/wugnot:3000" -args "100" -args "0" -args $(TX_EXPIRE) -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/router -func ExactInSwapRoute -args "gno.land/r/gnoswap/gns" -args "gno.land/r/gnoland/wugnot" -args 50000 -args "gno.land/r/gnoswap/gns:gno.land/r/gnoland/wugnot:3000" -args "100" -args "0" -args $(TX_EXPIRE) -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo @@ -214,19 +209,19 @@ swap-exact-in-gns-wugnot: swap-exact-out-gns-wugnot: $(info ************ swap gns -> wgnot, exact_out ************) # approve INPUT TOKEN to POOL - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_POOL) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin # approve OUTPUT TOKEN to ROUTER ( as 0.15% fee ) - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args $(ADDR_ROUTER) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_ROUTER) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/router -func ExactOutSwapRoute -args "gno.land/r/gnoswap/v1/gns" -args "gno.land/r/demo/wugnot" -args 50000 -args "gno.land/r/gnoswap/v1/gns:gno.land/r/demo/wugnot:3000" -args "100" -args "60000" -args $(TX_EXPIRE) -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/router -func ExactOutSwapRoute -args "gno.land/r/gnoswap/gns" -args "gno.land/r/gnoland/wugnot" -args 50000 -args "gno.land/r/gnoswap/gns:gno.land/r/gnoland/wugnot:3000" -args "100" -args "60000" -args $(TX_EXPIRE) -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # collect swap fee collect-swap-fee: $(info ************ collect swap fee ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func CollectFee -args 1 -args false -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/position -func CollectFee -args 1 -args false -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo @@ -234,92 +229,92 @@ collect-swap-fee: delegate: $(info ************ delegate 5_000_000_000 to self ************) # APPROVE FIRST - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_GOV_STAKER) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_GOV_STAKER) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin # DELEGATE - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/staker -func Delegate -args $(ADDR_GNOSWAP) -args 5000000000 -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/staker -func Delegate -args $(ADDR_GNOSWAP) -args 5000000000 -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # redelegate gns redelegate: $(info ************ redelegate 1_000_000_000 from self to self ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/staker -func Redelegate -args $(ADDR_GNOSWAP) -args $(ADDR_GNOSWAP) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/staker -func Redelegate -args $(ADDR_GNOSWAP) -args $(ADDR_GNOSWAP) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # undelegate gns undelegate: $(info ************ undelegate 1_000_000_000 from self ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/staker -func Undelegate -args $(ADDR_GNOSWAP) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/staker -func Undelegate -args $(ADDR_GNOSWAP) -args 1000000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # collect undelegated gns collect-undelegated: $(info ************ collect undelegated gns ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/staker -func CollectUndelegatedGns -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/staker -func CollectUndelegatedGns -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # propose text proposal propose-text: $(info ************ propose text ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func ProposeText -args "title_for_text" -args "desc_for_text" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func ProposeText -args "title_for_text" -args "desc_for_text" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # cancel proposal cancel-text: $(info ************ cancel text ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func Cancel -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func Cancel -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # propose community_pool send proposal propose-community: $(info ************ propose community pool spend ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func ProposeCommunityPoolSpend -args "title_for_spend" -args "desc_for_spend" -args $(ADDR_GNOSWAP) -args "gno.land/r/gnoswap/v1/gns" -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func ProposeCommunityPoolSpend -args "title_for_spend" -args "desc_for_spend" -args $(ADDR_GNOSWAP) -args "gno.land/r/gnoswap/gns" -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # vote proposal vote-community: $(info ************ vote community pool spend ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func Vote -args 2 -args true -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func Vote -args 2 -args true -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # execute proposal execute-community: $(info ************ execute community pool spend ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func Execute -args 2 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func Execute -args 2 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # propose parameter change proposal propose-param: $(info ************ propose param change ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func ProposeParameterChange -args "title param change" -args "desc param change" -args "2" -args "gno.land/r/gnoswap/v1/gns*EXE*SetAvgBlockTimeInMs*EXE*123*GOV*gno.land/r/gnoswap/v1/community_pool*EXE*TransferToken*EXE*gno.land/r/gnoswap/v1/gns,g17290cwvmrapvp869xfnhhawa8sm9edpufzat7d,905" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gov/governance -func ProposeParameterChange -args "title param change" -args "desc param change" -args "1" -args "gno.land/r/gnoswap/v1/community_pool*EXE*TransferToken*EXE*gno.land/r/gnoswap/gns,g17290cwvmrapvp869xfnhhawa8sm9edpufzat7d,905" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # create launchpad project create-launchpad-project: $(info ************ create bar project ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/test_token/bar -func Approve -args $(ADDR_LAUNCHPAD) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/launchpad -func CreateProject -args "Test Launch" -args "gno.land/r/gnoswap/v1/test_token/bar" -args "g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c" -args 10000000000 -args "gno.land/r/gnoswap/v1/gns" -args "0" -args 20 -args 30 -args 50 -args 1740385500 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/test_token/bar -func Approve -args $(ADDR_LAUNCHPAD) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/launchpad -func CreateProject -args "Test Launch" -args "gno.land/r/gnoswap/test_token/bar" -args "g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c" -args 10000000000 -args "gno.land/r/gnoswap/gns" -args "0" -args 20 -args 30 -args 50 -args 1740385500 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # deposit to project deposit-to-project: $(info ************ deposit to project ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/launchpad -func DepositGns -args "gno.land/r/gnoswap/v1/test_token/obl:4215:30" -args 1000000 -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/launchpad -func DepositGns -args "gno.land/r/gnoswap/test_token/obl:4215:30" -args 1000000 -args "" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # collect project token collect-project-token: $(info ************ collect project token ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/launchpad -func CollectRewardByProjectId -args "gno.land/r/gnoswap/v1/test_token/obl:4215" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/launchpad -func CollectRewardByProjectId -args "gno.land/r/gnoswap/test_token/obl:4215" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo \ No newline at end of file diff --git a/tests/volos/_info.mk b/tests/volos/_info.mk index 546bc2d..a690faa 100644 --- a/tests/volos/_info.mk +++ b/tests/volos/_info.mk @@ -1,4 +1,3 @@ - # Network configuration GNOLAND_RPC_URL=http://localhost:26657 CHAINID=dev @@ -13,4 +12,4 @@ ADDR_XVLS := g1hdpp5djpfj899j9t0kmezs4ke60k0nxhxnydq9 # std.DerivePkgAddr("gno.l ADDR_STAKER := g1xgaa5n8qtgl6z97aug8nvrtvm0l9ahvtghru5l # std.DerivePkgAddr("gno.land/r/volos/gov/staker") ADDR_GOVERNANCE := g1kp52puf7vuqptdg2kdqjmy45v70sh2s6g984f8 # std.DerivePkgAddr("gno.land/r/volos/gov/governance") -MAX_APPROVE := 9223372036854775806 +MAX_APPROVE := 9223372036854775806 \ No newline at end of file diff --git a/tests/volos/flashloan.gno b/tests/volos/flashloan.gno index 0808107..f8fe707 100644 --- a/tests/volos/flashloan.gno +++ b/tests/volos/flashloan.gno @@ -6,5 +6,5 @@ import ( func main() { // Test flash loan with GNS token - volos.FlashLoan(cross, "gno.land/r/gnoswap/v1/gns", 10000, "gno.land/r/gnoswap/v1/gns") + volos.FlashLoan(cross, "gno.land/r/gnoswap/gns", 10000, "gno.land/r/gnoswap/gns") } \ No newline at end of file diff --git a/tests/volos/gov_test.mk b/tests/volos/gov_test.mk index c1d27e1..85d70ad 100644 --- a/tests/volos/gov_test.mk +++ b/tests/volos/gov_test.mk @@ -12,19 +12,19 @@ gov-test-flow-no-voting: faucet-vls approve-vls-for-staking stake-vls faucet-all # Faucet VLS tokens faucet-vls: $(info ************ Faucet VLS tokens ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/vls -func Faucet -args 1000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/vls -func Faucet -args 1000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Approve VLS for staking approve-vls-for-staking: $(info ************ Approve VLS for staking ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/vls -func Approve -args $(ADDR_STAKER) -args 10000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/vls -func Approve -args $(ADDR_STAKER) -args 10000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Stake VLS to mint xVLS stake-vls: $(info ************ Stake VLS to mint xVLS ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/staker -func Stake -args 5000 -args $(ADMIN) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/staker -func Stake -args 5000 -args $(ADMIN) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Faucet VLS to all voters @@ -54,19 +54,21 @@ stake-all-voters: # Create a simple test proposal create-test-proposal: $(info ************ Create test proposal ************) - @echo "" | gnokey maketx run test1 create_proposals.gno -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/mocks -func CreateProposals -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo "waiting 3s for proposals to be processed..." + @sleep 3 @echo # Vote yes on the proposal vote-yes: $(info ************ Vote yes on proposal ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "I support this proposal" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "I support this proposal" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # All voters vote on proposal 1 vote-all-on-proposal1: $(info ************ All voters vote on proposal 1 ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "Admin supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "Admin supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "ADDR_USER_1 supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_1) @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "ADDR_USER_2 supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_2) @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "ADDR_USER_3 supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_3) @@ -75,13 +77,13 @@ vote-all-on-proposal1: # Vote no on the proposal vote-no: $(info ************ Vote no on proposal ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 2 -args "NO" -args "I do not support this proposal" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 2 -args "NO" -args "I do not support this proposal" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Vote abstain on the proposal vote-abstain: $(info ************ Vote abstain on proposal ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 3 -args "ABSTAIN" -args "I abstain from this proposal" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 3 -args "ABSTAIN" -args "I abstain from this proposal" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Multi-voter voting scenarios @@ -129,17 +131,17 @@ multi-voter-test: vote-voter1-yes vote-voter2-no vote-voter3-abstain vote-voter1 vote-all-on-all-proposals: $(info ************ All voters vote on all proposals ************) # Vote on proposal 1 - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "Admin supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "Admin supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "ADDR_USER_1 supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_1) @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "ADDR_USER_2 supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_2) @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 1 -args "YES" -args "ADDR_USER_3 supports proposal 1" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_3) # Vote on proposal 2 - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 2 -args "NO" -args "Admin opposes proposal 2" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 2 -args "NO" -args "Admin opposes proposal 2" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 2 -args "NO" -args "ADDR_USER_1 opposes proposal 2" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_1) @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 2 -args "NO" -args "ADDR_USER_2 opposes proposal 2" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_2) @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 2 -args "ABSTAIN" -args "ADDR_USER_3 abstains from proposal 2" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_3) # Vote on proposal 3 - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 3 -args "YES" -args "Admin supports proposal 3" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 3 -args "YES" -args "Admin supports proposal 3" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 3 -args "ABSTAIN" -args "ADDR_USER_1 abstains from proposal 3" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_1) @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 3 -args "YES" -args "ADDR_USER_2 supports proposal 3" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_2) @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Vote -args 3 -args "YES" -args "ADDR_USER_3 supports proposal 3" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" $(ADDR_USER_3) @@ -148,7 +150,7 @@ vote-all-on-all-proposals: # Execute the proposal execute-proposal: $(info ************ Execute proposal ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Execute -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/gov/governance -func Execute -args 1 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Check VLS balance diff --git a/tests/volos/key_setup.mk b/tests/volos/key_setup.mk deleted file mode 100644 index 4f67faa..0000000 --- a/tests/volos/key_setup.mk +++ /dev/null @@ -1,16 +0,0 @@ -# This file contains key setup and reset targets for the test suite. - -# Set keys used during the tests -set-keys: - $(info ************ Setup keys ************) - @echo "" | mkdir -p ~/.config/gno/data/ - @echo "" | [ -e ~/.config/gno/data/keys.db ] && mv ~/.config/gno/data/keys.db ~/.config/gno/data/keys.db.bak || true - @echo "" | cp -r ../../contract/keys.db ~/.config/gno/data - @echo - -# Reset local keys -reset-keys: - $(info ************ Reset keys ************) - @echo "" | rm -rf ~/.config/gno/data/keys.db - @echo "" | [ -e ~/.config/gno/data/keys.db.bak ] && mv ~/.config/gno/data/keys.db.bak ~/.config/gno/data/keys.db || true - @echo diff --git a/tests/volos/multi_ops_test.mk b/tests/volos/multi_ops_test.mk new file mode 100644 index 0000000..5c29221 --- /dev/null +++ b/tests/volos/multi_ops_test.mk @@ -0,0 +1,187 @@ +include _info.mk +include ../gnoswap-tests/_info.mk +include ../gnoswap-tests/test.mk + +# ============================================================================= +# MULTI-OPERATIONS WORKFLOW FOR GRAPH DATA GENERATION +# ============================================================================= +# +# This workflow performs multiple supply, withdraw, borrow, and repay operations +# to generate rich data points for utilization rate graphs and market analysis. +# +# PREREQUISITES: +# - Markets must be created first (run full-workflow from test.mk) +# - Sufficient token balances and allowances must be set up +# - Collateral must be supplied to enable borrowing +# +# DEPENDENCIES: +# - full-workflow (from test.mk) must be completed first +# - Note: 74% of GNS is already borrowed from full-workflow, LLTV is 75%, so only 1% headroom +# +# USAGE: +# make -f test.mk full-workflow # First create markets and basic setup +# make -f multi_ops_test.mk multi-ops-workflow # Then run this for data generation +# ============================================================================= + +# Multiple operations workflow to generate more data points for graphs +# This workflow performs multiple supply, withdraw, borrow, and repay operations +multi-ops-workflow: \ + supply-multi-1 withdraw-multi-1 borrow-multi-1 repay-multi-1 \ + supply-multi-2 withdraw-multi-2 borrow-multi-2 repay-multi-2 \ + supply-multi-3 withdraw-multi-3 borrow-multi-3 repay-multi-3 \ + supply-multi-4 withdraw-multi-4 borrow-multi-4 repay-multi-4 \ + supply-multi-5 withdraw-multi-5 borrow-multi-5 repay-multi-5 \ + check-final-positions + @echo "************ MULTI-OPS WORKFLOW FINISHED ************" + +# Ensure allowances are set for multi-operations +ensure-allowances: + $(info ************ Ensuring allowances for multi-operations ************) + # Approve GNS for Volos contract + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + # Approve WUGNOT for Volos contract + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +# Multi-operation targets for generating more data points +# Round 1 operations +supply-multi-1: + $(info ************ Multi-op Round 1: Supply GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 5000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +withdraw-multi-1: + $(info ************ Multi-op Round 1: Withdraw GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Withdraw -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 2000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +borrow-multi-1: + $(info ************ Multi-op Round 1: Borrow GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 200000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +repay-multi-1: + $(info ************ Multi-op Round 1: Repay GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Repay -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 1000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +# Round 2 operations +supply-multi-2: + $(info ************ Multi-op Round 2: Supply GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 300000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +withdraw-multi-2: + $(info ************ Multi-op Round 2: Withdraw GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Withdraw -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 1000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +borrow-multi-2: + $(info ************ Multi-op Round 2: Borrow GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 50000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +repay-multi-2: + $(info ************ Multi-op Round 2: Repay GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Repay -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 120000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +# Round 3 operations +supply-multi-3: + $(info ************ Multi-op Round 3: Supply GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 400000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +withdraw-multi-3: + $(info ************ Multi-op Round 3: Withdraw GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Withdraw -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 150000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +borrow-multi-3: + $(info ************ Multi-op Round 3: Borrow GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 3000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +repay-multi-3: + $(info ************ Multi-op Round 3: Repay GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Repay -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 200000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +# Round 4 operations +supply-multi-4: + $(info ************ Multi-op Round 4: Supply GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 600000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +withdraw-multi-4: + $(info ************ Multi-op Round 4: Withdraw GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Withdraw -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 2000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +borrow-multi-4: + $(info ************ Multi-op Round 4: Borrow GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 3000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +repay-multi-4: + $(info ************ Multi-op Round 4: Repay GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Repay -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 1000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +# Round 5 operations +supply-multi-5: + $(info ************ Multi-op Round 5: Supply GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 7000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +withdraw-multi-5: + $(info ************ Multi-op Round 5: Withdraw GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Withdraw -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 2500 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +borrow-multi-5: + $(info ************ Multi-op Round 5: Borrow GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 100000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +repay-multi-5: + $(info ************ Multi-op Round 5: Repay GNS ************) + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Repay -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 300000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + +# Check final positions after all multi-operations +check-final-positions: + $(info ************ Check Final Positions After Multi-Operations ************) + # Check GNS-WUGNOT market final state + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalSupplyAssets(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" + @echo + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalBorrowAssets(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" + @echo + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionSupplyShares(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\", \"$(ADMIN)\")" + @echo + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionBorrowShares(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\", \"$(ADMIN)\")" + @echo + +# Complete workflow with prerequisites check +multi-ops-complete: ensure-allowances multi-ops-workflow + @echo "************ MULTI-OPS COMPLETE WORKFLOW FINISHED ************" + +# Quick test to verify market exists before running multi-ops +verify-market-exists: + $(info ************ Verifying market exists before multi-operations ************) + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetMarket(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" + @echo + +# Check current utilization rate before multi-operations +check-current-utilization: + $(info ************ Check Current Utilization Rate ************) + # Get total supply assets + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalSupplyAssets(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" + @echo + # Get total borrow assets + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalBorrowAssets(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" + @echo + # Get LLTV (Liquidation LTV) + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetLLTV(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" + @echo diff --git a/tests/volos/test.mk b/tests/volos/test.mk index 8735a86..bd28239 100644 --- a/tests/volos/test.mk +++ b/tests/volos/test.mk @@ -1,252 +1,247 @@ include _info.mk -include key_setup.mk -include ../gnoswap/_info.mk -include ../gnoswap/test.mk +include ../gnoswap-tests/_info.mk +include ../gnoswap-tests/test.mk # Complete flow that includes both GNS-WUGNOT and BAR-WUGNOT operations -full-workflow: set-keys pool-create-gns-wugnot-default mint-gns-gnot enable-irm enable-lltv market-create-gns-wugnot supply-assets-gns-wugnot supply-collateral-gns-wugnot borrow-gns \ +full-workflow: pool-create-gns-wugnot-default mint-gns-gnot enable-irm enable-lltv market-create-gns-wugnot supply-assets-gns-wugnot supply-collateral-gns-wugnot borrow-gns \ pool-create-bar-wugnot-default mint-bar-wugnot market-create-bar-wugnot supply-assets-bar-wugnot supply-collateral-bar-wugnot borrow-bar \ - check-position-gns-wugnot check-position-bar-wugnot reset-keys + check-position-gns-wugnot check-position-bar-wugnot @echo "************ WORKFLOW FINISHED ************" # Enable the linear IRM enable-irm: $(info ************ Enable linear IRM ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func EnableIRM -args "linear" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func EnableIRM -args "linear" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Enable LLTV (75% = 75 as int64) enable-lltv: $(info ************ Enable LLTV 75% ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func EnableLLTV -args 75 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func EnableLLTV -args 75 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test market creation with GNS and WUGNOT market-create-gns-wugnot: $(info ************ Test creating market with GNS (supply/borrow) and WUGNOT (collateral) ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func CreateMarket -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000" -args false -args "linear" -args 75 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func CreateMarket -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000" -args false -args "linear" -args 75 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test market creation with GNS and WUGNOT market-create-bar-wugnot: $(info ************ Test creating market with BAR (supply/borrow) and WUGNOT (collateral) ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func CreateMarket -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/test_token/bar:3000" -args false -args "linear" -args 75 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func CreateMarket -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/bar:3000" -args false -args "linear" -args 75 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test getting pool price for GNS-WUGNOT market market-get-price-gns-wugnot: $(info ************ Test getting pool price for GNS-WUGNOT market ************) - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetMarketPrice(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetMarketPrice(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" @echo # Test getting market info for GNS-WUGNOT pair market-get-gns-wugnot: $(info ************ Test getting market info for GNS-WUGNOT pair ************) # GET TOTAL SUPPLY ASSETS - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalSupplyAssets(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalSupplyAssets(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" @echo # GET TOTAL SUPPLY SHARES - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalSupplyShares(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalSupplyShares(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" @echo # GET TOTAL BORROW ASSETS - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalBorrowAssets(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalBorrowAssets(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" @echo # GET TOTAL BORROW SHARES - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalBorrowShares(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetTotalBorrowShares(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" @echo # GET LIQUIDATION LTV - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetLLTV(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetLLTV(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" @echo # GET MARKET FEE - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetFee(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetFee(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" @echo # Test supplying assets to GNS-WUGNOT market supply-assets-gns-wugnot: $(info ************ Test supplying GNS assets to GNS-WUGNOT market ************) # APPROVE FIRST - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo - - # Wrap UGNOT to WUGNOT - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send "1000000000ugnot" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN SUPPLY - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -args 1000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 148000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test supplying shares to GNS-WUGNOT market supply-shares-gns-wugnot: $(info ************ Test supplying shares to GNS-WUGNOT market ************) # APPROVE FIRST - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 - @echo - - # Wrap UGNOT to WUGNOT - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send "1000000000ugnot" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN SUPPLY - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -args 0 -args 1000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 0 -args 1000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test withdrawing assets from GNS-WUGNOT market withdraw-assets-gns-wugnot: $(info ************ Test withdrawing GNS assets from GNS-WUGNOT market ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Withdraw -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -args 994940 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Withdraw -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 994940 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Check user position in GNS-WUGNOT market check-position-gns-wugnot: $(info ************ Check user position in GNS-WUGNOT market ************) # Check supply shares - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionSupplyShares(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\", \"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionSupplyShares(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\", \"$(ADMIN)\")" @echo # Check borrow shares - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionBorrowShares(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\", \"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionBorrowShares(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\", \"$(ADMIN)\")" @echo # Check collateral - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionCollateral(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0\", \"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionCollateral(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\", \"$(ADMIN)\")" @echo # Check GNS balance check-gns-balance: $(info ************ Check GNS balance ************) - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/gnoswap/v1/gns.BalanceOf(\"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/gnoswap/gns.BalanceOf(\"$(ADMIN)\")" @echo # Check Volos contract GNS balance check-volos-gns-balance: $(info ************ Check Volos contract GNS balance ************) - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/gnoswap/v1/gns.BalanceOf(\"$(ADDR_VOLOS)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/gnoswap/gns.BalanceOf(\"$(ADDR_VOLOS)\")" @echo # Check Volos contract WUGNOT balance check-volos-wugnot-balance: $(info ************ Check Volos contract WUGNOT balance ************) - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/demo/wugnot.BalanceOf(\"$(ADDR_VOLOS)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data ".BalanceOf(\"$(ADDR_VOLOS)\")" @echo # Test accruing interest on GNS-WUGNOT market accrue-interest-gns-wugnot: $(info ************ Test accruing interest on GNS-WUGNOT market ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func AccrueInterest -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func AccrueInterest -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Check WUGNOT balance check-wugnot-balance: $(info ************ Check WUGNOT balance ************) - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/demo/wugnot.BalanceOf(\"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data ".BalanceOf(\"$(ADMIN)\")" @echo # Wrap UGNOT to WUGNOT wrap-ugnot: $(info ************ Wrap UGNOT to WUGNOT ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send "1000000000ugnot" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Deposit -send "1000000000ugnot" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test borrowing GNS tokens borrow-gns: $(info ************ Test borrowing GNS tokens ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -args 740000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 74000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test repaying GNS tokens repay-gns: $(info ************ Test repaying GNS tokens ************) # APPROVE FIRST - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN REPAY - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Repay -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -args 50 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Repay -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 37000000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test liquidating GNS position liquidate-gns: $(info ************ Test liquidating GNS position ************) # APPROVE FIRST - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/gns -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN LIQUIDATE - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Liquidate -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -args $(ADMIN) -args 0 -args 25 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Liquidate -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args $(ADMIN) -args 0 -args 25 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test supplying collateral to GNS-WUGNOT market supply-collateral-gns-wugnot: $(info ************ Test supplying collateral to GNS-WUGNOT market ************) # APPROVE FIRST - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin + @echo + + # Wrap UGNOT to WUGNOT + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Deposit -send "1000000000ugnot" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN SUPPLY COLLATERAL - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func SupplyCollateral -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -args 1000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func SupplyCollateral -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 1000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test withdrawing collateral from GNS-WUGNOT market withdraw-collateral-gns-wugnot: $(info ************ Test withdrawing collateral from GNS-WUGNOT market ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func WithdrawCollateral -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/gns:3000:0" -args 500 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func WithdrawCollateral -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0" -args 500 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test supplying assets to BAR-WUGNOT market supply-assets-bar-wugnot: $(info ************ Test supplying BAR assets to BAR-WUGNOT market ************) # APPROVE FIRST - @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/v1/test_token/bar -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoswap/test_token/bar -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Wrap UGNOT to WUGNOT if needed - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Deposit -send "1000000000ugnot" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Deposit -send "1000000000ugnot" -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN SUPPLY - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/test_token/bar:3000:0" -args 1000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Supply -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/bar:3000:0" -args 1000000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test supplying collateral to BAR-WUGNOT market supply-collateral-bar-wugnot: $(info ************ Test supplying collateral to BAR-WUGNOT market ************) # APPROVE FIRST - @echo "" | gnokey maketx call -pkgpath gno.land/r/demo/wugnot -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/gnoland/wugnot -func Approve -args $(ADDR_VOLOS) -args $(MAX_APPROVE) -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # THEN SUPPLY COLLATERAL - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func SupplyCollateral -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/test_token/bar:3000:0" -args 1000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func SupplyCollateral -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/bar:3000:0" -args 1000000 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Test borrowing BAR tokens borrow-bar: $(info ************ Test borrowing BAR tokens ************) - @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/test_token/bar:3000:0" -args 500000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" test1 + @echo "" | gnokey maketx call -pkgpath gno.land/r/volos/core -func Borrow -args "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/bar:3000:0" -args 500000 -args 0 -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) -gas-fee 100000000ugnot -gas-wanted 1000000000 -memo "" gnoswap_admin @echo # Check user position in BAR-WUGNOT market check-position-bar-wugnot: $(info ************ Check user position in BAR-WUGNOT market ************) # Check supply shares - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionSupplyShares(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/test_token/bar:3000:0\", \"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionSupplyShares(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/bar:3000:0\", \"$(ADMIN)\")" @echo # Check borrow shares - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionBorrowShares(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/test_token/bar:3000:0\", \"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionBorrowShares(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/bar:3000:0\", \"$(ADMIN)\")" @echo # Check collateral - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionCollateral(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/test_token/bar:3000:0\", \"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetPositionCollateral(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/bar:3000:0\", \"$(ADMIN)\")" @echo # Check health factor - gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetHealthFactor(\"gno.land/r/demo/wugnot:gno.land/r/gnoswap/v1/test_token/bar:3000:0\", \"$(ADMIN)\")" + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetHealthFactor(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/test_token/bar:3000:0\", \"$(ADMIN)\")" @echo # Check volos owner @@ -258,7 +253,7 @@ check-volos-owner: # Test flash loan test-flashloan: $(info ************ Testing Flash Loan ************) - @echo "" | gnokey maketx run test1 flashloan.gno -gas-wanted 200000000 -gas-fee 1000000ugnot -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) + @echo "" | gnokey maketx run gnoswap_admin flashloan.gno -gas-wanted 200000000 -gas-fee 1000000ugnot -insecure-password-stdin=true -remote $(GNOLAND_RPC_URL) -broadcast=true -chainid $(CHAINID) @echo # Get Volos contract address derived from path @@ -266,3 +261,9 @@ get-volos-address: $(info ************ Getting Volos Contract Address ************) gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.GetAddress()" @echo + +# Test calculate borrow APR for GNS-WUGNOT market +test-calculate-borrow-apr: + $(info ************ Testing Calculate Borrow APR ************) + gnokey query vm/qeval -remote $(GNOLAND_RPC_URL) -data "gno.land/r/volos/core.CalculateBorrowAPR(\"gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000:0\")" + @echo \ No newline at end of file From 3e661cab53c7ca36491cf7f9cc5e86824bd39615 Mon Sep 17 00:00:00 2001 From: stefann-01 Date: Tue, 9 Sep 2025 11:47:54 +0200 Subject: [PATCH 2/4] - revert lost file - fix user addresses --- tests/gnoswap/test.mk | 8 ++++---- tests/volos/key_setup.mk | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 tests/volos/key_setup.mk diff --git a/tests/gnoswap/test.mk b/tests/gnoswap/test.mk index 28789ee..2260f18 100644 --- a/tests/gnoswap/test.mk +++ b/tests/gnoswap/test.mk @@ -9,10 +9,10 @@ QUX_PATH := gno.land/r/gnoswap/test_token/qux FOO_PATH := gno.land/r/gnoswap/test_token/foo ADDR_TEST_ADMIN := g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42 -ADDR_USER_1 := g1yupktql2l6jd8tkxu8c4rv4uc5l7cv3y70d7ln -ADDR_USER_2 := g194zxz7ahvkhj3p3hlxkc5axe4exe9ra0yzmhqd -ADDR_USER_3 := g1uf7c8tsuhsp5la6hepp5p6yjy390k6rcee8hg5 -ADDR_USER_4 := g133mzul2d7c43vr87s58797pkj6l7vpx30jcn9p +ADDR_USER_1 := g149gwxltsfg6dghhq0txu8kam7chfpt9amnx4yx +ADDR_USER_2 := g170akkhclm6dzv67sxpandkjyvtv92kvyzyd2q9 +ADDR_USER_3 := g135ut9uh8l3f092r46z52qwa3wgfa5q8qcjlehy +ADDR_USER_4 := g1upryqylqstmy9wfpq2jz6ep0udj9mau8st8hf2 .PHONY: transfer-base-token transfer-base-token: transfer-ugnot transfer-gns transfer-usdc transfer-baz transfer-bar transfer-obl transfer-qux transfer-foo diff --git a/tests/volos/key_setup.mk b/tests/volos/key_setup.mk new file mode 100644 index 0000000..6dc55b1 --- /dev/null +++ b/tests/volos/key_setup.mk @@ -0,0 +1,16 @@ +# This file contains key setup and reset targets for the test suite. + +# Set keys used during the tests +set-keys: + $(info ************ Setup keys ************) + @echo "" | mkdir -p ~/.config/gno/data/ + @echo "" | [ -e ~/.config/gno/data/keys.db ] && mv ~/.config/gno/data/keys.db ~/.config/gno/data/keys.db.bak || true + @echo "" | cp -r ../../contract/keys.db ~/.config/gno/data + @echo + +# Reset local keys +reset-keys: + $(info ************ Reset keys ************) + @echo "" | rm -rf ~/.config/gno/data/keys.db + @echo "" | [ -e ~/.config/gno/data/keys.db.bak ] && mv ~/.config/gno/data/keys.db.bak ~/.config/gno/data/keys.db || true + @echo \ No newline at end of file From acf0527629eec51d3fef4047e0ea494b06f32b56 Mon Sep 17 00:00:00 2001 From: stefann-01 Date: Tue, 9 Sep 2025 11:55:18 +0200 Subject: [PATCH 3/4] Remove unnecessary gnoswap files --- contract/p/gnoswap/fuzz/fuzz.gno | 549 ----- contract/p/gnoswap/fuzz/fuzz_test.gno | 320 --- contract/p/gnoswap/fuzz/gnomod.toml | 2 - contract/p/gnoswap/fuzz/seed.gno | 55 - contract/p/gnoswap/gnsmath/bit_math.gno | 80 - contract/p/gnoswap/gnsmath/bit_math_test.gno | 855 -------- contract/p/gnoswap/gnsmath/doc.gno | 9 - contract/p/gnoswap/gnsmath/errors.gno | 19 - contract/p/gnoswap/gnsmath/gnomod.toml | 2 - .../p/gnoswap/gnsmath/sqrt_price_math.gno | 313 --- .../gnoswap/gnsmath/sqrt_price_math_test.gno | 1454 ------------- contract/p/gnoswap/gnsmath/swap_math.gno | 234 -- contract/p/gnoswap/gnsmath/swap_math_test.gno | 851 -------- contract/p/gnoswap/int256/LICENSE | 21 - contract/p/gnoswap/int256/README.md | 35 - contract/p/gnoswap/int256/absolute.gno | 38 - contract/p/gnoswap/int256/absolute_test.gno | 184 -- contract/p/gnoswap/int256/arithmetic.gno | 319 --- contract/p/gnoswap/int256/arithmetic_test.gno | 1016 --------- contract/p/gnoswap/int256/bitwise.gno | 101 - contract/p/gnoswap/int256/bitwise_test.gno | 198 -- contract/p/gnoswap/int256/cmp.gno | 117 - contract/p/gnoswap/int256/cmp_test.gno | 316 --- contract/p/gnoswap/int256/conversion.gno | 120 -- contract/p/gnoswap/int256/conversion_test.gno | 540 ----- contract/p/gnoswap/int256/doc.gno | 13 - contract/p/gnoswap/int256/gnomod.toml | 2 - contract/p/gnoswap/int256/int256.gno | 182 -- contract/p/gnoswap/int256/int256_test.gno | 185 -- contract/p/gnoswap/rbac/README.md | 78 - contract/p/gnoswap/rbac/doc.gno | 139 -- contract/p/gnoswap/rbac/errors.gno | 18 - contract/p/gnoswap/rbac/gnomod.toml | 2 - contract/p/gnoswap/rbac/ownable.gno | 110 - contract/p/gnoswap/rbac/ownable_test.gno | 175 -- contract/p/gnoswap/rbac/rbac.gno | 146 -- contract/p/gnoswap/rbac/rbac_test.gno | 72 - contract/p/gnoswap/rbac/role.gno | 35 - contract/p/gnoswap/rbac/types.gno | 54 - contract/p/gnoswap/uint256/LICENSE | 28 - contract/p/gnoswap/uint256/README.md | 38 - contract/p/gnoswap/uint256/_helper_test.gno | 52 - contract/p/gnoswap/uint256/arithmetic.gno | 538 ----- .../p/gnoswap/uint256/arithmetic_test.gno | 1883 ----------------- contract/p/gnoswap/uint256/bits_table.gno | 115 - contract/p/gnoswap/uint256/bitwise.gno | 266 --- contract/p/gnoswap/uint256/bitwise_test.gno | 346 --- contract/p/gnoswap/uint256/cmp.gno | 103 - contract/p/gnoswap/uint256/cmp_test.gno | 229 -- contract/p/gnoswap/uint256/conversion.gno | 602 ------ .../p/gnoswap/uint256/conversion_test.gno | 60 - contract/p/gnoswap/uint256/doc.gno | 14 - contract/p/gnoswap/uint256/error.gno | 73 - contract/p/gnoswap/uint256/fullmath.gno | 106 - contract/p/gnoswap/uint256/fullmath_test.gno | 844 -------- contract/p/gnoswap/uint256/gnomod.toml | 2 - contract/p/gnoswap/uint256/gs_pointer.gno | 8 - contract/p/gnoswap/uint256/mod.gno | 605 ------ contract/p/gnoswap/uint256/uint256.gno | 303 --- contract/p/gnoswap/uint256/uint256_test.gno | 825 -------- contract/p/gnoswap/uint256/utils.gno | 22 - contract/r/gnoswap/access/README.md | 68 - contract/r/gnoswap/access/access.gno | 67 - contract/r/gnoswap/access/assert.gno | 145 -- contract/r/gnoswap/access/consts.gno | 5 - contract/r/gnoswap/access/errors.gno | 10 - contract/r/gnoswap/access/gnomod.toml | 2 - contract/r/gnoswap/access/swap_whitelist.gno | 77 - contract/r/gnoswap/emission/README.md | 107 - contract/r/gnoswap/emission/assert.gno | 82 - contract/r/gnoswap/emission/distribution.gno | 389 ---- contract/r/gnoswap/emission/emission.gno | 216 -- contract/r/gnoswap/emission/errors.gno | 21 - contract/r/gnoswap/emission/gnomod.toml | 2 - contract/r/gnoswap/emission/utils.gno | 37 - contract/r/gnoswap/gns/README.md | 60 - contract/r/gnoswap/gns/assert.gno | 14 - contract/r/gnoswap/gns/consts.gno | 29 - contract/r/gnoswap/gns/emission_state.gno | 166 -- contract/r/gnoswap/gns/errors.gno | 18 - contract/r/gnoswap/gns/getter.gno | 185 -- contract/r/gnoswap/gns/gnomod.toml | 2 - contract/r/gnoswap/gns/gns.gno | 309 --- contract/r/gnoswap/gns/gns_emission.gno | 28 - contract/r/gnoswap/gns/halving.gno | 205 -- contract/r/gnoswap/gns/utils.gno | 91 - contract/r/gnoswap/halt/README.md | 92 - contract/r/gnoswap/halt/assert.gno | 82 - contract/r/gnoswap/halt/config.gno | 122 -- contract/r/gnoswap/halt/doc.gno | 2 - contract/r/gnoswap/halt/errors.gno | 18 - contract/r/gnoswap/halt/getters.gno | 117 - contract/r/gnoswap/halt/gnomod.toml | 2 - contract/r/gnoswap/halt/halt.gno | 84 - contract/r/gnoswap/halt/types.gno | 89 - contract/r/gnoswap/rbac/README.md | 120 -- contract/r/gnoswap/rbac/assert.gno | 51 - contract/r/gnoswap/rbac/consts.gno | 40 - contract/r/gnoswap/rbac/emit.gno | 39 - contract/r/gnoswap/rbac/errors.gno | 20 - contract/r/gnoswap/rbac/gnomod.toml | 2 - contract/r/gnoswap/rbac/ownership.gno | 45 - contract/r/gnoswap/rbac/rbac.gno | 112 - contract/r/gnoswap/rbac/role.gno | 25 - contract/r/gnoswap/referral/README.md | 47 - contract/r/gnoswap/referral/doc.gno | 109 - contract/r/gnoswap/referral/errors.gno | 16 - contract/r/gnoswap/referral/global_keeper.gno | 72 - contract/r/gnoswap/referral/gnomod.toml | 2 - contract/r/gnoswap/referral/keeper.gno | 176 -- contract/r/gnoswap/referral/referral.gno | 56 - contract/r/gnoswap/referral/type.gno | 33 - contract/r/gnoswap/referral/utils.gno | 38 - contract/r/gnoswap/v1/common/consts.gno | 7 - contract/r/gnoswap/v1/common/doc.gno | 6 - contract/r/gnoswap/v1/common/errors.gno | 25 - contract/r/gnoswap/v1/common/gnomod.toml | 2 - .../r/gnoswap/v1/common/grc20reg_helper.gno | 86 - .../r/gnoswap/v1/common/liquidity_amounts.gno | 332 --- contract/r/gnoswap/v1/common/tick_math.gno | 263 --- .../r/gnoswap/v1/community_pool/README.md | 43 - .../v1/community_pool/community_pool.gno | 51 - contract/r/gnoswap/v1/community_pool/doc.gno | 6 - .../r/gnoswap/v1/community_pool/errors.gno | 13 - .../r/gnoswap/v1/community_pool/gnomod.toml | 2 - contract/r/gnoswap/v1/gnft/assert.gno | 37 - contract/r/gnoswap/v1/gnft/errors.gno | 31 - contract/r/gnoswap/v1/gnft/gnft.gno | 272 --- contract/r/gnoswap/v1/gnft/gnomod.toml | 2 - contract/r/gnoswap/v1/gnft/svg_generator.gno | 69 - contract/r/gnoswap/v1/gnft/utils.gno | 114 - contract/r/gnoswap/v1/gov/README.md | 103 - contract/r/gnoswap/v1/gov/doc.gno | 5 - contract/r/gnoswap/v1/gov/gnomod.toml | 2 - contract/r/gnoswap/v1/gov/governance/api.gno | 195 -- .../r/gnoswap/v1/gov/governance/assert.gno | 15 - .../r/gnoswap/v1/gov/governance/config.gno | 116 - .../r/gnoswap/v1/gov/governance/consts.gno | 16 - .../r/gnoswap/v1/gov/governance/counter.gno | 25 - contract/r/gnoswap/v1/gov/governance/doc.gno | 5 - .../r/gnoswap/v1/gov/governance/errors.gno | 37 - .../v1/gov/governance/getter_proposal.gno | 71 - .../gnoswap/v1/gov/governance/getter_vote.gno | 32 - .../r/gnoswap/v1/gov/governance/gnomod.toml | 2 - .../v1/gov/governance/governance_execute.gno | 253 --- .../v1/gov/governance/governance_propose.gno | 461 ---- .../v1/gov/governance/governance_vote.gno | 121 -- .../v1/gov/governance/parameter_registry.gno | 529 ----- .../governance/parameter_registry_handler.gno | 140 -- .../r/gnoswap/v1/gov/governance/proposal.gno | 280 --- .../gov/governance/proposal_action_status.gno | 128 -- .../v1/gov/governance/proposal_data.gno | 406 ---- .../v1/gov/governance/proposal_manager.gno | 95 - .../governance/proposal_schedule_status.gno | 108 - .../v1/gov/governance/proposal_status.gno | 314 --- .../gov/governance/proposal_vote_status.gno | 163 -- .../r/gnoswap/v1/gov/governance/state.gno | 239 --- .../r/gnoswap/v1/gov/governance/utils.gno | 159 -- .../gnoswap/v1/gov/governance/voting_info.gno | 150 -- .../gnoswap/v1/gov/staker/api_delegation.gno | 25 - .../r/gnoswap/v1/gov/staker/api_staker.gno | 77 - contract/r/gnoswap/v1/gov/staker/assert.gno | 27 - contract/r/gnoswap/v1/gov/staker/consts.gno | 12 - contract/r/gnoswap/v1/gov/staker/counter.gno | 13 - .../r/gnoswap/v1/gov/staker/delegation.gno | 164 -- .../v1/gov/staker/delegation_history.gno | 61 - .../v1/gov/staker/delegation_mananger.gno | 144 -- .../v1/gov/staker/delegation_record.gno | 156 -- .../v1/gov/staker/delegation_snapshot.gno | 159 -- .../v1/gov/staker/delegation_withdraw.gno | 177 -- contract/r/gnoswap/v1/gov/staker/doc.gno | 4 - .../v1/gov/staker/emission_reward_manager.gno | 235 -- .../v1/gov/staker/emission_reward_state.gno | 250 --- contract/r/gnoswap/v1/gov/staker/errors.gno | 32 - .../gov/staker/getter_delegation_snapshot.gno | 34 - contract/r/gnoswap/v1/gov/staker/gnomod.toml | 2 - .../staker/protocol_fee_reward_manager.gno | 298 --- .../gov/staker/protocol_fee_reward_state.gno | 298 --- .../gnoswap/v1/gov/staker/staker_delegate.gno | 469 ---- .../gov/staker/staker_delegation_snapshot.gno | 82 - .../r/gnoswap/v1/gov/staker/staker_reward.gno | 280 --- contract/r/gnoswap/v1/gov/staker/state.gno | 523 ----- contract/r/gnoswap/v1/gov/staker/util.gno | 124 -- contract/r/gnoswap/v1/gov/xgns/doc.gno | 4 - contract/r/gnoswap/v1/gov/xgns/errors.gno | 14 - contract/r/gnoswap/v1/gov/xgns/gnomod.toml | 2 - contract/r/gnoswap/v1/gov/xgns/xgns.gno | 126 -- contract/r/gnoswap/v1/launchpad/README.md | 66 - .../r/gnoswap/v1/launchpad/api_deposit.gno | 18 - .../r/gnoswap/v1/launchpad/api_project.gno | 92 - .../r/gnoswap/v1/launchpad/api_reward.gno | 26 - contract/r/gnoswap/v1/launchpad/assert.gno | 62 - contract/r/gnoswap/v1/launchpad/consts.gno | 44 - contract/r/gnoswap/v1/launchpad/counter.gno | 25 - contract/r/gnoswap/v1/launchpad/deposit.gno | 120 -- contract/r/gnoswap/v1/launchpad/errors.gno | 46 - contract/r/gnoswap/v1/launchpad/gnomod.toml | 2 - .../r/gnoswap/v1/launchpad/json_builder.gno | 102 - .../v1/launchpad/launchpad_deposit.gno | 188 -- .../v1/launchpad/launchpad_project.gno | 470 ---- .../v1/launchpad/launchpad_protocol_fee.gno | 24 - .../gnoswap/v1/launchpad/launchpad_reward.gno | 77 - .../v1/launchpad/launchpad_withdraw.gno | 116 - contract/r/gnoswap/v1/launchpad/project.gno | 256 --- .../v1/launchpad/project_condition.gno | 98 - .../r/gnoswap/v1/launchpad/project_tier.gno | 174 -- .../r/gnoswap/v1/launchpad/reward_manager.gno | 311 --- .../r/gnoswap/v1/launchpad/reward_state.gno | 158 -- contract/r/gnoswap/v1/launchpad/state.gno | 104 - contract/r/gnoswap/v1/launchpad/utils.gno | 76 - contract/r/gnoswap/v1/pool/README.md | 131 -- contract/r/gnoswap/v1/pool/api.gno | 53 - contract/r/gnoswap/v1/pool/assert.gno | 65 - contract/r/gnoswap/v1/pool/doc.gno | 11 - contract/r/gnoswap/v1/pool/errors.gno | 50 - contract/r/gnoswap/v1/pool/factory_param.gno | 143 -- contract/r/gnoswap/v1/pool/getter.gno | 183 -- contract/r/gnoswap/v1/pool/gnomod.toml | 2 - contract/r/gnoswap/v1/pool/json.gno | 275 --- contract/r/gnoswap/v1/pool/liquidity_math.gno | 43 - contract/r/gnoswap/v1/pool/manager.gno | 275 --- contract/r/gnoswap/v1/pool/pool.gno | 364 ---- contract/r/gnoswap/v1/pool/pool_type.gno | 272 --- contract/r/gnoswap/v1/pool/position.gno | 397 ---- contract/r/gnoswap/v1/pool/protocol_fee.gno | 199 -- contract/r/gnoswap/v1/pool/swap.gno | 647 ------ contract/r/gnoswap/v1/pool/tick.gno | 468 ---- contract/r/gnoswap/v1/pool/tick_bitmap.gno | 167 -- contract/r/gnoswap/v1/pool/transfer.gno | 208 -- contract/r/gnoswap/v1/pool/type.gno | 291 --- contract/r/gnoswap/v1/pool/utils.gno | 193 -- contract/r/gnoswap/v1/position/README.md | 157 -- contract/r/gnoswap/v1/position/api.gno | 152 -- contract/r/gnoswap/v1/position/assert.gno | 111 - contract/r/gnoswap/v1/position/burn.gno | 210 -- contract/r/gnoswap/v1/position/doc.gno | 5 - contract/r/gnoswap/v1/position/errors.gno | 40 - contract/r/gnoswap/v1/position/getter.gno | 104 - contract/r/gnoswap/v1/position/gnomod.toml | 2 - contract/r/gnoswap/v1/position/json.gno | 149 -- .../v1/position/liquidity_management.gno | 67 - contract/r/gnoswap/v1/position/manager.gno | 95 - contract/r/gnoswap/v1/position/mint.gno | 300 --- .../r/gnoswap/v1/position/native_token.gno | 171 -- contract/r/gnoswap/v1/position/position.gno | 542 ----- contract/r/gnoswap/v1/position/reposition.gno | 122 -- contract/r/gnoswap/v1/position/type.gno | 147 -- contract/r/gnoswap/v1/position/utils.gno | 225 -- contract/r/gnoswap/v1/protocol_fee/README.md | 57 - contract/r/gnoswap/v1/protocol_fee/api.gno | 161 -- contract/r/gnoswap/v1/protocol_fee/assert.gno | 46 - contract/r/gnoswap/v1/protocol_fee/consts.gno | 12 - contract/r/gnoswap/v1/protocol_fee/doc.gno | 6 - contract/r/gnoswap/v1/protocol_fee/errors.gno | 18 - contract/r/gnoswap/v1/protocol_fee/getter.gno | 99 - .../r/gnoswap/v1/protocol_fee/gnomod.toml | 2 - .../gnoswap/v1/protocol_fee/protocol_fee.gno | 231 -- contract/r/gnoswap/v1/protocol_fee/state.gno | 208 -- contract/r/gnoswap/v1/router/README.md | 115 - contract/r/gnoswap/v1/router/assert.gno | 19 - contract/r/gnoswap/v1/router/base.gno | 321 --- contract/r/gnoswap/v1/router/consts.gno | 16 - contract/r/gnoswap/v1/router/doc.gno | 9 - contract/r/gnoswap/v1/router/errors.gno | 37 - contract/r/gnoswap/v1/router/exact_in.gno | 175 -- contract/r/gnoswap/v1/router/exact_out.gno | 178 -- contract/r/gnoswap/v1/router/gnomod.toml | 2 - .../r/gnoswap/v1/router/protocol_fee_swap.gno | 104 - contract/r/gnoswap/v1/router/router.gno | 282 --- contract/r/gnoswap/v1/router/router_dry.gno | 250 --- contract/r/gnoswap/v1/router/swap_inner.gno | 186 -- contract/r/gnoswap/v1/router/swap_multi.gno | 259 --- contract/r/gnoswap/v1/router/swap_single.gno | 43 - contract/r/gnoswap/v1/router/type.gno | 189 -- contract/r/gnoswap/v1/router/utils.gno | 167 -- contract/r/gnoswap/v1/router/wrap_unwrap.gno | 98 - contract/r/gnoswap/v1/staker/README.md | 182 -- contract/r/gnoswap/v1/staker/api.gno | 310 --- contract/r/gnoswap/v1/staker/assert.gno | 196 -- .../staker/calculate_pool_position_reward.gno | 153 -- contract/r/gnoswap/v1/staker/consts.gno | 13 - contract/r/gnoswap/v1/staker/counter.gno | 21 - contract/r/gnoswap/v1/staker/doc.gno | 9 - contract/r/gnoswap/v1/staker/errors.gno | 48 - .../v1/staker/external_deposit_fee.gno | 173 -- .../gnoswap/v1/staker/external_incentive.gno | 224 -- .../gnoswap/v1/staker/external_token_list.gno | 132 -- contract/r/gnoswap/v1/staker/getter.gno | 410 ---- contract/r/gnoswap/v1/staker/gnomod.toml | 2 - contract/r/gnoswap/v1/staker/incentive_id.gno | 21 - contract/r/gnoswap/v1/staker/json.gno | 276 --- .../v1/staker/manage_pool_tier_and_warmup.gno | 173 -- contract/r/gnoswap/v1/staker/mint_stake.gno | 93 - .../v1/staker/protocol_fee_unstaking.gno | 84 - contract/r/gnoswap/v1/staker/query.gno | 127 -- .../gnoswap/v1/staker/reward_calculation.gno | 61 - .../staker/reward_calculation_incentives.gno | 223 -- .../v1/staker/reward_calculation_pool.gno | 523 ----- .../staker/reward_calculation_pool_tier.gno | 326 --- .../v1/staker/reward_calculation_tick.gno | 364 ---- .../v1/staker/reward_calculation_types.gno | 117 - .../v1/staker/reward_calculation_warmup.gno | 118 -- contract/r/gnoswap/v1/staker/staker.gno | 789 ------- contract/r/gnoswap/v1/staker/type.gno | 204 -- contract/r/gnoswap/v1/staker/utils.gno | 168 -- contract/r/gnoswap/v1/staker/wrap_unwrap.gno | 82 - 306 files changed, 47893 deletions(-) delete mode 100644 contract/p/gnoswap/fuzz/fuzz.gno delete mode 100644 contract/p/gnoswap/fuzz/fuzz_test.gno delete mode 100644 contract/p/gnoswap/fuzz/gnomod.toml delete mode 100644 contract/p/gnoswap/fuzz/seed.gno delete mode 100644 contract/p/gnoswap/gnsmath/bit_math.gno delete mode 100644 contract/p/gnoswap/gnsmath/bit_math_test.gno delete mode 100644 contract/p/gnoswap/gnsmath/doc.gno delete mode 100644 contract/p/gnoswap/gnsmath/errors.gno delete mode 100644 contract/p/gnoswap/gnsmath/gnomod.toml delete mode 100644 contract/p/gnoswap/gnsmath/sqrt_price_math.gno delete mode 100644 contract/p/gnoswap/gnsmath/sqrt_price_math_test.gno delete mode 100644 contract/p/gnoswap/gnsmath/swap_math.gno delete mode 100644 contract/p/gnoswap/gnsmath/swap_math_test.gno delete mode 100644 contract/p/gnoswap/int256/LICENSE delete mode 100644 contract/p/gnoswap/int256/README.md delete mode 100644 contract/p/gnoswap/int256/absolute.gno delete mode 100644 contract/p/gnoswap/int256/absolute_test.gno delete mode 100644 contract/p/gnoswap/int256/arithmetic.gno delete mode 100644 contract/p/gnoswap/int256/arithmetic_test.gno delete mode 100644 contract/p/gnoswap/int256/bitwise.gno delete mode 100644 contract/p/gnoswap/int256/bitwise_test.gno delete mode 100644 contract/p/gnoswap/int256/cmp.gno delete mode 100644 contract/p/gnoswap/int256/cmp_test.gno delete mode 100644 contract/p/gnoswap/int256/conversion.gno delete mode 100644 contract/p/gnoswap/int256/conversion_test.gno delete mode 100644 contract/p/gnoswap/int256/doc.gno delete mode 100644 contract/p/gnoswap/int256/gnomod.toml delete mode 100644 contract/p/gnoswap/int256/int256.gno delete mode 100644 contract/p/gnoswap/int256/int256_test.gno delete mode 100644 contract/p/gnoswap/rbac/README.md delete mode 100644 contract/p/gnoswap/rbac/doc.gno delete mode 100644 contract/p/gnoswap/rbac/errors.gno delete mode 100644 contract/p/gnoswap/rbac/gnomod.toml delete mode 100644 contract/p/gnoswap/rbac/ownable.gno delete mode 100644 contract/p/gnoswap/rbac/ownable_test.gno delete mode 100644 contract/p/gnoswap/rbac/rbac.gno delete mode 100644 contract/p/gnoswap/rbac/rbac_test.gno delete mode 100644 contract/p/gnoswap/rbac/role.gno delete mode 100644 contract/p/gnoswap/rbac/types.gno delete mode 100644 contract/p/gnoswap/uint256/LICENSE delete mode 100644 contract/p/gnoswap/uint256/README.md delete mode 100644 contract/p/gnoswap/uint256/_helper_test.gno delete mode 100644 contract/p/gnoswap/uint256/arithmetic.gno delete mode 100644 contract/p/gnoswap/uint256/arithmetic_test.gno delete mode 100644 contract/p/gnoswap/uint256/bits_table.gno delete mode 100644 contract/p/gnoswap/uint256/bitwise.gno delete mode 100644 contract/p/gnoswap/uint256/bitwise_test.gno delete mode 100644 contract/p/gnoswap/uint256/cmp.gno delete mode 100644 contract/p/gnoswap/uint256/cmp_test.gno delete mode 100644 contract/p/gnoswap/uint256/conversion.gno delete mode 100644 contract/p/gnoswap/uint256/conversion_test.gno delete mode 100644 contract/p/gnoswap/uint256/doc.gno delete mode 100644 contract/p/gnoswap/uint256/error.gno delete mode 100644 contract/p/gnoswap/uint256/fullmath.gno delete mode 100644 contract/p/gnoswap/uint256/fullmath_test.gno delete mode 100644 contract/p/gnoswap/uint256/gnomod.toml delete mode 100644 contract/p/gnoswap/uint256/gs_pointer.gno delete mode 100644 contract/p/gnoswap/uint256/mod.gno delete mode 100644 contract/p/gnoswap/uint256/uint256.gno delete mode 100644 contract/p/gnoswap/uint256/uint256_test.gno delete mode 100644 contract/p/gnoswap/uint256/utils.gno delete mode 100644 contract/r/gnoswap/access/README.md delete mode 100644 contract/r/gnoswap/access/access.gno delete mode 100644 contract/r/gnoswap/access/assert.gno delete mode 100644 contract/r/gnoswap/access/consts.gno delete mode 100644 contract/r/gnoswap/access/errors.gno delete mode 100644 contract/r/gnoswap/access/gnomod.toml delete mode 100644 contract/r/gnoswap/access/swap_whitelist.gno delete mode 100644 contract/r/gnoswap/emission/README.md delete mode 100644 contract/r/gnoswap/emission/assert.gno delete mode 100644 contract/r/gnoswap/emission/distribution.gno delete mode 100644 contract/r/gnoswap/emission/emission.gno delete mode 100644 contract/r/gnoswap/emission/errors.gno delete mode 100644 contract/r/gnoswap/emission/gnomod.toml delete mode 100644 contract/r/gnoswap/emission/utils.gno delete mode 100644 contract/r/gnoswap/gns/README.md delete mode 100644 contract/r/gnoswap/gns/assert.gno delete mode 100644 contract/r/gnoswap/gns/consts.gno delete mode 100644 contract/r/gnoswap/gns/emission_state.gno delete mode 100644 contract/r/gnoswap/gns/errors.gno delete mode 100644 contract/r/gnoswap/gns/getter.gno delete mode 100644 contract/r/gnoswap/gns/gnomod.toml delete mode 100644 contract/r/gnoswap/gns/gns.gno delete mode 100644 contract/r/gnoswap/gns/gns_emission.gno delete mode 100644 contract/r/gnoswap/gns/halving.gno delete mode 100644 contract/r/gnoswap/gns/utils.gno delete mode 100644 contract/r/gnoswap/halt/README.md delete mode 100644 contract/r/gnoswap/halt/assert.gno delete mode 100644 contract/r/gnoswap/halt/config.gno delete mode 100644 contract/r/gnoswap/halt/doc.gno delete mode 100644 contract/r/gnoswap/halt/errors.gno delete mode 100644 contract/r/gnoswap/halt/getters.gno delete mode 100644 contract/r/gnoswap/halt/gnomod.toml delete mode 100644 contract/r/gnoswap/halt/halt.gno delete mode 100644 contract/r/gnoswap/halt/types.gno delete mode 100644 contract/r/gnoswap/rbac/README.md delete mode 100644 contract/r/gnoswap/rbac/assert.gno delete mode 100644 contract/r/gnoswap/rbac/consts.gno delete mode 100644 contract/r/gnoswap/rbac/emit.gno delete mode 100644 contract/r/gnoswap/rbac/errors.gno delete mode 100644 contract/r/gnoswap/rbac/gnomod.toml delete mode 100644 contract/r/gnoswap/rbac/ownership.gno delete mode 100644 contract/r/gnoswap/rbac/rbac.gno delete mode 100644 contract/r/gnoswap/rbac/role.gno delete mode 100644 contract/r/gnoswap/referral/README.md delete mode 100644 contract/r/gnoswap/referral/doc.gno delete mode 100644 contract/r/gnoswap/referral/errors.gno delete mode 100644 contract/r/gnoswap/referral/global_keeper.gno delete mode 100644 contract/r/gnoswap/referral/gnomod.toml delete mode 100644 contract/r/gnoswap/referral/keeper.gno delete mode 100644 contract/r/gnoswap/referral/referral.gno delete mode 100644 contract/r/gnoswap/referral/type.gno delete mode 100644 contract/r/gnoswap/referral/utils.gno delete mode 100644 contract/r/gnoswap/v1/common/consts.gno delete mode 100644 contract/r/gnoswap/v1/common/doc.gno delete mode 100644 contract/r/gnoswap/v1/common/errors.gno delete mode 100644 contract/r/gnoswap/v1/common/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/common/grc20reg_helper.gno delete mode 100644 contract/r/gnoswap/v1/common/liquidity_amounts.gno delete mode 100644 contract/r/gnoswap/v1/common/tick_math.gno delete mode 100644 contract/r/gnoswap/v1/community_pool/README.md delete mode 100644 contract/r/gnoswap/v1/community_pool/community_pool.gno delete mode 100644 contract/r/gnoswap/v1/community_pool/doc.gno delete mode 100644 contract/r/gnoswap/v1/community_pool/errors.gno delete mode 100644 contract/r/gnoswap/v1/community_pool/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/gnft/assert.gno delete mode 100644 contract/r/gnoswap/v1/gnft/errors.gno delete mode 100644 contract/r/gnoswap/v1/gnft/gnft.gno delete mode 100644 contract/r/gnoswap/v1/gnft/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/gnft/svg_generator.gno delete mode 100644 contract/r/gnoswap/v1/gnft/utils.gno delete mode 100644 contract/r/gnoswap/v1/gov/README.md delete mode 100644 contract/r/gnoswap/v1/gov/doc.gno delete mode 100644 contract/r/gnoswap/v1/gov/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/gov/governance/api.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/assert.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/config.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/consts.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/counter.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/doc.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/errors.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/getter_proposal.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/getter_vote.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/gov/governance/governance_execute.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/governance_propose.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/governance_vote.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/parameter_registry.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/parameter_registry_handler.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/proposal.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_action_status.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_data.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_manager.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_schedule_status.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_status.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/proposal_vote_status.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/state.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/utils.gno delete mode 100644 contract/r/gnoswap/v1/gov/governance/voting_info.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/api_delegation.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/api_staker.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/assert.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/consts.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/counter.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/delegation.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_history.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_mananger.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_record.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_snapshot.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/delegation_withdraw.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/doc.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/emission_reward_manager.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/emission_reward_state.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/errors.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/getter_delegation_snapshot.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_manager.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_state.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/staker_delegate.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/staker_delegation_snapshot.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/staker_reward.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/state.gno delete mode 100644 contract/r/gnoswap/v1/gov/staker/util.gno delete mode 100644 contract/r/gnoswap/v1/gov/xgns/doc.gno delete mode 100644 contract/r/gnoswap/v1/gov/xgns/errors.gno delete mode 100644 contract/r/gnoswap/v1/gov/xgns/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/gov/xgns/xgns.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/README.md delete mode 100644 contract/r/gnoswap/v1/launchpad/api_deposit.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/api_project.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/api_reward.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/assert.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/consts.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/counter.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/deposit.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/errors.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/launchpad/json_builder.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_deposit.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_project.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_protocol_fee.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_reward.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/launchpad_withdraw.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/project.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/project_condition.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/project_tier.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/reward_manager.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/reward_state.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/state.gno delete mode 100644 contract/r/gnoswap/v1/launchpad/utils.gno delete mode 100644 contract/r/gnoswap/v1/pool/README.md delete mode 100644 contract/r/gnoswap/v1/pool/api.gno delete mode 100644 contract/r/gnoswap/v1/pool/assert.gno delete mode 100644 contract/r/gnoswap/v1/pool/doc.gno delete mode 100644 contract/r/gnoswap/v1/pool/errors.gno delete mode 100644 contract/r/gnoswap/v1/pool/factory_param.gno delete mode 100644 contract/r/gnoswap/v1/pool/getter.gno delete mode 100644 contract/r/gnoswap/v1/pool/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/pool/json.gno delete mode 100644 contract/r/gnoswap/v1/pool/liquidity_math.gno delete mode 100644 contract/r/gnoswap/v1/pool/manager.gno delete mode 100644 contract/r/gnoswap/v1/pool/pool.gno delete mode 100644 contract/r/gnoswap/v1/pool/pool_type.gno delete mode 100644 contract/r/gnoswap/v1/pool/position.gno delete mode 100644 contract/r/gnoswap/v1/pool/protocol_fee.gno delete mode 100644 contract/r/gnoswap/v1/pool/swap.gno delete mode 100644 contract/r/gnoswap/v1/pool/tick.gno delete mode 100644 contract/r/gnoswap/v1/pool/tick_bitmap.gno delete mode 100644 contract/r/gnoswap/v1/pool/transfer.gno delete mode 100644 contract/r/gnoswap/v1/pool/type.gno delete mode 100644 contract/r/gnoswap/v1/pool/utils.gno delete mode 100644 contract/r/gnoswap/v1/position/README.md delete mode 100644 contract/r/gnoswap/v1/position/api.gno delete mode 100644 contract/r/gnoswap/v1/position/assert.gno delete mode 100644 contract/r/gnoswap/v1/position/burn.gno delete mode 100644 contract/r/gnoswap/v1/position/doc.gno delete mode 100644 contract/r/gnoswap/v1/position/errors.gno delete mode 100644 contract/r/gnoswap/v1/position/getter.gno delete mode 100644 contract/r/gnoswap/v1/position/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/position/json.gno delete mode 100644 contract/r/gnoswap/v1/position/liquidity_management.gno delete mode 100644 contract/r/gnoswap/v1/position/manager.gno delete mode 100644 contract/r/gnoswap/v1/position/mint.gno delete mode 100644 contract/r/gnoswap/v1/position/native_token.gno delete mode 100644 contract/r/gnoswap/v1/position/position.gno delete mode 100644 contract/r/gnoswap/v1/position/reposition.gno delete mode 100644 contract/r/gnoswap/v1/position/type.gno delete mode 100644 contract/r/gnoswap/v1/position/utils.gno delete mode 100644 contract/r/gnoswap/v1/protocol_fee/README.md delete mode 100644 contract/r/gnoswap/v1/protocol_fee/api.gno delete mode 100644 contract/r/gnoswap/v1/protocol_fee/assert.gno delete mode 100644 contract/r/gnoswap/v1/protocol_fee/consts.gno delete mode 100644 contract/r/gnoswap/v1/protocol_fee/doc.gno delete mode 100644 contract/r/gnoswap/v1/protocol_fee/errors.gno delete mode 100644 contract/r/gnoswap/v1/protocol_fee/getter.gno delete mode 100644 contract/r/gnoswap/v1/protocol_fee/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/protocol_fee/protocol_fee.gno delete mode 100644 contract/r/gnoswap/v1/protocol_fee/state.gno delete mode 100644 contract/r/gnoswap/v1/router/README.md delete mode 100644 contract/r/gnoswap/v1/router/assert.gno delete mode 100644 contract/r/gnoswap/v1/router/base.gno delete mode 100644 contract/r/gnoswap/v1/router/consts.gno delete mode 100644 contract/r/gnoswap/v1/router/doc.gno delete mode 100644 contract/r/gnoswap/v1/router/errors.gno delete mode 100644 contract/r/gnoswap/v1/router/exact_in.gno delete mode 100644 contract/r/gnoswap/v1/router/exact_out.gno delete mode 100644 contract/r/gnoswap/v1/router/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/router/protocol_fee_swap.gno delete mode 100644 contract/r/gnoswap/v1/router/router.gno delete mode 100644 contract/r/gnoswap/v1/router/router_dry.gno delete mode 100644 contract/r/gnoswap/v1/router/swap_inner.gno delete mode 100644 contract/r/gnoswap/v1/router/swap_multi.gno delete mode 100644 contract/r/gnoswap/v1/router/swap_single.gno delete mode 100644 contract/r/gnoswap/v1/router/type.gno delete mode 100644 contract/r/gnoswap/v1/router/utils.gno delete mode 100644 contract/r/gnoswap/v1/router/wrap_unwrap.gno delete mode 100644 contract/r/gnoswap/v1/staker/README.md delete mode 100644 contract/r/gnoswap/v1/staker/api.gno delete mode 100644 contract/r/gnoswap/v1/staker/assert.gno delete mode 100644 contract/r/gnoswap/v1/staker/calculate_pool_position_reward.gno delete mode 100644 contract/r/gnoswap/v1/staker/consts.gno delete mode 100644 contract/r/gnoswap/v1/staker/counter.gno delete mode 100644 contract/r/gnoswap/v1/staker/doc.gno delete mode 100644 contract/r/gnoswap/v1/staker/errors.gno delete mode 100644 contract/r/gnoswap/v1/staker/external_deposit_fee.gno delete mode 100644 contract/r/gnoswap/v1/staker/external_incentive.gno delete mode 100644 contract/r/gnoswap/v1/staker/external_token_list.gno delete mode 100644 contract/r/gnoswap/v1/staker/getter.gno delete mode 100644 contract/r/gnoswap/v1/staker/gnomod.toml delete mode 100644 contract/r/gnoswap/v1/staker/incentive_id.gno delete mode 100644 contract/r/gnoswap/v1/staker/json.gno delete mode 100644 contract/r/gnoswap/v1/staker/manage_pool_tier_and_warmup.gno delete mode 100644 contract/r/gnoswap/v1/staker/mint_stake.gno delete mode 100644 contract/r/gnoswap/v1/staker/protocol_fee_unstaking.gno delete mode 100644 contract/r/gnoswap/v1/staker/query.gno delete mode 100644 contract/r/gnoswap/v1/staker/reward_calculation.gno delete mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_incentives.gno delete mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_pool.gno delete mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_pool_tier.gno delete mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_tick.gno delete mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_types.gno delete mode 100644 contract/r/gnoswap/v1/staker/reward_calculation_warmup.gno delete mode 100644 contract/r/gnoswap/v1/staker/staker.gno delete mode 100644 contract/r/gnoswap/v1/staker/type.gno delete mode 100644 contract/r/gnoswap/v1/staker/utils.gno delete mode 100644 contract/r/gnoswap/v1/staker/wrap_unwrap.gno diff --git a/contract/p/gnoswap/fuzz/fuzz.gno b/contract/p/gnoswap/fuzz/fuzz.gno deleted file mode 100644 index be07f85..0000000 --- a/contract/p/gnoswap/fuzz/fuzz.gno +++ /dev/null @@ -1,549 +0,0 @@ -// Package fuzz provides property-based testing utilities for generating random test data. -// It supports various data types including integers, strings, and 256-bit numbers, -// with configurable ranges and boundary value testing. -package fuzz - -import ( - "errors" - "math" - "math/rand" - "strconv" - "unicode/utf8" - - xs "gno.land/p/wyhaines/rand/xorshift64star" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -const ( - MAX_UINT64 string = "18446744073709551615" - MAX_INT128 string = "170141183460469231731687303715884105727" - MAX_UINT128 string = "340282366920938463463374607431768211455" -) - -// Generator is the interface that wraps the basic Generate method. -// Implementations return randomly generated values of their specific type. -type Generator interface { - Generate() any -} - -// seedCounter ensures different seeds for each generator instance. -// We use a function-scoped approach to avoid realm modification issues. -var seedCounter uint64 - -// getNextSeed returns a new seed value for RNG initialization. -// It uses a counter with a prime multiplier to ensure uniqueness. -func getNextSeed() uint64 { - // Increment counter and combine with time for uniqueness - seedCounter++ - // Use a prime multiplier to spread values - return seedCounter * 2654435761 -} - -// initRNG initializes the random number generator if not already initialized. -func initRNG(rng **xs.Xorshift64Star) { - if *rng == nil { - // Use entropy-based seed to avoid realm issues - *rng = xs.New() - } -} - -// TODO: add more generators -var ( - _ Generator = (*IntGenerator)(nil) - _ Generator = (*Uint32Generator)(nil) - _ Generator = (*NumberStringGenerator)(nil) -) - -// IntGenerator generates random integers within a specified range. -// The zero value generates full int32 range values. -type IntGenerator struct { - min, max int - rng *xs.Xorshift64Star -} - -// Generate returns a random integer within the generator's range. -func (g *IntGenerator) Generate() any { - initRNG(&g.rng) - if g.min == 0 && g.max == 0 { - return int(int32(g.rng.Uint64())) - } - size := uint64(g.max - g.min + 1) - return int(g.rng.Uint64()%size) + g.min -} - -// IntRange creates an IntGenerator that produces values between min and max inclusive. -func IntRange(min, max int) Generator { - return &IntGenerator{ - min: min, - max: max, - rng: nil, // Will be initialized on first use - } -} - -// IntRangeWithSeed creates an IntGenerator with the specified seed for reproducible generation. -func IntRangeWithSeed(min, max int, seed uint64) Generator { - return &IntGenerator{ - min: min, - max: max, - rng: xs.New(seed), - } -} - -// Uint32Generator generates random uint32 values within a specified range. -// The zero value generates full uint32 range values. -type Uint32Generator struct { - min, max uint32 - rng *xs.Xorshift64Star -} - -// Generate returns a random uint32 within the generator's range. -func (g *Uint32Generator) Generate() any { - initRNG(&g.rng) - if g.min == 0 && g.max == 0 { - return uint32(g.rng.Uint64()) - } - size := uint64(g.max - g.min + 1) - return uint32(g.rng.Uint64()%size) + g.min -} - -// Uint32Range creates a Uint32Generator that produces values between min and max inclusive. -func Uint32Range(min, max uint32) Generator { - return &Uint32Generator{ - min: min, - max: max, - rng: nil, // Will be initialized on first use - } -} - -// Uint32RangeWithSeed creates a Uint32Generator with the specified seed for reproducible generation. -func Uint32RangeWithSeed(min, max uint32, seed uint64) Generator { - return &Uint32Generator{ - min: min, - max: max, - rng: xs.New(seed), - } -} - -// Uint32 creates a Uint32Generator that produces values between 0 and 100. -func Uint32() Generator { - return Uint32Range(0, 100) -} - -// BoolGenerator generates random boolean values. -type BoolGenerator struct { - rng *xs.Xorshift64Star -} - -// Generate returns a random boolean value. -func (g *BoolGenerator) Generate() any { - initRNG(&g.rng) - return g.rng.Uint64()%2 == 0 -} - -// Bool creates a BoolGenerator that produces random boolean values. -func Bool() Generator { - return &BoolGenerator{rng: nil} // Will be initialized on first use -} - -// BoolWithSeed creates a BoolGenerator with the specified seed for reproducible generation. -func BoolWithSeed(seed uint64) Generator { - return &BoolGenerator{rng: xs.New(seed)} -} - -// StringGenerator generates random ASCII strings within a specified length range. -type StringGenerator struct { - minLen, maxLen int - rng *xs.Xorshift64Star -} - -// Generate returns a random ASCII string within the generator's length range. -func (g *StringGenerator) Generate() interface{} { - initRNG(&g.rng) - if g.maxLen == 0 { - g.maxLen = 10 - } - lengthRange := uint64(g.maxLen - g.minLen + 1) - length := int(g.rng.Uint64()%lengthRange) + g.minLen - result := make([]byte, length) - for i := range result { - result[i] = byte(g.rng.Uint64()%94 + 33) - } - return string(result) -} - -// StringWithSeed creates a StringGenerator with the specified seed for reproducible generation. -func StringWithSeed(minLen, maxLen int, seed uint64) Generator { - return &StringGenerator{ - minLen: minLen, - maxLen: maxLen, - rng: xs.New(seed), - } -} - -// NumberStringGenerator generates numeric strings in various bases (binary, octal, decimal, hex). -type NumberStringGenerator struct { - min, max int64 - baseTypes []int - rng *xs.Xorshift64Star -} - -// Generate returns a numeric string in a random base format (binary, octal, decimal, or hexadecimal). -func (g *NumberStringGenerator) Generate() any { - initRNG(&g.rng) - - if g.min == 0 && g.max == 0 { - g.max = 1000000 - } - - size := uint64(g.max - g.min + 1) - value := int64(g.rng.Uint64()%size) + g.min - - if len(g.baseTypes) == 0 { - g.baseTypes = []int{2, 8, 10, 16} - } - - baseIndex := g.rng.Uint64() % uint64(len(g.baseTypes)) - base := g.baseTypes[baseIndex] - - switch base { - case 2: - return "0b" + strconv.FormatInt(value, 2) - case 8: - return "0o" + strconv.FormatInt(value, 8) - case 16: - return "0x" + strconv.FormatInt(value, 16) - default: - return strconv.FormatInt(value, 10) - } -} - -// NumberString creates a NumberStringGenerator with default range (0-1000000) and all base formats. -func NumberString() Generator { - return &NumberStringGenerator{ - min: 0, - max: 1000000, - baseTypes: []int{2, 8, 10, 16}, - rng: nil, // Will be initialized on first use - } -} - -// NumberStringRange creates a NumberStringGenerator with the specified range and base formats. -func NumberStringRange(min, max int64, bases ...int) Generator { - if len(bases) == 0 { - bases = []int{2, 8, 10, 16} - } - return &NumberStringGenerator{ - min: min, - max: max, - baseTypes: bases, - rng: nil, // Will be initialized on first use - } -} - -// Config controls fuzzing parameters for property-based testing. -type Config struct { - Iterations int - Shrink bool - Seed uint64 // Optional: if provided, uses this seed for reproducibility -} - -// Result contains the outcome of a fuzzing run. -type Result struct { - Failed bool - FailingInput any - Iterations int -} - -// abs returns the absolute value of x. -func abs(x int) int { - if x < 0 { - return -x - } - return x -} - -// shrinkInt attempts to find a simpler failing input through iterative shrinking. -func shrinkInt(property func(int) bool, failingInput int) int { - current := failingInput - for attempt := 0; attempt < 100; attempt++ { - candidates := []int{0, 1, -1, current / 2, current - 1, current + 1} - for _, candidate := range candidates { - if !property(candidate) && abs(candidate) <= abs(current) { - current = candidate - } - } - } - return current -} - -// GenerateBoundary returns boundary values for the specified type. -// Supported types: "int", "uint", "string", "numberString", "slice". -func GenerateBoundary(vt string) []any { - switch vt { - case "int": - return []any{ - 0, 1, -1, - int(math.MaxInt), int(math.MinInt), - int(math.MaxInt - 1), int(math.MinInt + 1), - } - case "uint": - return []any{ - uint(0), uint(1), - uint(math.MaxUint), uint(math.MaxUint - 1), - } - case "string": - return []any{ - "", " ", "\n", "\t", - "a", "A", "0", - generateLongString(100), - generateUnicodeString(), - } - case "numberString": - return []any{ - "0", "1", "-1", - "0b0", "0b1", "0b1111111111111111", - "0o0", "0o7", "0o777", - "0x0", "0x1", "0xFFFF", "0xffffffff", - "9223372036854775807", "-9223372036854775808", - } - case "slice": - return []any{ - []int{}, - generateLargeSlice(10000), - } - } - return nil -} - -// generateLongString creates a string of 'a' characters with the specified length. -func generateLongString(n int) string { - b := make([]byte, n) - for i := range b { - b[i] = 'a' - } - return string(b) -} - -// generateUnicodeString creates a string containing various Unicode characters including Greek, CJK, and emoji. -func generateUnicodeString() string { - runes := []rune{'α', 'β', 'γ', '漢', '字', '🌟'} - result := "" - for _, r := range runes { - if utf8.ValidRune(r) { - result += string(r) - } - } - return result -} - -// generateLargeSlice creates a slice of n random integers between 0 and 99. -func generateLargeSlice(n int) []int { - slice := make([]int, n) - for i := 0; i < n; i++ { - slice[i] = rand.IntN(100) - } - return slice -} - -// Uint256Generator generates random uint256 values for testing. -type Uint256Generator struct { - allowZero bool - maxBits uint - seedMgr *SeedManager -} - -// NewUint256Generator creates a new Uint256Generator with the specified zero-allowance and bit constraints. -func NewUint256Generator(allowZero bool, maxBits uint) *Uint256Generator { - return &Uint256Generator{ - allowZero: allowZero, - maxBits: maxBits, - seedMgr: NewSeedManager(), - } -} - -// Generate returns a random uint256 value according to the generator's constraints. -// Returns special values (zero, one, MAX_UINT128, MAX_INT128, MAX_UINT64) or random values within maxBits. -func (g *Uint256Generator) Generate() any { - if g.seedMgr == nil { - g.seedMgr = NewSeedManager() - } - - // Generate special cases - specialCase := g.seedMgr.CreateIntGenerator(0, 10).Generate().(int) - switch specialCase { - case 0: - if g.allowZero { - return u256.Zero() - } - case 1: - return u256.One() - case 2: - // MAX_UINT128 - return u256.MustFromDecimal(MAX_UINT128) - case 3: - val := u256.MustFromDecimal(MAX_UINT128) - return new(u256.Uint).Add(val, u256.One()) - case 4: - return u256.MustFromDecimal(MAX_INT128) - case 5: - return u256.MustFromDecimal(MAX_UINT64) - } - - // random value within maxBits - bits := g.seedMgr.CreateIntGenerator(1, int(g.maxBits)).Generate().(int) - val := u256.One() - val = new(u256.Uint).Lsh(val, uint(bits)) - - offset := g.seedMgr.CreateIntGenerator(0, 1000).Generate().(int) - val = new(u256.Uint).Add(val, u256.NewUint(uint64(offset))) - - return val -} - -// Int256Generator generates random int256 values for testing. -type Int256Generator struct { - allowZero bool - allowNegative bool - maxBits uint - seedMgr *SeedManager -} - -// NewInt256Generator creates a new Int256Generator with the specified zero, negative, and bit constraints. -func NewInt256Generator(allowZero bool, allowNegative bool, maxBits uint) *Int256Generator { - return &Int256Generator{ - allowZero: allowZero, - allowNegative: allowNegative, - maxBits: maxBits, - seedMgr: NewSeedManager(), - } -} - -// Generate returns a random int256 value according to the generator's constraints. -// Returns special values or random positive/negative values within maxBits range. -func (g *Int256Generator) Generate() any { - if g.seedMgr == nil { - g.seedMgr = NewSeedManager() - } - - specialCase := g.seedMgr.CreateIntGenerator(0, 10).Generate().(int) - switch specialCase { - case 0: - if g.allowZero { - return i256.Zero() - } - case 1: - return i256.One() - case 2: - return i256.MustFromDecimal(MAX_INT128) - case 3: - val := i256.MustFromDecimal(MAX_INT128) - return new(i256.Int).Add(val, i256.One()) - case 4: - if g.allowNegative { - val := i256.MustFromDecimal(MAX_INT128) - return i256.Zero().Neg(val) - } - } - - bits := g.seedMgr.CreateIntGenerator(1, int(g.maxBits)).Generate().(int) - uval := u256.One() - uval = new(u256.Uint).Lsh(uval, uint(bits)) - - val := i256.FromUint256(uval) - - // Randomly make negative - if g.allowNegative && g.seedMgr.CreateBoolGenerator().Generate().(bool) { - val = i256.Zero().Neg(val) - } - - return val -} - -// FuzzWithConfig runs property-based testing with the specified configuration. -// Note: config.Seed is ignored as entropy-based seeding is used. -func FuzzWithConfig(config Config, property func(int) bool) *Result { - // Note: config.Seed is ignored as we use entropy-based seeding - return FuzzWithConfigAndGen(config, &IntGenerator{rng: nil}, property) -} - -// FuzzWithConfigAndGen runs property-based testing with a custom generator and configuration. -// Returns a Result containing test outcome, failing input (if any), and iteration count. -func FuzzWithConfigAndGen(config Config, gen Generator, property func(int) bool) *Result { - result := &Result{} - iterations := config.Iterations - if iterations == 0 { - iterations = 100 - } - var failingInput int - for i := 0; i < iterations; i++ { - result.Iterations++ - value := gen.Generate().(int) - if !property(value) { - failingInput = value - result.Failed = true - break - } - } - if !result.Failed { - return result - } - if config.Shrink { - result.FailingInput = shrinkInt(property, failingInput) - } else { - result.FailingInput = failingInput - } - return result -} - -// Fuzz runs property-based testing on the given property function. -// Supports func(int) bool and func(string) bool property types. Returns error on property violation. -func Fuzz(property interface{}) error { - if fn, ok := property.(func(int) bool); ok { - return FuzzWithGen(IntRange(0, 1000), fn) - } - if fn, ok := property.(func(string) bool); ok { - return fuzzString(fn) - } - return errors.New("unsupported property type") -} - -// fuzzString runs property-based testing for string properties with 100 iterations. -func fuzzString(fn func(string) bool) error { - gen := &StringGenerator{rng: nil} // Will be initialized on first use - for i := 0; i < 100; i++ { - if !fn(gen.Generate().(string)) { - return errors.New("property violation") - } - } - return nil -} - -// FuzzWithGen runs property-based testing using a custom generator with 100 iterations. -func FuzzWithGen(gen Generator, property func(int) bool) error { - for i := 0; i < 100; i++ { - value := gen.Generate().(int) - if !property(value) { - return errors.New("property violation") - } - } - return nil -} - -// FuzzN runs property-based testing with multiple parameters. -// Currently supports func(int, int) bool property type with 100 iterations per test. -func FuzzN(property interface{}) error { - if fn, ok := property.(func(int, int) bool); ok { - gen := &IntGenerator{rng: nil} // Will be initialized on first use - for i := 0; i < 100; i++ { - a := gen.Generate().(int) - b := gen.Generate().(int) - if !fn(a, b) { - return errors.New("property violation") - } - } - return nil - } - return errors.New("unsupported property type") -} diff --git a/contract/p/gnoswap/fuzz/fuzz_test.gno b/contract/p/gnoswap/fuzz/fuzz_test.gno deleted file mode 100644 index fde0a96..0000000 --- a/contract/p/gnoswap/fuzz/fuzz_test.gno +++ /dev/null @@ -1,320 +0,0 @@ -package fuzz - -import ( - "math" - "strconv" - "strings" - "testing" -) - -type TestCase struct { - orderId int - amount uint32 - express bool - notes string -} - -func generateTestCase() TestCase { - return TestCase{ - orderId: IntRange(-10, 1000).Generate().(int), - amount: Uint32Range(0, 500).Generate().(uint32), - express: Bool().Generate().(bool), - notes: (&StringGenerator{minLen: 0, maxLen: 50}).Generate().(string), - } -} - -func validateOrder(orderId int, amount uint32, express bool, notes string) bool { - if orderId <= 0 { - return false - } - if amount == 0 { - return false - } - if express && amount < 100 { - return false - } - if len(notes) > 1000 { - return false - } - return true -} - -func TestComplexPropertyWithWrapper(t *testing.T) { - const total = 100 - failures := 0 - - for i := 0; i < total; i++ { - tc := generateTestCase() - result := validateOrder(tc.orderId, tc.amount, tc.express, tc.notes) - - if tc.orderId <= 0 && result { - t.Errorf("Should fail for non-positive orderId: %+v", tc) - failures++ - } - if tc.amount == 0 && result { - t.Errorf("Should fail for zero amount: %+v", tc) - failures++ - } - if tc.express && tc.amount < 100 && result { - t.Errorf("Should fail for express with low amount: %+v", tc) - failures++ - } - } - - if failures > 0 { - t.Logf("Total failures: %d out of %d", failures, total) - } -} - -func TestFuzzSingleIntParameter(t *testing.T) { - double := func(x int) int { - return x * 2 - } - - err := Fuzz(func(x int) bool { - return double(x) == x*2 - }) - if err != nil { - t.Errorf("property failed when it shouldn't: %v", err) - } -} - -func TestShouldDetectsViolation(t *testing.T) { - failures := func(x int) int { - if x < -100 { - return x // <- bug: negative result - } - return abs(x) - } - - err := FuzzWithGen( - IntRange(-200, 200), - func(x int) bool { - return failures(x) >= 0 - }, - ) - - if err == nil { - t.Errorf("Expected property violation but none was detected") - } -} - -func TestFuzzWithMultipleParameters(t *testing.T) { - add := func(a, b int) int { - return a + b - } - - err := FuzzN(func(a, b int) bool { - return add(a, b) == add(b, a) // should be commutative - }) - if err != nil { - t.Errorf("property failed when it shouldn't: %v", err) - } -} - -func TestFuzzStringParameter(t *testing.T) { - strLen := func(s string) int { - return len(s) - } - - err := Fuzz(func(s string) bool { - return strLen(s) >= 0 - }) - if err != nil { - t.Errorf("property failed when it shouldn't: %v", err) - } -} - -func TestCustomGenerator(t *testing.T) { - isPositive := func(x int) bool { - return x > 0 - } - - err := FuzzWithGen( - IntRange(1, 1000), - func(x int) bool { - return isPositive(x) - }, - ) - if err != nil { - t.Errorf("property failed when it shouldn't: %v", err) - } -} - -func TestShrinking(t *testing.T) { - failure := func(x int) bool { - return x < 100 - } - - result := FuzzWithConfigAndGen( - Config{Shrink: true, Iterations: 100}, - IntRange(0, 200), - failure, - ) - - if !result.Failed { - t.Errorf("Expected property violation but none was detected") - } - if result.FailingInput != 100 { - t.Errorf("Expected failing input to be 100 but got %v", result.FailingInput) - } -} - -func TestGenerateBoundaryValues_Int(t *testing.T) { - values := GenerateBoundary("int") - expected := map[any]bool{ - 0: true, 1: true, -1: true, - int(math.MaxInt): true, int(math.MinInt): true, - int(math.MaxInt - 1): true, int(math.MinInt + 1): true, - } - - for _, v := range values { - if _, ok := expected[v]; !ok { - t.Errorf("Unexpected value: %v", v) - } - delete(expected, v) - } - for missing := range expected { - t.Errorf("Missing value: %v", missing) - } -} - -func TestGenerateBoundaryValues_Uint(t *testing.T) { - values := GenerateBoundary("uint") - expected := map[any]bool{ - uint(0): true, uint(1): true, - uint(math.MaxUint): true, uint(math.MaxUint - 1): true, - } - - for _, v := range values { - if _, ok := expected[v]; !ok { - t.Errorf("Unexpected value: %v", v) - } - delete(expected, v) - } - for missing := range expected { - t.Errorf("Missing value: %v", missing) - } -} - -func TestGenerateBoundaryValues_String(t *testing.T) { - values := GenerateBoundary("string") - if len(values) == 0 { - t.Errorf("Expected some values but got none") - } - for _, v := range values { - if _, ok := v.(string); !ok { - t.Errorf("Expected string value but got %T", v) - } - } -} - -func TestGenerateBoundaryValues_Slice(t *testing.T) { - values := GenerateBoundary("slice") - if len(values) == 0 { - t.Errorf("Expected some values but got none") - } - for _, v := range values { - if _, ok := v.([]int); !ok { - t.Errorf("Expected []int value but got %T", v) - } - } -} - -func TestGenerateBoundaryValues_UnknownType(t *testing.T) { - values := GenerateBoundary("invalid-type") - if len(values) != 0 { - t.Errorf("Expected no values but got %v", values) - } -} - -func TestNumberStringGenerator(t *testing.T) { - gen := NumberString() - - for i := 0; i < 100; i++ { - value := gen.Generate().(string) - - if strings.HasPrefix(value, "0b") { - binary := strings.TrimPrefix(value, "0b") - if _, err := strconv.ParseInt(binary, 2, 64); err != nil { - t.Errorf("Invalid binary string: %s", value) - } - } else if strings.HasPrefix(value, "0o") { - octal := strings.TrimPrefix(value, "0o") - if _, err := strconv.ParseInt(octal, 8, 64); err != nil { - t.Errorf("Invalid octal string: %s", value) - } - } else if strings.HasPrefix(value, "0x") { - hex := strings.TrimPrefix(value, "0x") - if _, err := strconv.ParseInt(hex, 16, 64); err != nil { - t.Errorf("Invalid hex string: %s", value) - } - } else { - if _, err := strconv.ParseInt(value, 10, 64); err != nil { - t.Errorf("Invalid decimal string: %s", value) - } - } - } -} - -func TestNumberStringGeneratorWithRange(t *testing.T) { - gen := NumberStringRange(-100, 100, 10, 16) - - for i := 0; i < 50; i++ { - value := gen.Generate().(string) - - var parsed int64 - var err error - - if strings.HasPrefix(value, "0x") { - hex := strings.TrimPrefix(value, "0x") - parsed, err = strconv.ParseInt(hex, 16, 64) - } else { - parsed, err = strconv.ParseInt(value, 10, 64) - } - - if err != nil { - t.Errorf("Failed to parse number string: %s", value) - continue - } - - if parsed < -100 || parsed > 100 { - t.Errorf("Value out of range: %d from %s", parsed, value) - } - } -} - -func TestGenerateBoundaryValues_NumberString(t *testing.T) { - values := GenerateBoundary("numberString") - - if len(values) == 0 { - t.Errorf("Expected boundary values for numberString") - return - } - - expectedPatterns := []string{ - "0", "1", "-1", - "0b", "0o", "0x", - } - - foundPatterns := make(map[string]bool) - for _, v := range values { - s, ok := v.(string) - if !ok { - t.Errorf("Expected string value but got %T", v) - continue - } - - for _, pattern := range expectedPatterns { - if strings.HasPrefix(s, pattern) || s == pattern { - foundPatterns[pattern] = true - } - } - } - - for _, pattern := range expectedPatterns[:3] { - if !foundPatterns[pattern] { - t.Errorf("Missing boundary value pattern: %s", pattern) - } - } -} diff --git a/contract/p/gnoswap/fuzz/gnomod.toml b/contract/p/gnoswap/fuzz/gnomod.toml deleted file mode 100644 index a9b6adf..0000000 --- a/contract/p/gnoswap/fuzz/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/p/gnoswap/fuzz" -gno = "0.9" diff --git a/contract/p/gnoswap/fuzz/seed.gno b/contract/p/gnoswap/fuzz/seed.gno deleted file mode 100644 index 23f3db9..0000000 --- a/contract/p/gnoswap/fuzz/seed.gno +++ /dev/null @@ -1,55 +0,0 @@ -package fuzz - -import ( - "time" -) - -// SeedManager manages unique seed generation for fuzz testing. -// Each instance maintains its own seed sequence, avoiding global state issues -// and ensuring diverse random sequences across different generators. -type SeedManager struct { - baseSeed uint64 - counter uint64 -} - -// NewSeedManager creates a new seed manager with time-based initial seed. -func NewSeedManager() *SeedManager { - return &SeedManager{ - baseSeed: uint64(time.Now().UnixNano()), - counter: 0, - } -} - -// NewSeedManagerWithSeed creates a seed manager with the specified base seed for reproducible tests. -func NewSeedManagerWithSeed(seed uint64) *SeedManager { - return &SeedManager{ - baseSeed: seed, - counter: 0, - } -} - -// NextSeed returns the next unique seed value using prime multiplier 2654435761 for good distribution. -func (sm *SeedManager) NextSeed() uint64 { - sm.counter++ - return sm.baseSeed + sm.counter*2654435761 -} - -// CreateIntGenerator creates an IntGenerator with a unique seed. -func (sm *SeedManager) CreateIntGenerator(min, max int) Generator { - return IntRangeWithSeed(min, max, sm.NextSeed()) -} - -// CreateUint32Generator creates a Uint32Generator with a unique seed. -func (sm *SeedManager) CreateUint32Generator(min, max uint32) Generator { - return Uint32RangeWithSeed(min, max, sm.NextSeed()) -} - -// CreateBoolGenerator creates a BoolGenerator with a unique seed. -func (sm *SeedManager) CreateBoolGenerator() Generator { - return BoolWithSeed(sm.NextSeed()) -} - -// CreateStringGenerator creates a StringGenerator with a unique seed. -func (sm *SeedManager) CreateStringGenerator(minLen, maxLen int) Generator { - return StringWithSeed(minLen, maxLen, sm.NextSeed()) -} diff --git a/contract/p/gnoswap/gnsmath/bit_math.gno b/contract/p/gnoswap/gnsmath/bit_math.gno deleted file mode 100644 index a122f9e..0000000 --- a/contract/p/gnoswap/gnsmath/bit_math.gno +++ /dev/null @@ -1,80 +0,0 @@ -package gnsmath - -import ( - u256 "gno.land/p/gnoswap/uint256" -) - -var ( - msbShifts = []bitShift{ - {u256.Zero().Lsh(u256.One(), 128), 128}, // 2^128 - {u256.Zero().Lsh(u256.One(), 64), 64}, // 2^64 - {u256.Zero().Lsh(u256.One(), 32), 32}, // 2^32 - {u256.Zero().Lsh(u256.One(), 16), 16}, // 2^16 - {u256.Zero().Lsh(u256.One(), 8), 8}, // 2^8 - {u256.Zero().Lsh(u256.One(), 4), 4}, // 2^4 - {u256.Zero().Lsh(u256.One(), 2), 2}, // 2^2 - {u256.Zero().Lsh(u256.One(), 1), 1}, // 2^1 - } - - lsbShifts = []bitShift{ - {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 128), u256.One()), 128}, // 2^128 - 1 - {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 64), u256.One()), 64}, // 2^64 - 1 - {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 32), u256.One()), 32}, // 2^32 - 1 - {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 16), u256.One()), 16}, // 2^16 - 1 - {u256.Zero().Sub(u256.Zero().Lsh(u256.One(), 8), u256.One()), 8}, // 2^8 - 1 - {u256.NewUint(0xf), 4}, // 2^4 - 1 = 15 - {u256.NewUint(0x3), 2}, // 2^2 - 1 = 3 - {u256.NewUint(0x1), 1}, // 2^1 - 1 = 1 - } -) - -// bitShift represents a bit pattern and corresponding shift amount for bit manipulation. -type bitShift struct { - bitPattern *u256.Uint - shift uint -} - -// BitMathMostSignificantBit returns the 0-based position of the most significant bit in x. -// This function is essential for AMM calculations involving price ranges and tick boundaries. -// Panics if x is zero. -func BitMathMostSignificantBit(x *u256.Uint) uint8 { - if x.IsZero() { - panic(errMSBZeroInput) - } - - temp := x.Clone() - r := uint8(0) - - for _, s := range msbShifts { - if temp.Gte(s.bitPattern) { - temp = temp.Rsh(temp, s.shift) - r += uint8(s.shift) - } - } - - return r -} - -// BitMathLeastSignificantBit returns the 0-based position of the least significant bit in x. -// This function is used in AMM calculations for efficient bit manipulation and range queries. -// Panics if x is zero. -func BitMathLeastSignificantBit(x *u256.Uint) uint8 { - if x.IsZero() { - panic(errLSBZeroInput) - } - - temp := x.Clone() - hasSetBits := u256.Zero() - r := uint8(255) - - for _, s := range lsbShifts { - hasSetBits = hasSetBits.And(temp, s.bitPattern) - if !hasSetBits.IsZero() { - r -= uint8(s.shift) - } else { - temp = temp.Rsh(temp, s.shift) - } - } - - return r -} diff --git a/contract/p/gnoswap/gnsmath/bit_math_test.gno b/contract/p/gnoswap/gnsmath/bit_math_test.gno deleted file mode 100644 index c4d00ef..0000000 --- a/contract/p/gnoswap/gnsmath/bit_math_test.gno +++ /dev/null @@ -1,855 +0,0 @@ -package gnsmath - -import ( - "testing" - - "gno.land/p/nt/uassert" - u256 "gno.land/p/gnoswap/uint256" -) - -func TestBitMathMostSignificantBit(t *testing.T) { - tests := []struct { - name string - input string - expected uint8 - shouldPanic bool - }{ - { - name: "zero_panics", - input: "0", - shouldPanic: true, - }, - - // Basic cases - { - name: "one", - input: "1", - expected: 0, - }, - { - name: "two", - input: "2", - expected: 1, - }, - - // Small numbers - { - name: "three", - input: "3", - expected: 1, - }, - { - name: "four", - input: "4", - expected: 2, - }, - { - name: "five", - input: "5", - expected: 2, - }, - { - name: "255", - input: "255", - expected: 7, - }, - { - name: "256", - input: "256", - expected: 8, - }, - { - name: "257", - input: "257", - expected: 8, - }, - - // Boundary values for each shift level - { - name: "2^16-1", - input: "65535", - expected: 15, - }, - { - name: "2^16", - input: "65536", - expected: 16, - }, - { - name: "2^16+1", - input: "65537", - expected: 16, - }, - { - name: "2^32-1", - input: "4294967295", - expected: 31, - }, - { - name: "2^32", - input: "4294967296", - expected: 32, - }, - { - name: "2^32+1", - input: "4294967297", - expected: 32, - }, - { - name: "2^64-1", - input: "18446744073709551615", - expected: 63, - }, - { - name: "2^64", - input: "18446744073709551616", - expected: 64, - }, - { - name: "2^64+1", - input: "18446744073709551617", - expected: 64, - }, - { - name: "2^128-1", - input: "340282366920938463463374607431768211455", - expected: 127, - }, - { - name: "2^128", - input: "340282366920938463463374607431768211456", - expected: 128, - }, - { - name: "2^128+1", - input: "340282366920938463463374607431768211457", - expected: 128, - }, - - // Large numbers - { - name: "2^255", - input: "57896044618658097711785492504343953926634992332820282019728792003956564819968", - expected: 255, - }, - { - name: "max_uint256", - input: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - expected: 255, - }, - - // Special bit patterns - { - name: "alternating_bits_0x5555", - input: "21845", // 0x5555 - expected: 14, - }, - { - name: "alternating_bits_0xAAAA", - input: "43690", // 0xAAAA - expected: 15, - }, - { - name: "all_ones_32bit", - input: "4294967295", // 0xFFFFFFFF - expected: 31, - }, - { - name: "high_bit_only_64", - input: "9223372036854775808", // 0x8000000000000000 - expected: 63, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.shouldPanic { - uassert.PanicsWithMessage(t, errMSBZeroInput.Error(), func() { - BitMathMostSignificantBit(u256.MustFromDecimal(tc.input)) - }) - return - } - - x := u256.MustFromDecimal(tc.input) - result := BitMathMostSignificantBit(x) - uassert.Equal(t, result, tc.expected) - }) - } -} - -func TestBitMathMostSignificantBit_PowersOfTwo(t *testing.T) { - // Test all powers of 2 from 2^0 to 2^255 - for i := 0; i < 256; i++ { - num := new(u256.Uint).Lsh(u256.One(), uint(i)) - result := BitMathMostSignificantBit(num) - uassert.Equal(t, result, uint8(i)) - } -} - -func TestBitMathLeastSignificantBit(t *testing.T) { - tests := []struct { - name string - input string - expected uint8 - shouldPanic bool - }{ - { - name: "zero_panics", - input: "0", - shouldPanic: true, - }, - - // Basic cases - { - name: "one", - input: "1", - expected: 0, - }, - { - name: "two", - input: "2", - expected: 1, - }, - - // Odd numbers (LSB = 0) - { - name: "three", - input: "3", - expected: 0, - }, - { - name: "five", - input: "5", - expected: 0, - }, - { - name: "255", - input: "255", - expected: 0, - }, - { - name: "max_uint256_odd", - input: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - expected: 0, - }, - - // Even numbers - { - name: "four", - input: "4", - expected: 2, - }, - { - name: "six", - input: "6", - expected: 1, - }, - { - name: "eight", - input: "8", - expected: 3, - }, - { - name: "256", - input: "256", - expected: 8, - }, - { - name: "1024", - input: "1024", - expected: 10, - }, - - // Powers of 2 boundaries - { - name: "2^16", - input: "65536", - expected: 16, - }, - { - name: "2^32", - input: "4294967296", - expected: 32, - }, - { - name: "2^64", - input: "18446744073709551616", - expected: 64, - }, - { - name: "2^128", - input: "340282366920938463463374607431768211456", - expected: 128, - }, - { - name: "2^255", - input: "57896044618658097711785492504343953926634992332820282019728792003956564819968", - expected: 255, - }, - - // Special patterns - { - name: "0xF0", - input: "240", - expected: 4, - }, - { - name: "0xFF00", - input: "65280", - expected: 8, - }, - { - name: "0xFFFF0000", - input: "4294901760", - expected: 16, - }, - - // Multiple bits with different LSB - { - name: "12", // 0b1100 - input: "12", - expected: 2, - }, - { - name: "96", // 0b1100000 - input: "96", - expected: 5, - }, - { - name: "192", // 0b11000000 - input: "192", - expected: 6, - }, - - // Large numbers with low LSB - { - name: "2^255+2^10", - input: "57896044618658097711785492504343953926634992332820282019728792003956564820992", - expected: 10, - }, - { - name: "2^200+2^20", - input: "1606938044258990275541962092341162602522202993782792835302425600", - expected: 10, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if tc.shouldPanic { - uassert.PanicsWithMessage(t, errLSBZeroInput.Error(), func() { - BitMathLeastSignificantBit(u256.MustFromDecimal(tc.input)) - }) - return - } - - x := u256.MustFromDecimal(tc.input) - result := BitMathLeastSignificantBit(x) - uassert.Equal(t, result, tc.expected) - }) - } -} - -func TestBitMathLeastSignificantBit_PowersOfTwo(t *testing.T) { - // Test all powers of 2 from 2^0 to 2^255 - for i := 0; i < 256; i++ { - num := new(u256.Uint).Lsh(u256.One(), uint(i)) - result := BitMathLeastSignificantBit(num) - uassert.Equal(t, result, uint8(i)) - } -} - -func TestBitMath_Consistency(t *testing.T) { - tests := []struct { - name string - input string - }{ - {"small_number", "42"}, - {"medium_number", "1234567890"}, - {"large_number", "123456789012345678901234567890123456789"}, - {"half_max", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - {"near_max", "115792089237316195423570985008687907853269984665640564039457584007913129639934"}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := u256.MustFromDecimal(tc.input) - msb := BitMathMostSignificantBit(x) - lsb := BitMathLeastSignificantBit(x) - - // MSB must be >= LSB - if msb < lsb { - t.Errorf("Invalid result: MSB(%d) < LSB(%d) for %s", msb, lsb, tc.input) - } - }) - } -} - -func TestBitMath_SingleBitConsistency(t *testing.T) { - // For single bit numbers, MSB == LSB - for i := 0; i < 256; i++ { - x := new(u256.Uint).Lsh(u256.One(), uint(i)) - msb := BitMathMostSignificantBit(x) - lsb := BitMathLeastSignificantBit(x) - - if msb != lsb || msb != uint8(i) { - t.Errorf("Single bit 2^%d: MSB=%d, LSB=%d, expected both=%d", i, msb, lsb, i) - } - } -} - -// Additional test cases to add to the existing test file - -func TestBitMathLeastSignificantBit_MaxUint256MinusOne(t *testing.T) { - // MAX_UINT256 - 1 = ...639934 (even number, LSB should be 1) - maxMinusOne := new(u256.Uint).Sub( - u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935"), - u256.One(), - ) - - result := BitMathLeastSignificantBit(maxMinusOne) - uassert.Equal(t, result, uint8(1)) -} - -func TestBitMath_PseudoRandomLargeNumbers(t *testing.T) { - testCases := []struct { - name string - input string - expMSB uint8 - expLSB uint8 - }{ - { - name: "large_random_1", - input: "98765432109876543210987654321098765432109876543210987654321098765432", - expMSB: 225, - expLSB: 3, - }, - { - name: "large_random_2", - input: "11111111111111111111111111111111111111111111111111111111111111111111", - expMSB: 222, - expLSB: 0, - }, - { - name: "large_random_3", - input: "99999999999999999999999999999999999999999999999999999999999999999999", - expMSB: 225, - expLSB: 0, - }, - { - name: "large_prime_like", - input: "57896044618658097711785492504343953926634992332820282019728792003956564819973", - expMSB: 255, - expLSB: 0, - }, - { - name: "fibonacci_large", - input: "354224848179261915075", - expMSB: 68, - expLSB: 0, - }, - { - name: "near_max_even", - input: "115792089237316195423570985008687907853269984665640564039457584007913129639934", - expMSB: 255, - expLSB: 1, - }, - { - name: "half_max", - input: "57896044618658097711785492504343953926634992332820282019728792003956564819967", - expMSB: 254, - expLSB: 0, - }, - { - name: "quarter_max", - input: "28948022309329048855892746252171976963317496166410141009864396001978282409983", - expMSB: 253, - expLSB: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - x := u256.MustFromDecimal(tc.input) - - msb := BitMathMostSignificantBit(x) - lsb := BitMathLeastSignificantBit(x) - - uassert.Equal(t, msb, tc.expMSB) - uassert.Equal(t, lsb, tc.expLSB) - - // Verify MSB >= LSB - if msb < lsb { - t.Errorf("Invalid: MSB(%d) < LSB(%d)", msb, lsb) - } - }) - } -} - -func TestBitMath_ActualValues(t *testing.T) { - testValues := []struct { - name string - input string - }{ - {"2^200+2^20", "1606938044258990275541962092341162602522202993782792835302425600"}, - {"large1", "98765432109876543210987654321098765432109876543210987654321098765432"}, - {"large2", "11111111111111111111111111111111111111111111111111111111111111111111"}, - {"large3", "99999999999999999999999999999999999999999999999999999999999999999999"}, - {"half_max", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - {"quarter_max", "28948022309329048855892746252171976963317496166410141009864396001978282409983"}, - } - - for _, tc := range testValues { - t.Run(tc.name, func(t *testing.T) { - x := u256.MustFromDecimal(tc.input) - msb := BitMathMostSignificantBit(x) - lsb := BitMathLeastSignificantBit(x) - - t.Logf("%s: MSB=%d, LSB=%d", tc.name, msb, lsb) - - temp := x.Clone() - actualLSB := uint8(0) - for i := uint8(0); i < 255; i++ { - if new(u256.Uint).And(temp, u256.One()).Eq(u256.One()) { - actualLSB = i - break - } - temp.Rsh(temp, 1) - } - - if actualLSB != lsb { - t.Errorf("Calculated LSB %d doesn't match actual %d", lsb, actualLSB) - } - }) - } -} - -func TestBitMath_VerifyCalculations(t *testing.T) { - verifyTests := []struct { - name string - input string - binaryForm string - expectedMSB uint8 - expectedLSB uint8 - }{ - { - name: "complex_number_with_msb_209", - input: "1606938044258990275541962092341162602522202993782792835302425600", - binaryForm: "complex number", - expectedMSB: 209, - expectedLSB: 10, - }, - { - name: "verify_12", - input: "12", - binaryForm: "0b1100", - expectedMSB: 3, - expectedLSB: 2, - }, - { - name: "verify_96", - input: "96", - binaryForm: "0b1100000", - expectedMSB: 6, - expectedLSB: 5, - }, - } - - for _, tc := range verifyTests { - t.Run(tc.name, func(t *testing.T) { - x := u256.MustFromDecimal(tc.input) - - msb := BitMathMostSignificantBit(x) - lsb := BitMathLeastSignificantBit(x) - - if msb != tc.expectedMSB { - t.Errorf("MSB for %s: got %d, expected %d", tc.name, msb, tc.expectedMSB) - } - if lsb != tc.expectedLSB { - t.Errorf("LSB for %s: got %d, expected %d", tc.name, lsb, tc.expectedLSB) - } - }) - } -} - -func TestBitMath_ShiftTableCompleteness(t *testing.T) { - // Verify msbShifts covers all necessary ranges - t.Run("msb_shifts_coverage", func(t *testing.T) { - expectedShifts := []uint{128, 64, 32, 16, 8, 4, 2, 1} - - if len(msbShifts) != len(expectedShifts) { - t.Errorf("msbShifts has %d entries, expected %d", len(msbShifts), len(expectedShifts)) - } - - for i, expected := range expectedShifts { - if i < len(msbShifts) && msbShifts[i].shift != expected { - t.Errorf("msbShifts[%d].shift = %d, expected %d", i, msbShifts[i].shift, expected) - } - } - - // Verify bit patterns are correct powers of 2 - for i, s := range msbShifts { - // Calculate expected value: 2^shift - expected := new(u256.Uint).Lsh(u256.One(), s.shift) - if !s.bitPattern.Eq(expected) { - t.Errorf("msbShifts[%d].bitPattern incorrect for shift %d", i, s.shift) - } - } - }) - - // Verify lsbShifts covers all necessary ranges - t.Run("lsb_shifts_coverage", func(t *testing.T) { - expectedShifts := []uint{128, 64, 32, 16, 8, 4, 2, 1} - - if len(lsbShifts) != len(expectedShifts) { - t.Errorf("lsbShifts has %d entries, expected %d", len(lsbShifts), len(expectedShifts)) - } - - for i, expected := range expectedShifts { - if i < len(lsbShifts) && lsbShifts[i].shift != expected { - t.Errorf("lsbShifts[%d].shift = %d, expected %d", i, lsbShifts[i].shift, expected) - } - } - - // Verify bit patterns are correct (2^shift - 1) - for i, s := range lsbShifts { - // Calculate expected value: 2^shift - 1 - powerOfTwo := new(u256.Uint).Lsh(u256.One(), s.shift) - expected := new(u256.Uint).Sub(powerOfTwo, u256.One()) - - if !s.bitPattern.Eq(expected) { - t.Errorf("lsbShifts[%d].bitPattern incorrect for shift %d", i, s.shift) - } - } - }) -} - -// TestBitMathInputPreservation verifies that bit math functions do not mutate their input. -func TestBitMathInputPreservation(t *testing.T) { - cases := []struct { - name string - val string - }{ - {"zero_plus_one", "1"}, - {"small", "12345"}, - {"power_of_two_small", "256"}, // 2^8 - {"power_of_two_medium", "4294967296"}, // 2^32 - {"power_of_two_large", "340282366920938463463374607431768211456"}, // 2^128 - {"max_uint256", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, // 2^256 - 1 - {"random_large", "98765432109876543210987654321098765432109876543210987654321098765432"}, - {"all_bits_set_lower", "4294967295"}, // 0xFFFFFFFF - {"alternating_bits", "12297829382473034410"}, // 0xAAAAAAAAAAAAAAAA - } - - for _, tc := range cases { - t.Run(tc.name+"/MSB", func(t *testing.T) { - x := u256.MustFromDecimal(tc.val) - before := x.Clone() - _ = BitMathMostSignificantBit(x) - - // Verify input was not mutated - if !x.Eq(before) { - t.Errorf("MSB mutated input: before=%s, after=%s", - before.ToString(), x.ToString()) - } - }) - - t.Run(tc.name+"/LSB", func(t *testing.T) { - x := u256.MustFromDecimal(tc.val) - before := x.Clone() - _ = BitMathLeastSignificantBit(x) - - // Verify input was not mutated - if !x.Eq(before) { - t.Errorf("LSB mutated input: before=%s, after=%s", - before.ToString(), x.ToString()) - } - }) - } -} - -// TestBitMathIdempotency ensures that multiple calls with the same input produce consistent results -func TestBitMathIdempotency(t *testing.T) { - values := []string{ - "42", - "65535", - "340282366920938463463374607431768211456", - "98765432109876543210987654321098765432109876543210987654321098765432", - } - - for _, v := range values { - x := u256.MustFromDecimal(v) - - // Call MSB multiple times and verify consistency - msb1 := BitMathMostSignificantBit(x) - msb2 := BitMathMostSignificantBit(x) - msb3 := BitMathMostSignificantBit(x) - - if msb1 != msb2 || msb2 != msb3 { - t.Errorf("MSB inconsistent for %s: %d, %d, %d", v, msb1, msb2, msb3) - } - - // Call LSB multiple times and verify consistency - lsb1 := BitMathLeastSignificantBit(x) - lsb2 := BitMathLeastSignificantBit(x) - lsb3 := BitMathLeastSignificantBit(x) - - if lsb1 != lsb2 || lsb2 != lsb3 { - t.Errorf("LSB inconsistent for %s: %d, %d, %d", v, lsb1, lsb2, lsb3) - } - - // Final verification that input remains unchanged after multiple calls - if !x.Eq(u256.MustFromDecimal(v)) { - t.Errorf("Value mutated after multiple calls: %s", v) - } - } -} - -// TestBitMathMemoryReuse specifically tests temp.Clone() is used instead of mutating the input parameter. -func TestBitMathMemoryReuse(t *testing.T) { - // Test with 2^128 - a value that triggers multiple shift operations - v := "340282366920938463463374607431768211456" - - t.Run("MSB_no_reallocation", func(t *testing.T) { - x := u256.MustFromDecimal(v) - - // Store the original pointer value (*Uint itself, not address of x) - originalPtr := x - - // Execute the function that previously had mutation issues - msb := BitMathMostSignificantBit(x) - - // Verify the pointer hasn't changed (no reallocation) - if x != originalPtr { - t.Error("Pointer value changed - unexpected reallocation occurred") - } - - // Verify the value hasn't been mutated - if !x.Eq(u256.MustFromDecimal(v)) { - t.Error("Value was mutated") - } - - // Verify the result is correct - if msb != 128 { - t.Errorf("Incorrect MSB: got %d, expected 128", msb) - } - }) - - t.Run("LSB_no_reallocation", func(t *testing.T) { - x := u256.MustFromDecimal(v) - - // Store the original pointer value (*Uint itself, not address of x) - originalPtr := x - - // Execute the function - lsb := BitMathLeastSignificantBit(x) - - // Verify the pointer hasn't changed (no reallocation) - if x != originalPtr { - t.Error("Pointer value changed - unexpected reallocation occurred") - } - - // Verify the value hasn't been mutated - if !x.Eq(u256.MustFromDecimal(v)) { - t.Error("Value was mutated") - } - - // Verify the result is correct (2^128 has LSB at position 128) - if lsb != 128 { - t.Errorf("Incorrect LSB: got %d, expected 128", lsb) - } - }) -} - -func TestBitMath_SpecialPatterns(t *testing.T) { - tests := []struct { - name string - input string - expMSB uint8 - expLSB uint8 - desc string - }{ - { - name: "all_even_bits", - input: "6148914691236517205", // 0x5555555555555555 - expMSB: 62, - expLSB: 0, - desc: "alternating 01 pattern", - }, - { - name: "all_odd_bits", - input: "12297829382473034410", // 0xAAAAAAAAAAAAAAAA - expMSB: 63, - expLSB: 1, - desc: "alternating 10 pattern", - }, - { - name: "one_bit_per_byte", - input: "72340172838076673", // 0x0101010101010101 - expMSB: 56, - expLSB: 0, - desc: "one bit set per byte", - }, - { - name: "high_byte_only", - input: "18374686479671623680", // 0xFF00000000000000 - expMSB: 63, - expLSB: 56, - desc: "only highest byte set", - }, - { - name: "mersenne_127", - input: "170141183460469231731687303715884105727", // 2^127 - 1 - expMSB: 126, - expLSB: 0, - desc: "Mersenne prime 2^127-1", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := u256.MustFromDecimal(tc.input) - - msb := BitMathMostSignificantBit(x) - lsb := BitMathLeastSignificantBit(x) - - uassert.Equal(t, msb, tc.expMSB) - uassert.Equal(t, lsb, tc.expLSB) - }) - } -} - -// Benchmark-style exhaustive test for small ranges -func TestBitMath_ExhaustiveSmallRange(t *testing.T) { - // Test first 1000 numbers exhaustively - for i := uint64(1); i <= 1000; i++ { - x := u256.NewUint(i) - msb := BitMathMostSignificantBit(x) - lsb := BitMathLeastSignificantBit(x) - - // Manual calculation for verification - expectedMSB := uint8(0) - // Start from bit 63 and work down to find the highest set bit - for bit := 63; bit >= 0; bit-- { - if i&(1<= liquidityShifted - if denominator.Gte(liquidityShifted) { - return u256.MulDivRoundingUp(liquidityShifted, currentSqrtPriceX96, denominator) - } - } - - // fallback: liquidityShifted / ((liquidityShifted / sqrtPrice) + amount) - divValue := u256.Zero().Div(liquidityShifted, currentSqrtPriceX96) - denominator := u256.Zero().Add(divValue, amountToAdd) - return u256.DivRoundingUp(liquidityShifted, denominator) -} - -// getNextPriceAmount0Remove calculates the next sqrt price when removing token0 liquidity, -// rounding up to ensure conservative pricing for the protocol. -// This internal function handles the case where token0 is being removed from the pool. -// Panics if validation checks fail (invalid pool sqrt price calculation). -func getNextPriceAmount0Remove( - currentSqrtPriceX96, liquidity, amountToRemove *u256.Uint, -) *u256.Uint { - // liquidityShifted = liquidity << 96 - liquidityShifted := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) - // amountTimesSqrtPrice = amountToRemove * currentSqrtPriceX96 - amountTimesSqrtPrice := u256.Zero().Mul(amountToRemove, currentSqrtPriceX96) - - // Validation checks - quotientCheck := u256.Zero().Div(amountTimesSqrtPrice, amountToRemove) - if !quotientCheck.Eq(currentSqrtPriceX96) || !liquidityShifted.Gt(amountTimesSqrtPrice) { - panic(errInvalidPoolSqrtPrice) - } - - denominator := u256.Zero().Sub(liquidityShifted, amountTimesSqrtPrice) - return u256.MulDivRoundingUp(liquidityShifted, currentSqrtPriceX96, denominator) -} - -// getNextSqrtPriceFromAmount0RoundingUp calculates the next sqrt price based on token0 amount, -// always rounding up to ensure conservative pricing in both exact output and exact input cases. -// The add parameter determines whether liquidity is being added (true) or removed (false). -func getNextSqrtPriceFromAmount0RoundingUp( - sqrtPX96 *u256.Uint, - liquidity *u256.Uint, - amount *u256.Uint, - add bool, -) *u256.Uint { - // Shortcut: if no amount, return original price - if amount.IsZero() { - return sqrtPX96 - } - - if add { - return getNextPriceAmount0Add(sqrtPX96, liquidity, amount) - } - return getNextPriceAmount0Remove(sqrtPX96, liquidity, amount) -} - -// getNextPriceAmount1Add calculates the next sqrt price when adding token1, -// preserving rounding-down logic for the final result. -// This internal function handles the case where token1 is being added to the pool. -func getNextPriceAmount1Add( - sqrtPX96, liquidity, amount *u256.Uint, -) *u256.Uint { - var quotient *u256.Uint - - if amount.Lte(max160) { - // Use local variables to avoid allocation conflicts - shifted := u256.Zero().Lsh(amount, Q96_RESOLUTION) - quotient = u256.Zero().Div(shifted, liquidity) - } else { - quotient = u256.MulDiv(amount, q96, liquidity) - } - - return u256.Zero().Add(sqrtPX96, quotient) -} - -// getNextPriceAmount1Remove calculates the next sqrt price when removing token1, -// preserving rounding-down logic for the final result. -// This internal function handles the case where token1 is being removed from the pool. -// Panics if sqrt price would exceed quotient. -func getNextPriceAmount1Remove( - sqrtPX96, liquidity, amount *u256.Uint, -) *u256.Uint { - var quotient *u256.Uint - - if amount.Lte(max160) { - shifted := u256.Zero().Lsh(amount, Q96_RESOLUTION) - quotient = u256.DivRoundingUp(shifted, liquidity) - } else { - quotient = u256.MulDivRoundingUp(amount, q96, liquidity) - } - - if !sqrtPX96.Gt(quotient) { - panic(errSqrtPriceExceedsQuotient) - } - - return u256.Zero().Sub(sqrtPX96, quotient) -} - -// getNextSqrtPriceFromAmount1RoundingDown calculates the next sqrt price based on token1 amount, -// always rounding down to ensure conservative pricing in both exact output and exact input cases. -// The add parameter determines whether liquidity is being added (true) or removed (false). -func getNextSqrtPriceFromAmount1RoundingDown( - sqrtPX96, - liquidity, - amount *u256.Uint, - add bool, -) *u256.Uint { - // Shortcut: if no amount, return original price - if amount.IsZero() { - return sqrtPX96 - } - - if add { - return getNextPriceAmount1Add(sqrtPX96, liquidity, amount) - } - return getNextPriceAmount1Remove(sqrtPX96, liquidity, amount) -} - -// getNextSqrtPriceFromInput calculates the next sqrt price after adding tokens to the pool, -// rounding up for conservative pricing in both swap directions. -// The zeroForOne parameter indicates swap direction (token0 for token1 when true). -// Panics if sqrtPX96 or liquidity is zero. -func getNextSqrtPriceFromInput( - sqrtPX96, liquidity, amountIn *u256.Uint, - zeroForOne bool, -) *u256.Uint { - if sqrtPX96.IsZero() { - panic(errSqrtPriceZero) - } - - if liquidity.IsZero() { - panic(errLiquidityZero) - } - - if zeroForOne { - return getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true) - } - - return getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true) -} - -// getNextSqrtPriceFromOutput calculates the next sqrt price after removing tokens from the pool, -// using different rounding directions based on swap direction. -// The zeroForOne parameter indicates swap direction (token0 for token1 when true). -// Panics if sqrtPX96 or liquidity is zero. -func getNextSqrtPriceFromOutput( - sqrtPX96, liquidity, amountOut *u256.Uint, - zeroForOne bool, -) *u256.Uint { - if sqrtPX96.IsZero() { - panic(errSqrtPriceZero) - } - - if liquidity.IsZero() { - panic(errLiquidityZero) - } - - if zeroForOne { - return getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false) - } - - return getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false) -} - -// getAmount0DeltaHelper calculates the absolute token0 amount difference between two price ranges, -// automatically swapping inputs to ensure correct ordering. The roundUp parameter controls -// rounding direction for the final result to ensure conservative AMM calculations. -// Panics if sqrtRatioAX96 is zero. -func getAmount0DeltaHelper( - sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint, - roundUp bool, -) *u256.Uint { - if sqrtRatioAX96.Gt(sqrtRatioBX96) { - sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96 - } - - // Use local variables for thread safety - numerator := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) - difference := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) - - if sqrtRatioAX96.IsZero() { - panic(errSqrtRatioAX96Zero) - } - - if roundUp { - intermediate := u256.MulDivRoundingUp(numerator, difference, sqrtRatioBX96) - return u256.DivRoundingUp(intermediate, sqrtRatioAX96) - } - - intermediate := u256.MulDiv(numerator, difference, sqrtRatioBX96) - return u256.Zero().Div(intermediate, sqrtRatioAX96) -} - -// getAmount1DeltaHelper calculates the absolute token1 amount difference between two price ranges, -// automatically swapping inputs to ensure correct ordering. The roundUp parameter controls -// rounding direction for the final result to ensure conservative AMM calculations. -func getAmount1DeltaHelper( - sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint, - roundUp bool, -) *u256.Uint { - if sqrtRatioAX96.Gt(sqrtRatioBX96) { - sqrtRatioAX96, sqrtRatioBX96 = sqrtRatioBX96, sqrtRatioAX96 - } - - // amount1 = liquidity * (sqrtB - sqrtA) / 2^96 - // Use local variable for thread safety - difference := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) - - if roundUp { - return u256.MulDivRoundingUp(liquidity, difference, q96) - } - - return u256.MulDiv(liquidity, difference, q96) -} - -// GetAmount0Delta calculates the token0 amount difference within a price range, returning -// a signed int256 value that is negative when liquidity is negative. Rounds down for -// negative liquidity and up for positive liquidity. -// Panics if any input is nil or if the result overflows int256. -func GetAmount0Delta( - sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, - liquidity *i256.Int, -) *i256.Int { - if sqrtRatioAX96 == nil || sqrtRatioBX96 == nil || liquidity == nil { - panic(errGetAmount0DeltaNilInput) - } - - if liquidity.IsNeg() { - u := getAmount0DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), false) - if u.Gt(maxInt256) { - // if u > (2**255 - 1), cannot cast to int256 - panic(errAmount0DeltaOverflow) - } - - // Convert to i256 and negate properly - return i256.Zero().Neg(i256.FromUint256(u)) - } - - u := getAmount0DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), true) - if u.Gt(maxInt256) { - // if u > (2**255 - 1), cannot cast to int256 - panic(errAmount0DeltaOverflow) - } - - return i256.FromUint256(u) -} - -// GetAmount1Delta calculates the token1 amount difference within a price range, returning -// a signed int256 value that is negative when liquidity is negative. Rounds down for -// negative liquidity and up for positive liquidity. -// Panics if any input is nil or if the result overflows int256. -func GetAmount1Delta( - sqrtRatioAX96, sqrtRatioBX96 *u256.Uint, - liquidity *i256.Int, -) *i256.Int { - if sqrtRatioAX96 == nil || sqrtRatioBX96 == nil || liquidity == nil { - panic(errGetAmount1DeltaNilInput) - } - - if liquidity.IsNeg() { - u := getAmount1DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), false) - if u.Gt(maxInt256) { - // if u > (2**255 - 1), cannot cast to int256 - panic(errAmount1DeltaOverflow) - } - - // Convert to i256 and negate properly - return i256.Zero().Neg(i256.FromUint256(u)) - } - - u := getAmount1DeltaHelper(sqrtRatioAX96, sqrtRatioBX96, liquidity.Abs(), true) - if u.Gt(maxInt256) { - // if u > (2**255 - 1), cannot cast to int256 - panic(errAmount1DeltaOverflow) - } - - return i256.FromUint256(u) -} diff --git a/contract/p/gnoswap/gnsmath/sqrt_price_math_test.gno b/contract/p/gnoswap/gnsmath/sqrt_price_math_test.gno deleted file mode 100644 index e23f988..0000000 --- a/contract/p/gnoswap/gnsmath/sqrt_price_math_test.gno +++ /dev/null @@ -1,1454 +0,0 @@ -package gnsmath - -import ( - "testing" - - "gno.land/p/nt/uassert" - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -// Test data constants -var ( - Q96 = u256.MustFromDecimal("79228162514264337593543950336") // 2^96 - MAX_UINT256 = u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") - MAX_UINT160 = u256.MustFromDecimal("1461446703485210103287273052203988822378723970342") -) - -// TestGetNextPriceAmount0Add tests the internal helper for adding token0 -func TestGetNextPriceAmount0Add(t *testing.T) { - tests := []struct { - name string - currentSqrtPrice *u256.Uint - liquidity *u256.Uint - amountToAdd *u256.Uint - expected *u256.Uint - }{ - // Normal cases - { - name: "simple_case: price halves", - currentSqrtPrice: encodePriceSqrt("1", "1"), // √(1/1) * 2^96 - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountToAdd: u256.MustFromDecimal("1000000000000000000"), - expected: new(u256.Uint).Rsh(encodePriceSqrt("1", "1"), 1), // ÷2 - }, - { - name: "normal_addition", - currentSqrtPrice: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountToAdd: u256.MustFromDecimal("100000000000000000"), - expected: u256.MustFromDecimal("72025602285694852357767227579"), - }, - { - name: "small_amount_high_liquidity", - currentSqrtPrice: Q96, - liquidity: u256.MustFromDecimal("100000000000000000000"), - amountToAdd: u256.One(), - expected: u256.MustFromDecimal("79228162514264337592751668711"), - }, - { - name: "overflow_path_fallback", - currentSqrtPrice: encodePriceSqrt("1", "1"), - liquidity: u256.One(), - amountToAdd: encodePriceSqrt("1", "1"), - expected: u256.One(), - }, - { - name: "one_tick_no_move_when_amount_too_small", - currentSqrtPrice: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(2)), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountToAdd: u256.One(), - expected: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(2)), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := getNextPriceAmount0Add(tt.currentSqrtPrice, tt.liquidity, tt.amountToAdd) - uassert.Equal(t, tt.expected.ToString(), result.ToString()) - }) - } -} - -// TestGetNextPriceAmount0Remove tests the internal helper for removing token0 -func TestGetNextPriceAmount0Remove(t *testing.T) { - tiny := u256.One() - medium := u256.MustFromDecimal("100000000000000000") - huge := u256.MustFromDecimal("1000000000000000000000000000000") - - tests := []struct { - name string - current *u256.Uint - liquidity *u256.Uint - amountToRemove *u256.Uint - shouldPanic bool - panicMsg string - validate func(t *testing.T, before, after *u256.Uint) - }{ - { - name: "tiny_removal_monotonic", - current: Q96, - liquidity: u256.MustFromDecimal("100000000000000000000"), - amountToRemove: tiny, - validate: func(t *testing.T, before, after *u256.Uint) { - uassert.True(t, after.Gt(before), "tiny removal must increase price") - }, - }, - { - name: "medium_removal_larger_increase", - current: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountToRemove: medium, - validate: func(t *testing.T, before, after *u256.Uint) { - uassert.True(t, after.Gt(before), "medium removal must increase price") - diff := new(u256.Uint).Sub(after, before) - uassert.True(t, diff.Gte(u256.One()), "price increase ≥ 1 tick") - }, - }, - { - name: "remove_at_min_boundary", - current: MIN_SQRT_RATIO, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountToRemove: tiny, - validate: func(t *testing.T, before, after *u256.Uint) { - uassert.True(t, after.Gt(before), "removal at MIN should increase price") - }, - }, - { - name: "remove_at_high_price", - current: u256.MustFromDecimal("1000000000000000000000000000000"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountToRemove: tiny, - validate: func(t *testing.T, before, after *u256.Uint) { - uassert.True(t, after.Gt(before), "removal should increase price") - }, - }, - { - name: "remove_near_max_with_huge_liquidity", - current: new(u256.Uint).Sub(MAX_SQRT_RATIO, u256.NewUint(1000000)), - liquidity: u256.MustFromDecimal("100000000000000000000000000"), - amountToRemove: tiny, - validate: func(t *testing.T, before, after *u256.Uint) { - uassert.True(t, after.Gt(before), "removal should increase price") - }, - }, - { - name: "overflow_check_fail", - current: Q96, - liquidity: u256.MustFromDecimal("1"), - amountToRemove: MAX_UINT256, - shouldPanic: true, - panicMsg: errInvalidPoolSqrtPrice.Error(), - }, - { - name: "insufficient_liquidity", - current: Q96, - liquidity: u256.MustFromDecimal("1000"), - amountToRemove: u256.MustFromDecimal("100000000000000000000"), - shouldPanic: true, - panicMsg: errInvalidPoolSqrtPrice.Error(), - }, - { - name: "overflow_path_large_remove_panics", - current: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountToRemove: huge, - shouldPanic: true, - panicMsg: errInvalidPoolSqrtPrice.Error(), - }, - { - name: "remove_at_max_boundary_insufficient_liquidity", - current: new(u256.Uint).Sub(MAX_SQRT_RATIO, u256.One()), // MAX - 1 - liquidity: u256.MustFromDecimal("1000000000000000000000"), - amountToRemove: u256.MustFromDecimal("1000"), - shouldPanic: true, - panicMsg: errInvalidPoolSqrtPrice.Error(), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - before := tt.current.Clone() - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - _ = getNextPriceAmount0Remove(tt.current, tt.liquidity, tt.amountToRemove) - }) - return - } - after := getNextPriceAmount0Remove(tt.current, tt.liquidity, tt.amountToRemove) - tt.validate(t, before, after) - }) - } -} - -// TestGetNextSqrtPriceFromAmount0RoundingUp tests price calculation when adding/removing token0 -func TestGetNextSqrtPriceFromAmount0RoundingUp(t *testing.T) { - tests := []struct { - name string - sqrtPX96 *u256.Uint - liquidity *u256.Uint - amount *u256.Uint - add bool - expected *u256.Uint - shouldPanic bool - panicMsg string - }{ - // Basic functionality - { - name: "zero_amount_returns_same_price", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("2000000"), - amount: u256.Zero(), - add: true, - expected: Q96, - }, - { - name: "add_token0_decreases_price", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("100000000000000000"), - add: true, - expected: u256.MustFromDecimal("72025602285694852357767227579"), - }, - { - name: "remove_token0_increases_price", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("100000000000000000"), - add: false, - expected: u256.MustFromDecimal("88031291682515930659493278152"), - }, - // Boundary cases - { - name: "add_near_min_clamps", - sqrtPX96: u256.MustFromDecimal("4295128741"), - liquidity: u256.MustFromDecimal("1000"), - amount: u256.MustFromDecimal("1000000000000000000000"), - add: true, - expected: u256.MustFromDecimal("4074254652"), - }, - { - name: "min_boundary_stays_same", - sqrtPX96: u256.MustFromDecimal("4295128740"), - liquidity: u256.MustFromDecimal("1000"), - amount: u256.MustFromDecimal("1000000"), - add: true, - expected: u256.MustFromDecimal("4295128740"), - }, - { - name: "min_boundary_no_change_tiny_amount", - sqrtPX96: u256.MustFromDecimal("4295128740"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("1"), - add: true, - expected: u256.MustFromDecimal("4295128740"), - }, - { - name: "increase_price_from_min", - sqrtPX96: u256.MustFromDecimal("4295128739"), - liquidity: u256.MustFromDecimal("1000"), - amount: u256.MustFromDecimal("1"), - add: false, - expected: u256.MustFromDecimal("4295128740"), - }, - // Error cases - { - name: "remove_causes_overflow", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1"), - amount: MAX_UINT256, - add: false, - shouldPanic: true, - panicMsg: errInvalidPoolSqrtPrice.Error(), - }, - { - name: "remove_insufficient_liquidity", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000"), - amount: u256.MustFromDecimal("100000000000000000000"), - add: false, - shouldPanic: true, - panicMsg: errInvalidPoolSqrtPrice.Error(), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - getNextSqrtPriceFromAmount0RoundingUp(tt.sqrtPX96, tt.liquidity, tt.amount, tt.add) - }) - return - } - - result := getNextSqrtPriceFromAmount0RoundingUp(tt.sqrtPX96, tt.liquidity, tt.amount, tt.add) - uassert.Equal(t, tt.expected.ToString(), result.ToString()) - }) - } -} - -// TestGetNextPriceAmount1Add tests the internal helper for adding token1 -func TestGetNextPriceAmount1Add(t *testing.T) { - tests := []struct { - name string - sqrtPX96 *u256.Uint - liquidity *u256.Uint - amount *u256.Uint - expected *u256.Uint - shouldPanic bool - panicMsg string - }{ - { - name: "normal_addition", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("100000000000000000"), - expected: u256.MustFromDecimal("87150978765690771352898345369"), - }, - { - name: "amount_lte_max160", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("1000000000000000000"), - expected: u256.MustFromDecimal("158456325028528675187087900672"), - }, - { - name: "amount_gt_max160", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: new(u256.Uint).Add(MAX_UINT160, u256.MustFromDecimal("1000000000000000000")), - expected: u256.MustFromDecimal("115787736929662111563370814583278000176356624084966981749664"), - }, - { - name: "very_small_liquidity", - sqrtPX96: Q96, - liquidity: u256.One(), - amount: u256.MustFromDecimal("1000"), - expected: u256.MustFromDecimal("79307390676778601931137494286336"), - }, - { - name: "add_at_max_boundary", - sqrtPX96: MAX_SQRT_RATIO, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("1000"), - expected: u256.MustFromDecimal("1461446703485210103287273052203988901606886484606"), - }, - { - name: "add_near_max_increases_price", - sqrtPX96: new(u256.Uint).Sub(MAX_SQRT_RATIO, u256.MustFromDecimal("1000000000000")), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("100000000000000000"), - expected: u256.MustFromDecimal("1461446703485210103295195868455415255138078365375"), - }, - { - name: "add_at_min_increases_price", - sqrtPX96: MIN_SQRT_RATIO, - liquidity: u256.MustFromDecimal("1000000"), - amount: u256.MustFromDecimal("100"), - expected: u256.MustFromDecimal("7922816251426438054483134"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - getNextPriceAmount1Add(tt.sqrtPX96, tt.liquidity, tt.amount) - }) - return - } - - result := getNextPriceAmount1Add(tt.sqrtPX96, tt.liquidity, tt.amount) - uassert.Equal(t, tt.expected.ToString(), result.ToString()) - }) - } -} - -// TestGetNextPriceAmount1Remove tests the internal helper for removing token1 -func TestGetNextPriceAmount1Remove(t *testing.T) { - tests := []struct { - name string - sqrtPX96 *u256.Uint - liquidity *u256.Uint - amount *u256.Uint - expected *u256.Uint - shouldPanic bool - panicMsg string - }{ - { - name: "normal_removal_small", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("100000000000000000"), // 0.1 - expected: u256.MustFromDecimal("71305346262837903834189555302"), - }, - { - name: "normal_removal_medium", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("500000000000000000"), // 0.5 - expected: u256.MustFromDecimal("39614081257132168796771975168"), - }, - - { - name: "zero_amount_no_change", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.Zero(), - expected: Q96, - }, - { - name: "tiny_amount_tiny_change", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("100000000000000000000"), - amount: u256.One(), - expected: u256.MustFromDecimal("79228162514264337592751668710"), - }, - { - name: "amount_lte_max160_path_small", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("10000000000000000000"), - amount: u256.MustFromDecimal("1000000000000000000"), - expected: u256.MustFromDecimal("71305346262837903834189555302"), - }, - { - name: "medium_amount_normal_path", - sqrtPX96: new(u256.Uint).Mul(Q96, u256.NewUint(2)), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("10000000000000000"), - expected: u256.MustFromDecimal("157664043403386031811152461168"), - }, - { - name: "very_small_liquidity", - sqrtPX96: Q96, - liquidity: u256.One(), - amount: u256.MustFromDecimal("1"), - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - { - name: "very_large_liquidity", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000000000000"), - amount: u256.MustFromDecimal("1000000000000000000"), - expected: u256.MustFromDecimal("79228162435036175079279612742"), - }, - { - name: "high_price_removal", - sqrtPX96: u256.MustFromDecimal("1000000000000000000000000000000"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("1000"), - expected: u256.MustFromDecimal("999999999999999920771837485735"), - }, - { - name: "insufficient_liquidity_panic", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000"), - amount: u256.MustFromDecimal("1000000000000000000"), - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - { - name: "price_below_min_panic", - sqrtPX96: MIN_SQRT_RATIO, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.One(), - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - { - name: "rounding_up_verification", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("999999999999999999"), - amount: u256.MustFromDecimal("333333333333333333"), - expected: u256.MustFromDecimal("52818775009509558395695966890"), - }, - { - name: "quotient_exactly_sqrtPX96_minus_1", - sqrtPX96: u256.MustFromDecimal("79228162514264337593543950336"), - liquidity: u256.MustFromDecimal("79228162514264337593543950336"), - amount: u256.MustFromDecimal("79228162514264337593543950335"), - expected: u256.One(), - }, - { - name: "quotient_exactly_equals_sqrtPX96", - sqrtPX96: u256.MustFromDecimal("1000000000000000000"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("1000000000000000000"), - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - { - name: "amount_just_above_max160_boundary", - sqrtPX96: u256.MustFromDecimal("200000000000000000000000000000000000000"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: new(u256.Uint).Add(MAX_UINT160, u256.One()), // MAX_UINT160 + 1 - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - { - name: "exact_division_no_rounding", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("1000000000000000000"), - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - { - name: "quotient_almost_equals_price", - sqrtPX96: u256.MustFromDecimal("1000000000000000000"), - liquidity: u256.MustFromDecimal("1000000"), - amount: u256.MustFromDecimal("12589254117"), - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - { - name: "quotient_exactly_equals_sqrtPX96", - sqrtPX96: u256.MustFromDecimal("1000000000000000000"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("1000000000000000000"), - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - { - name: "near_overflow_large_values", - sqrtPX96: new(u256.Uint).Rsh(MAX_UINT256, 10), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: new(u256.Uint).Rsh(MAX_UINT256, 200), - expected: u256.MustFromDecimal("113078212145816597093331040047546785012958969394330622548958957437722684299"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - getNextPriceAmount1Remove(tt.sqrtPX96, tt.liquidity, tt.amount) - }) - return - } - - result := getNextPriceAmount1Remove(tt.sqrtPX96, tt.liquidity, tt.amount) - uassert.Equal(t, tt.expected.ToString(), result.ToString()) - }) - } -} - -// TestGetNextSqrtPriceFromAmount1RoundingDown tests price calculation when adding/removing token1 -func TestGetNextSqrtPriceFromAmount1RoundingDown(t *testing.T) { - tests := []struct { - name string - sqrtPX96 *u256.Uint - liquidity *u256.Uint - amount *u256.Uint - add bool - expected *u256.Uint - shouldPanic bool - panicMsg string - }{ - { - name: "zero_amount_returns_same_price", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.Zero(), - add: true, - expected: Q96, - }, - { - name: "delegates_to_add_function", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("100000000000000000"), - add: true, - expected: u256.MustFromDecimal("87150978765690771352898345369"), - }, - { - name: "delegates_to_remove_function", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: u256.MustFromDecimal("100000000000000000"), - add: false, - expected: u256.MustFromDecimal("71305346262837903834189555302"), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - getNextSqrtPriceFromAmount1RoundingDown(tt.sqrtPX96, tt.liquidity, tt.amount, tt.add) - }) - return - } - - result := getNextSqrtPriceFromAmount1RoundingDown(tt.sqrtPX96, tt.liquidity, tt.amount, tt.add) - uassert.Equal(t, tt.expected.ToString(), result.ToString()) - }) - } -} - -// TestGetNextSqrtPriceFromInput tests input swap calculations -func TestGetNextSqrtPriceFromInput(t *testing.T) { - tests := []struct { - name string - sqrtPX96 *u256.Uint - liquidity *u256.Uint - amountIn *u256.Uint - zeroForOne bool - expected string - shouldPanic bool - panicMsg string - }{ - { - name: "zero_price_panics", - sqrtPX96: u256.Zero(), - liquidity: u256.One(), - amountIn: u256.One(), - zeroForOne: true, - shouldPanic: true, - panicMsg: errSqrtPriceZero.Error(), - }, - { - name: "zero_liquidity_panics", - sqrtPX96: u256.One(), - liquidity: u256.Zero(), - amountIn: u256.One(), - zeroForOne: true, - shouldPanic: true, - panicMsg: errLiquidityZero.Error(), - }, - { - name: "delegates_to_amount0_function", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountIn: u256.MustFromDecimal("100000000000000000"), - zeroForOne: true, - expected: "72025602285694852357767227579", - }, - { - name: "delegates_to_amount1_function", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountIn: u256.MustFromDecimal("100000000000000000"), - zeroForOne: false, - expected: "87150978765690771352898345369", - }, - { - name: "zero_amount_no_change", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountIn: u256.Zero(), - zeroForOne: true, - expected: Q96.ToString(), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - getNextSqrtPriceFromInput(tt.sqrtPX96, tt.liquidity, tt.amountIn, tt.zeroForOne) - }) - return - } - - result := getNextSqrtPriceFromInput(tt.sqrtPX96, tt.liquidity, tt.amountIn, tt.zeroForOne) - uassert.Equal(t, tt.expected, result.ToString()) - }) - } -} - -// TestGetNextSqrtPriceFromOutput tests output swap calculations -func TestGetNextSqrtPriceFromOutput(t *testing.T) { - tests := []struct { - name string - sqrtPX96 *u256.Uint - liquidity *u256.Uint - amountOut *u256.Uint - zeroForOne bool - expected string - shouldPanic bool - panicMsg string - }{ - { - name: "zero_price_panics", - sqrtPX96: u256.Zero(), - liquidity: u256.One(), - amountOut: u256.One(), - zeroForOne: true, - shouldPanic: true, - panicMsg: errSqrtPriceZero.Error(), - }, - { - name: "zero_liquidity_panics", - sqrtPX96: u256.One(), - liquidity: u256.Zero(), - amountOut: u256.One(), - zeroForOne: true, - shouldPanic: true, - panicMsg: errLiquidityZero.Error(), - }, - { - name: "delegates_to_amount1_for_zeroForOne", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountOut: u256.MustFromDecimal("100000000000000000"), - zeroForOne: true, - expected: "71305346262837903834189555302", - }, - { - name: "delegates_to_amount0_for_oneForZero", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountOut: u256.MustFromDecimal("100000000000000000"), - zeroForOne: false, - expected: "88031291682515930659493278152", - }, - { - name: "zero_amount_no_change", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountOut: u256.Zero(), - zeroForOne: true, - expected: Q96.ToString(), - }, - { - name: "delegates_panic_from_internal", - sqrtPX96: Q96, - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountOut: u256.MustFromDecimal("1000000000000000000"), - zeroForOne: true, - shouldPanic: true, - panicMsg: errSqrtPriceExceedsQuotient.Error(), - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - getNextSqrtPriceFromOutput(tt.sqrtPX96, tt.liquidity, tt.amountOut, tt.zeroForOne) - }) - return - } - - result := getNextSqrtPriceFromOutput(tt.sqrtPX96, tt.liquidity, tt.amountOut, tt.zeroForOne) - uassert.Equal(t, tt.expected, result.ToString()) - }) - } -} - -// TestGetAmount0DeltaHelper tests amount0 calculation helper -func TestGetAmount0DeltaHelper(t *testing.T) { - tests := []struct { - name string - sqrtRatioAX96 *u256.Uint - sqrtRatioBX96 *u256.Uint - liquidity *u256.Uint - roundUp bool - expected string - shouldPanic bool - panicMsg string - }{ - // Basic cases - { - name: "zero_liquidity", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("2", "1"), - liquidity: u256.Zero(), - roundUp: true, - expected: "0", - }, - { - name: "equal_prices", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("1", "1"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "0", - }, - { - name: "swapped_inputs", - sqrtRatioAX96: encodePriceSqrt("2", "1"), - sqrtRatioBX96: encodePriceSqrt("1", "1"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "292893218813452476", - }, - // Rounding tests - { - name: "round_up_true", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "90909090909090910", - }, - { - name: "round_up_false", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: false, - expected: "90909090909090909", - }, - // Error cases - { - name: "zero_sqrtRatioA", - sqrtRatioAX96: u256.Zero(), - sqrtRatioBX96: u256.MustFromDecimal("1000000"), - liquidity: u256.MustFromDecimal("1000000"), - roundUp: true, - shouldPanic: true, - panicMsg: errSqrtRatioAX96Zero.Error(), - }, - { - name: "zero_sqrtRatioB_gets_swapped", - sqrtRatioAX96: u256.MustFromDecimal("1000000"), - sqrtRatioBX96: u256.Zero(), - liquidity: u256.MustFromDecimal("1000000"), - roundUp: true, - shouldPanic: true, - panicMsg: errSqrtRatioAX96Zero.Error(), - }, - // Extreme values - { - name: "min_price_range", - sqrtRatioAX96: MIN_SQRT_RATIO, - sqrtRatioBX96: u256.MustFromDecimal("4295128740"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "4294644427204583875464618656", - }, - { - name: "max_price_range", - sqrtRatioAX96: u256.MustFromDecimal("1461446703485210103287273052203988822378723970340"), - sqrtRatioBX96: MAX_SQRT_RATIO, - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "1", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - getAmount0DeltaHelper(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity, tt.roundUp) - }) - return - } - - result := getAmount0DeltaHelper(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity, tt.roundUp) - uassert.Equal(t, tt.expected, result.ToString()) - }) - } -} - -// TestGetAmount1DeltaHelper tests amount1 calculation helper -func TestGetAmount1DeltaHelper(t *testing.T) { - tests := []struct { - name string - sqrtRatioAX96 *u256.Uint - sqrtRatioBX96 *u256.Uint - liquidity *u256.Uint - roundUp bool - expected string - }{ - // Basic cases - { - name: "zero_liquidity", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("2", "1"), - liquidity: u256.Zero(), - roundUp: true, - expected: "0", - }, - { - name: "equal_prices", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("1", "1"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "0", - }, - { - name: "swapped_inputs", - sqrtRatioAX96: encodePriceSqrt("2", "1"), - sqrtRatioBX96: encodePriceSqrt("1", "1"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "414213562373095049", - }, - // Rounding tests - { - name: "round_up_true", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "100000000000000000", - }, - { - name: "round_up_false", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: false, - expected: "99999999999999999", - }, - // Extreme values - { - name: "min_price_range", - sqrtRatioAX96: MIN_SQRT_RATIO, - sqrtRatioBX96: u256.MustFromDecimal("4295128740"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: true, - expected: "1", - }, - { - name: "max_price_range", - sqrtRatioAX96: u256.MustFromDecimal("1461446703485210103287273052203988822378723970340"), - sqrtRatioBX96: MAX_SQRT_RATIO, - liquidity: u256.MustFromDecimal("1000000000000000000"), - roundUp: false, - expected: "0", - }, - // Small liquidity, large price difference - { - name: "small_liquidity_large_diff", - sqrtRatioAX96: encodePriceSqrt("1", "100"), - sqrtRatioBX96: encodePriceSqrt("100", "1"), - liquidity: u256.One(), - roundUp: true, - expected: "10", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := getAmount1DeltaHelper(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity, tt.roundUp) - uassert.Equal(t, tt.expected, result.ToString()) - }) - } -} - -// TestGetAmount0Delta tests string representation with signed liquidity -func TestGetAmount0Delta(t *testing.T) { - tests := []struct { - name string - sqrtRatioAX96 *u256.Uint - sqrtRatioBX96 *u256.Uint - liquidity *i256.Int - expected string - shouldPanic bool - panicMsg string - }{ - // Nil checks - { - name: "nil_sqrtRatioA", - sqrtRatioAX96: nil, - sqrtRatioBX96: u256.MustFromDecimal("1000000"), - liquidity: i256.FromUint256(u256.MustFromDecimal("5000000")), - shouldPanic: true, - panicMsg: errGetAmount0DeltaNilInput.Error(), - }, - { - name: "nil_sqrtRatioB", - sqrtRatioAX96: u256.MustFromDecimal("1000000"), - sqrtRatioBX96: nil, - liquidity: i256.FromUint256(u256.MustFromDecimal("5000000")), - shouldPanic: true, - panicMsg: errGetAmount0DeltaNilInput.Error(), - }, - { - name: "nil_liquidity", - sqrtRatioAX96: u256.MustFromDecimal("1000000"), - sqrtRatioBX96: u256.MustFromDecimal("2000000"), - liquidity: nil, - shouldPanic: true, - panicMsg: errGetAmount0DeltaNilInput.Error(), - }, - // Positive liquidity (roundUp = true) - { - name: "positive_liquidity", - sqrtRatioAX96: u256.MustFromDecimal("1000000"), - sqrtRatioBX96: u256.MustFromDecimal("2000000"), - liquidity: i256.FromUint256(u256.MustFromDecimal("5000000")), - expected: "198070406285660843983859875840", - }, - { - name: "positive_liquidity_equal_prices", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("1", "1"), - liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000")), - expected: "0", - }, - // Negative liquidity (roundDown = false) - { - name: "negative_liquidity", - sqrtRatioAX96: u256.MustFromDecimal("1000000"), - sqrtRatioBX96: u256.MustFromDecimal("2000000"), - liquidity: i256.New().Neg(i256.FromUint256(u256.MustFromDecimal("5000000"))), - expected: "-198070406285660843983859875840", - }, - // Zero liquidity - { - name: "zero_liquidity", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("2", "1"), - liquidity: i256.Zero(), - expected: "0", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - GetAmount0Delta(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) - }) - return - } - - result := GetAmount0Delta(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) - uassert.Equal(t, tt.expected, result.ToString()) - }) - } -} - -// TestGetAmount1Delta tests string representation with signed liquidity -func TestGetAmount1Delta(t *testing.T) { - tests := []struct { - name string - sqrtRatioAX96 *u256.Uint - sqrtRatioBX96 *u256.Uint - liquidity *i256.Int - expected string - shouldPanic bool - panicMsg string - }{ - // Nil checks - { - name: "nil_all_parameters", - sqrtRatioAX96: nil, - sqrtRatioBX96: nil, - liquidity: nil, - shouldPanic: true, - panicMsg: errGetAmount1DeltaNilInput.Error(), - }, - // Positive liquidity (roundUp = true) - { - name: "positive_liquidity", - sqrtRatioAX96: u256.MustFromDecimal("1000000"), - sqrtRatioBX96: u256.MustFromDecimal("2000000"), - liquidity: i256.FromUint256(u256.MustFromDecimal("5000000")), - expected: "1", - }, - { - name: "positive_liquidity_large_range", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("4", "1"), - liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000")), - expected: "1000000000000000000", - }, - // Negative liquidity (roundDown = false) - { - name: "negative_liquidity", - sqrtRatioAX96: u256.MustFromDecimal("1000000"), - sqrtRatioBX96: u256.MustFromDecimal("2000000"), - liquidity: i256.New().Neg(i256.FromUint256(u256.MustFromDecimal("5000000"))), - expected: "0", - }, - { - name: "negative_liquidity_large", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("4", "1"), - liquidity: i256.New().Neg(i256.FromUint256(u256.MustFromDecimal("1000000000000000000"))), - expected: "-1000000000000000000", - }, - { - name: "positive_liquidity_large_values", - sqrtRatioAX96: Q96, - sqrtRatioBX96: new(u256.Uint).Mul(Q96, u256.NewUint(2)), - liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000000000000")), - expected: "1000000000000000000000000000", - }, - // Zero liquidity - { - name: "zero_liquidity", - sqrtRatioAX96: encodePriceSqrt("1", "1"), - sqrtRatioBX96: encodePriceSqrt("2", "1"), - liquidity: i256.Zero(), - expected: "0", - }, - // Equal prices - { - name: "equal_prices_positive_liquidity", - sqrtRatioAX96: Q96, - sqrtRatioBX96: Q96, - liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000")), - expected: "0", - }, - // Swapped inputs - { - name: "swapped_prices_positive_liquidity", - sqrtRatioAX96: encodePriceSqrt("4", "1"), - sqrtRatioBX96: encodePriceSqrt("1", "1"), // B < A, will be swapped - liquidity: i256.FromUint256(u256.MustFromDecimal("1000000000000000000")), - expected: "1000000000000000000", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - GetAmount1Delta(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) - }) - return - } - - result := GetAmount1Delta(tt.sqrtRatioAX96, tt.sqrtRatioBX96, tt.liquidity) - - if tt.name == "positive_liquidity_large_values" { - if tt.expected != result.ToString() { - t.Logf("Result for %s: %s", tt.name, result) - } - } - - uassert.Equal(t, tt.expected, result.ToString()) - }) - } -} - -// TestInputParameterImmutability verifies functions don't modify inputs -func TestInputParameterImmutability(t *testing.T) { - tests := []struct { - name string - fn func(*testing.T) - }{ - { - name: "getNextPriceAmount0Add_immutable", - fn: func(t *testing.T) { - sqrtP := Q96 - liquidity := u256.MustFromDecimal("1000000000000000000") - amount := u256.MustFromDecimal("100000000000000000") - - sqrtPCopy := sqrtP.ToString() - liquidityCopy := liquidity.ToString() - amountCopy := amount.ToString() - - _ = getNextPriceAmount0Add(sqrtP, liquidity, amount) - - uassert.Equal(t, sqrtPCopy, sqrtP.ToString()) - uassert.Equal(t, liquidityCopy, liquidity.ToString()) - uassert.Equal(t, amountCopy, amount.ToString()) - }, - }, - { - name: "getAmount0DeltaHelper_parameter_swap_immutable", - fn: func(t *testing.T) { - sqrtA := u256.MustFromDecimal("1000000") - sqrtB := u256.MustFromDecimal("500000") - liquidity := u256.MustFromDecimal("1000000") - - originalA := sqrtA.ToString() - originalB := sqrtB.ToString() - - _ = getAmount0DeltaHelper(sqrtA, sqrtB, liquidity, true) - - uassert.Equal(t, originalA, sqrtA.ToString()) - uassert.Equal(t, originalB, sqrtB.ToString()) - }, - }, - { - name: "all_input_functions_immutable", - fn: func(t *testing.T) { - sqrtP := Q96 - liquidity := u256.MustFromDecimal("1000000000000000000") - amount := u256.MustFromDecimal("1000000") - - original := sqrtP.ToString() - - // Test all input/output functions - _ = getNextSqrtPriceFromInput(sqrtP, liquidity, amount, true) - uassert.Equal(t, original, sqrtP.ToString()) - - _ = getNextSqrtPriceFromInput(sqrtP, liquidity, amount, false) - uassert.Equal(t, original, sqrtP.ToString()) - - _ = getNextSqrtPriceFromOutput(sqrtP, liquidity, amount, true) - uassert.Equal(t, original, sqrtP.ToString()) - - _ = getNextSqrtPriceFromOutput(sqrtP, liquidity, amount, false) - uassert.Equal(t, original, sqrtP.ToString()) - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.fn(t) - }) - } -} - -// TestRoundingConsistency verifies rounding behavior -func TestRoundingConsistency(t *testing.T) { - tests := []struct { - name string - sqrtA *u256.Uint - sqrtB *u256.Uint - liquidity *u256.Uint - isToken0 bool - }{ - { - name: "amount0_minimal_difference", - sqrtA: Q96, - sqrtB: u256.MustFromDecimal("79228162514264337593543950337"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - isToken0: true, - }, - { - name: "amount1_minimal_difference", - sqrtA: Q96, - sqrtB: u256.MustFromDecimal("79228162514264337593543950337"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - isToken0: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var amountRoundUp, amountRoundDown *u256.Uint - - if tt.isToken0 { - amountRoundUp = getAmount0DeltaHelper(tt.sqrtA, tt.sqrtB, tt.liquidity, true) - amountRoundDown = getAmount0DeltaHelper(tt.sqrtA, tt.sqrtB, tt.liquidity, false) - } else { - amountRoundUp = getAmount1DeltaHelper(tt.sqrtA, tt.sqrtB, tt.liquidity, true) - amountRoundDown = getAmount1DeltaHelper(tt.sqrtA, tt.sqrtB, tt.liquidity, false) - } - - // Round up should always be >= round down - uassert.True(t, amountRoundUp.Gte(amountRoundDown), "Round up should be >= round down") - - // Difference should be at most 1 - if !amountRoundUp.IsZero() || !amountRoundDown.IsZero() { - diff := new(u256.Uint).Sub(amountRoundUp, amountRoundDown) - uassert.True(t, diff.Lte(u256.One()), "Rounding difference should be at most 1") - } - }) - } -} - -// TestMathematicalInvariants tests important mathematical properties -func TestMathematicalInvariants(t *testing.T) { - // Test 1: Round trip precision - t.Run("round_trip_precision", func(t *testing.T) { - testCases := []struct { - liquidity string - amount string - maxError string - }{ - {"100000000000000000000", "100000000000000000", "792281625"}, - {"10000000000000000000", "100000000000000000", "7922816251"}, - {"1000000000000000000", "100000000000000000", "79228162513"}, - } - - for _, tc := range testCases { - sqrtP := Q96 - liquidity := u256.MustFromDecimal(tc.liquidity) - amount := u256.MustFromDecimal(tc.amount) - maxError := u256.MustFromDecimal(tc.maxError) - - // Add then remove same amount - priceAfterAdd := getNextSqrtPriceFromInput(sqrtP, liquidity, amount, true) - actualAmount := getAmount0DeltaHelper(priceAfterAdd, sqrtP, liquidity, false) - priceAfterRemove := getNextSqrtPriceFromOutput(priceAfterAdd, liquidity, actualAmount, false) - - // Calculate error - var diff *u256.Uint - if priceAfterRemove.Gt(sqrtP) { - diff = new(u256.Uint).Sub(priceAfterRemove, sqrtP) - } else { - diff = new(u256.Uint).Sub(sqrtP, priceAfterRemove) - } - - if diff.Gt(maxError) { - t.Errorf("Round trip error too large for liquidity %s: %s wei (max: %s)", - tc.liquidity, diff.ToString(), maxError.ToString()) - } - } - }) - - // Test 2: Monotonicity - t.Run("monotonicity", func(t *testing.T) { - sqrtP := Q96 - liquidity := u256.MustFromDecimal("1000000000000000000") - - amounts := []*u256.Uint{ - u256.MustFromDecimal("1000"), - u256.MustFromDecimal("10000"), - u256.MustFromDecimal("100000"), - u256.MustFromDecimal("1000000"), - } - - var prevPrice *u256.Uint = sqrtP - - // Adding more token0 should decrease price more - for i, amount := range amounts { - newPrice := getNextSqrtPriceFromInput(sqrtP, liquidity, amount, true) - - if i > 0 && !newPrice.Lt(prevPrice) { - t.Errorf("Monotonicity violated: larger amount didn't decrease price more") - } - - prevPrice = newPrice - } - }) - - // Test 3: Symmetry - t.Run("symmetry", func(t *testing.T) { - sqrtP := Q96 - liquidity := u256.MustFromDecimal("1000000000000000000") - amount := u256.MustFromDecimal("100000000000000000") - - // Add token0, get amount1 out - priceAfter0 := getNextSqrtPriceFromInput(sqrtP, liquidity, amount, true) - amount1Out := getAmount1DeltaHelper(priceAfter0, sqrtP, liquidity, false) - - // Add token1, get amount0 out - priceAfter1 := getNextSqrtPriceFromInput(sqrtP, liquidity, amount, false) - amount0Out := getAmount0DeltaHelper(sqrtP, priceAfter1, liquidity, false) - - // The product of price ratios should be close to 1 - // Due to rounding, we allow small deviation - t.Logf("Token0 in: %s, Token1 out: %s", amount.ToString(), amount1Out.ToString()) - t.Logf("Token1 in: %s, Token0 out: %s", amount.ToString(), amount0Out.ToString()) - }) -} - -// TestPathIndependence verifies swap order doesn't affect final state -func TestPathIndependence(t *testing.T) { - startPrice := u256.MustFromDecimal("79228162514264337593543950336") - liquidity := u256.MustFromDecimal("1000000000000000000") - - amount0 := u256.MustFromDecimal("10000000000000000") - amount1 := u256.MustFromDecimal("10000000000000000") - - // Path 1: Add token0, then add token1 - price1 := getNextSqrtPriceFromInput(startPrice, liquidity, amount0, true) - price1 = getNextSqrtPriceFromInput(price1, liquidity, amount1, false) - - // Path 2: Add token1, then add token0 - price2 := getNextSqrtPriceFromInput(startPrice, liquidity, amount1, false) - price2 = getNextSqrtPriceFromInput(price2, liquidity, amount0, true) - - // Calculate percentage difference - var diff *u256.Uint - if price1.Gt(price2) { - diff = new(u256.Uint).Sub(price1, price2) - } else { - diff = new(u256.Uint).Sub(price2, price1) - } - - avgPrice := new(u256.Uint).Add(price1, price2) - avgPrice.Div(avgPrice, u256.NewUint(2)) - percentDiff := new(u256.Uint).Mul(diff, u256.MustFromDecimal("10000")) - percentDiff.Div(percentDiff, avgPrice) - - t.Logf("Path 1 final price: %s", price1.ToString()) - t.Logf("Path 2 final price: %s", price2.ToString()) - t.Logf("Percentage difference: %s basis points", percentDiff.ToString()) - - // Should be less than 10 basis points (0.1%) - maxAllowedDiff := u256.NewUint(10) - if percentDiff.Gt(maxAllowedDiff) { - t.Errorf("Path independence error too large: %s bp (max allowed: %s bp)", - percentDiff.ToString(), maxAllowedDiff.ToString()) - } -} - -// TestHelperFunctionConsistency verifies helper functions work together correctly -func TestHelperFunctionConsistency(t *testing.T) { - sqrtP := Q96 - liquidity := u256.MustFromDecimal("1000000000000000000") - amount := u256.MustFromDecimal("100000000000000000") - - // Test token0 add path - t.Run("token0_add_consistency", func(t *testing.T) { - // Direct calculation - directPrice := getNextPriceAmount0Add(sqrtP, liquidity, amount) - - // Through wrapper - wrapperPrice := getNextSqrtPriceFromAmount0RoundingUp(sqrtP, liquidity, amount, true) - - uassert.Equal(t, directPrice.ToString(), wrapperPrice.ToString()) - }) - - // Test token0 remove path - t.Run("token0_remove_consistency", func(t *testing.T) { - // Use smaller amount to avoid insufficient liquidity - smallAmount := u256.MustFromDecimal("1000000") - - // Direct calculation - directPrice := getNextPriceAmount0Remove(sqrtP, liquidity, smallAmount) - - // Through wrapper - wrapperPrice := getNextSqrtPriceFromAmount0RoundingUp(sqrtP, liquidity, smallAmount, false) - - uassert.Equal(t, directPrice.ToString(), wrapperPrice.ToString()) - }) - - // Test token1 paths similarly - t.Run("token1_consistency", func(t *testing.T) { - // Add - directAdd := getNextPriceAmount1Add(sqrtP, liquidity, amount) - wrapperAdd := getNextSqrtPriceFromAmount1RoundingDown(sqrtP, liquidity, amount, true) - uassert.Equal(t, directAdd.ToString(), wrapperAdd.ToString()) - - // Remove - smallAmount := u256.MustFromDecimal("1000000") - directRemove := getNextPriceAmount1Remove(sqrtP, liquidity, smallAmount) - wrapperRemove := getNextSqrtPriceFromAmount1RoundingDown(sqrtP, liquidity, smallAmount, false) - uassert.Equal(t, directRemove.ToString(), wrapperRemove.ToString()) - }) -} - -// Helper functions -func encodePriceSqrt(reserve1, reserve0 string) *u256.Uint { - reserve1Uint := u256.MustFromDecimal(reserve1) - reserve0Uint := u256.MustFromDecimal(reserve0) - - if reserve0Uint.IsZero() { - panic("division by zero") - } - - // numerator = reserve1 * (2^192) - two192 := new(u256.Uint).Lsh(u256.NewUint(1), 192) - numerator := new(u256.Uint).Mul(reserve1Uint, two192) - - // ratioX192 = numerator / reserve0 - ratioX192 := new(u256.Uint).Div(numerator, reserve0Uint) - - // Return sqrt(ratioX192) - return sqrt(ratioX192) -} - -func sqrt(x *u256.Uint) *u256.Uint { - if x.IsZero() { - return u256.NewUint(0) - } - - z := new(u256.Uint).Set(x) - y := new(u256.Uint).Rsh(z, 1) - - for y.Cmp(z) < 0 { - z.Set(y) - temp := new(u256.Uint).Div(x, z) - y.Add(z, temp).Rsh(y, 1) - } - return z -} diff --git a/contract/p/gnoswap/gnsmath/swap_math.gno b/contract/p/gnoswap/gnsmath/swap_math.gno deleted file mode 100644 index d106ad1..0000000 --- a/contract/p/gnoswap/gnsmath/swap_math.gno +++ /dev/null @@ -1,234 +0,0 @@ -package gnsmath - -import ( - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -const denominator = uint64(1_000_000) - -// SwapMathComputeSwapStep computes the next sqrt price, amount in, amount out, and fee amount -// for a swap step within a single tick range. -// -// Parameters: -// - sqrtRatioCurrentX96: current sqrt price in Q96 format -// - sqrtRatioTargetX96: target sqrt price (tick boundary) -// - liquidity: available liquidity in the range -// - amountRemaining: amount left to swap (positive=exact in, negative=exact out) -// - feePips: fee in hundredths of a bip (3000 = 0.3%) -// -// Returns sqrtRatioNextX96, amountIn, amountOut, feeAmount. -func SwapMathComputeSwapStep( - sqrtRatioCurrentX96 *u256.Uint, - sqrtRatioTargetX96 *u256.Uint, - liquidity *u256.Uint, - amountRemaining *i256.Int, - feePips uint64, -) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) { - if sqrtRatioCurrentX96 == nil || sqrtRatioTargetX96 == nil || - liquidity == nil || amountRemaining == nil { - panic("SwapMathComputeSwapStep: invalid input") - } - - // This function is publicly accessible and can be called by external users or contracts. - // While the pool realm only uses predefined fee values (100, 500, 3000, 10000) which are safely within range, - // external callers could potentially pass any feePips value. The fee calculation involves subtracting feePips - // from 1000000 (representing 100%), and if feePips exceeds 1000000, it would cause an underflow, - // leading to incorrect fee calculations. - if feePips > denominator { - panic("SwapMathComputeSwapStep: feePips must be less than or equal to 1000000") - } - - // zeroForOne determines swap direction based on the relationship of current vs. target - zeroForOne := sqrtRatioCurrentX96.Gte(sqrtRatioTargetX96) - - // POSITIVE == EXACT_IN => Estimated AmountOut - // NEGATIVE == EXACT_OUT => Estimated AmountIn - exactIn := !amountRemaining.IsNeg() - - amountRemainingAbs := amountRemaining.Abs() - feeRateInPips := u256.NewUint(feePips) - withoutFeeRateInPips := u256.NewUint(denominator - feePips) - - sqrtRatioNextX96 := u256.Zero() - amountIn := u256.Zero() - amountOut := u256.Zero() - feeAmount := u256.Zero() - - if exactIn { - // Handle EXACT_IN scenario as a separate function - sqrtRatioNextX96, amountIn = handleExactIn( - zeroForOne, - sqrtRatioCurrentX96, - sqrtRatioTargetX96, - liquidity, - amountRemainingAbs, // use absolute value here - withoutFeeRateInPips, - ) - } else { - // Handle EXACT_OUT scenario as a separate function - sqrtRatioNextX96, amountOut = handleExactOut( - zeroForOne, - sqrtRatioCurrentX96, - sqrtRatioTargetX96, - liquidity, - amountRemainingAbs, - ) - } - - // isMax checks if we've hit the boundary price (target) - isMax := sqrtRatioTargetX96.Eq(sqrtRatioNextX96) - - // Calculate final amountIn, amountOut if needed - if zeroForOne { - // If isMax && exactIn, we already have the correct amountIn - if !(isMax && exactIn) { - amountIn = getAmount0DeltaHelper( - sqrtRatioNextX96, - sqrtRatioCurrentX96, - liquidity, - true, - ) - } - // If isMax && !exactIn, we already have the correct amountOut - if !(isMax && !exactIn) { - amountOut = getAmount1DeltaHelper( - sqrtRatioNextX96, - sqrtRatioCurrentX96, - liquidity, - false, - ) - } - } else { - if !(isMax && exactIn) { - amountIn = getAmount1DeltaHelper( - sqrtRatioCurrentX96, - sqrtRatioNextX96, - liquidity, - true, - ) - } - if !(isMax && !exactIn) { - amountOut = getAmount0DeltaHelper( - sqrtRatioCurrentX96, - sqrtRatioNextX96, - liquidity, - false, - ) - } - } - - // If we're in EXACT_OUT mode but overcalculated 'amountOut' - if !exactIn && amountOut.Gt(amountRemainingAbs) { - amountOut = amountRemainingAbs - } - - // Fee logic - // If exactIn and we haven't hit the target, the difference is the fee - // Else, compute fee from feePips - if exactIn && !sqrtRatioNextX96.Eq(sqrtRatioTargetX96) { - feeAmount = u256.Zero().Sub(amountRemainingAbs, amountIn) - } else { - feeAmount = u256.MulDivRoundingUp( - amountIn, - feeRateInPips, - withoutFeeRateInPips, - ) - } - - // Final sanity check for resulting price - if sqrtRatioNextX96.Lt(MIN_SQRT_RATIO) || sqrtRatioNextX96.Gt(MAX_SQRT_RATIO) { - panic(errInvalidPoolSqrtPrice) - } - - return sqrtRatioNextX96, amountIn, amountOut, feeAmount -} - -// handleExactIn handles the EXACT_IN scenario for swaps, returning the next sqrt price and provisional -// amount in while accounting for fees by reducing the input amount. -// This internal function processes swaps where the input amount is specified exactly. -func handleExactIn( - zeroForOne bool, - sqrtRatioCurrentX96, - sqrtRatioTargetX96, - liquidity, - amountRemainingAbs, - withoutFeeRateInPips *u256.Uint, -) (*u256.Uint, *u256.Uint) { - amountRemainingLessFee := u256.MulDiv( - amountRemainingAbs, - withoutFeeRateInPips, - u256.NewUint(denominator), - ) - - // Special case: - // When the remaining amount to be swapped becomes 1 during a tick swap, - // the swap fee becomes less than 0. - // At this point, check whether the swap is no longer being executed. - if amountRemainingLessFee.IsZero() { - return sqrtRatioCurrentX96, u256.Zero() - } - - var amountIn *u256.Uint - if zeroForOne { - amountIn = getAmount0DeltaHelper( - sqrtRatioTargetX96, - sqrtRatioCurrentX96, - liquidity, - true, - ) - } else { - amountIn = getAmount1DeltaHelper( - sqrtRatioCurrentX96, - sqrtRatioTargetX96, - liquidity, - true, - ) - } - - if amountRemainingLessFee.Gte(amountIn) { - return sqrtRatioTargetX96, amountIn - } - - // We don't reach target price; use partial move - nextSqrt := getNextSqrtPriceFromInput( - sqrtRatioCurrentX96, - liquidity, - amountRemainingLessFee, - zeroForOne, - ) - - // Return the partially moved price and calculate amountIn later - // This avoids double calculation and ensures consistency - return nextSqrt, amountRemainingLessFee -} - -// handleExactOut handles the EXACT_OUT scenario for swaps, returning the next sqrt price and provisional -// amount out while checking if sufficient liquidity exists to fulfill the requested output amount. -// This internal function processes swaps where the output amount is specified exactly. -func handleExactOut( - zeroForOne bool, - sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, amountRemainingAbs *u256.Uint, -) (*u256.Uint, *u256.Uint) { - var amountOut *u256.Uint - if zeroForOne { - amountOut = getAmount1DeltaHelper(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false) - } else { - amountOut = getAmount0DeltaHelper(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false) - } - - // Fast path: if sufficient liquidity, use target price - if amountRemainingAbs.Gte(amountOut) { - return sqrtRatioTargetX96, amountOut - } - - // Otherwise, partial move: compute next price from residual output amount - nextSqrt := getNextSqrtPriceFromOutput( - sqrtRatioCurrentX96, - liquidity, - amountRemainingAbs, - zeroForOne, - ) - - return nextSqrt, amountRemainingAbs -} diff --git a/contract/p/gnoswap/gnsmath/swap_math_test.gno b/contract/p/gnoswap/gnsmath/swap_math_test.gno deleted file mode 100644 index a6e4bc5..0000000 --- a/contract/p/gnoswap/gnsmath/swap_math_test.gno +++ /dev/null @@ -1,851 +0,0 @@ -package gnsmath - -import ( - "testing" - - "gno.land/p/nt/uassert" - "gno.land/p/nt/ufmt" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -func TestSwapMathComputeSwapStep(t *testing.T) { - tests := []struct { - name string - currentX96, targetX96 *u256.Uint - liquidity *u256.Uint - amountRemaining *i256.Int - feePips uint64 - sqrtNextX96 *u256.Uint - chkSqrtNextX96 func(sqrtRatioNextX96, priceTarget *u256.Uint) - amountIn, amountOut, feeAmount string - }{ - // Basic swap - { - name: "exact_amount_in_capped_at_price_target_one_for_zero", - currentX96: encodePriceSqrtTest(t, "1", "1"), - targetX96: encodePriceSqrtTest(t, "101", "100"), - liquidity: u256.MustFromDecimal("2000000000000000000"), - amountRemaining: i256.MustFromDecimal("1000000000000000000"), - feePips: 600, - sqrtNextX96: encodePriceSqrtTest(t, "101", "100"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) - }, - amountIn: "9975124224178055", - amountOut: "9925619580021728", - feeAmount: "5988667735148", - }, - { - name: "exact_amount_out_capped_at_price_target_one_for_zero", - currentX96: encodePriceSqrtTest(t, "1", "1"), - targetX96: encodePriceSqrtTest(t, "101", "100"), - liquidity: u256.MustFromDecimal("2000000000000000000"), - amountRemaining: i256.MustFromDecimal("-1000000000000000000"), - feePips: 600, - sqrtNextX96: encodePriceSqrtTest(t, "101", "100"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) - }, - amountIn: "9975124224178055", - amountOut: "9925619580021728", - feeAmount: "5988667735148", - }, - { - name: "exact_amount_in_fully_spent_one_for_zero", - currentX96: encodePriceSqrtTest(t, "1", "1"), - targetX96: encodePriceSqrtTest(t, "1000", "100"), - liquidity: u256.MustFromDecimal("2000000000000000000"), - amountRemaining: i256.MustFromDecimal("1000000000000000000"), - sqrtNextX96: encodePriceSqrtTest(t, "1000", "100"), - feePips: 600, - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Lte(priceTarget)) - }, - amountIn: "999400000000000000", - amountOut: "666399946655997866", - feeAmount: "600000000000000", - }, - { - name: "exact_amount_out_fully_received_one_for_zero", - currentX96: encodePriceSqrtTest(t, "1", "1"), - targetX96: encodePriceSqrtTest(t, "1000", "100"), - liquidity: u256.MustFromDecimal("2000000000000000000"), - amountRemaining: i256.MustFromDecimal("-1000000000000000000"), - feePips: 600, - sqrtNextX96: encodePriceSqrtTest(t, "1000", "100"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Lt(priceTarget)) - }, - amountIn: "2000000000000000000", - amountOut: "1000000000000000000", - feeAmount: "1200720432259356", - }, - { - name: "amount_out_capped_at_desired_amount", - currentX96: u256.MustFromDecimal("417332158212080721273783715441582"), - targetX96: u256.MustFromDecimal("1452870262520218020823638996"), - liquidity: u256.MustFromDecimal("159344665391607089467575320103"), - amountRemaining: i256.MustFromDecimal("-1"), - feePips: 1, - sqrtNextX96: u256.MustFromDecimal("417332158212080721273783715441581"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) - }, - amountIn: "1", - amountOut: "1", - feeAmount: "1", - }, - // Edge cases - zero values - { - name: "zero_liquidity", - currentX96: encodePriceSqrtTest(t, "1", "1"), - targetX96: encodePriceSqrtTest(t, "2", "1"), - liquidity: u256.Zero(), - amountRemaining: i256.MustFromDecimal("1000000"), - feePips: 3000, - sqrtNextX96: encodePriceSqrtTest(t, "1", "1"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(encodePriceSqrtTest(t, "2", "1"))) - }, - amountIn: "0", - amountOut: "0", - feeAmount: "0", - }, - { - name: "zero_amount_remaining", - currentX96: encodePriceSqrtTest(t, "1", "1"), - targetX96: encodePriceSqrtTest(t, "2", "1"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountRemaining: i256.Zero(), - feePips: 3000, - sqrtNextX96: encodePriceSqrtTest(t, "1", "1"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(encodePriceSqrtTest(t, "1", "1"))) - }, - amountIn: "0", - amountOut: "0", - feeAmount: "0", - }, - // Edge cases - extreme prices - { - name: "extreme_low_price_with_fee", - currentX96: MIN_SQRT_RATIO, - targetX96: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(1000)), - liquidity: u256.MustFromDecimal("1"), - amountRemaining: i256.MustFromDecimal("1000000"), - feePips: 1, - sqrtNextX96: MIN_SQRT_RATIO, - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Gte(MIN_SQRT_RATIO)) - }, - amountIn: "1", - amountOut: "4294643428317", - feeAmount: "1", - }, - // Fee edge cases - { - name: "entire_input_amount_taken_as_fee", - currentX96: u256.MustFromDecimal("4295128739"), - targetX96: u256.MustFromDecimal("79887613182836312"), - liquidity: u256.MustFromDecimal("1985041575832132834610021537970"), - amountRemaining: i256.MustFromDecimal("10"), - feePips: 1872, - sqrtNextX96: u256.MustFromDecimal("4295128739"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) - }, - amountIn: "0", - amountOut: "0", - feeAmount: "10", - }, - { - name: "maximum_fee_100_percent", - currentX96: encodePriceSqrtTest(t, "1", "1"), - targetX96: encodePriceSqrtTest(t, "2", "1"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amountRemaining: i256.MustFromDecimal("1000000"), - feePips: 1000000, - sqrtNextX96: encodePriceSqrtTest(t, "1", "1"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(encodePriceSqrtTest(t, "1", "1"))) - }, - amountIn: "0", - amountOut: "0", - feeAmount: "1000000", - }, - { - name: "rounding_to_zero_after_fee", - currentX96: Q96, - targetX96: new(u256.Uint).Lsh(Q96, 1), - liquidity: u256.MustFromDecimal("1000000"), - amountRemaining: i256.MustFromDecimal("1"), - feePips: 999999, - sqrtNextX96: Q96, - chkSqrtNextX96: func(sqrt, target *u256.Uint) { - uassert.True(t, sqrt.Eq(Q96)) - }, - amountIn: "0", - amountOut: "0", - feeAmount: "1", - }, - // Insufficient liquidity cases - { - name: "insufficient_liquidity_zero_for_one_exact_output", - currentX96: u256.MustFromDecimal("20282409603651670423947251286016"), - targetX96: u256.MustFromDecimal("22310650564016837466341976414617"), - liquidity: u256.MustFromDecimal("1024"), - amountRemaining: i256.MustFromDecimal("-4"), - feePips: 3000, - sqrtNextX96: u256.MustFromDecimal("22310650564016837466341976414617"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) - }, - amountIn: "26215", - amountOut: "0", - feeAmount: "79", - }, - { - name: "insufficient_liquidity_one_for_zero_exact_output", - currentX96: u256.MustFromDecimal("20282409603651670423947251286016"), - targetX96: u256.MustFromDecimal("18254168643286503381552526157414"), - liquidity: u256.MustFromDecimal("1024"), - amountRemaining: i256.MustFromDecimal("-263000"), - feePips: 3000, - sqrtNextX96: u256.MustFromDecimal("18254168643286503381552526157414"), - chkSqrtNextX96: func(sqrtRatioNextX96, priceTarget *u256.Uint) { - uassert.True(t, sqrtRatioNextX96.Eq(priceTarget)) - }, - amountIn: "1", - amountOut: "26214", - feeAmount: "1", - }, - // Target price uses partial input amount (removed problematic test case) - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sqrtRatioNextX96, amountIn, amountOut, feeAmount := SwapMathComputeSwapStep( - test.currentX96, test.targetX96, test.liquidity, test.amountRemaining, test.feePips, - ) - test.chkSqrtNextX96(sqrtRatioNextX96, test.sqrtNextX96) - uassert.Equal(t, amountIn.ToString(), test.amountIn) - uassert.Equal(t, amountOut.ToString(), test.amountOut) - uassert.Equal(t, feeAmount.ToString(), test.feeAmount) - }) - } -} - -func TestSwapMathFeeConsistency(t *testing.T) { - tests := []struct { - name string - current *u256.Uint - target *u256.Uint - liquidity *u256.Uint - amount *i256.Int - fee_pips uint64 - }{ - { - name: "fee_consistency_100_pips", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000000000000000"), - fee_pips: 100, - }, - { - name: "fee_consistency_500_pips", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000000000000000"), - fee_pips: 500, - }, - { - name: "fee_consistency_3000_pips", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000000000000000"), - fee_pips: 3000, - }, - { - name: "fee_consistency_10000_pips", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000000000000000"), - fee_pips: 10000, - }, - { - name: "fee_consistency_tiny_liquidity", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "121", "100"), - liquidity: u256.NewUint(1), // low liquidity - amount: i256.MustFromDecimal("1000000"), - fee_pips: 3000, - }, - { - name: "fee_consistency_max_fee", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "121", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000"), - fee_pips: 999999, // 99.9999% - }, - { - name: "fee_consistency_zero_liquidity_exactIn", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "2", "1"), - liquidity: u256.Zero(), - amount: i256.MustFromDecimal("1000000"), - fee_pips: 3000, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sqrtNext, amountIn, _, feeAmount := SwapMathComputeSwapStep( - test.current, test.target, test.liquidity, test.amount, test.fee_pips, - ) - - amount_in_num := amountIn - fee_amount_num := feeAmount - sqrt_next_num := sqrtNext - - // Check if it's a partial fill (didn't reach target) - is_partial_fill := !sqrt_next_num.Eq(test.target) - - if is_partial_fill && !test.amount.IsNeg() { // exactIn mode - // For partial fills in exactIn: fee = amountRemaining - amountIn - expected_fee := new(u256.Uint).Sub(test.amount.Abs(), amount_in_num) - uassert.True(t, fee_amount_num.Eq(expected_fee), - ufmt.Sprintf("Partial fill fee should be %s, got %s", expected_fee.ToString(), fee_amount_num.ToString())) - } else { - // Normal case: correct formula is amountIn * feePips / (1e6 - feePips) - fee_denominator := new(u256.Uint).Sub(u256.NewUint(1000000), u256.NewUint(test.fee_pips)) - expected_fee := u256.MulDivRoundingUp(amount_in_num, u256.NewUint(test.fee_pips), fee_denominator) - - uassert.True(t, fee_amount_num.Eq(expected_fee), - ufmt.Sprintf("Fee %s should equal %s (fee_pips: %d)", fee_amount_num.ToString(), expected_fee.ToString(), test.fee_pips)) - } - }) - } -} - -func TestSwapMathPriceBounds(t *testing.T) { - tests := []struct { - name string - current *u256.Uint - target *u256.Uint - liquidity *u256.Uint - amount *i256.Int - fee_pips uint64 - zero_for_one bool - }{ - { - name: "zero_for_one_price_decreases", - current: encodePriceSqrtTest(t, "100", "100"), - target: encodePriceSqrtTest(t, "90", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000000000000"), - fee_pips: 3000, - zero_for_one: true, - }, - { - name: "one_for_zero_price_increases", - current: encodePriceSqrtTest(t, "100", "100"), - target: encodePriceSqrtTest(t, "110", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000000000000"), - fee_pips: 3000, - zero_for_one: false, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sqrtNext, _, _, _ := SwapMathComputeSwapStep( - test.current, test.target, test.liquidity, test.amount, test.fee_pips, - ) - - sqrt_next_num := sqrtNext - - if test.zero_for_one { - uassert.True(t, sqrt_next_num.Gte(test.target), - "Price should not go below target for zero_for_one") - uassert.True(t, sqrt_next_num.Lte(test.current), - "Price should decrease for zero_for_one") - } else { - uassert.True(t, sqrt_next_num.Lte(test.target), - "Price should not go above target for one_for_zero") - uassert.True(t, sqrt_next_num.Gte(test.current), - "Price should increase for one_for_zero") - } - }) - } -} - -func TestSwapMathSymmetry(t *testing.T) { - tests := []struct { - name string - current *u256.Uint - target *u256.Uint - liquidity *u256.Uint - amount *i256.Int - fee_pips uint64 - }{ - { - name: "zero_for_one_symmetry", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "4", "1"), - liquidity: u256.MustFromDecimal("1000000000"), - amount: i256.MustFromDecimal("1000"), - fee_pips: 3000, - }, - { - name: "one_for_zero_symmetry", - current: encodePriceSqrtTest(t, "4", "1"), - target: encodePriceSqrtTest(t, "1", "1"), - liquidity: u256.MustFromDecimal("1000000000"), - amount: i256.MustFromDecimal("1000"), - fee_pips: 3000, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - // Forward swap - sqrt1, _, out1, fee1 := SwapMathComputeSwapStep( - test.current, test.target, test.liquidity, test.amount, test.fee_pips, - ) - - // Reverse swap - out1_neg := i256.FromUint256(out1.Clone()).Neg(i256.FromUint256(out1.Clone())) - - sqrt2, _, _, fee2 := SwapMathComputeSwapStep( - sqrt1, test.current, test.liquidity, out1_neg, test.fee_pips, - ) - - // Price should return to original - uassert.True(t, sqrt2.Eq(test.current), - "Price should return to original: got %s, want %s", - sqrt2.ToString(), test.current.ToString(), - ) - - // Verify fees are deducted - total_fees := new(u256.Uint).Add(fee1, fee2) - recovered := new(u256.Uint).Sub(u256.MustFromDecimal(test.amount.ToString()), total_fees) - uassert.True(t, recovered.Gt(u256.Zero()), - "Recovered amount %s should be > 0", recovered.ToString(), - ) - }) - } -} - -func TestSwapMathBoundaries(t *testing.T) { - tests := []struct { - name string - current_x96 *u256.Uint - target_x96 *u256.Uint - liquidity *u256.Uint - amount_remaining *i256.Int - fee_pips uint64 - expect_price_move bool - expect_amount_in bool - }{ - { - name: "min_boundary_one_for_zero", - current_x96: MIN_SQRT_RATIO, - target_x96: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(10000)), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount_remaining: i256.MustFromDecimal("1000000000000000"), - fee_pips: 3000, - expect_price_move: true, - expect_amount_in: true, - }, - { - name: "max_boundary_zero_for_one", - current_x96: MAX_SQRT_RATIO, - target_x96: new(u256.Uint).Sub(MAX_SQRT_RATIO, u256.NewUint(10000)), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount_remaining: i256.MustFromDecimal("1000000000000000"), - fee_pips: 3000, - expect_price_move: true, - expect_amount_in: true, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sqrt_next, amount_in, _, _ := SwapMathComputeSwapStep( - test.current_x96, test.target_x96, test.liquidity, test.amount_remaining, test.fee_pips, - ) - - sqrt_next_num := sqrt_next - amount_in_num := amount_in - - if test.expect_price_move { - uassert.True(t, !sqrt_next_num.Eq(test.current_x96), - "Price should move from boundary") - } - if test.expect_amount_in { - uassert.True(t, amount_in_num.Gt(u256.Zero()), - "Should have non-zero amount_in") - } - }) - } -} - -func TestSwapMathPartialFill(t *testing.T) { - tests := []struct { - name string - current *u256.Uint - target *u256.Uint - liquidity *u256.Uint - amount *i256.Int - fee_pips uint64 - }{ - { - name: "zero_for_one_partial_fill", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "100", "1"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000"), - fee_pips: 3000, - }, - { - name: "one_for_zero_partial_fill", - current: encodePriceSqrtTest(t, "100", "1"), - target: encodePriceSqrtTest(t, "1", "1"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000"), - fee_pips: 3000, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sqrt_next_str, in_str, _, fee_str := SwapMathComputeSwapStep( - test.current, - test.target, - test.liquidity, - test.amount, - test.fee_pips, - ) - sqrt_next := sqrt_next_str - in_amt := in_str - fee_amt := fee_str - - // Should not reach target with small amount - uassert.True(t, !sqrt_next.Eq(test.target), "Price should move but not reach target") - - // Fee equals remainder when partial fill - expected_fee := new(u256.Uint).Sub(u256.MustFromDecimal(test.amount.ToString()), in_amt) - uassert.True(t, fee_amt.Eq(expected_fee), - "Fee %s should equal remainder %s", - fee_amt.ToString(), expected_fee.ToString(), - ) - }) - } -} - -func TestSwapMathComputeSwapStepFail(t *testing.T) { - tests := []struct { - name string - current_x96 *u256.Uint - target_x96 *u256.Uint - liquidity *u256.Uint - amount_remaining *i256.Int - fee_pips uint64 - expected_message string - }{ - { - name: "nil_inputs", - current_x96: nil, - target_x96: nil, - liquidity: nil, - amount_remaining: nil, - fee_pips: 600, - expected_message: "SwapMathComputeSwapStep: invalid input", - }, - { - name: "fee_pips_exceeds_maximum", - current_x96: encodePriceSqrtTest(t, "1", "1"), - target_x96: encodePriceSqrtTest(t, "101", "100"), - liquidity: u256.MustFromDecimal("2000000000000000000"), - amount_remaining: i256.MustFromDecimal("1000000000000000000"), - fee_pips: 1000001, - expected_message: "SwapMathComputeSwapStep: feePips must be less than or equal to 1000000", - }, - { - name: "sqrt_price_below_minimum", - current_x96: u256.MustFromDecimal("2"), - target_x96: u256.MustFromDecimal("1"), - liquidity: u256.MustFromDecimal("1"), - amount_remaining: i256.MustFromDecimal("100"), - fee_pips: 1, - expected_message: errInvalidPoolSqrtPrice.Error(), - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - uassert.PanicsWithMessage(t, test.expected_message, func() { - SwapMathComputeSwapStep( - test.current_x96, - test.target_x96, - test.liquidity, - test.amount_remaining, - test.fee_pips, - ) - }) - }) - } -} - -func TestSwapMathHighPrecision(t *testing.T) { - tests := []struct { - name string - current *u256.Uint - target *u256.Uint - liquidity *u256.Uint - amount *i256.Int - fee_pips uint64 - }{ - { - name: "high_precision_small_amounts", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "101", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1"), - fee_pips: 3000, - }, - { - name: "high_precision_large_amounts", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "121", "100"), - liquidity: u256.MustFromDecimal("340282366920938463463374607431768211455"), - amount: i256.MustFromDecimal("340282366920938463463374607431768211455"), - fee_pips: 3000, - }, - { - name: "precision_near_price_boundaries", - current: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(1)), - target: new(u256.Uint).Add(MIN_SQRT_RATIO, u256.NewUint(1000)), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000"), - fee_pips: 3000, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sqrtNext, amountIn, amountOut, feeAmount := SwapMathComputeSwapStep( - test.current, test.target, test.liquidity, test.amount, test.fee_pips, - ) - - // Basic sanity checks - sqrtNextNum := sqrtNext - amountInNum := amountIn - amountOutNum := amountOut - feeAmountNum := feeAmount - - uassert.True(t, sqrtNextNum.Gte(MIN_SQRT_RATIO), "sqrt price should be >= MIN_SQRT_RATIO") - uassert.True(t, sqrtNextNum.Lte(MAX_SQRT_RATIO), "sqrt price should be <= MAX_SQRT_RATIO") - uassert.True(t, amountInNum.Gte(u256.Zero()), "amountIn should be >= 0") - uassert.True(t, amountOutNum.Gte(u256.Zero()), "amountOut should be >= 0") - uassert.True(t, feeAmountNum.Gte(u256.Zero()), "feeAmount should be >= 0") - - // For exact input, total consumption should not exceed input - if !test.amount.IsNeg() { - total := new(u256.Uint).Add(amountInNum, feeAmountNum) - uassert.True(t, total.Lte(test.amount.Abs()), - ufmt.Sprintf("Total consumption %s should not exceed input %s", - total.ToString(), test.amount.Abs().ToString())) - } - }) - } -} - -func TestSwapMathExtremeFees(t *testing.T) { - tests := []struct { - name string - fee_pips uint64 - expect_no_swap bool - }{ - { - name: "minimal_fee_1_pip", - fee_pips: 1, - expect_no_swap: false, - }, - { - name: "low_fee_10_pips", - fee_pips: 10, - expect_no_swap: false, - }, - { - name: "medium_fee_3000_pips", - fee_pips: 3000, - expect_no_swap: false, - }, - { - name: "high_fee_50000_pips", - fee_pips: 50000, - expect_no_swap: false, - }, - { - name: "very_high_fee_500000_pips", - fee_pips: 500000, - expect_no_swap: false, - }, - } - - current := encodePriceSqrtTest(t, "1", "1") - target := encodePriceSqrtTest(t, "121", "100") - liquidity := u256.MustFromDecimal("1000000000000000000") - amount := i256.MustFromDecimal("1000000") - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sqrtNext, amountIn, _, feeAmount := SwapMathComputeSwapStep( - current, target, liquidity, amount, test.fee_pips, - ) - - amountInNum := amountIn - feeAmountNum := feeAmount - sqrtNextNum := sqrtNext - - if test.expect_no_swap { - // With extreme fees, most/all input goes to fees, no actual swap - uassert.True(t, amountInNum.IsZero() || amountInNum.Lt(u256.NewUint(100)), - "With extreme fees, amountIn should be very small or zero") - uassert.True(t, sqrtNextNum.Eq(current), - "With extreme fees, price should not move significantly") - } - - // Fee should never exceed original amount - uassert.True(t, feeAmountNum.Lte(amount.Abs()), - "Fee should not exceed input amount") - }) - } -} - -func TestSwapMathConsistencyChecks(t *testing.T) { - tests := []struct { - name string - current *u256.Uint - target *u256.Uint - liquidity *u256.Uint - amount *i256.Int - fee_pips uint64 - }{ - { - name: "consistency_small_amounts", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "110", "100"), - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000"), - fee_pips: 3000, - }, - { - name: "consistency_equal_prices", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "1", "1"), // same price - liquidity: u256.MustFromDecimal("1000000000000000000"), - amount: i256.MustFromDecimal("1000000"), - fee_pips: 3000, - }, - { - name: "consistency_minimal_liquidity", - current: encodePriceSqrtTest(t, "1", "1"), - target: encodePriceSqrtTest(t, "121", "100"), - liquidity: u256.NewUint(1), - amount: i256.MustFromDecimal("1000"), - fee_pips: 3000, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - sqrtNext, amountIn, amountOut, feeAmount := SwapMathComputeSwapStep( - test.current, test.target, test.liquidity, test.amount, test.fee_pips, - ) - - sqrtNextNum := sqrtNext - amountInNum := amountIn - amountOutNum := amountOut - feeAmountNum := feeAmount - - // Check that price movement is in the right direction - zeroForOne := test.current.Gte(test.target) - if !test.amount.IsNeg() { // exact input - if zeroForOne { - uassert.True(t, sqrtNextNum.Lte(test.current), - "For zeroForOne, price should decrease or stay same") - uassert.True(t, sqrtNextNum.Gte(test.target), - "Price should not go below target") - } else { - uassert.True(t, sqrtNextNum.Gte(test.current), - "For oneForZero, price should increase or stay same") - uassert.True(t, sqrtNextNum.Lte(test.target), - "Price should not go above target") - } - } - - // Special case: same price should result in no swap - if test.current.Eq(test.target) { - uassert.True(t, sqrtNextNum.Eq(test.current), - "When current == target, price should not change") - uassert.True(t, amountOutNum.IsZero(), - "When current == target, amountOut should be 0") - } - - // Conservation check: for exact input - if !test.amount.IsNeg() { - total := new(u256.Uint).Add(amountInNum, feeAmountNum) - uassert.True(t, total.Lte(test.amount.Abs()), - "amountIn + feeAmount should not exceed input") - } - }) - } -} - -// Helper functions - -func encodePriceSqrtTest(t *testing.T, reserve1, reserve0 string) *u256.Uint { - t.Helper() - - reserve1_uint := u256.MustFromDecimal(reserve1) - reserve0_uint := u256.MustFromDecimal(reserve0) - - if reserve0_uint.IsZero() { - panic("division by zero") - } - - two_192 := new(u256.Uint).Lsh(u256.NewUint(1), 192) - numerator := new(u256.Uint).Mul(reserve1_uint, two_192) - ratio_x192 := new(u256.Uint).Div(numerator, reserve0_uint) - - return sqrtTest(t, ratio_x192) -} - -func sqrtTest(t *testing.T, x *u256.Uint) *u256.Uint { - t.Helper() - - if x.IsZero() { - return u256.NewUint(0) - } - - z := new(u256.Uint).Set(x) - y := new(u256.Uint).Rsh(z, 1) - - temp := new(u256.Uint) - for y.Cmp(z) < 0 { - z.Set(y) - temp.Div(x, z) - y.Add(z, temp).Rsh(y, 1) - } - return z -} diff --git a/contract/p/gnoswap/int256/LICENSE b/contract/p/gnoswap/int256/LICENSE deleted file mode 100644 index fc7e78a..0000000 --- a/contract/p/gnoswap/int256/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Trịnh Đức Bảo Linh(Kevin) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/contract/p/gnoswap/int256/README.md b/contract/p/gnoswap/int256/README.md deleted file mode 100644 index 920113a..0000000 --- a/contract/p/gnoswap/int256/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# int256 - -256-bit signed integer arithmetic for GnoSwap. - -## Overview - -Fixed-size 256-bit signed integer library optimized for AMM calculations with overflow detection. - -## Features - -- Fixed 256-bit size (predictable gas costs) -- Two's complement representation -- Overflow detection on all operations -- AMM-optimized functions -- Range: -(2^255) to 2^255-1 - -## Usage - -```go -import i256 "gno.land/p/gnoswap/int256" - -// Create values -a := i256.NewInt(100) -b := i256.MustFromDecimal("-1000") - -// Arithmetic with overflow detection -result, overflow := new(i256.Int).AddOverflow(a, b) -if overflow { - // Handle overflow -} -``` - -## Implementation - -Built on [uint256](../uint256) for underlying arithmetic. \ No newline at end of file diff --git a/contract/p/gnoswap/int256/absolute.gno b/contract/p/gnoswap/int256/absolute.gno deleted file mode 100644 index 584be44..0000000 --- a/contract/p/gnoswap/int256/absolute.gno +++ /dev/null @@ -1,38 +0,0 @@ -package int256 - -import ( - "gno.land/p/gnoswap/uint256" -) - -// Abs returns the absolute value of z. -func (z *Int) Abs() *uint256.Uint { - return z.abs.Clone() -} - -// AbsGt returns true if the absolute value of z is greater than x. -func (z *Int) AbsGt(x *uint256.Uint) bool { - return z.abs.Gt(x) -} - -// AbsLt returns true if the absolute value of z is less than x. -func (z *Int) AbsLt(x *uint256.Uint) bool { - return z.abs.Lt(x) -} - -// AbsOverflow sets z to the absolute value of x and returns z and whether overflow occurred. -// Overflow occurs when x is the minimum int256 value (-2^255), as its absolute value (2^255) -// cannot be represented in a signed 256-bit integer. -func (z *Int) AbsOverflow(x *Int) (*Int, bool) { - z = z.initiateAbs() - - // overflow can be happen when negating a minimum of int256 value - if x.neg && x.abs.Eq(MinInt256().abs) { - z.Set(x) // keep the original value - return z, true - } - - z.abs.Set(x.abs) - z.neg = false - - return z, false -} diff --git a/contract/p/gnoswap/int256/absolute_test.gno b/contract/p/gnoswap/int256/absolute_test.gno deleted file mode 100644 index 2162d62..0000000 --- a/contract/p/gnoswap/int256/absolute_test.gno +++ /dev/null @@ -1,184 +0,0 @@ -package int256 - -import ( - "testing" - - "gno.land/p/gnoswap/uint256" -) - -func TestAbs(t *testing.T) { - tests := []struct { - x, want string - }{ - {"0", "0"}, - {"1", "1"}, - {"-1", "1"}, - {"-2", "2"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - got := x.Abs() - - if got.ToString() != tc.want { - t.Errorf("Abs(%s) = %v, want %v", tc.x, got.ToString(), tc.want) - } - } -} - -func TestAbsGt(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "0", "false"}, - {"1", "0", "true"}, - {"-1", "0", "true"}, - {"-1", "1", "false"}, - {"-2", "1", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "false"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.AbsGt(y) - - if got != (tc.want == "true") { - t.Errorf("AbsGt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestAbsLt(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "0", "false"}, - {"1", "0", "false"}, - {"-1", "0", "false"}, - {"-1", "1", "false"}, - {"-2", "1", "false"}, - {"-5", "10", "true"}, - {"31330", "31337", "true"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "false"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "false"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "false"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.AbsLt(y) - - if got != (tc.want == "true") { - t.Errorf("AbsLt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestInt_AbsOverflow(t *testing.T) { - tests := []struct { - name string - x *Int - wantResult string - wantOverflow bool - }{ - { - name: "zero", - x: Zero(), - wantResult: "0", - wantOverflow: false, - }, - { - name: "positive number", - x: NewInt(100), - wantResult: "100", - wantOverflow: false, - }, - { - name: "negative number", - x: NewInt(-100), - wantResult: "100", - wantOverflow: false, - }, - { - name: "max_int256", - x: MustFromDecimal("57896044618658097711785492504343953926634992332820282019728792003956564819967"), - wantResult: "57896044618658097711785492504343953926634992332820282019728792003956564819967", - wantOverflow: false, - }, - { - name: "min_int256", - x: MustFromDecimal("-57896044618658097711785492504343953926634992332820282019728792003956564819968"), - wantResult: "-57896044618658097711785492504343953926634992332820282019728792003956564819968", - wantOverflow: true, - }, - { - name: "min_int256 + 1", - x: MustFromDecimal("-57896044618658097711785492504343953926634992332820282019728792003956564819967"), - wantResult: "57896044618658097711785492504343953926634992332820282019728792003956564819967", - wantOverflow: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - z := new(Int) - gotResult, gotOverflow := z.AbsOverflow(tt.x) - - if gotOverflow != tt.wantOverflow { - t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) - return - } - - if gotResult == nil { - t.Error("unexpected nil result") - return - } - - if gotResult.ToString() != tt.wantResult { - t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) - } - - // abs value must be GTE 0 (if there is no overflow) - if !tt.wantOverflow && gotResult.neg && !gotResult.IsZero() { - t.Error("absolute value cannot be negative") - } - - // original value must not be modified - originalValue := tt.x.ToString() - if tt.x.ToString() != originalValue { - t.Errorf("original value was modified: got %v, want %v", - tt.x.ToString(), originalValue) - } - }) - } -} diff --git a/contract/p/gnoswap/int256/arithmetic.gno b/contract/p/gnoswap/int256/arithmetic.gno deleted file mode 100644 index 606bd01..0000000 --- a/contract/p/gnoswap/int256/arithmetic.gno +++ /dev/null @@ -1,319 +0,0 @@ -package int256 - -import "gno.land/p/gnoswap/uint256" - -// Add sets z to the sum x+y and returns z. -func (z *Int) Add(x, y *Int) *Int { - z = z.initiateAbs() - - if x.neg == y.neg { - // If both numbers have the same sign, add their absolute values - z.abs.Add(x.abs, y.abs) - z.neg = x.neg - } else { - // If signs are different, subtract the smaller absolute value from the larger - if x.abs.Cmp(y.abs) >= 0 { - z.abs.Sub(x.abs, y.abs) - z.neg = x.neg - } else { - z.abs.Sub(y.abs, x.abs) - z.neg = y.neg - } - } - - // Ensure zero is always positive - if z.abs.IsZero() { - z.neg = false - } - - return z -} - -// AddOverflow sets z to the sum x+y and returns z and whether overflow occurred. -// Overflow occurs when the result exceeds the int256 range [-2^255, 2^255-1]. -func (z *Int) AddOverflow(x, y *Int) (*Int, bool) { - z = z.initiateAbs() - - if x.neg == y.neg { - // same sign - var overflow bool - z.abs, overflow = z.abs.AddOverflow(x.abs, y.abs) - z.neg = x.neg - - if overflow { - return z, true - } - - // check int256 range - if z.neg { - if z.abs.Cmp(MinInt256().abs) > 0 { - return z, true - } - } else { - if z.abs.Cmp(MaxInt256().abs) > 0 { - return z, true - } - } - } else { - // handle different sign by subtracting absolute values - if x.abs.Cmp(y.abs) >= 0 { - z.abs.Sub(x.abs, y.abs) - z.neg = x.neg - } else { - z.abs.Sub(y.abs, x.abs) - z.neg = y.neg - } - } - - // overflow can be happen when result is 0 - if z.abs.IsZero() { - z.neg = false - } - - return z, false -} - -// AddUint256 sets z to the sum x+y, where y is a uint256, and returns z. -func (z *Int) AddUint256(x *Int, y *uint256.Uint) *Int { - z = z.initiateAbs() - - if x.neg { - if x.abs.Gt(y) { - z.abs.Sub(x.abs, y) - z.neg = true - } else { - z.abs.Sub(y, x.abs) - z.neg = false - } - } else { - z.abs.Add(x.abs, y) - z.neg = false - } - return z -} - -// AddDelta adds a signed int256 value y to an unsigned uint256 value x and stores the result in z. -func AddDelta(z, x *uint256.Uint, y *Int) { - if y.neg { - z.Sub(x, y.abs) - } else { - z.Add(x, y.abs) - } -} - -// AddDeltaOverflow adds a signed int256 value y to an unsigned uint256 value x, stores the result in z, -// and returns true if overflow occurs. -func AddDeltaOverflow(z, x *uint256.Uint, y *Int) bool { - var overflow bool - if y.neg { - _, overflow = z.SubOverflow(x, y.abs) - } else { - _, overflow = z.AddOverflow(x, y.abs) - } - return overflow -} - -// Sub sets z to the difference x-y and returns z. -func (z *Int) Sub(x, y *Int) *Int { - z = z.initiateAbs() - - if x.neg != y.neg { - // If sign are different, add the absolute values - z.abs.Add(x.abs, y.abs) - z.neg = x.neg - } else { - // If signs are the same, subtract the smaller absolute value from the larger - if x.abs.Cmp(y.abs) >= 0 { - z.abs = z.abs.Sub(x.abs, y.abs) - z.neg = x.neg - } else { - z.abs.Sub(y.abs, x.abs) - z.neg = !x.neg - } - } - - // Ensure zero is always positive - if z.abs.IsZero() { - z.neg = false - } - return z -} - -// SubUint256 sets z to the difference x-y, where y is a uint256, and returns z. -func (z *Int) SubUint256(x *Int, y *uint256.Uint) *Int { - z = z.initiateAbs() - - if x.neg { - z.abs.Add(x.abs, y) - z.neg = true - } else { - if x.abs.Lt(y) { - z.abs.Sub(y, x.abs) - z.neg = true - } else { - z.abs.Sub(x.abs, y) - z.neg = false - } - } - return z -} - -// SubOverflow sets z to the difference x-y and returns z and whether overflow occurred. -// Overflow occurs when subtracting a positive number from the minimum int256 value -// or a negative number from the maximum int256 value. -func (z *Int) SubOverflow(x, y *Int) (*Int, bool) { - z = z.initiateAbs() - - // must keep the original value of y - negY := y.Clone() - negY.neg = !y.neg && !y.IsZero() // reverse sign if y is not zero - - // x + (-y) - return z.AddOverflow(x, negY) -} - -// Mul sets z to the product x*y and returns z. -func (z *Int) Mul(x, y *Int) *Int { - z = z.initiateAbs() - - z.abs = z.abs.Mul(x.abs, y.abs) - z.neg = x.neg != y.neg && !z.abs.IsZero() // 0 has no sign - return z -} - -// MulUint256 sets z to the product x*y, where y is a uint256, and returns z. -func (z *Int) MulUint256(x *Int, y *uint256.Uint) *Int { - z = z.initiateAbs() - - z.abs.Mul(x.abs, y) - if z.abs.IsZero() { - z.neg = false - } else { - z.neg = x.neg - } - return z -} - -// MulOverflow sets z to the product x*y and returns z and whether overflow occurred. -// Multiplication frequently overflows when multiplying large numbers or when -// the product exceeds the int256 range [-2^255, 2^255-1]. -func (z *Int) MulOverflow(x, y *Int) (*Int, bool) { - z = z.initiateAbs() - - // always 0. no need to check overflow - if x.IsZero() || y.IsZero() { - z.abs.Clear() - z.neg = false - return z, false - } - - // multiply with absolute values - absResult, overflow := z.abs.MulOverflow(x.abs, y.abs) - z.abs = absResult - - // calculate the result's sign - z.neg = x.neg != y.neg - - if overflow { - return z, true - } - - if z.neg { - if z.abs.Cmp(MinInt256().abs) > 0 { - return z, true - } - } else { - if z.abs.Cmp(MaxInt256().abs) > 0 { - return z, true - } - } - - return z, false -} - -// Div sets z to the quotient x/y for y != 0 and returns z. -// Panics if y == 0. -func (z *Int) Div(x, y *Int) *Int { - z = z.initiateAbs() - - if y.abs.IsZero() { - panic("division by zero") - } - - z.abs.Div(x.abs, y.abs) - z.neg = (x.neg != y.neg) && !z.abs.IsZero() // 0 has no sign - - return z -} - -// DivUint256 sets z to the quotient x/y, where y is a uint256, and returns z. -// If y == 0, z is set to 0. -func (z *Int) DivUint256(x *Int, y *uint256.Uint) *Int { - z = z.initiateAbs() - - z.abs.Div(x.abs, y) - if z.abs.IsZero() { - z.neg = false - } else { - z.neg = x.neg - } - return z -} - -// Quo sets z to the quotient x/y for y != 0 and returns z. -// It implements truncated division (like Go). Panics if y == 0. -// This differs from mempooler int256 which requires manual panic handling. -func (z *Int) Quo(x, y *Int) *Int { - if y.IsZero() { - panic("division by zero") - } - - z = z.initiateAbs() - - z.abs = z.abs.Div(x.abs, y.abs) - z.neg = !(z.abs.IsZero()) && x.neg != y.neg // 0 has no sign - return z -} - -// Rem sets z to the remainder x%y for y != 0 and returns z. -// It implements truncated modulus (like Go). Panics if y == 0. -// This differs from mempooler int256 which requires manual panic handling. -func (z *Int) Rem(x, y *Int) *Int { - if y.IsZero() { - panic("division by zero") - } - - z = z.initiateAbs() - - z.abs.Mod(x.abs, y.abs) - z.neg = z.abs.Sign() > 0 && x.neg // 0 has no sign - return z -} - -// Mod sets z to the modulus x%y for y != 0 and returns z. -// If y == 0, z is set to 0 (differs from big.Int behavior). -func (z *Int) Mod(x, y *Int) *Int { - z = z.initiateAbs() - - if x.neg { - z.abs.Div(x.abs, y.abs) - z.abs.Add(z.abs, one) - z.abs.Mul(z.abs, y.abs) - z.abs.Sub(z.abs, x.abs) - z.abs.Mod(z.abs, y.abs) - } else { - z.abs.Mod(x.abs, y.abs) - } - z.neg = false - return z -} - -// MaxInt256 returns the maximum value for a 256-bit signed integer (2^255 - 1). -func MaxInt256() *Int { - return MustFromDecimal("57896044618658097711785492504343953926634992332820282019728792003956564819967") -} - -// MinInt256 returns the minimum value for a 256-bit signed integer (-2^255). -func MinInt256() *Int { - return MustFromDecimal("-57896044618658097711785492504343953926634992332820282019728792003956564819968") -} diff --git a/contract/p/gnoswap/int256/arithmetic_test.gno b/contract/p/gnoswap/int256/arithmetic_test.gno deleted file mode 100644 index efc4fa3..0000000 --- a/contract/p/gnoswap/int256/arithmetic_test.gno +++ /dev/null @@ -1,1016 +0,0 @@ -package int256 - -import ( - "testing" - - "gno.land/p/gnoswap/uint256" -) - -func TestAdd(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "1"}, - {"1", "0", "1"}, - {"1", "1", "2"}, - {"1", "2", "3"}, - // NEGATIVE - {"-1", "1", "0"}, - {"1", "-1", "0"}, - {"3", "-3", "0"}, - {"-1", "-1", "-2"}, - {"-1", "-2", "-3"}, - {"-1", "3", "2"}, - {"3", "-1", "2"}, - // OVERFLOW - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "0"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Add(x, y) - - if got.Neq(want) { - t.Errorf("Add(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestAddUint256(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "1"}, - {"1", "0", "1"}, - {"1", "1", "2"}, - {"1", "2", "3"}, - {"-1", "1", "0"}, - {"-1", "3", "2"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "1"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639934", "-1"}, - // OVERFLOW - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", "0"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.AddUint256(x, y) - - if got.Neq(want) { - t.Errorf("AddUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestAddDelta(t *testing.T) { - tests := []struct { - z, x, y, want string - }{ - {"0", "0", "0", "0"}, - {"0", "0", "1", "1"}, - {"0", "1", "0", "1"}, - {"0", "1", "1", "2"}, - {"1", "2", "3", "5"}, - {"5", "10", "-3", "7"}, - // underflow - {"1", "2", "-3", "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - } - - for _, tc := range tests { - z, err := uint256.FromDecimal(tc.z) - if err != nil { - t.Error(err) - continue - } - - x, err := uint256.FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := uint256.FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - AddDelta(z, x, y) - - if z.Neq(want) { - t.Errorf("AddDelta(%s, %s, %s) = %v, want %v", tc.z, tc.x, tc.y, z.ToString(), want.ToString()) - } - } -} - -func TestAddDeltaOverflow(t *testing.T) { - tests := []struct { - z, x, y string - want bool - }{ - {"0", "0", "0", false}, - // underflow - {"1", "2", "-3", true}, - } - - for _, tc := range tests { - z, err := uint256.FromDecimal(tc.z) - if err != nil { - t.Error(err) - continue - } - - x, err := uint256.FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - result := AddDeltaOverflow(z, x, y) - if result != tc.want { - t.Errorf("AddDeltaOverflow(%s, %s, %s) = %v, want %v", tc.z, tc.x, tc.y, result, tc.want) - } - } -} - -func TestAddOverflow(t *testing.T) { - maxInt256 := MaxInt256() - minInt256 := MinInt256() - - tests := []struct { - name string - x *Int - y *Int - wantResult string - wantOverflow bool - }{ - // Basic cases (no overflow) - { - name: "positive + positive (no overflow)", - x: NewInt(100), - y: NewInt(200), - wantResult: "300", - wantOverflow: false, - }, - { - name: "negative + negative (no overflow)", - x: NewInt(-100), - y: NewInt(-200), - wantResult: "-300", - wantOverflow: false, - }, - // Boundary cases - near maximum value - { - name: "max_int256 + 0", - x: maxInt256, - y: Zero(), - wantResult: maxInt256.ToString(), - wantOverflow: false, - }, - { - name: "max_int256 - 1 + 1", - x: new(Int).Sub(maxInt256, One()), - y: One(), - wantResult: maxInt256.ToString(), - wantOverflow: false, - }, - { - name: "max_int256 + 1", - x: maxInt256, - y: One(), - wantResult: "", // overflow - wantOverflow: true, - }, - - // Boundary cases - near minimum value - { - name: "min_int256 + 0", - x: minInt256, - y: Zero(), - wantResult: minInt256.ToString(), - wantOverflow: false, - }, - { - name: "min_int256 + 1 - 1", - x: new(Int).Add(minInt256, One()), - y: NewInt(-1), - wantResult: minInt256.ToString(), - wantOverflow: false, - }, - { - name: "min_int256 + (-1)", - x: minInt256, - y: NewInt(-1), - wantResult: "", // overflow - wantOverflow: true, - }, - - // Special cases - { - name: "max_int256 + min_int256", - x: maxInt256, - y: minInt256, - wantResult: "-1", - wantOverflow: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - z := new(Int) - gotResult, gotOverflow := z.AddOverflow(tt.x, tt.y) - - if gotOverflow != tt.wantOverflow { - t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) - return - } - - if !gotOverflow { - if gotResult == nil { - t.Error("unexpected nil result for non-overflow case") - return - } - if gotResult.ToString() != tt.wantResult { - t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) - } - } - - // Commutativity test only for non-overflow cases - if !tt.wantOverflow { - reverseResult, reverseOverflow := z.AddOverflow(tt.y, tt.x) - if reverseOverflow != gotOverflow { - t.Error("addition is not commutative for overflow") - } - if reverseResult.ToString() != gotResult.ToString() { - t.Error("addition is not commutative for result") - } - } - }) - } -} - -func TestSub(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"1", "0", "1"}, - {"1", "1", "0"}, - {"-1", "1", "-2"}, - {"1", "-1", "2"}, - {"-1", "-1", "0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "0", "-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {x: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", y: "1", want: "0"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Sub(x, y) - - if got.Neq(want) { - t.Errorf("Sub(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestSubUint256(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "-1"}, - {"1", "0", "1"}, - {"1", "1", "0"}, - {"1", "2", "-1"}, - {"-1", "1", "-2"}, - {"-1", "3", "-4"}, - // underflow - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "1", "-0"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "-1"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "3", "-2"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.SubUint256(x, y) - - if got.Neq(want) { - t.Errorf("SubUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestMul(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"5", "3", "15"}, - {"-5", "3", "-15"}, - {"5", "-3", "-15"}, - {"0", "3", "0"}, - {"3", "0", "0"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Mul(x, y) - - if got.Neq(want) { - t.Errorf("Mul(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestInt_SubOverflow(t *testing.T) { - maxInt256 := MaxInt256() - minInt256 := MinInt256() - - tests := []struct { - name string - x *Int - y *Int - wantResult string - wantOverflow bool - }{ - { - name: "positive - positive (no overflow)", - x: NewInt(200), - y: NewInt(100), - wantResult: "100", - wantOverflow: false, - }, - { - name: "negative - negative (no overflow)", - x: NewInt(-200), - y: NewInt(-300), - wantResult: "100", - wantOverflow: false, - }, - { - name: "positive - negative (no overflow)", - x: NewInt(200), - y: NewInt(-100), - wantResult: "300", - wantOverflow: false, - }, - { - name: "max_int256 - 0", - x: maxInt256, - y: Zero(), - wantResult: maxInt256.ToString(), - wantOverflow: false, - }, - { - name: "min_int256 - 0", - x: minInt256, - y: Zero(), - wantResult: minInt256.ToString(), - wantOverflow: false, - }, - { - name: "max_int256 - (-1)", // max_int256 + 1 -> overflow - x: maxInt256, - y: NewInt(-1), - wantResult: "", - wantOverflow: true, - }, - { - name: "min_int256 - 1", // min_int256 - 1 -> overflow - x: minInt256, - y: One(), - wantResult: "", - wantOverflow: true, - }, - { - name: "0 - 0", - x: Zero(), - y: Zero(), - wantResult: "0", - wantOverflow: false, - }, - { - name: "min_int256 - min_int256", - x: minInt256, - y: minInt256, - wantResult: "0", - wantOverflow: false, - }, - { - name: "max_int256 - max_int256", - x: maxInt256, - y: maxInt256, - wantResult: "0", - wantOverflow: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // z := new(Int) - z := New() - gotResult, gotOverflow := z.SubOverflow(tt.x, tt.y) - - if gotOverflow != tt.wantOverflow { - t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) - return - } - - if !gotOverflow { - if gotResult == nil { - t.Error("unexpected nil result for non-overflow case") - return - } - if gotResult.ToString() != tt.wantResult { - t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) - } - } - }) - } -} - -func TestMulUint256(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "0"}, - {"1", "0", "0"}, - {"1", "1", "1"}, - {"1", "2", "2"}, - {"-1", "1", "-1"}, - {"-1", "3", "-3"}, - {"3", "4", "12"}, - {"-3", "4", "-12"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-115792089237316195423570985008687907853269984665640564039457584007913129639932"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "115792089237316195423570985008687907853269984665640564039457584007913129639932"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.MulUint256(x, y) - - if got.Neq(want) { - t.Errorf("MulUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestInt_MulOverflow(t *testing.T) { - maxInt256 := MaxInt256() - minInt256 := MinInt256() - - tests := []struct { - name string - x *Int - y *Int - wantResult string - wantOverflow bool - }{ - { - name: "positive * positive (no overflow)", - x: NewInt(100), - y: NewInt(100), - wantResult: "10000", - wantOverflow: false, - }, - { - name: "negative * negative (no overflow)", - x: NewInt(-100), - y: NewInt(-100), - wantResult: "10000", - wantOverflow: false, - }, - { - name: "positive * negative (no overflow)", - x: NewInt(100), - y: NewInt(-100), - wantResult: "-10000", - wantOverflow: false, - }, - { - name: "0 * positive", - x: Zero(), - y: NewInt(100), - wantResult: "0", - wantOverflow: false, - }, - { - name: "positive * 0", - x: NewInt(100), - y: Zero(), - wantResult: "0", - wantOverflow: false, - }, - { - name: "0 * 0", - x: Zero(), - y: Zero(), - wantResult: "0", - wantOverflow: false, - }, - { - name: "max_int256 * 1", - x: maxInt256, - y: One(), - wantResult: maxInt256.ToString(), - wantOverflow: false, - }, - { - name: "min_int256 * 1", - x: minInt256, - y: One(), - wantResult: minInt256.ToString(), - wantOverflow: false, - }, - { - name: "min_int256 * -1", - x: minInt256, - y: NewInt(-1), - wantResult: "", // overflow because abs(min_int256) > max_int256 - wantOverflow: true, - }, - { - name: "max_int256 * 2", - x: maxInt256, - y: NewInt(2), - wantResult: "", - wantOverflow: true, - }, - { - name: "min_int256 * 2", - x: minInt256, - y: NewInt(2), - wantResult: "", - wantOverflow: true, - }, - { - name: "half_max * 2", - x: MustFromDecimal("28948022309329048855892746252171976963317496332820282019728792003956564819983"), // (2^255-1)/2 - y: NewInt(2), - wantResult: "", - wantOverflow: true, - }, - { - name: "(half_max + 1) * 2", - x: new(Int).Add(new(Int).Div(maxInt256, NewInt(2)), One()), - y: NewInt(2), - wantResult: "", - wantOverflow: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - z := new(Int) - gotResult, gotOverflow := z.MulOverflow(tt.x, tt.y) - - if gotOverflow != tt.wantOverflow { - t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) - return - } - - if !gotOverflow { - if gotResult == nil { - t.Error("unexpected nil result for non-overflow case") - return - } - if gotResult.ToString() != tt.wantResult { - t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) - } - } - - if !tt.wantOverflow { - reverseResult, reverseOverflow := z.MulOverflow(tt.y, tt.x) - if reverseOverflow != gotOverflow { - t.Error("multiplication is not commutative for overflow") - } - if reverseResult.ToString() != gotResult.ToString() { - t.Error("multiplication is not commutative for result") - } - } - }) - } -} - -func TestDiv(t *testing.T) { - tests := []struct { - x, y, expected string - }{ - {"1", "1", "1"}, - {"0", "1", "0"}, - {"-1", "1", "-1"}, - {"1", "-1", "-1"}, - {"-1", "-1", "1"}, - {"-6", "3", "-2"}, - {"10", "-2", "-5"}, - {"-10", "3", "-3"}, - {"7", "3", "2"}, - {"-7", "3", "-2"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, // Max uint256 / 2 - } - - for _, tt := range tests { - t.Run(tt.x+"/"+tt.y, func(t *testing.T) { - x := MustFromDecimal(tt.x) - y := MustFromDecimal(tt.y) - result := Zero().Div(x, y) - if result.ToString() != tt.expected { - t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, result.ToString(), tt.expected) - } - if result.abs.IsZero() && result.neg { - t.Errorf("Div(%s, %s) resulted in negative zero", tt.x, tt.y) - } - }) - } - - t.Run("Division by zero", func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Errorf("Div(1, 0) did not panic") - } - }() - x := MustFromDecimal("1") - y := MustFromDecimal("0") - Zero().Div(x, y) - }) -} - -func TestDivUint256(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "0"}, - {"1", "0", "0"}, - {"1", "1", "1"}, - {"1", "2", "0"}, - {"-1", "1", "-1"}, - {"-1", "3", "0"}, - {"4", "3", "1"}, - {"25", "5", "5"}, - {"25", "4", "6"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "-57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639934", "2", "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := uint256.FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.DivUint256(x, y) - - if got.Neq(want) { - t.Errorf("DivUint256(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestQuo(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "0"}, - {"0", "-1", "0"}, - {"10", "1", "10"}, - {"10", "-1", "-10"}, - {"-10", "1", "-10"}, - {"-10", "-1", "10"}, - {"10", "-3", "-3"}, - {"10", "3", "3"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Quo(x, y) - - if got.Neq(want) { - t.Errorf("Quo(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestRem(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "0"}, - {"0", "-1", "0"}, - {"10", "1", "0"}, - {"10", "-1", "0"}, - {"-10", "1", "0"}, - {"-10", "-1", "0"}, - {"10", "3", "1"}, - {"10", "-3", "1"}, - {"-10", "3", "-1"}, - {"-10", "-3", "-1"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Rem(x, y) - - if got.Neq(want) { - t.Errorf("Rem(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestMod(t *testing.T) { - tests := []struct { - x, y, want string - }{ - {"0", "1", "0"}, - {"0", "-1", "0"}, - {"10", "0", "0"}, - {"10", "1", "0"}, - {"10", "-1", "0"}, - {"-10", "0", "0"}, - {"-10", "1", "0"}, - {"-10", "-1", "0"}, - {"10", "3", "1"}, - {"10", "-3", "1"}, - {"-10", "3", "2"}, - {"-10", "-3", "2"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Mod(x, y) - - if got.Neq(want) { - t.Errorf("Mod(%s, %s) = %v, want %v", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestNilInitialization(t *testing.T) { - tests := []struct { - name string - setup func() (*Int, error) - wantStr string - }{ - { - name: "AddUint256 with nil abs", - setup: func() (*Int, error) { - z := new(Int) - x := uint256.NewUint(5) - return z.AddUint256(z, x), nil - }, - wantStr: "5", - }, - { - name: "SubUint256 with nil abs", - setup: func() (*Int, error) { - z := new(Int) - x := uint256.NewUint(5) - return z.SubUint256(z, x), nil - }, - wantStr: "-5", - }, - { - name: "MulUint256 with nil abs", - setup: func() (*Int, error) { - z := new(Int) - x := uint256.NewUint(5) - return z.MulUint256(z, x), nil - }, - wantStr: "0", - }, - { - name: "DivUint256 with nil abs", - setup: func() (*Int, error) { - z := new(Int) - x := uint256.NewUint(5) - return z.DivUint256(z, x), nil - }, - wantStr: "0", - }, - { - name: "Mod with nil abs", - setup: func() (*Int, error) { - z := new(Int) - x := MustFromDecimal("5") - defer func() { - if r := recover(); r != nil { - t.Errorf("Mod with nil abs panicked: %v", r) - } - }() - return z.Mod(z, x), nil - }, - wantStr: "0", - }, - { - name: "Chained operations with nil abs", - setup: func() (*Int, error) { - z := new(Int) - x := uint256.NewUint(5) - y := uint256.NewUint(3) - // (0 + 5) * 3 - return z.AddUint256(z, x).MulUint256(z, y), nil - }, - wantStr: "15", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := tt.setup() - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - got := result.ToString() - if got != tt.wantStr { - t.Errorf("%s: got %v, want %v", tt.name, got, tt.wantStr) - } - }) - } -} diff --git a/contract/p/gnoswap/int256/bitwise.gno b/contract/p/gnoswap/int256/bitwise.gno deleted file mode 100644 index ef0bd95..0000000 --- a/contract/p/gnoswap/int256/bitwise.gno +++ /dev/null @@ -1,101 +0,0 @@ -package int256 - -import ( - "gno.land/p/gnoswap/uint256" -) - -// Or sets z to the bitwise OR of x and y and returns z. -// The operation handles two's complement representation for negative numbers. -func (z *Int) Or(x, y *Int) *Int { - if x.neg == y.neg { - if x.neg { - // (-x) | (-y) == ^(x-1) | ^(y-1) == ^((x-1) & (y-1)) == -(((x-1) & (y-1)) + 1) - x1 := new(uint256.Uint).Sub(x.abs, one) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.And(x1, y1), one) - z.neg = true // z cannot be zero if x and y are negative - return z - } - - // x | y == x | y - z.abs = z.abs.Or(x.abs, y.abs) - z.neg = false - return z - } - - // x.neg != y.neg - if x.neg { - x, y = y, x // | is symmetric - } - - // x | (-y) == x | ^(y-1) == ^((y-1) &^ x) == -(^((y-1) &^ x) + 1) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.AndNot(y1, x.abs), one) - z.neg = true // z cannot be zero if one of x or y is negative - - return z -} - -// And sets z to the bitwise AND of x and y and returns z. -// The operation handles two's complement representation for negative numbers. -func (z *Int) And(x, y *Int) *Int { - if x.neg == y.neg { - if x.neg { - // (-x) & (-y) == ^(x-1) & ^(y-1) == ^((x-1) | (y-1)) == -(((x-1) | (y-1)) + 1) - x1 := new(uint256.Uint).Sub(x.abs, one) - y1 := new(uint256.Uint).Sub(y.abs, one) - z.abs = z.abs.Add(z.abs.Or(x1, y1), one) - z.neg = true // z cannot be zero if x and y are negative - return z - } - - // x & y == x & y - z.abs = z.abs.And(x.abs, y.abs) - z.neg = false - return z - } - - // x.neg != y.neg - // REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1192-1202;drc=d57303e65f00b84b528ee682747dbe1fd3316d30 - if x.neg { - x, y = y, x // & is symmetric - } - - // x & (-y) == x & ^(y-1) == x &^ (y-1) - y1 := new(uint256.Uint).Sub(y.abs, uint256.One()) - z.abs = z.abs.AndNot(x.abs, y1) - z.neg = false - return z -} - -// Rsh sets z to x right-shifted by n bits and returns z. -// It performs arithmetic right shift, preserving the sign bit. -// This differs from the original implementation which used math/big. -func (z *Int) Rsh(x *Int, n uint) *Int { - if !x.neg { - z.abs.Rsh(x.abs, n) - z.neg = x.neg - return z - } - - // REF: https://cs.opensource.google/go/go/+/refs/tags/go1.22.1:src/math/big/int.go;l=1118-1126;drc=d57303e65f00b84b528ee682747dbe1fd3316d30 - t := NewInt(0).Sub(FromUint256(x.abs), NewInt(1)) - t = t.Rsh(t, n) - - _tmp := t.Add(t, NewInt(1)) - z.abs = _tmp.Abs() - z.neg = true - - return z -} - -// Lsh sets z to x left-shifted by n bits and returns z. -func (z *Int) Lsh(x *Int, n uint) *Int { - z.abs.Lsh(x.abs, n) - if z.abs.IsZero() { - z.neg = false - } else { - z.neg = x.neg - } - return z -} diff --git a/contract/p/gnoswap/int256/bitwise_test.gno b/contract/p/gnoswap/int256/bitwise_test.gno deleted file mode 100644 index 9c4e7da..0000000 --- a/contract/p/gnoswap/int256/bitwise_test.gno +++ /dev/null @@ -1,198 +0,0 @@ -package int256 - -import ( - "testing" - - "gno.land/p/gnoswap/uint256" -) - -func TestOr(t *testing.T) { - tests := []struct { - name string - x, y, want Int - }{ - { - name: "all zeroes", - x: Int{abs: uint256.Zero(), neg: false}, - y: Int{abs: uint256.Zero(), neg: false}, - want: Int{abs: uint256.Zero(), neg: false}, - }, - { - name: "all ones", - x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - want: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - }, - { - name: "mixed", - x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - y: Int{abs: uint256.NewUint(0), neg: false}, - want: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := New() - got.Or(&tc.x, &tc.y) - - if got.Neq(&tc.want) { - t.Errorf("Or(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) - } - }) - } -} - -func TestAnd(t *testing.T) { - tests := []struct { - name string - x, y, want Int - }{ - { - name: "all zeroes", - x: Int{abs: uint256.Zero(), neg: false}, - y: Int{abs: uint256.Zero(), neg: false}, - want: Int{abs: uint256.Zero(), neg: false}, - }, - { - name: "all ones", - x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - want: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - }, - { - name: "mixed", - x: Int{abs: uint256.Zero(), neg: false}, - y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - want: Int{abs: uint256.Zero(), neg: false}, - }, - { - name: "mixed 2", - x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - y: Int{abs: uint256.Zero(), neg: false}, - want: Int{abs: uint256.Zero(), neg: false}, - }, - { - name: "mixed 3", - x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - y: Int{abs: uint256.Zero(), neg: false}, - want: Int{abs: uint256.Zero(), neg: false}, - }, - { - name: "one operand zero", - x: Int{abs: uint256.Zero(), neg: false}, - y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - want: Int{abs: uint256.Zero(), neg: false}, - }, - { - name: "one operand all ones", - x: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - y: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - want: Int{abs: uint256.NewUint(0).SetAllOne(), neg: false}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - got := New() - got.And(&tc.x, &tc.y) - - if got.Neq(&tc.want) { - t.Errorf("And(%v, %v) = %v, want %v", tc.x, tc.y, got, tc.want) - } - }) - } -} - -func TestRsh(t *testing.T) { - tests := []struct { - x string - n uint - want string - }{ - {"1024", 0, "1024"}, - {"1024", 1, "512"}, - {"1024", 2, "256"}, - {"1024", 10, "1"}, - {"1024", 11, "0"}, - {"18446744073709551615", 0, "18446744073709551615"}, - {"18446744073709551615", 1, "9223372036854775807"}, - {"18446744073709551615", 62, "3"}, - {"18446744073709551615", 63, "1"}, - {"18446744073709551615", 64, "0"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 0, "115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 1, "57896044618658097711785492504343953926634992332820282019728792003956564819967"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 128, "340282366920938463463374607431768211455"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 255, "1"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", 256, "0"}, - {"-1024", 0, "-1024"}, - {"-1024", 1, "-512"}, - {"-1024", 2, "-256"}, - {"-1024", 10, "-1"}, - {"-1024", 10, "-1"}, - {"-9223372036854775808", 0, "-9223372036854775808"}, - {"-9223372036854775808", 1, "-4611686018427387904"}, - {"-9223372036854775808", 62, "-2"}, - {"-9223372036854775808", 63, "-1"}, - {"-9223372036854775808", 64, "-1"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 0, "-57896044618658097711785492504343953926634992332820282019728792003956564819968"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 1, "-28948022309329048855892746252171976963317496166410141009864396001978282409984"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 253, "-4"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 254, "-2"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 255, "-1"}, - {"-57896044618658097711785492504343953926634992332820282019728792003956564819968", 256, "-1"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Rsh(x, tc.n) - - if got.ToString() != tc.want { - t.Errorf("Rsh(%s, %d) = %v, want %v", tc.x, tc.n, got.ToString(), tc.want) - } - } -} - -func TestLsh(t *testing.T) { - tests := []struct { - x string - n uint - want string - }{ - {"1", 0, "1"}, - {"1", 1, "2"}, - {"1", 2, "4"}, - {"2", 0, "2"}, - {"2", 1, "4"}, - {"2", 2, "8"}, - {"-2", 0, "-2"}, - {"-4", 0, "-4"}, - {"-8", 0, "-8"}, - {"-1", 255, "-57896044618658097711785492504343953926634992332820282019728792003956564819968"}, - {"-1", 256, "0"}, - {"-2", 255, "0"}, - {"-4", 254, "0"}, - {"-8", 253, "0"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - got := New() - got.Lsh(x, tc.n) - - if got.ToString() != tc.want { - t.Errorf("Lsh(%s, %d) = %v, want %v", tc.x, tc.n, got.ToString(), tc.want) - } - } -} diff --git a/contract/p/gnoswap/int256/cmp.gno b/contract/p/gnoswap/int256/cmp.gno deleted file mode 100644 index 5a89177..0000000 --- a/contract/p/gnoswap/int256/cmp.gno +++ /dev/null @@ -1,117 +0,0 @@ -package int256 - -// Eq returns true if z equals x. -// Panics if either z or x is nil. -func (z *Int) Eq(x *Int) bool { - if z == nil || x == nil { - panic("int256: comparing with nil") - } - return (z.neg == x.neg) && z.abs.Eq(x.abs) -} - -// Neq returns true if z does not equal x. -// Panics if either z or x is nil. -func (z *Int) Neq(x *Int) bool { - if z == nil || x == nil { - panic("int256: comparing with nil") - } - return !z.Eq(x) -} - -// Cmp compares z and x and returns -1 if z < x, 0 if z == x, or +1 if z > x. -// Panics if either z or x is nil. -func (z *Int) Cmp(x *Int) (r int) { - if z == nil || x == nil { - panic("int256: comparing with nil") - } - // x cmp y == x cmp y - // x cmp (-y) == x - // (-x) cmp y == y - // (-x) cmp (-y) == -(x cmp y) - switch { - case z == x: - // nothing to do - case z.neg == x.neg: - r = z.abs.Cmp(x.abs) - if z.neg { - r = -r - } - case z.neg: - r = -1 - default: - r = 1 - } - return -} - -// IsZero returns true if z equals 0. -func (z *Int) IsZero() bool { - return z.abs.IsZero() -} - -// IsNeg returns true if z is negative. -func (z *Int) IsNeg() bool { - return z.neg -} - -// Lt returns true if z is less than x. -// Panics if either z or x is nil. -func (z *Int) Lt(x *Int) bool { - if z == nil || x == nil { - panic("int256: comparing with nil") - } - if z.neg { - if x.neg { - return z.abs.Gt(x.abs) - } else { - return true - } - } else { - if x.neg { - return false - } else { - return z.abs.Lt(x.abs) - } - } -} - -// Lte returns true if z is less than or equal to x. -func (z *Int) Lte(x *Int) bool { - return z.Lt(x) || z.Eq(x) -} - -// Gt returns true if z is greater than x. -// Panics if either z or x is nil. -func (z *Int) Gt(x *Int) bool { - if z == nil || x == nil { - panic("int256: comparing with nil") - } - if z.neg { - if x.neg { - return z.abs.Lt(x.abs) - } else { - return false - } - } else { - if x.neg { - return true - } else { - return z.abs.Gt(x.abs) - } - } -} - -// Gte returns true if z is greater than or equal to x. -func (z *Int) Gte(x *Int) bool { - return z.Gt(x) || z.Eq(x) -} - -// Clone creates a new Int identical to z. -func (z *Int) Clone() *Int { - return &Int{z.abs.Clone(), z.neg} -} - -// IsOverflow returns true if z's absolute value has overflowed. -func (z *Int) IsOverflow() bool { - return z.abs.IsOverflow() -} diff --git a/contract/p/gnoswap/int256/cmp_test.gno b/contract/p/gnoswap/int256/cmp_test.gno deleted file mode 100644 index 944424a..0000000 --- a/contract/p/gnoswap/int256/cmp_test.gno +++ /dev/null @@ -1,316 +0,0 @@ -package int256 - -import ( - "testing" -) - -func TestEq(t *testing.T) { - tests := []struct { - x, y string - want bool - }{ - {"0", "0", true}, - {"0", "1", false}, - {"1", "0", false}, - {"-1", "0", false}, - {"0", "-1", false}, - {"1", "1", true}, - {"-1", "-1", true}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Eq(y) - if got != tc.want { - t.Errorf("Eq(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestNeq(t *testing.T) { - tests := []struct { - x, y string - want bool - }{ - {"0", "0", false}, - {"0", "1", true}, - {"1", "0", true}, - {"-1", "0", true}, - {"0", "-1", true}, - {"1", "1", false}, - {"-1", "-1", false}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Neq(y) - if got != tc.want { - t.Errorf("Neq(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestCmp(t *testing.T) { - tests := []struct { - x, y string - want int - }{ - {"0", "0", 0}, - {"0", "1", -1}, - {"1", "0", 1}, - {"-1", "0", -1}, - {"0", "-1", 1}, - {"1", "1", 0}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", 1}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Cmp(y) - if got != tc.want { - t.Errorf("Cmp(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestIsZero(t *testing.T) { - tests := []struct { - x string - want bool - }{ - {"0", true}, - {"-0", true}, - {"1", false}, - {"-1", false}, - {"10", false}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - got := x.IsZero() - if got != tc.want { - t.Errorf("IsZero(%s) = %v, want %v", tc.x, got, tc.want) - } - } -} - -func TestIsNeg(t *testing.T) { - tests := []struct { - x string - want bool - }{ - {"0", false}, - {"-0", true}, // TODO: should this be false? - {"1", false}, - {"-1", true}, - {"10", false}, - {"-10", true}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - got := x.IsNeg() - if got != tc.want { - t.Errorf("IsNeg(%s) = %v, want %v", tc.x, got, tc.want) - } - } -} - -func TestLt(t *testing.T) { - tests := []struct { - x, y string - want bool - }{ - {"0", "0", false}, - {"0", "1", true}, - {"1", "0", false}, - {"-1", "0", true}, - {"0", "-1", false}, - {"1", "1", false}, - {"-1", "-1", false}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", false}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Lt(y) - if got != tc.want { - t.Errorf("Lt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestGt(t *testing.T) { - tests := []struct { - x, y string - want bool - }{ - {"0", "0", false}, - {"0", "1", false}, - {"1", "0", true}, - {"-1", "0", false}, - {"0", "-1", true}, - {"1", "1", false}, - {"-1", "-1", false}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "-115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Gt(y) - if got != tc.want { - t.Errorf("Gt(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestClone(t *testing.T) { - tests := []struct { - x string - }{ - {"0"}, - {"-0"}, - {"1"}, - {"-1"}, - {"10"}, - {"-10"}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - {"-115792089237316195423570985008687907853269984665640564039457584007913129639935"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y := x.Clone() - - if x.Cmp(y) != 0 { - t.Errorf("Clone(%s) = %v, want %v", tc.x, y, x) - } - } -} - -func TestNilChecks(t *testing.T) { - validInt := NewInt(123) - - tests := []struct { - name string - fn func() - wantPanic string - }{ - { - name: "Eq with nil", - fn: func() { validInt.Eq(nil) }, - wantPanic: "int256: comparing with nil", - }, - { - name: "Neq with nil", - fn: func() { validInt.Neq(nil) }, - wantPanic: "int256: comparing with nil", - }, - { - name: "Cmp with nil", - fn: func() { validInt.Cmp(nil) }, - wantPanic: "int256: comparing with nil", - }, - { - name: "Lt with nil", - fn: func() { validInt.Lt(nil) }, - wantPanic: "int256: comparing with nil", - }, - { - name: "Gt with nil", - fn: func() { validInt.Gt(nil) }, - wantPanic: "int256: comparing with nil", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - defer func() { - r := recover() - if r == nil { - t.Errorf("%s: expected panic but got none", tt.name) - return - } - if r.(string) != tt.wantPanic { - t.Errorf("%s: got panic %v, want %v", tt.name, r, tt.wantPanic) - } - }() - - tt.fn() - }) - } -} diff --git a/contract/p/gnoswap/int256/conversion.gno b/contract/p/gnoswap/int256/conversion.gno deleted file mode 100644 index 75527da..0000000 --- a/contract/p/gnoswap/int256/conversion.gno +++ /dev/null @@ -1,120 +0,0 @@ -package int256 - -import ( - "gno.land/p/gnoswap/uint256" -) - -// SetInt64 sets z to x and returns z. -// -// This implementation uses two's complement to handle the edge case of math.MinInt64. -// When x = math.MinInt64 (-2^63), negating it would cause an overflow since 2^63 -// cannot be represented as a positive int64. By converting to uint64 first and then -// applying two's complement (^u + 1) when negative, we correctly handle all int64 -// values including the minimum value without overflow. -func (z *Int) SetInt64(x int64) *Int { - z = z.initiateAbs() - if z.abs == nil { - panic("int256_SetInt64(): abs is nil") - } - u := uint64(x) - neg := x < 0 - if neg { - u = ^u + 1 // |x| = two's complement magnitude - } - z.abs = z.abs.SetUint64(u) - z.neg = neg && u != 0 // prevent -0 - return z -} - -// SetUint64 sets z to x and returns z. -func (z *Int) SetUint64(x uint64) *Int { - z = z.initiateAbs() - - if z.abs == nil { - panic("int256_SetUint64(): abs is nil") - } - z.abs = z.abs.SetUint64(x) - z.neg = false - return z -} - -// Uint64 returns the lower 64 bits of z as a uint64. -func (z *Int) Uint64() uint64 { - return z.abs.Uint64() -} - -// Int64 returns the lower 64 bits of z, interpreted as a signed int64 (two's complement). -// -// Since int64 already uses two's complement representation internally, -// we can simply apply two's complement to the unsigned magnitude when negative and cast -// to int64. This approach correctly handles all edge cases including when the magnitude -// equals 2^63 (which represents math.MinInt64 when negative) without special casing. -func (z *Int) Int64() int64 { - u := z.abs.Uint64() // lower 64 bits of magnitude - if z.neg { - u = ^u + 1 // apply two's complement for negative sign - } - return int64(u) // reinterpret as two's complement int64 -} - -// Neg sets z to -x and returns z. -func (z *Int) Neg(x *Int) *Int { - z.abs.Set(x.abs) - if z.abs.IsZero() { - z.neg = false - } else { - z.neg = !x.neg - } - return z -} - -// NegOverflow sets z to -x and returns z and whether overflow occurred. -// Overflow occurs when negating the minimum int256 value (-2^255). -func (z *Int) NegOverflow(x *Int) (*Int, bool) { - z = z.initiateAbs() - - if x.IsZero() { - z.abs.Clear() - z.neg = false - return z, false - } - - if x.neg && x.abs.Eq(MinInt256().abs) { - z.Set(x) // must preserve the original value - return z, true - } - - z.abs.Set(x.abs) - z.neg = !x.neg - - return z, false -} - -// Set sets z to x and returns z. -func (z *Int) Set(x *Int) *Int { - z.abs.Set(x.abs) - z.neg = x.neg - return z -} - -// SetUint256 sets z to the value of x and returns z. -func (z *Int) SetUint256(x *uint256.Uint) *Int { - z.abs.Set(x) - z.neg = false - return z -} - -// ToString returns the decimal string representation of z. -// Panics if z is nil. -// This differs from the original mempooler int256 implementation. -func (z *Int) ToString() string { - if z == nil { - panic("int256: nil pointer to ToString()") - } - - t := z.abs.Dec() - if z.neg { - return "-" + t - } - return t -} diff --git a/contract/p/gnoswap/int256/conversion_test.gno b/contract/p/gnoswap/int256/conversion_test.gno deleted file mode 100644 index aae2b82..0000000 --- a/contract/p/gnoswap/int256/conversion_test.gno +++ /dev/null @@ -1,540 +0,0 @@ -package int256 - -import ( - "testing" - - "gno.land/p/nt/ufmt" - - "gno.land/p/gnoswap/uint256" -) - -func TestSetInt64(t *testing.T) { - tests := []struct { - x int64 - want string - }{ - {0, "0"}, - {1, "1"}, - {-1, "-1"}, - {9223372036854775807, "9223372036854775807"}, - {-9223372036854775808, "-9223372036854775808"}, - } - - for _, tc := range tests { - var z Int - z.SetInt64(tc.x) - - got := z.ToString() - if got != tc.want { - t.Errorf("SetInt64(%d) = %s, want %s", tc.x, got, tc.want) - } - } -} - -func TestSetInt64MinValueOverflow(t *testing.T) { - const minInt64 = -9223372036854775808 // -2^63 - const maxInt64 = 9223372036854775807 // 2^63 - 1 - - tests := []struct { - name string - x int64 - want string - }{ - { - name: "MinInt64 should not cause overflow", - x: minInt64, - want: "-9223372036854775808", - }, - { - name: "MaxInt64 works correctly", - x: maxInt64, - want: "9223372036854775807", - }, - { - name: "MinInt64 + 1", - x: minInt64 + 1, - want: "-9223372036854775807", - }, - { - name: "Negative one", - x: -1, - want: "-1", - }, - { - name: "Zero", - x: 0, - want: "0", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var z Int - z.SetInt64(tc.x) - - got := z.ToString() - if got != tc.want { - t.Errorf("SetInt64(%d) = %s, want %s", tc.x, got, tc.want) - } - - // Verify the internal representation is correct - if tc.x < 0 { - if !z.neg { - t.Errorf("SetInt64(%d): expected neg=true, got neg=false", tc.x) - } - // Check magnitude for MinInt64 - if tc.x == minInt64 { - expectedMag := uint64(1 << 63) // 2^63 - gotMag := z.abs.Uint64() - if gotMag != expectedMag { - t.Errorf("SetInt64(%d): magnitude = %d, want %d", tc.x, gotMag, expectedMag) - } - } - } else if tc.x > 0 { - if z.neg { - t.Errorf("SetInt64(%d): expected neg=false, got neg=true", tc.x) - } - } else { // tc.x == 0 - if z.neg { - t.Errorf("SetInt64(0): expected neg=false (no -0), got neg=true") - } - } - }) - } -} - -func TestSetUint64(t *testing.T) { - tests := []struct { - x uint64 - want string - }{ - {0, "0"}, - {1, "1"}, - } - - for _, tc := range tests { - var z Int - z.SetUint64(tc.x) - - got := z.ToString() - if got != tc.want { - t.Errorf("SetUint64(%d) = %s, want %s", tc.x, got, tc.want) - } - } -} - -func TestUint64(t *testing.T) { - tests := []struct { - x string - want uint64 - }{ - {"0", 0}, - {"1", 1}, - {"9223372036854775807", 9223372036854775807}, - {"9223372036854775808", 9223372036854775808}, - {"18446744073709551615", 18446744073709551615}, - {"18446744073709551616", 0}, - {"18446744073709551617", 1}, - {"-1", 1}, - {"-18446744073709551615", 18446744073709551615}, - {"-18446744073709551616", 0}, - {"-18446744073709551617", 1}, - } - - for _, tc := range tests { - z := MustFromDecimal(tc.x) - - got := z.Uint64() - if got != tc.want { - t.Errorf("Uint64(%s) = %d, want %d", tc.x, got, tc.want) - } - } -} - -func TestInt64(t *testing.T) { - tests := []struct { - x string - want int64 - }{ - {"0", 0}, - {"1", 1}, - {"-1", -1}, - {"9223372036854775807", 9223372036854775807}, - {"-9223372036854775808", -9223372036854775808}, - {"9223372036854775808", -9223372036854775808}, - {"-9223372036854775809", 9223372036854775807}, - {"18446744073709551616", 0}, - {"18446744073709551617", 1}, - {"18446744073709551615", -1}, - {"-18446744073709551615", 1}, - } - - for _, tc := range tests { - z := MustFromDecimal(tc.x) - - got := z.Int64() - if got != tc.want { - t.Errorf("Int64(%s) = %d, want %d", tc.x, got, tc.want) - } - } -} - -func TestInt64EdgeCases(t *testing.T) { - const minInt64 = -9223372036854775808 // -2^63 - const maxInt64 = 9223372036854775807 // 2^63 - 1 - - tests := []struct { - name string - setupInt func() *Int - want int64 - description string - }{ - { - name: "MinInt64 from SetInt64", - setupInt: func() *Int { - z := new(Int) - return z.SetInt64(minInt64) - }, - want: minInt64, - description: "SetInt64(MinInt64) should round-trip correctly", - }, - { - name: "MaxInt64 from SetInt64", - setupInt: func() *Int { - z := new(Int) - return z.SetInt64(maxInt64) - }, - want: maxInt64, - description: "SetInt64(MaxInt64) should round-trip correctly", - }, - { - name: "Magnitude 2^63 with negative sign", - setupInt: func() *Int { - // Create Int with magnitude = 2^63 and neg = true - z := new(Int) - z.abs = uint256.NewUint(1 << 63) - z.neg = true - return z - }, - want: minInt64, - description: "Magnitude 2^63 with neg=true should return MinInt64", - }, - { - name: "Magnitude 2^63 with positive sign", - setupInt: func() *Int { - // Create Int with magnitude = 2^63 and neg = false - z := new(Int) - z.abs = uint256.NewUint(1 << 63) - z.neg = false - return z - }, - want: minInt64, // Wraps around due to two's complement - description: "Magnitude 2^63 with neg=false wraps to MinInt64", - }, - { - name: "Large positive value wrapping", - setupInt: func() *Int { - // 2^64 - 1 (max uint64) - z := new(Int) - z.abs = uint256.NewUint(18446744073709551615) - z.neg = false - return z - }, - want: -1, - description: "Max uint64 wraps to -1 in int64", - }, - { - name: "Negative large value", - setupInt: func() *Int { - // -(2^64 - 1) - z := new(Int) - z.abs = uint256.NewUint(18446744073709551615) - z.neg = true - return z - }, - want: 1, - description: "-(Max uint64) becomes 1 due to two's complement", - }, - { - name: "Zero", - setupInt: func() *Int { - return Zero() - }, - want: 0, - description: "Zero should return 0", - }, - { - name: "Negative zero prevention", - setupInt: func() *Int { - z := new(Int) - z.abs = uint256.NewUint(0) - z.neg = true // This should be normalized to false - return z - }, - want: 0, - description: "Negative zero should return 0", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - z := tc.setupInt() - got := z.Int64() - - if got != tc.want { - t.Errorf("%s: got %d, want %d", tc.description, got, tc.want) - } - }) - } -} - -// TestInt64RoundTrip verifies that SetInt64 and Int64 work correctly together -func TestInt64RoundTrip(t *testing.T) { - // Test all interesting int64 values - values := []int64{ - 0, 1, -1, - 127, -128, // int8 boundaries - 32767, -32768, // int16 boundaries - 2147483647, -2147483648, // int32 boundaries - 9223372036854775807, -9223372036854775808, // int64 boundaries (MaxInt64, MinInt64) - 1234567890, -1234567890, - } - - for _, v := range values { - t.Run(ufmt.Sprintf("RoundTrip_%d", v), func(t *testing.T) { - z := new(Int) - z.SetInt64(v) - - got := z.Int64() - if got != v { - t.Errorf("Round trip failed: SetInt64(%d).Int64() = %d", v, got) - } - }) - } -} - -func TestNeg(t *testing.T) { - tests := []struct { - x string - want string - }{ - {"0", "0"}, - {"1", "-1"}, - {"-1", "1"}, - {"9223372036854775807", "-9223372036854775807"}, - {"-18446744073709551615", "18446744073709551615"}, - } - - for _, tc := range tests { - z := MustFromDecimal(tc.x) - z.Neg(z) - - got := z.ToString() - if got != tc.want { - t.Errorf("Neg(%s) = %s, want %s", tc.x, got, tc.want) - } - } -} - -func TestInt_NegOverflow(t *testing.T) { - maxInt256 := MaxInt256() - minInt256 := MinInt256() - - negMaxInt256 := New().Neg(maxInt256) - - tests := []struct { - name string - x *Int - wantResult string - wantOverflow bool - }{ - { - name: "negate zero", - x: Zero(), - wantResult: "0", - wantOverflow: false, - }, - { - name: "negate positive", - x: NewInt(100), - wantResult: "-100", - wantOverflow: false, - }, - { - name: "negate negative", - x: NewInt(-100), - wantResult: "100", - wantOverflow: false, - }, - { - name: "negate max_int256", - x: maxInt256, - wantResult: negMaxInt256.ToString(), - wantOverflow: false, - }, - { - name: "negate min_int256", - x: minInt256, - wantResult: minInt256.ToString(), // must preserve the original value - wantOverflow: true, - }, - { - name: "negate (min_int256 + 1)", - x: new(Int).Add(minInt256, One()), - wantResult: new(Int).Sub(maxInt256, Zero()).ToString(), - wantOverflow: false, - }, - { - name: "negate (max_int256 - 1)", - x: MustFromDecimal("57896044618658097711785492504343953926634992332820282019728792003956564819966"), // max_int256 - 1 - wantResult: "-57896044618658097711785492504343953926634992332820282019728792003956564819966", - wantOverflow: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - z := new(Int) - gotResult, gotOverflow := z.NegOverflow(tt.x) - - if gotOverflow != tt.wantOverflow { - t.Errorf("overflow = %v, want %v", gotOverflow, tt.wantOverflow) - return - } - - if gotResult == nil { - t.Error("unexpected nil result") - return - } - - if gotResult.ToString() != tt.wantResult { - // use almost equal comparison to handle the precision issue - diff := new(Int).Sub(gotResult, MustFromDecimal(tt.wantResult)) - if diff.Abs().Cmp(uint256.NewUint(1)) > 0 { - t.Errorf("result = %v, want %v", gotResult.ToString(), tt.wantResult) - } - } - - // double negation test (only if there is no overflow) - if !tt.wantOverflow { - doubleNegResult, doubleNegOverflow := new(Int).NegOverflow(gotResult) - if doubleNegOverflow { - t.Error("unexpected overflow in double negation") - } - if doubleNegResult.ToString() != tt.x.ToString() { - t.Errorf("double negation result = %v, want %v", - doubleNegResult.ToString(), tt.x.ToString()) - } - } - }) - } -} - -func TestSet(t *testing.T) { - tests := []struct { - x string - want string - }{ - {"0", "0"}, - {"1", "1"}, - {"-1", "-1"}, - {"9223372036854775807", "9223372036854775807"}, - {"-18446744073709551615", "-18446744073709551615"}, - } - - for _, tc := range tests { - z := MustFromDecimal(tc.x) - z.Set(z) - - got := z.ToString() - if got != tc.want { - t.Errorf("set(%s) = %s, want %s", tc.x, got, tc.want) - } - } -} - -func TestSetUint256(t *testing.T) { - tests := []struct { - x string - want string - }{ - {"0", "0"}, - {"1", "1"}, - {"9223372036854775807", "9223372036854775807"}, - {"18446744073709551615", "18446744073709551615"}, - } - - for _, tc := range tests { - got := New() - - z := uint256.MustFromDecimal(tc.x) - got.SetUint256(z) - - if got.ToString() != tc.want { - t.Errorf("SetUint256(%s) = %s, want %s", tc.x, got.ToString(), tc.want) - } - } -} - -func TestToString(t *testing.T) { - tests := []struct { - name string - setup func() *Int - expected string - }{ - { - name: "Zero from subtraction", - setup: func() *Int { - minusThree := MustFromDecimal("-3") - three := MustFromDecimal("3") - return Zero().Add(minusThree, three) - }, - expected: "0", - }, - { - name: "Zero from right shift", - setup: func() *Int { - return Zero().Rsh(One(), 1234) - }, - expected: "0", - }, - { - name: "Positive number", - setup: func() *Int { - return MustFromDecimal("42") - }, - expected: "42", - }, - { - name: "Negative number", - setup: func() *Int { - return MustFromDecimal("-42") - }, - expected: "-42", - }, - { - name: "Large positive number", - setup: func() *Int { - return MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") - }, - expected: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, - { - name: "Large negative number", - setup: func() *Int { - return MustFromDecimal("-115792089237316195423570985008687907853269984665640564039457584007913129639935") - }, - expected: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - z := tt.setup() - result := z.ToString() - if result != tt.expected { - t.Errorf("ToString() = %s, want %s", result, tt.expected) - } - }) - } -} diff --git a/contract/p/gnoswap/int256/doc.gno b/contract/p/gnoswap/int256/doc.gno deleted file mode 100644 index 8e1d681..0000000 --- a/contract/p/gnoswap/int256/doc.gno +++ /dev/null @@ -1,13 +0,0 @@ -// Package int256 implements 256-bit signed integer arithmetic for GnoSwap. -// -// This package provides an Int type that represents a 256-bit signed integer -// using two's complement representation. It supports the full range from -// -(2^255) to 2^255-1, with arithmetic operations that detect overflow. -// -// The implementation follows Ethereum's int256 semantics, ensuring compatibility -// for cross-chain DeFi protocols. Operations are optimized for common AMM -// calculations including tick math and price computations. -// -// Critical operations like Add, Sub, and Mul return overflow flags, enabling -// safe handling of edge cases in financial calculations. -package int256 diff --git a/contract/p/gnoswap/int256/gnomod.toml b/contract/p/gnoswap/int256/gnomod.toml deleted file mode 100644 index e5fdea9..0000000 --- a/contract/p/gnoswap/int256/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/p/gnoswap/int256" -gno = "0.9" diff --git a/contract/p/gnoswap/int256/int256.gno b/contract/p/gnoswap/int256/int256.gno deleted file mode 100644 index 3cd68d2..0000000 --- a/contract/p/gnoswap/int256/int256.gno +++ /dev/null @@ -1,182 +0,0 @@ -package int256 - -import ( - "errors" - - "gno.land/p/gnoswap/uint256" -) - -var ( - errEmptyString = errors.New("empty string") - errStringContainsOnlySign = errors.New("string contains only sign") - errInvalidSignInMiddleOfNumber = errors.New("invalid sign in middle of number") -) - -var one = uint256.NewUint(1) - -// Int represents a 256-bit signed integer. -// It uses a sign-magnitude representation with abs storing the absolute value -// and neg indicating whether the number is negative. -type Int struct { - abs *uint256.Uint - neg bool -} - -// Zero returns a new Int set to 0. -func Zero() *Int { - return NewInt(0) -} - -// One returns a new Int set to 1. -func One() *Int { - return NewInt(1) -} - -// Sign returns the sign of x. -// It returns -1 if x < 0, 0 if x == 0, and +1 if x > 0. -func (z *Int) Sign() int { - z.initiateAbs() - - if z.abs.IsZero() { - return 0 - } - if z.neg { - return -1 - } - return 1 -} - -// New returns a new Int set to 0. -func New() *Int { - return &Int{ - abs: new(uint256.Uint), - } -} - -// NewInt allocates and returns a new Int set to x. -func NewInt(x int64) *Int { - return New().SetInt64(x) -} - -// FromDecimal returns a new Int from a decimal string and an error if the string is not valid. -func FromDecimal(s string) (*Int, error) { - return new(Int).SetString(s) -} - -// MustFromDecimal returns a new Int from a decimal string. -// Panics if the string is not a valid decimal. -func MustFromDecimal(s string) *Int { - z, err := FromDecimal(s) - if err != nil { - panic(err) - } - return z -} - -// SetString sets z to the value of s and returns z and an error. -// It uses a bit masking technique for efficient sign character detection, -// based on "Bit Twiddling Hacks" by Sean Eron Anderson. -// This approach provides significantly better performance than string scanning methods. -func (z *Int) SetString(s string) (*Int, error) { - if len(s) == 0 { - return nil, errEmptyString - } - - // check sign only in the first character - neg := false - switch s[0] { - case '+': - s = s[1:] - case '-': - neg = true - s = s[1:] - } - - // check if the string is empty after removing the sign - if len(s) == 0 { - return nil, errStringContainsOnlySign - } - - // Parallel comparison technique for sign detection - // Process in 8-byte chunks - sLen := len(s) - i := 0 - - // Process 8 bytes at a time - for i+7 < sLen { - // Convert 8 bytes into a single uint64 - // This method processes bytes directly, so no endianness issues - // - // access up to s[i+7] is safe, then we can reduce the number of bounds checks - _ = s[i+7] - chunk := uint64(s[i]) | uint64(s[i+1])<<8 - chunk |= uint64(s[i+2])<<16 | uint64(s[i+3])<<24 - chunk |= uint64(s[i+4])<<32 | uint64(s[i+5])<<40 - chunk |= uint64(s[i+6])<<48 | uint64(s[i+7])<<56 - - // Operation to check for '+' (0x2B) - // Subtracting 0x2B from each byte makes '+' bytes become 0 - // Subtracting 0x01 makes bytes in ASCII range (0-127) have 0 in their highest bit - // Therefore, AND with 0x80 to check for zero bytes - plusTest := ((chunk ^ 0x2B2B2B2B2B2B2B2B) - 0x0101010101010101) & 0x8080808080808080 - - // check for '-' (0x2D) - minusTest := ((chunk ^ 0x2D2D2D2D2D2D2D2D) - 0x0101010101010101) & 0x8080808080808080 - - // If either test is non-zero, a sign character exists - if (plusTest | minusTest) != 0 { - return nil, errInvalidSignInMiddleOfNumber - } - - i += 8 - } - - // Process remaining bytes - for ; i < sLen; i++ { - if s[i] == '+' || s[i] == '-' { - return nil, errInvalidSignInMiddleOfNumber - } - } - - abs, err := uint256.FromDecimal(s) - if err != nil { - return nil, err - } - - return &Int{abs, neg}, nil -} - -// FromUint256 creates a new Int from a uint256.Uint. -// It returns nil if the input is nil. -func FromUint256(x *uint256.Uint) *Int { - if x == nil { - return nil - } - z := Zero() - - z.SetUint256(x) - return z -} - -// NilToZero returns z if it's non-nil, otherwise returns a new Int set to 0. -// This differs from the original mempooler int256 implementation. -func (z *Int) NilToZero() *Int { - if z == nil { - return NewInt(0) - } - return z -} - -// initiateAbs ensures z and z.abs are initialized. -// If z is nil, it returns a new Int set to 0. -// If z.abs is nil, it initializes it to a new uint256.Uint. -// This differs from mempooler int256 by also checking if z itself is nil. -func (z *Int) initiateAbs() *Int { - if z == nil { - return NewInt(0) - } - if z.abs == nil { - z.abs = new(uint256.Uint) - } - return z -} diff --git a/contract/p/gnoswap/int256/int256_test.gno b/contract/p/gnoswap/int256/int256_test.gno deleted file mode 100644 index f8e3060..0000000 --- a/contract/p/gnoswap/int256/int256_test.gno +++ /dev/null @@ -1,185 +0,0 @@ -// ported from github.com/mempooler/int256 -package int256 - -import ( - "testing" -) - -func TestSign(t *testing.T) { - tests := []struct { - x string - want int - }{ - {"0", 0}, - {"1", 1}, - {"-1", -1}, - } - - for _, tt := range tests { - z := MustFromDecimal(tt.x) - got := z.Sign() - if got != tt.want { - t.Errorf("Sign(%s) = %d, want %d", tt.x, got, tt.want) - } - } -} - -func TestSetString(t *testing.T) { - tests := []struct { - input string - wantErr bool - wantVal string - wantSign bool - }{ - {"123", false, "123", false}, - {"+123", false, "123", false}, - {"-123", false, "123", true}, - {"9223372036854775807", false, "9223372036854775807", false}, - {"-9223372036854775808", false, "9223372036854775808", true}, - - {"++123", true, "", false}, - {"--123", true, "", false}, - {"+-123", true, "", false}, - {"-+123", true, "", false}, - {"+++123", true, "", false}, - {"---123", true, "", false}, - {"+-+-123", true, "", false}, - {"922337203-6854775807", true, "", false}, - - {"1+23", true, "", false}, - {"1-23", true, "", false}, - {"12+3", true, "", false}, - - // scientific notation not allowed - {"-1e23", true, "", false}, - {"1e-23", true, "", false}, - {"1e+23", true, "", false}, - - {"", true, "", false}, - {"+", true, "", false}, - {"-", true, "", false}, - {"+-", true, "", false}, - } - - for _, tt := range tests { - z, err := new(Int).SetString(tt.input) - - if tt.wantErr { - if err == nil { - t.Errorf("SetString(%q) = %v, want error", tt.input, z) - } - continue - } - - if err != nil { - t.Errorf("SetString(%q) returned unexpected error: %v", tt.input, err) - continue - } - - if got := z.abs.ToString(); got != tt.wantVal { - t.Errorf("SetString(%q).abs = %s, want %s", tt.input, got, tt.wantVal) - } - - if got := z.neg; got != tt.wantSign { - t.Errorf("SetString(%q).neg = %v, want %v", tt.input, got, tt.wantSign) - } - } -} - -func TestInitiateAbs(t *testing.T) { - tests := []struct { - name string - input *Int - wantNil bool - wantZero bool - }{ - { - name: "nil input returns new zero Int", - input: nil, - wantNil: false, - wantZero: true, - }, - { - name: "nil abs field gets initialized", - input: &Int{abs: nil, neg: false}, - wantNil: false, - wantZero: true, - }, - { - name: "existing abs field remains unchanged", - input: NewInt(123), - wantNil: false, - wantZero: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result := tc.input.initiateAbs() - - if result == nil { - t.Error("initiateAbs returned nil") - return - } - - if (result.abs == nil) != tc.wantNil { - t.Errorf("abs field nil status = %v, want %v", result.abs == nil, tc.wantNil) - } - - isZero := result.abs != nil && result.abs.IsZero() - if isZero != tc.wantZero { - t.Errorf("IsZero() = %v, want %v", isZero, tc.wantZero) - } - }) - } -} - -func TestInitiateAbsInOperation(t *testing.T) { - tests := []struct { - name string - setup func() *Int - op func(*Int) *Int - want string - }{ - { - name: "Add with nil receiver", - setup: func() *Int { return nil }, - op: func(z *Int) *Int { return z.Add(NewInt(10), NewInt(20)) }, - want: "30", - }, - { - name: "Add with nil abs field", - setup: func() *Int { return &Int{abs: nil} }, - op: func(z *Int) *Int { return z.Add(NewInt(10), NewInt(20)) }, - want: "30", - }, - { - name: "Sub with nil receiver", - setup: func() *Int { return nil }, - op: func(z *Int) *Int { return z.Sub(NewInt(30), NewInt(20)) }, - want: "10", - }, - { - name: "Sub with nil abs field", - setup: func() *Int { return &Int{abs: nil} }, - op: func(z *Int) *Int { return z.Sub(NewInt(30), NewInt(20)) }, - want: "10", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - z := tc.setup() - result := tc.op(z) - - if result == nil { - t.Error("operation returned nil") - return - } - - if got := result.ToString(); got != tc.want { - t.Errorf("got %v, want %v", got, tc.want) - } - }) - } -} diff --git a/contract/p/gnoswap/rbac/README.md b/contract/p/gnoswap/rbac/README.md deleted file mode 100644 index b0f7860..0000000 --- a/contract/p/gnoswap/rbac/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# RBAC - -Role-Based Access Control package for Gno smart contracts. - -## Overview - -Flexible RBAC system enabling dynamic role and permission management without contract redeployment. - -## Features - -- Dynamic role registration -- Multiple permissions per role -- Declarative role definition -- Custom permission logic -- Runtime updates - -## Core API - -```go -// Create RBAC manager -func New() *RBAC - -// Role management -func (rb *RBAC) RegisterRole(roleName string) error -func (rb *RBAC) DeclareRole(roleName string, opts ...RoleOption) error - -// Permission management -func (rb *RBAC) RegisterPermission(roleName, permissionName string, checker PermissionChecker) error -func (rb *RBAC) UpdatePermission(roleName, permissionName string, newChecker PermissionChecker) error -func (rb *RBAC) RemovePermission(roleName, permissionName string) error - -// Access control -func (rb *RBAC) CheckPermission(roleName, permissionName string, caller Address) error - -// Permission checker type -type PermissionChecker func(caller std.Address) error -``` - -## Usage - -```go -// Create manager -manager := rbac.New() - -// Register role -manager.RegisterRole("admin") - -// Add permission -adminChecker := func(caller std.Address) error { - if caller != adminAddr { - return errors.New("not admin") - } - return nil -} -manager.RegisterPermission("admin", "access", adminChecker) - -// Declarative role setup -manager.DeclareRole("editor", - rbac.WithPermission("edit", editorChecker)) - -// Check access -err := manager.CheckPermission("admin", "access", caller) -``` - -## Architecture - -``` -Client → RBAC → Role → PermissionChecker - ↓ ↓ ↓ - Manager Storage Validation -``` - -## Security - -- No direct address-to-role mapping -- Custom validation logic per permission -- Runtime permission updates -- Isolated permission checks \ No newline at end of file diff --git a/contract/p/gnoswap/rbac/doc.gno b/contract/p/gnoswap/rbac/doc.gno deleted file mode 100644 index eafa943..0000000 --- a/contract/p/gnoswap/rbac/doc.gno +++ /dev/null @@ -1,139 +0,0 @@ -// Package rbac provides a flexible, upgradeable Role-Based Access Control (RBAC) -// system for Gno smart contracts and related applications. It decouples authorization -// logic from fixed addresses, enabling dynamic registration, update, and removal of roles -// and permissions. -// -// ## Overview -// -// The RBAC package encapsulates a manager that maintains an internal registry of roles. -// Each role is defined by a unique name and a set of permissions. A permission is -// represented by a `PermissionChecker` function that validates whether a given caller -// (`std.Address`) satisfies the required access conditions. -// -// Key components of this package include: -// -// 1. **Role**: Represents a role with a name and a collection of permission-checking functions. -// 2. **PermissionChecker**: A function type defined as `func(caller std.Address) error`, -// used to verify access for a given permission. -// 3. **RBAC Manager**: The core type (RBAC) that encapsulates role registration, permission -// assignment, verification, updating, and removal. -// -// ## Key Features -// -// - **Dynamic Role Management**: Roles can be registered, and permissions can be assigned -// or updated at runtime without requiring contract redeployment. -// - **Multiple Permissions per Role**: A single role can have multiple permissions, -// each with its own validation logic. -// - **Declarative Role Definition**: The package supports a Functional Option pattern, -// allowing roles and their permissions to be defined declaratively via functions like -// `DeclareRole` and `WithPermission`. -// - **Encapsulation**: Internal state (roles registry) is encapsulated within the RBAC -// manager, preventing unintended external modifications. -// - **Flexible Validation**: Permission checkers can implement custom logic, supporting -// arbitrary access control policies. -// -// ## Workflow -// -// Typical usage of the RBAC package includes the following steps: -// -// 1. **Initialization**: Create a new RBAC manager using `NewRBAC()`. -// 2. **Role Registration**: Register roles using `RegisterRole` or declaratively with -// `DeclareRole`. -// 3. **Permission Assignment**: Add permissions to roles using `RegisterPermission` or the -// `WithPermission` option during role declaration. -// 4. **Permission Verification**: Validate access by invoking `CheckPermission` with the -// role name, permission name, and the caller's address (std.Address). -// -// ## Example Usage -// -// The following example demonstrates how to use the RBAC package in both traditional and -// declarative styles: -// -// ```gno -// package main -// -// import ( -// -// "std" -// -// "gno.land/p/gnoswap/rbac" -// "gno.land/p/nt/ufmt" -// -// ) -// -// func main() { -// // Create a new RBAC manager -// manager := rbac.NewRBAC() -// -// // Define example addresses -// adminAddr := std.Address("admin") -// userAddr := std.Address("user") -// -// // --- Traditional Role Registration --- -// // Register an "admin" role -// if err := manager.RegisterRole("admin"); err != nil { -// panic(err) -// } -// -// // Register an "access" permission for the "admin" role. -// // The checker verifies that the caller matches adminAddr. -// adminChecker := func(caller std.Address) error { -// if caller != adminAddr { -// return ufmt.Errorf("caller %s is not admin", caller) -// } -// return nil -// } -// if err := manager.RegisterPermission("admin", "access", adminChecker); err != nil { -// panic(err) -// } -// -// // --- Declarative Role Registration --- -// // Register an "editor" role with a "modify" permission using the Functional Option pattern. -// editorChecker := func(caller std.Address) error { -// if caller != userAddr { -// return ufmt.Errorf("caller %s is not editor", caller) -// } -// return nil -// } -// if err := manager.DeclareRole("editor", rbac.WithPermission("modify", editorChecker)); err != nil { -// panic(err) -// } -// -// // --- Permission Check --- -// // Check if adminAddr has the "access" permission on the "admin" role. -// if err := manager.CheckPermission("admin", "access", adminAddr); err != nil { -// println("Access denied for admin:", err) -// } else { -// println("Admin access granted") -// } -// } -// -// ``` -// -// ## Error Handling -// -// The package reports errors using the ufmt.Errorf function. Typical errors include: -// -// - Registering a role that already exists. -// - Attempting to register a permission for a non-existent role. -// - Verifying a permission that does not exist on a role. -// - Failing a permission check due to a caller not meeting the required conditions. -// -// ## Limitations and Considerations -// -// - This RBAC implementation does not directly map addresses to roles; instead, it verifies -// the caller against permission-checking functions registered for a role. -// - Address validation relies on the logic provided within each PermissionChecker. Ensure that -// your checkers properly validate `std.Address` values (which follow the Bech32 format). -// - The encapsulated RBAC manager is designed to minimize external mutation, but integrating it -// with other modules may require additional mapping between addresses and roles. -// -// # Notes -// -// - The RBAC system is designed to be upgradeable, enabling contracts to modify permission -// logic without redeploying the entire contract. -// - Both imperative and declarative styles are supported, providing flexibility to developers. -// -// Package rbac is intended for use in Gno smart contracts and other systems requiring dynamic, -// upgradeable access control mechanisms. -package rbac diff --git a/contract/p/gnoswap/rbac/errors.gno b/contract/p/gnoswap/rbac/errors.gno deleted file mode 100644 index 990d654..0000000 --- a/contract/p/gnoswap/rbac/errors.gno +++ /dev/null @@ -1,18 +0,0 @@ -package rbac - -import ( - "errors" -) - -var ( - ErrNoPendingOwner = errors.New("no pending owner") - ErrUnauthorized = errors.New("caller is not owner") - ErrPendingUnauthorized = errors.New("caller is not pending owner") - ErrInvalidAddress = errors.New("invalid address") - - ErrInvalidRoleName = errors.New("invalid role name") - ErrRoleDoesNotExist = errors.New("role does not exist") - ErrRoleAlreadyExists = errors.New("role already exists") - ErrCannotRegisterSystemRole = errors.New("cannot register system role") - ErrCannotRemoveSystemRole = errors.New("cannot remove system role") -) diff --git a/contract/p/gnoswap/rbac/gnomod.toml b/contract/p/gnoswap/rbac/gnomod.toml deleted file mode 100644 index ec2120e..0000000 --- a/contract/p/gnoswap/rbac/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/p/gnoswap/rbac" -gno = "0.9" diff --git a/contract/p/gnoswap/rbac/ownable.gno b/contract/p/gnoswap/rbac/ownable.gno deleted file mode 100644 index fa3be7f..0000000 --- a/contract/p/gnoswap/rbac/ownable.gno +++ /dev/null @@ -1,110 +0,0 @@ -package rbac - -import ( - "std" -) - -const ( - OwnershipTransferEvent = "OwnershipTransfer" - OwnershipTransferStartedEvent = "OwnershipTransferStarted" -) - -// Ownable2Step implements a two-step ownership transfer mechanism. -// It requires the new owner to explicitly accept ownership before the transfer is completed, -// preventing accidental transfers to incorrect addresses. -type Ownable2Step struct { - owner std.Address - pendingOwner std.Address -} - -// newOwnable2StepWithAddress creates a new Ownable2Step instance with addr as owner. -func newOwnable2StepWithAddress(addr std.Address) *Ownable2Step { - return &Ownable2Step{ - owner: addr, - pendingOwner: "", - } -} - -// TransferOwnership initiates ownership transfer by setting newOwner as pending owner. -// The new owner must call AcceptOwnership to complete the transfer. -// Only the current owner can call this function. -func (o *Ownable2Step) TransferOwnership(newOwner std.Address) error { - if !o.OwnedByOriginCaller() { - return ErrUnauthorized - } - - if !newOwner.IsValid() { - return ErrInvalidAddress - } - - o.pendingOwner = newOwner - - std.Emit( - OwnershipTransferStartedEvent, - "from", o.owner.String(), - "to", newOwner.String(), - ) - - return nil -} - -// AcceptOwnership completes the ownership transfer. -// Must be called by the pending owner. Panics if no pending owner exists or caller is not the pending owner. -func (o *Ownable2Step) AcceptOwnership() error { - if o.pendingOwner.String() == "" { - return ErrNoPendingOwner - } - - if std.OriginCaller() != o.pendingOwner { - return ErrPendingUnauthorized - } - - prevOwner := o.owner - o.owner = o.pendingOwner - o.pendingOwner = "" - - std.Emit( - OwnershipTransferEvent, - "from", prevOwner.String(), - "to", o.owner.String(), - ) - - return nil -} - -// DropOwnership removes the owner, disabling all owner-only actions. -// When used at the top level, it disables all owner-only functions. -// When embedded, it acts like a burn function, removing ownership from the struct. -// Only the current owner can call this function. -func (o *Ownable2Step) DropOwnership() error { - if !o.OwnedByOriginCaller() { - return ErrUnauthorized - } - - prevOwner := o.owner - o.owner = "" - o.pendingOwner = "" - - std.Emit( - OwnershipTransferEvent, - "from", prevOwner.String(), - "to", "", - ) - - return nil -} - -// Owner returns the current owner address. Returns empty address if ownership has been dropped. -func (o *Ownable2Step) Owner() std.Address { - return o.owner -} - -// PendingOwner returns the pending owner address during ownership transfer. Returns empty address if no transfer is pending. -func (o *Ownable2Step) PendingOwner() std.Address { - return o.pendingOwner -} - -// OwnedByOriginCaller returns true if the origin caller is the current owner. -func (o *Ownable2Step) OwnedByOriginCaller() bool { - return std.OriginCaller() == o.owner -} diff --git a/contract/p/gnoswap/rbac/ownable_test.gno b/contract/p/gnoswap/rbac/ownable_test.gno deleted file mode 100644 index 6544148..0000000 --- a/contract/p/gnoswap/rbac/ownable_test.gno +++ /dev/null @@ -1,175 +0,0 @@ -package rbac - -import ( - "std" - "testing" - - "gno.land/p/nt/testutils" - "gno.land/p/nt/uassert" - "gno.land/p/nt/urequire" -) - -var ( - alice = testutils.TestAddress("alice") - bob = testutils.TestAddress("bob") -) - -func TestNewWithAddress(t *testing.T) { - o := newOwnable2StepWithAddress(alice) - - got := o.Owner() - pendingOwner := o.PendingOwner() - - uassert.Equal(t, got, alice) - uassert.Equal(t, pendingOwner.String(), "") -} - -func TestInitiateTransferOwnership(t *testing.T) { - testing.SetOriginCaller(alice) - - o := newOwnable2StepWithAddress(alice) - - err := o.TransferOwnership(bob) - urequire.NoError(t, err) - - owner := o.Owner() - pendingOwner := o.PendingOwner() - - uassert.Equal(t, owner, alice) - uassert.Equal(t, pendingOwner, bob) -} - -func TestTransferOwnership(t *testing.T) { - testing.SetOriginCaller(alice) - - o := newOwnable2StepWithAddress(alice) - - err := o.TransferOwnership(bob) - urequire.NoError(t, err) - - owner := o.Owner() - pendingOwner := o.PendingOwner() - - uassert.Equal(t, owner, alice) - uassert.Equal(t, pendingOwner, bob) - - testing.SetOriginCaller(bob) - - err = o.AcceptOwnership() - urequire.NoError(t, err) - - owner = o.Owner() - pendingOwner = o.PendingOwner() - - uassert.Equal(t, owner, bob) - uassert.Equal(t, pendingOwner.String(), "") -} - -func TestOwnedByOriginCaller(t *testing.T) { - testing.SetOriginCaller(alice) - - o := newOwnable2StepWithAddress(alice) - unauthorizedCaller := bob - testing.SetOriginCaller(unauthorizedCaller) - uassert.False(t, o.OwnedByOriginCaller()) -} - -func TestOwnedByOriginCallerUnauthorized(t *testing.T) { - testing.SetOriginCaller(alice) - testing.SetRealm(std.NewUserRealm(alice)) - - var o *Ownable2Step - func() { - testing.SetRealm(std.NewCodeRealm("gno.land/r/test/test")) - o = newOwnable2StepWithAddress(alice) - }() - - uassert.True(t, o.OwnedByOriginCaller()) - - unauthorizedCaller := bob - testing.SetRealm(std.NewUserRealm(unauthorizedCaller)) - uassert.False(t, o.OwnedByOriginCaller()) -} - -func TestDropOwnership(t *testing.T) { - testing.SetOriginCaller(alice) - - o := New() - - err := o.DropOwnership() - urequire.NoError(t, err, "DropOwnership failed") - - owner := o.Owner() - uassert.Empty(t, owner, "owner should be empty") -} - -func TestDropOwnershipVulnerability(t *testing.T) { - testing.SetOriginCaller(alice) - o := New() - - // alice initiates ownership transfer to bob - err := o.TransferOwnership(bob) - urequire.NoError(t, err) - - // alice drops ownership while ownership transfer is pending - err = o.DropOwnership() - urequire.NoError(t, err) - - owner := o.Owner() - pendingOwner := o.PendingOwner() - uassert.Empty(t, owner, "owner should be empty") - uassert.Empty(t, pendingOwner.String(), "pending owner should be empty") - - // verify bob can no longer claim ownership - testing.SetOriginCaller(bob) - err = o.AcceptOwnership() - uassert.ErrorContains(t, err, ErrNoPendingOwner.Error()) -} - -// Errors - -func TestErrUnauthorized(t *testing.T) { - testing.SetOriginCaller(alice) - - o := New() - - testing.SetOriginCaller(bob) - - uassert.ErrorContains(t, o.TransferOwnership(alice), ErrUnauthorized.Error()) - uassert.ErrorContains(t, o.DropOwnership(), ErrUnauthorized.Error()) -} - -func TestErrInvalidAddress(t *testing.T) { - testing.SetOriginCaller(alice) - - o := New() - - err := o.TransferOwnership("") - uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) - - err = o.TransferOwnership("10000000001000000000100000000010000000001000000000") - uassert.ErrorContains(t, err, ErrInvalidAddress.Error()) -} - -func TestErrNoPendingOwner(t *testing.T) { - testing.SetOriginCaller(alice) - - o := New() - - err := o.AcceptOwnership() - uassert.ErrorContains(t, err, ErrNoPendingOwner.Error()) -} - -func TestErrPendingUnauthorized(t *testing.T) { - testing.SetOriginCaller(alice) - - o := New() - - err := o.TransferOwnership(bob) - urequire.NoError(t, err) - - testing.SetOriginCaller(alice) - - err = o.AcceptOwnership() - uassert.ErrorContains(t, err, ErrPendingUnauthorized.Error()) -} diff --git a/contract/p/gnoswap/rbac/rbac.gno b/contract/p/gnoswap/rbac/rbac.gno deleted file mode 100644 index 03cf341..0000000 --- a/contract/p/gnoswap/rbac/rbac.gno +++ /dev/null @@ -1,146 +0,0 @@ -package rbac - -import ( - "std" -) - -// RBAC encapsulates and manages roles and their permissions. -// It combines role management with two-step ownership transfer functionality. -type RBAC struct { - ownable *Ownable2Step - // roles maps role names to their respective `Role` objects - roles map[string]*Role -} - -// New creates a new RBAC instance with the origin caller as owner. -func New() *RBAC { - return &RBAC{ - ownable: newOwnable2StepWithAddress(std.OriginCaller()), - roles: make(map[string]*Role), - } -} - -// NewRBACWithAddress creates a new RBAC instance with addr as owner. -func NewRBACWithAddress(addr std.Address) *RBAC { - return &RBAC{ - ownable: newOwnable2StepWithAddress(addr), - roles: make(map[string]*Role), - } -} - -// IsAuthorized checks if addr has the specified roleName. Returns false if the role does not exist. -func (rb *RBAC) IsAuthorized(roleName string, addr std.Address) bool { - role, exists := rb.roles[roleName] - if !exists { - return false - } - - return role.IsAuthorized(addr) -} - -// RegisterRole registers a new role with roleName. Returns ErrRoleAlreadyExists if the role already exists. -func (rb *RBAC) RegisterRole(roleName string) error { - if rb.existsRole(roleName) { - return ErrRoleAlreadyExists - } - - rb.roles[roleName] = NewRole(roleName) - - return nil -} - -// RemoveRole removes roleName from the RBAC system. Returns ErrRoleDoesNotExist if the role doesn't exist or ErrCannotRemoveSystemRole for system roles. -func (rb *RBAC) RemoveRole(roleName string) error { - if !rb.existsRole(roleName) { - return ErrRoleDoesNotExist - } - - // Check if it's a system role - if IsSystemRole(roleName) { - return ErrCannotRemoveSystemRole - } - - // Simply delete the role since permissions are no longer managed here - delete(rb.roles, roleName) - - return nil -} - -// GetRoleAddresses returns a map of all role names to their assigned addresses. -func (rb *RBAC) GetRoleAddresses() map[string]std.Address { - addresses := make(map[string]std.Address) - - for role := range rb.roles { - addresses[role] = rb.roles[role].Address() - } - - return addresses -} - -// GetRoleAddress returns the address assigned to roleName. Returns ErrRoleDoesNotExist if the role doesn't exist. -func (rb *RBAC) GetRoleAddress(roleName string) (std.Address, error) { - role, exists := rb.roles[roleName] - if !exists { - return "", ErrRoleDoesNotExist - } - - return role.Address(), nil -} - -// UpdateRoleAddress assigns addr to roleName. Returns ErrRoleDoesNotExist if the role doesn't exist or ErrInvalidAddress if addr is invalid. -func (rb *RBAC) UpdateRoleAddress(roleName string, addr std.Address) error { - role, exists := rb.roles[roleName] - if !exists { - return ErrRoleDoesNotExist - } - - if !addr.IsValid() { - return ErrInvalidAddress - } - - role.setAddress(addr) - - return nil -} - -// RemoveRoleAddress removes the address assignment from roleName. Only the owner can call this function. Returns ErrUnauthorized if caller is not owner or ErrRoleDoesNotExist if role doesn't exist. -func (rb *RBAC) RemoveRoleAddress(roleName string) error { - if !rb.ownable.OwnedByOriginCaller() { - return ErrUnauthorized - } - - role, exists := rb.roles[roleName] - if !exists { - return ErrRoleDoesNotExist - } - - role.setAddress("") - - return nil -} - -// Owner returns the current owner address. -func (rb *RBAC) Owner() std.Address { return rb.ownable.Owner() } - -// OwnedByOriginCaller returns true if the origin caller is the current owner. -func (rb *RBAC) OwnedByOriginCaller() bool { return rb.ownable.OwnedByOriginCaller() } - -// PendingOwner returns the pending owner address during ownership transfer. -func (rb *RBAC) PendingOwner() std.Address { return rb.ownable.PendingOwner() } - -// AcceptOwnership completes the ownership transfer process. Must be called by the pending owner. -func (rb *RBAC) AcceptOwnership() error { return rb.ownable.AcceptOwnership() } - -// DropOwnership removes the owner, effectively disabling owner-only actions. Only the current owner can call this function. -func (rb *RBAC) DropOwnership() error { return rb.ownable.DropOwnership() } - -// TransferOwnership initiates the two-step ownership transfer process to newOwner. Only the current owner can call this function. -func (rb *RBAC) TransferOwnership(newOwner std.Address) error { - return rb.ownable.TransferOwnership(newOwner) -} - -// existsRole checks if name exists in the RBAC system. -func (rb *RBAC) existsRole(name string) bool { - _, exists := rb.roles[name] - return exists -} diff --git a/contract/p/gnoswap/rbac/rbac_test.gno b/contract/p/gnoswap/rbac/rbac_test.gno deleted file mode 100644 index 4573caa..0000000 --- a/contract/p/gnoswap/rbac/rbac_test.gno +++ /dev/null @@ -1,72 +0,0 @@ -package rbac - -import ( - "std" - "testing" - - "gno.land/p/nt/testutils" - "gno.land/p/nt/uassert" - "gno.land/p/nt/ufmt" -) - -var ( - adminRole = "admin" - editorRole = "editor" - - adminAddr = testutils.TestAddress(adminRole) - userAddr = testutils.TestAddress("user") - editorAddr = testutils.TestAddress(editorRole) -) - -func adminChecker(caller std.Address) error { - if caller != adminAddr { - return ufmt.Errorf("caller is not admin") - } - return nil -} - -func editorChecker(caller std.Address) error { - if caller != editorAddr { - return ufmt.Errorf("caller is not editor") - } - return nil -} - -func TestRegisterRole(t *testing.T) { - manager := New() - - err := manager.RegisterRole(adminRole) - uassert.NoError(t, err) - - err = manager.RegisterRole(adminRole) - uassert.Error(t, err) -} - -func TestRemoveRole(t *testing.T) { - manager := New() - - t.Run("success remove non-system role", func(t *testing.T) { - // First register role and permission - err := manager.RegisterRole(editorRole) - uassert.NoError(t, err) - - // Remove role - err = manager.RemoveRole(editorRole) - uassert.NoError(t, err) - }) - - t.Run("fail to remove non-existent role", func(t *testing.T) { - err := manager.RemoveRole("non_existent_role") - uassert.Error(t, err) - }) - - t.Run("fail to remove system role", func(t *testing.T) { - // Register system role - err := manager.RegisterRole(adminRole) - uassert.NoError(t, err) - - // Try to remove system role - err = manager.RemoveRole(adminRole) - uassert.Error(t, err) - }) -} diff --git a/contract/p/gnoswap/rbac/role.gno b/contract/p/gnoswap/rbac/role.gno deleted file mode 100644 index 46c82de..0000000 --- a/contract/p/gnoswap/rbac/role.gno +++ /dev/null @@ -1,35 +0,0 @@ -package rbac - -import ( - "std" -) - -// Role represents a role with a name and an assigned address. -type Role struct { - // name represents the role's identifier - name string - address string -} - -// NewRole creates a new Role instance with roleName. -func NewRole(roleName string) *Role { - return &Role{ - name: roleName, - address: "", - } -} - -// Name returns the role's name. -func (r *Role) Name() string { return r.name } - -// Address returns the address assigned to this role. Returns empty address if no address is assigned. -func (r *Role) Address() std.Address { return std.Address(r.address) } - -// IsEmpty returns true if no address is assigned to this role. -func (r *Role) IsEmpty() bool { return r.address == "" } - -// IsAuthorized returns true if addr matches the role's assigned address. -func (r *Role) IsAuthorized(addr std.Address) bool { return r.address == addr.String() } - -// setAddress assigns addr to this role. -func (r *Role) setAddress(addr std.Address) { r.address = addr.String() } diff --git a/contract/p/gnoswap/rbac/types.gno b/contract/p/gnoswap/rbac/types.gno deleted file mode 100644 index 135b0ab..0000000 --- a/contract/p/gnoswap/rbac/types.gno +++ /dev/null @@ -1,54 +0,0 @@ -package rbac - -// SystemRole represents a predefined system role that cannot be removed. -type SystemRole string - -var ( - ROLE_ADMIN SystemRole = "admin" - ROLE_DEVOPS SystemRole = "devops" - ROLE_COMMUNITY_POOL SystemRole = "community_pool" - ROLE_GOVERNANCE SystemRole = "governance" - ROLE_GOV_STAKER SystemRole = "gov_staker" - ROLE_XGNS SystemRole = "xgns" - ROLE_POOL SystemRole = "pool" - ROLE_POSITION SystemRole = "position" - ROLE_ROUTER SystemRole = "router" - ROLE_STAKER SystemRole = "staker" - ROLE_EMISSION SystemRole = "emission" - ROLE_LAUNCHPAD SystemRole = "launchpad" - ROLE_PROTOCOL_FEE SystemRole = "protocol_fee" -) - -var systemRoleNames = map[string]SystemRole{ - "admin": ROLE_ADMIN, - "devops": ROLE_DEVOPS, - "community_pool": ROLE_COMMUNITY_POOL, - "governance": ROLE_GOVERNANCE, - "gov_staker": ROLE_GOV_STAKER, - "xgns": ROLE_XGNS, - "pool": ROLE_POOL, - "position": ROLE_POSITION, - "router": ROLE_ROUTER, - "staker": ROLE_STAKER, - "emission": ROLE_EMISSION, - "launchpad": ROLE_LAUNCHPAD, - "protocol_fee": ROLE_PROTOCOL_FEE, -} - -// String returns the string representation of the SystemRole. -// Returns "Unknown" if the role is not a valid system role. -func (r SystemRole) String() string { - roleName := string(r) - if _, ok := systemRoleNames[roleName]; !ok { - return "Unknown" - } - - return roleName -} - -// IsSystemRole returns true if roleName is a system role. -func IsSystemRole(roleName string) bool { - _, ok := systemRoleNames[roleName] - - return ok -} diff --git a/contract/p/gnoswap/uint256/LICENSE b/contract/p/gnoswap/uint256/LICENSE deleted file mode 100644 index 505e432..0000000 --- a/contract/p/gnoswap/uint256/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -BSD 3-Clause License - -Copyright 2020 uint256 Authors - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/contract/p/gnoswap/uint256/README.md b/contract/p/gnoswap/uint256/README.md deleted file mode 100644 index e290ec8..0000000 --- a/contract/p/gnoswap/uint256/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# uint256 - -256-bit unsigned integer arithmetic for GnoSwap. - -## Overview - -Fixed-size 256-bit unsigned integer library optimized for AMM calculations with overflow detection and precise MulDiv operations. - -## Features - -- Fixed 256-bit size (4 uint64 values) -- Overflow detection on all operations -- Optimized MulDiv for precise calculations -- String conversion (decimal, hex, binary) -- Range: 0 to 2^256-1 - -## Usage - -```go -import u256 "gno.land/p/gnoswap/uint256" - -// Create values -a := u256.NewUint(1000) -b := u256.MustFromDecimal("1000000000000000000") - -// Arithmetic with overflow detection -result, overflow := new(u256.Uint).AddOverflow(a, b) -if overflow { - // Handle overflow -} - -// Precise MulDiv (a * b / c) -result := u256.MulDiv(a, b, c) -``` - -## Credits - -Ported from [holiman/uint256](https://github.com/holiman/uint256) \ No newline at end of file diff --git a/contract/p/gnoswap/uint256/_helper_test.gno b/contract/p/gnoswap/uint256/_helper_test.gno deleted file mode 100644 index 59d90e7..0000000 --- a/contract/p/gnoswap/uint256/_helper_test.gno +++ /dev/null @@ -1,52 +0,0 @@ -package uint256 - -import ( - "testing" -) - -func shouldEQ(t *testing.T, got, expected any) { - if got != expected { - t.Errorf("got %v, expected %v", got, expected) - } -} - -func shouldNEQ(t *testing.T, got, expected any) { - if got == expected { - t.Errorf("got %v, didn't expected %v", got, expected) - } -} - -func shouldPanic(t *testing.T, f func()) { - defer func() { - if r := recover(); r == nil { - t.Errorf("expected panic") - } - }() - f() -} - -func shouldPanicWithMsg(t *testing.T, f func(), msg string) { - defer func() { - if r := recover(); r == nil { - t.Errorf("The code did not panic") - } else { - if r != msg { - t.Errorf("excepted panic(%v), got(%v)", msg, r) - } - } - }() - f() -} - -// for original tests -func parseUint(s string) *Uint { - if len(s) >= 2 && s[:2] == "0x" { - return MustFromHex(s) - } - return MustFromDecimal(s) -} - -// for testing.T -func parseUintT(t *testing.T, s string) *Uint { - return parseUint(s) -} diff --git a/contract/p/gnoswap/uint256/arithmetic.gno b/contract/p/gnoswap/uint256/arithmetic.gno deleted file mode 100644 index ca67a03..0000000 --- a/contract/p/gnoswap/uint256/arithmetic.gno +++ /dev/null @@ -1,538 +0,0 @@ -// arithmetic provides arithmetic operations for Uint objects. -// This includes basic binary operations such as addition, subtraction, multiplication, division, and modulo operations -// as well as overflow checks, and negation. These functions are essential for numeric -// calculations using 256-bit unsigned integers. -package uint256 - -import ( - "math/bits" -) - -// Add sets z to the sum x+y and returns z. -func (z *Uint) Add(x, y *Uint) *Uint { - var carry uint64 - z.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0) - z.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry) - z.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry) - z.arr[3], _ = bits.Add64(x.arr[3], y.arr[3], carry) - return z -} - -// AddOverflow sets z to the sum x+y and returns z and true if overflow occurred. -func (z *Uint) AddOverflow(x, y *Uint) (*Uint, bool) { - var carry uint64 - z.arr[0], carry = bits.Add64(x.arr[0], y.arr[0], 0) - z.arr[1], carry = bits.Add64(x.arr[1], y.arr[1], carry) - z.arr[2], carry = bits.Add64(x.arr[2], y.arr[2], carry) - z.arr[3], carry = bits.Add64(x.arr[3], y.arr[3], carry) - return z, carry != 0 -} - -// Sub sets z to the difference x-y and returns z. -func (z *Uint) Sub(x, y *Uint) *Uint { - var carry uint64 - z.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0) - z.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry) - z.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry) - z.arr[3], _ = bits.Sub64(x.arr[3], y.arr[3], carry) - return z -} - -// SubOverflow sets z to the difference x-y and returns z and true if underflow occurred. -func (z *Uint) SubOverflow(x, y *Uint) (*Uint, bool) { - var carry uint64 - z.arr[0], carry = bits.Sub64(x.arr[0], y.arr[0], 0) - z.arr[1], carry = bits.Sub64(x.arr[1], y.arr[1], carry) - z.arr[2], carry = bits.Sub64(x.arr[2], y.arr[2], carry) - z.arr[3], carry = bits.Sub64(x.arr[3], y.arr[3], carry) - return z, carry != 0 -} - -// Neg returns -x mod 2^256. -func (z *Uint) Neg(x *Uint) *Uint { - return z.Sub(Zero(), x) -} - -// Mul sets z to the product x*y and returns z. -func (z *Uint) Mul(x, y *Uint) *Uint { - var ( - res Uint - carry uint64 - res1, res2, res3 uint64 - ) - - carry, res.arr[0] = bits.Mul64(x.arr[0], y.arr[0]) - carry, res1 = umulHop(carry, x.arr[1], y.arr[0]) - carry, res2 = umulHop(carry, x.arr[2], y.arr[0]) - res3 = x.arr[3]*y.arr[0] + carry - - carry, res.arr[1] = umulHop(res1, x.arr[0], y.arr[1]) - carry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry) - res3 = res3 + x.arr[2]*y.arr[1] + carry - - carry, res.arr[2] = umulHop(res2, x.arr[0], y.arr[2]) - res3 = res3 + x.arr[1]*y.arr[2] + carry - - res.arr[3] = res3 + x.arr[0]*y.arr[3] - - return z.Set(&res) -} - -// MulOverflow sets z to the product x*y and returns z and true if overflow occurred. -func (z *Uint) MulOverflow(x, y *Uint) (*Uint, bool) { - p := umul(x, y) - copy(z.arr[:], p[:4]) - return z, (p[4] | p[5] | p[6] | p[7]) != 0 -} - -// Div sets z to the quotient x/y and returns z. -// If y == 0, z is set to 0. -func (z *Uint) Div(x, y *Uint) *Uint { - if y.IsZero() || y.Gt(x) { - return z.Clear() - } - if x.Eq(y) { - return z.SetOne() - } - // Shortcut some cases - if x.IsUint64() { - return z.SetUint64(x.Uint64() / y.Uint64()) - } - - // At this point, we know - // x/y ; x > y > 0 - - var quot Uint - udivrem(quot.arr[:], x.arr[:], y) - return z.Set(") -} - -// Mod sets z to the modulus x%y for y != 0 and returns z. -// If y == 0, z is set to 0 (this differs from big.Int behavior). -func (z *Uint) Mod(x, y *Uint) *Uint { - if x.IsZero() || y.IsZero() { - return z.Clear() - } - switch x.Cmp(y) { - case -1: - // x < y - copy(z.arr[:], x.arr[:]) - return z - case 0: - // x == y - return z.Clear() // They are equal - } - - // At this point: - // x != 0 - // y != 0 - // x > y - - // Shortcut trivial case - if x.IsUint64() { - return z.SetUint64(x.Uint64() % y.Uint64()) - } - - var quot Uint - *z = udivrem(quot.arr[:], x.arr[:], y) - return z -} - -// MulMod sets z to (x * y) mod m and returns z. -// If m == 0, z is set to 0 (this differs from big.Int behavior). -func (z *Uint) MulMod(x, y, m *Uint) *Uint { - if x.IsZero() || y.IsZero() || m.IsZero() { - return z.Clear() - } - p := umul(x, y) - - if m.arr[3] != 0 { - mu := Reciprocal(m) - r := reduce4(p, m, mu) - return z.Set(&r) - } - - var ( - pl = Uint{arr: [4]uint64{p[0], p[1], p[2], p[3]}} - ph = Uint{arr: [4]uint64{p[4], p[5], p[6], p[7]}} - ) - - // If the multiplication is within 256 bits use Mod(). - if ph.IsZero() { - return z.Mod(&pl, m) - } - - var quot [8]uint64 - rem := udivrem(quot[:], p[:], m) - return z.Set(&rem) -} - -// DivMod sets z to the quotient x/y and m to the modulus x%y, returning the pair (z, m). -// If y == 0, both z and m are set to 0 (this differs from big.Int behavior). -func (z *Uint) DivMod(x, y, m *Uint) (*Uint, *Uint) { - if y.IsZero() { - return z.Clear(), m.Clear() - } - - switch x.Cmp(y) { - case -1: - // x < y - return z.Clear(), m.Set(x) - case 0: - // x == y - return z.SetOne(), m.Clear() - } - - // At this point: - // x != 0 - // y != 0 - // x > y - - // Shortcut trivial case - if x.IsUint64() { - x0, y0 := x.Uint64(), y.Uint64() - return z.SetUint64(x0 / y0), m.SetUint64(x0 % y0) - } - - var quot Uint - *m = udivrem(quot.arr[:], x.arr[:], y) - *z = quot - return z, m -} - -// Exp sets z to base**exponent mod 2**256 and returns z. -// The result is wrapped at 2^256 boundary. -func (z *Uint) Exp(base, exponent *Uint) *Uint { - res := Uint{arr: [4]uint64{1, 0, 0, 0}} - multiplier := *base - expBitLen := exponent.BitLen() - - curBit := 0 - word := exponent.arr[0] - for ; curBit < expBitLen && curBit < 64; curBit++ { - if word&1 == 1 { - res.Mul(&res, &multiplier) - } - multiplier.squared() - word >>= 1 - } - - word = exponent.arr[1] - for ; curBit < expBitLen && curBit < 128; curBit++ { - if word&1 == 1 { - res.Mul(&res, &multiplier) - } - multiplier.squared() - word >>= 1 - } - - word = exponent.arr[2] - for ; curBit < expBitLen && curBit < 192; curBit++ { - if word&1 == 1 { - res.Mul(&res, &multiplier) - } - multiplier.squared() - word >>= 1 - } - - word = exponent.arr[3] - for ; curBit < expBitLen && curBit < 256; curBit++ { - if word&1 == 1 { - res.Mul(&res, &multiplier) - } - multiplier.squared() - word >>= 1 - } - return z.Set(&res) -} - -func (z *Uint) squared() { - var ( - res Uint - carry0, carry1, carry2 uint64 - res1, res2 uint64 - ) - - carry0, res.arr[0] = bits.Mul64(z.arr[0], z.arr[0]) - carry0, res1 = umulHop(carry0, z.arr[0], z.arr[1]) - carry0, res2 = umulHop(carry0, z.arr[0], z.arr[2]) - - carry1, res.arr[1] = umulHop(res1, z.arr[0], z.arr[1]) - carry1, res2 = umulStep(res2, z.arr[1], z.arr[1], carry1) - - carry2, res.arr[2] = umulHop(res2, z.arr[0], z.arr[2]) - - res.arr[3] = 2*(z.arr[0]*z.arr[3]+z.arr[1]*z.arr[2]) + carry0 + carry1 + carry2 - - z.Set(&res) -} - -// udivrem divides u by d and produces both quotient and remainder. -// The quotient is stored in provided quot - len(u)-len(d)+1 words. -// It loosely follows the Knuth's division algorithm (sometimes referenced as "schoolbook" division) using 64-bit words. -// See Knuth, Volume 2, section 4.3.1, Algorithm D. -func udivrem(quot, u []uint64, d *Uint) (rem Uint) { - var dLen int - for i := len(d.arr) - 1; i >= 0; i-- { - if d.arr[i] != 0 { - dLen = i + 1 - break - } - } - - shift := uint(bits.LeadingZeros64(d.arr[dLen-1])) - - var dnStorage Uint - dn := dnStorage.arr[:dLen] - for i := dLen - 1; i > 0; i-- { - dn[i] = (d.arr[i] << shift) | (d.arr[i-1] >> (64 - shift)) - } - dn[0] = d.arr[0] << shift - - var uLen int - for i := len(u) - 1; i >= 0; i-- { - if u[i] != 0 { - uLen = i + 1 - break - } - } - - if uLen < dLen { - copy(rem.arr[:], u) - return rem - } - - var unStorage [9]uint64 - un := unStorage[:uLen+1] - un[uLen] = u[uLen-1] >> (64 - shift) - for i := uLen - 1; i > 0; i-- { - un[i] = (u[i] << shift) | (u[i-1] >> (64 - shift)) - } - un[0] = u[0] << shift - - // TODO: Skip the highest word of numerator if not significant. - - if dLen == 1 { - r := udivremBy1(quot, un, dn[0]) - rem.SetUint64(r >> shift) - return rem - } - - udivremKnuth(quot, un, dn) - - for i := 0; i < dLen-1; i++ { - rem.arr[i] = (un[i] >> shift) | (un[i+1] << (64 - shift)) - } - rem.arr[dLen-1] = un[dLen-1] >> shift - - return rem -} - -// umul computes full 256 x 256 -> 512 multiplication. -func umul(x, y *Uint) [8]uint64 { - var ( - res [8]uint64 - carry, carry4, carry5, carry6 uint64 - res1, res2, res3, res4, res5 uint64 - ) - - carry, res[0] = bits.Mul64(x.arr[0], y.arr[0]) - carry, res1 = umulHop(carry, x.arr[1], y.arr[0]) - carry, res2 = umulHop(carry, x.arr[2], y.arr[0]) - carry4, res3 = umulHop(carry, x.arr[3], y.arr[0]) - - carry, res[1] = umulHop(res1, x.arr[0], y.arr[1]) - carry, res2 = umulStep(res2, x.arr[1], y.arr[1], carry) - carry, res3 = umulStep(res3, x.arr[2], y.arr[1], carry) - carry5, res4 = umulStep(carry4, x.arr[3], y.arr[1], carry) - - carry, res[2] = umulHop(res2, x.arr[0], y.arr[2]) - carry, res3 = umulStep(res3, x.arr[1], y.arr[2], carry) - carry, res4 = umulStep(res4, x.arr[2], y.arr[2], carry) - carry6, res5 = umulStep(carry5, x.arr[3], y.arr[2], carry) - - carry, res[3] = umulHop(res3, x.arr[0], y.arr[3]) - carry, res[4] = umulStep(res4, x.arr[1], y.arr[3], carry) - carry, res[5] = umulStep(res5, x.arr[2], y.arr[3], carry) - res[7], res[6] = umulStep(carry6, x.arr[3], y.arr[3], carry) - - return res -} - -// umulStep computes (hi * 2^64 + lo) = z + (x * y) + carry. -func umulStep(z, x, y, carry uint64) (hi, lo uint64) { - hi, lo = bits.Mul64(x, y) - lo, carry = bits.Add64(lo, carry, 0) - hi, _ = bits.Add64(hi, 0, carry) - lo, carry = bits.Add64(lo, z, 0) - hi, _ = bits.Add64(hi, 0, carry) - return hi, lo -} - -// umulHop computes (hi * 2^64 + lo) = z + (x * y) -func umulHop(z, x, y uint64) (hi, lo uint64) { - hi, lo = bits.Mul64(x, y) - lo, carry := bits.Add64(lo, z, 0) - hi, _ = bits.Add64(hi, 0, carry) - return hi, lo -} - -// udivremBy1 divides u by single normalized word d and produces both quotient and remainder. -// The quotient is stored in provided quot. -func udivremBy1(quot, u []uint64, d uint64) (rem uint64) { - reciprocal := reciprocal2by1(d) - rem = u[len(u)-1] // Set the top word as remainder. - for j := len(u) - 2; j >= 0; j-- { - quot[j], rem = udivrem2by1(rem, u[j], d, reciprocal) - } - return rem -} - -// udivremKnuth implements the division of u by normalized multiple word d from the Knuth's division algorithm. -// The quotient is stored in provided quot - len(u)-len(d) words. -// Updates u to contain the remainder - len(d) words. -func udivremKnuth(quot, u, d []uint64) { - dh := d[len(d)-1] - dl := d[len(d)-2] - reciprocal := reciprocal2by1(dh) - - for j := len(u) - len(d) - 1; j >= 0; j-- { - u2 := u[j+len(d)] - u1 := u[j+len(d)-1] - u0 := u[j+len(d)-2] - - var qhat, rhat uint64 - if u2 >= dh { // Division overflows. - qhat = ^uint64(0) - // TODO: Add "qhat one to big" adjustment (not needed for correctness, but helps avoiding "add back" case). - } else { - qhat, rhat = udivrem2by1(u2, u1, dh, reciprocal) - ph, pl := bits.Mul64(qhat, dl) - if ph > rhat || (ph == rhat && pl > u0) { - qhat-- - // TODO: Add "qhat one to big" adjustment (not needed for correctness, but helps avoiding "add back" case). - } - } - - // Multiply and subtract. - borrow := subMulTo(u[j:], d, qhat) - u[j+len(d)] = u2 - borrow - if u2 < borrow { // Too much subtracted, add back. - qhat-- - u[j+len(d)] += addTo(u[j:], d) - } - - quot[j] = qhat // Store quotient digit. - } -} - -// isBitSet returns true if bit n-th is set, where n = 0 is LSB. -// The n must be <= 255. -func (z *Uint) isBitSet(n uint) bool { - return (z.arr[n/64] & (1 << (n % 64))) != 0 -} - -func (z *Uint) IsOverflow() bool { - return z.isBitSet(255) -} - -// addTo computes x += y. -// Requires len(x) >= len(y). -func addTo(x, y []uint64) uint64 { - var carry uint64 - for i := 0; i < len(y); i++ { - x[i], carry = bits.Add64(x[i], y[i], carry) - } - return carry -} - -// subMulTo computes x -= y * multiplier. -// Requires len(x) >= len(y). -func subMulTo(x, y []uint64, multiplier uint64) uint64 { - var borrow uint64 - for i := 0; i < len(y); i++ { - s, carry1 := bits.Sub64(x[i], borrow, 0) - ph, pl := bits.Mul64(y[i], multiplier) - t, carry2 := bits.Sub64(s, pl, 0) - x[i] = t - borrow = ph + carry1 + carry2 - } - return borrow -} - -// reciprocal2by1 computes <^d, ^0> / d. -func reciprocal2by1(d uint64) uint64 { - reciprocal, _ := bits.Div64(^d, ^uint64(0), d) - return reciprocal -} - -// udivrem2by1 divides / d and produces both quotient and remainder. -// It uses the provided d's reciprocal. -// Implementation ported from https://github.com/chfast/intx and is based on -// "Improved division by invariant integers", Algorithm 4. -func udivrem2by1(uh, ul, d, reciprocal uint64) (quot, rem uint64) { - qh, ql := bits.Mul64(reciprocal, uh) - ql, carry := bits.Add64(ql, ul, 0) - qh, _ = bits.Add64(qh, uh, carry) - qh++ - - r := ul - qh*d - - if r > ql { - qh-- - r += d - } - - if r >= d { - qh++ - r -= d - } - - return qh, r -} - -// MustDiv sets z to the quotient x/y and returns z. -// It panics if y == 0. Used in critical AMM paths where division by zero represents a programming error. -func (z *Uint) MustDiv(x, y *Uint) *Uint { - if y.IsZero() { - panic("division by zero") - } - return z.Div(x, y) -} - -// MustMod sets z to the modulus x%y and returns z. -// It panics if y == 0. Used in critical AMM paths where modulo by zero represents a programming error. -func (z *Uint) MustMod(x, y *Uint) *Uint { - if y.IsZero() { - panic("modulo by zero") - } - return z.Mod(x, y) -} - -// MustMulMod sets z to (x * y) mod m and returns z. -// It panics if m == 0. Used in critical AMM paths where modulo by zero represents a programming error. -func (z *Uint) MustMulMod(x, y, m *Uint) *Uint { - if m.IsZero() { - panic("modulo by zero") - } - return z.MulMod(x, y, m) -} - -// MustDivMod sets z to the quotient x/y and m to the modulus x%y, returning the pair (z, m). -// It panics if y == 0. Used in critical AMM paths where division by zero represents a programming error. -func (z *Uint) MustDivMod(x, y, m *Uint) (*Uint, *Uint) { - if y.IsZero() { - panic("division by zero") - } - return z.DivMod(x, y, m) -} - -// MustMul sets z to the product x*y and returns z. -// It panics on overflow. Used in critical AMM calculations where overflow represents a programming error. -func (z *Uint) MustMul(x, y *Uint) *Uint { - result, overflow := z.MulOverflow(x, y) - if overflow { - panic("uint256: multiplication overflow") - } - return result -} diff --git a/contract/p/gnoswap/uint256/arithmetic_test.gno b/contract/p/gnoswap/uint256/arithmetic_test.gno deleted file mode 100644 index 3e0c7ed..0000000 --- a/contract/p/gnoswap/uint256/arithmetic_test.gno +++ /dev/null @@ -1,1883 +0,0 @@ -package uint256 - -import ( - "testing" -) - -// TestAdd verifies addition operations -func TestAdd(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - }{ - // Basic addition - { - name: "zero_plus_zero", - x: "0", - y: "0", - want: "0", - }, - { - name: "zero_plus_one", - x: "0", - y: "1", - want: "1", - }, - { - name: "one_plus_one", - x: "1", - y: "1", - want: "2", - }, - { - name: "ten_plus_ten", - x: "10", - y: "10", - want: "20", - }, - - // uint64 boundary - { - name: "uint64_max_plus_one", - x: "18446744073709551615", - y: "1", - want: "18446744073709551616", - }, - { - name: "uint64_max_plus_uint64_max", - x: "18446744073709551615", - y: "18446744073709551615", - want: "36893488147419103230", - }, - - // Carry propagation - { - name: "carry_through_single_word", - x: "0xFFFFFFFFFFFFFFFF", - y: "1", - want: "0x10000000000000000", - }, - { - name: "carry_through_multiple_words", - x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - y: "1", - want: "0x100000000000000000000000000000000", - }, - - // Maximum value edge cases - { - name: "max_plus_zero", - x: MAX_UINT256, - y: "0", - want: MAX_UINT256, - }, - { - name: "max_plus_one_wraps_to_zero", - x: MAX_UINT256, - y: "1", - want: "0", - }, - { - name: "half_max_plus_half_max_plus_one", - x: "57896044618658097711785492504343953926634992332820282019728792003956564819967", - y: "57896044618658097711785492504343953926634992332820282019728792003956564819968", - want: MAX_UINT256, - }, - - // Additional boundary cases - { - name: "max_minus_one_plus_one", - x: "115792089237316195423570985008687907853269984665640564039457584007913129639934", - y: "1", - want: MAX_UINT256, - }, - { - name: "max_minus_one_plus_two", - x: "115792089237316195423570985008687907853269984665640564039457584007913129639934", - y: "2", - want: "0", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUintT(t, tc.x) - y := parseUintT(t, tc.y) - want := parseUintT(t, tc.want) - - got := new(Uint).Add(x, y) - if !got.Eq(want) { - t.Errorf("Add(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - }) - } -} - -// TestAddOverflow verifies addition with overflow detection -func TestAddOverflow(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - overflow bool - }{ - { - name: "no_overflow_small_numbers", - x: "100", - y: "200", - want: "300", - overflow: false, - }, - { - name: "no_overflow_at_boundary", - x: MAX_UINT256, - y: "0", - want: MAX_UINT256, - overflow: false, - }, - { - name: "overflow_max_plus_one", - x: MAX_UINT256, - y: "1", - want: "0", - overflow: true, - }, - { - name: "overflow_max_plus_max", - x: MAX_UINT256, - y: MAX_UINT256, - want: "115792089237316195423570985008687907853269984665640564039457584007913129639934", - overflow: true, - }, - { - name: "overflow_with_carry_propagation", - x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - y: "1", - want: "0", - overflow: true, - }, - { - name: "no_overflow_half_max_times_two", - x: "57896044618658097711785492504343953926634992332820282019728792003956564819967", - y: "57896044618658097711785492504343953926634992332820282019728792003956564819967", - want: "115792089237316195423570985008687907853269984665640564039457584007913129639934", - overflow: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - want := parseUint(tc.want) - - got, overflow := new(Uint).AddOverflow(x, y) - if !got.Eq(want) { - t.Errorf("AddOverflow(%s, %s) result = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - if overflow != tc.overflow { - t.Errorf("AddOverflow(%s, %s) overflow = %v, want %v", tc.x, tc.y, overflow, tc.overflow) - } - }) - } -} - -// TestSub verifies subtraction operations -func TestSub(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - }{ - // Basic subtraction - { - name: "one_minus_zero", - x: "1", - y: "0", - want: "1", - }, - { - name: "one_minus_one", - x: "1", - y: "1", - want: "0", - }, - { - name: "thousand_minus_hundred", - x: "31337", - y: "1337", - want: "30000", - }, - - // Underflow cases (wraps around) - { - name: "zero_minus_one_wraps_to_max", - x: "0", - y: "1", - want: MAX_UINT256, - }, - { - name: "small_minus_large", - x: "2", - y: "3", - want: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, - - // Maximum value cases - { - name: "max_minus_zero", - x: MAX_UINT256, - y: "0", - want: MAX_UINT256, - }, - { - name: "max_minus_max", - x: MAX_UINT256, - y: MAX_UINT256, - want: "0", - }, - - // Borrow propagation - { - name: "borrow_propagation_single_word", - x: "0x10000000000000000", - y: "1", - want: "0xFFFFFFFFFFFFFFFF", - }, - { - name: "borrow_propagation_multiple_words", - x: "0x100000000000000000000000000000000", - y: "1", - want: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - }, - { - name: "borrow_propagation_all_words", - x: "0", // Start with 0 and add 1, then subtract 1 to test borrow - y: "1", - want: MAX_UINT256, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - want := parseUint(tc.want) - - got := new(Uint).Sub(x, y) - if !got.Eq(want) { - t.Errorf("Sub(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - }) - } -} - -// TestSubOverflow verifies subtraction with underflow detection -func TestSubOverflow(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - underflow bool - }{ - { - name: "no_underflow_normal_case", - x: "1000", - y: "100", - want: "900", - underflow: false, - }, - { - name: "no_underflow_equal_values", - x: "12345", - y: "12345", - want: "0", - underflow: false, - }, - { - name: "underflow_zero_minus_one", - x: "0", - y: "1", - want: MAX_UINT256, - underflow: true, - }, - { - name: "underflow_small_minus_large", - x: "100", - y: "1000", - want: "115792089237316195423570985008687907853269984665640564039457584007913129639036", - underflow: true, - }, - { - name: "underflow_one_minus_max", - x: "1", - y: MAX_UINT256, - want: "2", - underflow: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - want := parseUint(tc.want) - - got, underflow := new(Uint).SubOverflow(x, y) - if !got.Eq(want) { - t.Errorf("SubOverflow(%s, %s) result = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - if underflow != tc.underflow { - t.Errorf("SubOverflow(%s, %s) underflow = %v, want %v", tc.x, tc.y, underflow, tc.underflow) - } - }) - } -} - -// TestMul verifies multiplication operations -func TestMul(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - }{ - // Basic multiplication - { - name: "zero_times_zero", - x: "0", - y: "0", - want: "0", - }, - { - name: "one_times_one", - x: "1", - y: "1", - want: "1", - }, - { - name: "ten_times_ten", - x: "10", - y: "10", - want: "100", - }, - - // uint64 overflow but not uint256 - { - name: "uint64_max_times_two", - x: "18446744073709551615", - y: "2", - want: "36893488147419103230", - }, - { - name: "large_numbers_no_overflow", - x: "0xFFFFFFFFFFFFFFFF", - y: "0xFFFFFFFFFFFFFFFF", - want: "0xFFFFFFFFFFFFFFFE0000000000000001", - }, - - // Maximum value cases - { - name: "max_times_zero", - x: MAX_UINT256, - y: "0", - want: "0", - }, - { - name: "max_times_one", - x: MAX_UINT256, - y: "1", - want: MAX_UINT256, - }, - - // Q128 related (sqrt of max uint256) - { - name: "q128_minus_one_squared", - x: "340282366920938463463374607431768211455", - y: "340282366920938463463374607431768211455", - want: "115792089237316195423570985008687907852589419931798687112530834793049593217025", - }, - - // Q96 related (Uniswap V3 price) - { - name: "q96_squared", - x: "79228162514264337593543950336", - y: "79228162514264337593543950336", - want: "6277101735386680763835789423207666416102355444464034512896", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - want := parseUint(tc.want) - - got := new(Uint).Mul(x, y) - if !got.Eq(want) { - t.Errorf("Mul(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - }) - } -} - -// TestMulOverflow verifies multiplication with overflow detection -func TestMulOverflow(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - overflow bool - }{ - { - name: "no_overflow_small_numbers", - x: "1000", - y: "2000", - want: "2000000", - overflow: false, - }, - { - name: "no_overflow_at_boundary", - x: "340282366920938463463374607431768211455", - y: "340282366920938463463374607431768211455", - want: "115792089237316195423570985008687907852589419931798687112530834793049593217025", - overflow: false, - }, - { - name: "overflow_q128_squared", - x: "340282366920938463463374607431768211456", - y: "340282366920938463463374607431768211456", - want: "0", - overflow: true, - }, - { - name: "overflow_max_times_two", - x: MAX_UINT256, - y: "2", - want: "115792089237316195423570985008687907853269984665640564039457584007913129639934", - overflow: true, - }, - { - name: "overflow_max_times_max", - x: MAX_UINT256, - y: MAX_UINT256, - want: "1", - overflow: true, - }, - { - name: "no_overflow_large_numbers", - x: "340282366920938463463374607431768211455", // 2^128 - 1 - y: "340282366920938463463374607431768211456", // 2^128 - want: "115792089237316195423570985008687907852929702298719625575994209400481361428480", - overflow: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - want := parseUint(tc.want) - - got, overflow := new(Uint).MulOverflow(x, y) - if !got.Eq(want) { - t.Errorf("MulOverflow(%s, %s) result = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - if overflow != tc.overflow { - t.Errorf("MulOverflow(%s, %s) overflow = %v, want %v", tc.x, tc.y, overflow, tc.overflow) - } - }) - } -} - -// TestDiv verifies division operations -func TestDiv(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - wantErr bool - }{ - // Basic division - { - name: "ten_div_two", - x: "10", - y: "2", - want: "5", - wantErr: false, - }, - { - name: "exact_division", - x: "31337", - y: "3", - want: "10445", - wantErr: false, - }, - { - name: "division_with_remainder", - x: "10", - y: "3", - want: "3", - wantErr: false, - }, - - // Special cases - { - name: "zero_div_nonzero", - x: "0", - y: "31337", - want: "0", - wantErr: false, - }, - { - name: "smaller_div_larger", - x: "2", - y: "31337", - want: "0", - wantErr: false, - }, - { - name: "equal_div_equal", - x: "31337", - y: "31337", - want: "1", - wantErr: false, - }, - - // Division by zero - { - name: "div_by_zero", - x: "31337", - y: "0", - want: "", - wantErr: true, - }, - { - name: "zero_div_zero", - x: "0", - y: "0", - want: "", - wantErr: true, - }, - - // Large numbers - { - name: "max_div_one", - x: MAX_UINT256, - y: "1", - want: MAX_UINT256, - wantErr: false, - }, - { - name: "max_div_two", - x: MAX_UINT256, - y: "2", - want: "57896044618658097711785492504343953926634992332820282019728792003956564819967", - wantErr: false, - }, - { - name: "max_div_max", - x: MAX_UINT256, - y: MAX_UINT256, - want: "1", - wantErr: false, - }, - { - name: "large_div_large", - x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - y: "0xFFFFFFFFFFFFFFFF", - want: "0x10000000000000001", - wantErr: false, - }, - - // Verify truncation (rounds down) - { - name: "verify_truncation", - x: "1000000000000000001", - y: "1000000000000000000", - want: "1", - wantErr: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - - got := new(Uint).Div(x, y) - - if tc.wantErr { - // Division by zero returns 0 - if !got.IsZero() { - t.Errorf("Div(%s, %s) expected zero for division by zero", tc.x, tc.y) - } - return - } - - want := parseUint(tc.want) - if !got.Eq(want) { - t.Errorf("Div(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - }) - } -} - -// TestMod verifies modulo operations -func TestMod(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - wantErr bool - }{ - // Basic modulo - { - name: "ten_mod_three", - x: "10", - y: "3", - want: "1", - wantErr: false, - }, - { - name: "exact_division_mod", - x: "100", - y: "10", - want: "0", - wantErr: false, - }, - { - name: "large_mod_calculation", - x: "31337", - y: "3", - want: "2", - wantErr: false, - }, - - // Special cases - { - name: "zero_mod_nonzero", - x: "0", - y: "31337", - want: "0", - wantErr: false, - }, - { - name: "smaller_mod_larger", - x: "2", - y: "31337", - want: "2", - wantErr: false, - }, - { - name: "equal_mod_equal", - x: "31337", - y: "31337", - want: "0", - wantErr: false, - }, - - // Modulo by zero - { - name: "mod_by_zero", - x: "31337", - y: "0", - want: "", - wantErr: true, - }, - { - name: "zero_mod_zero", - x: "0", - y: "0", - want: "", - wantErr: true, - }, - - // Large numbers - { - name: "max_mod_small", - x: MAX_UINT256, - y: "1000000", - want: "639935", - wantErr: false, - }, - { - name: "power_of_two_mod", - x: "0xFFFFFFFFFFFFFFFF", - y: "0x10000000000000000", - want: "0xFFFFFFFFFFFFFFFF", - wantErr: false, - }, - { - name: "max_mod_large_prime", - x: MAX_UINT256, - y: "115792089237316195423570985008687907853269984665640564039457584007913129639747", // Large prime - want: "188", - wantErr: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - - got := new(Uint).Mod(x, y) - - if tc.wantErr { - // Modulo by zero returns 0 - if !got.IsZero() { - t.Errorf("Mod(%s, %s) expected zero for modulo by zero", tc.x, tc.y) - } - return - } - - want := parseUint(tc.want) - if !got.Eq(want) { - t.Errorf("Mod(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - }) - } -} - -// TestDivMod verifies combined division and modulo operations -func TestDivMod(t *testing.T) { - tests := []struct { - name string - x string - y string - wantDiv string - wantMod string - wantErr bool - }{ - // Basic cases - { - name: "simple_divmod", - x: "10", - y: "3", - wantDiv: "3", - wantMod: "1", - wantErr: false, - }, - { - name: "exact_divmod", - x: "100", - y: "10", - wantDiv: "10", - wantMod: "0", - wantErr: false, - }, - { - name: "large_divmod", - x: "31337", - y: "3", - wantDiv: "10445", - wantMod: "2", - wantErr: false, - }, - - // Special cases - { - name: "zero_divmod_nonzero", - x: "0", - y: "31337", - wantDiv: "0", - wantMod: "0", - wantErr: false, - }, - { - name: "smaller_divmod_larger", - x: "2", - y: "31337", - wantDiv: "0", - wantMod: "2", - wantErr: false, - }, - { - name: "equal_divmod_equal", - x: "31337", - y: "31337", - wantDiv: "1", - wantMod: "0", - wantErr: false, - }, - - // Division by zero - { - name: "divmod_by_zero", - x: "31337", - y: "0", - wantDiv: "", - wantMod: "", - wantErr: true, - }, - - // Large numbers - { - name: "max_divmod_small", - x: MAX_UINT256, - y: "1000000", - wantDiv: "115792089237316195423570985008687907853269984665640564039457584007913129", - wantMod: "639935", - wantErr: false, - }, - - // Power of two optimization cases - { - name: "divmod_by_2", - x: "31337", - y: "2", - wantDiv: "15668", - wantMod: "1", - wantErr: false, - }, - { - name: "divmod_by_4", - x: "31337", - y: "4", - wantDiv: "7834", - wantMod: "1", - wantErr: false, - }, - { - name: "divmod_by_256", - x: "0xABCDEF1234567890", - y: "0x100", - wantDiv: "0xABCDEF12345678", - wantMod: "0x90", - wantErr: false, - }, - { - name: "divmod_by_2_64", - x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - y: "0x10000000000000000", // 2^64 - wantDiv: "0xFFFFFFFFFFFFFFFF", - wantMod: "0xFFFFFFFFFFFFFFFF", - wantErr: false, - }, - { - name: "max_divmod_power_of_two", - x: MAX_UINT256, - y: "0x10000000000000000", // 2^64 - wantDiv: "6277101735386680763835789423207666416102355444464034512895", - wantMod: "0xFFFFFFFFFFFFFFFF", - wantErr: false, - }, - { - name: "divmod_by_2_128", - x: MAX_UINT256, - y: "340282366920938463463374607431768211456", // 2^128 - wantDiv: "340282366920938463463374607431768211455", - wantMod: "340282366920938463463374607431768211455", - wantErr: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - - gotDiv := new(Uint) - gotMod := new(Uint) - gotDiv, gotMod = gotDiv.DivMod(x, y, gotMod) - - if tc.wantErr { - // Division by zero returns (0, 0) - if !gotDiv.IsZero() || !gotMod.IsZero() { - t.Errorf("DivMod(%s, %s) expected (0, 0) for division by zero", tc.x, tc.y) - } - return - } - - wantDiv := parseUint(tc.wantDiv) - wantMod := parseUint(tc.wantMod) - - if !gotDiv.Eq(wantDiv) { - t.Errorf("DivMod(%s, %s) div = %s, want %s", tc.x, tc.y, gotDiv.ToString(), tc.wantDiv) - } - if !gotMod.Eq(wantMod) { - t.Errorf("DivMod(%s, %s) mod = %s, want %s", tc.x, tc.y, gotMod.ToString(), tc.wantMod) - } - }) - } -} - -// TestMulMod verifies modular multiplication operations -func TestMulMod(t *testing.T) { - tests := []struct { - name string - x string - y string - m string - want string - wantErr bool - }{ - // Basic cases - { - name: "simple_mulmod", - x: "10", - y: "20", - m: "7", - want: "4", // (10 * 20) % 7 = 200 % 7 = 4 - wantErr: false, - }, - { - name: "exact_mulmod", - x: "10", - y: "20", - m: "200", - want: "0", - wantErr: false, - }, - - // Edge cases - { - name: "zero_times_any_mod", - x: "0", - y: "12345", - m: "100", - want: "0", - wantErr: false, - }, - { - name: "one_times_any_mod", - x: "1", - y: "12345", - m: "100", - want: "45", - wantErr: false, - }, - { - name: "mulmod_by_zero", - x: "10", - y: "20", - m: "0", - want: "", - wantErr: true, - }, - - // Fast path: small product - { - name: "fast_path_small_product", - x: "1000000", - y: "2000000", - m: "999999", - want: "2", // (1000000 * 2000000) % 999999 = 2 - wantErr: false, - }, - { - name: "fast_path_exact_256_bits", - x: "0x1FFFFFFFFFFFFFFFF", // Just over 2^64 - y: "2", - m: "0xFFFFFFFFFFFFFFFF", // 2^64 - 1 - want: "2", - wantErr: false, - }, - - // Large numbers that would overflow in regular multiplication - { - name: "large_mulmod_no_overflow", - x: "340282366920938463463374607431768211456", // 2^128 - y: "340282366920938463463374607431768211456", - m: MAX_UINT256, - want: "1", - wantErr: false, - }, - - // Reduce4 path: large modulus - { - name: "reduce4_path_large_modulus", - x: MAX_UINT256, - y: MAX_UINT256, - m: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 - want: "1", - wantErr: false, - }, - { - name: "reduce4_path_exact_division", - x: "340282366920938463463374607431768211456", - y: "340282366920938463463374607431768211456", - m: "115792089237316195423570985008687907852589419931798687112530834793049593217024", - want: "680564733841876926926749214863536422912", - wantErr: false, - }, - - // Q96 price calculation (corrected) - { - name: "q96_price_calculation", - x: "79228162514264337593543950336", // 2^96 - y: "1000000000000000000", // 1e18 - m: "79228162514264337593543950335", // 2^96 - 1 - want: "1000000000000000000", // Corrected from 1000000000000000001 - wantErr: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - m := parseUint(tc.m) - - got := new(Uint).MulMod(x, y, m) - - if tc.wantErr { - // MulMod by zero returns 0 - if !got.IsZero() { - t.Errorf("MulMod(%s, %s, %s) expected zero for modulo by zero", tc.x, tc.y, tc.m) - } - return - } - - want := parseUint(tc.want) - if !got.Eq(want) { - t.Errorf("MulMod(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.m, got.ToString(), tc.want) - } - }) - } -} - -// TestNeg verifies negation operations -func TestNeg(t *testing.T) { - tests := []struct { - name string - x string - want string - }{ - { - name: "neg_zero", - x: "0", - want: "0", - }, - { - name: "neg_one", - x: "1", - want: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, - { - name: "neg_two", - x: "2", - want: "115792089237316195423570985008687907853269984665640564039457584007913129639934", - }, - { - name: "neg_large", - x: "31337", - want: "115792089237316195423570985008687907853269984665640564039457584007913129608599", - }, - { - name: "neg_neg_large", - x: "115792089237316195423570985008687907853269984665640564039457584007913129608599", - want: "31337", - }, - { - name: "neg_max", - x: MAX_UINT256, - want: "1", - }, - { - name: "neg_half", - x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 - want: "57896044618658097711785492504343953926634992332820282019728792003956564819968", - }, - { - name: "neg_max_minus_one", - x: "115792089237316195423570985008687907853269984665640564039457584007913129639934", - want: "2", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - want := parseUint(tc.want) - - got := new(Uint).Neg(x) - if !got.Eq(want) { - t.Errorf("Neg(%s) = %s, want %s", tc.x, got.ToString(), tc.want) - } - }) - } -} - -// TestExp verifies exponentiation operations -func TestExp(t *testing.T) { - tests := []struct { - name string - base string - exponent string - want string - }{ - // Basic cases - { - name: "zero_power_zero", - base: "0", - exponent: "0", - want: "1", // Mathematical convention - }, - { - name: "any_power_zero", - base: "31337", - exponent: "0", - want: "1", - }, - { - name: "zero_power_any", - base: "0", - exponent: "31337", - want: "0", - }, - { - name: "one_power_any", - base: "1", - exponent: "31337", - want: "1", - }, - { - name: "two_power_three", - base: "2", - exponent: "3", - want: "8", - }, - - // Powers of 2 - { - name: "two_power_64", - base: "2", - exponent: "64", - want: "18446744073709551616", - }, - { - name: "two_power_96", - base: "2", - exponent: "96", - want: "79228162514264337593543950336", - }, - { - name: "two_power_128", - base: "2", - exponent: "128", - want: "340282366920938463463374607431768211456", - }, - { - name: "two_power_255", - base: "2", - exponent: "255", - want: "57896044618658097711785492504343953926634992332820282019728792003956564819968", - }, - { - name: "two_power_256_wraps", - base: "2", - exponent: "256", - want: "0", - }, - - // Non-power-of-two cases - { - name: "three_power_small", - base: "3", - exponent: "5", - want: "243", - }, - { - name: "three_power_large", - base: "3", - exponent: "100", - want: "515377520732011331036461129765621272702107522001", - }, - { - name: "five_power_fifty", - base: "5", - exponent: "50", - want: "88817841970012523233890533447265625", - }, - { - name: "seven_power_thirty", - base: "7", - exponent: "30", - want: "22539340290692258087863249", - }, - - // Other cases - { - name: "large_base_small_exp", - base: "31337", - exponent: "3", - want: "30773171189753", - }, - { - name: "large_base_large_exp", - base: "12345678901234567890", - exponent: "3", - want: "1881676372353657772490265749424677022198701224860897069000", - }, - { - name: "max_base_exp_one", - base: MAX_UINT256, - exponent: "1", - want: MAX_UINT256, - }, - { - name: "max_base_exp_two", - base: MAX_UINT256, - exponent: "2", - want: "1", // Wraps around - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - base := parseUint(tc.base) - exponent := parseUint(tc.exponent) - want := parseUint(tc.want) - - got := new(Uint).Exp(base, exponent) - if !got.Eq(want) { - t.Errorf("Exp(%s, %s) = %s, want %s", tc.base, tc.exponent, got.ToString(), tc.want) - } - }) - } -} - -// TestMustMul verifies MustMul panic behavior -func TestMustMul(t *testing.T) { - t.Run("normal_multiplication", func(t *testing.T) { - x := NewUint(1000) - y := NewUint(2000) - result := new(Uint).MustMul(x, y) - if result.ToString() != "2000000" { - t.Errorf("MustMul(1000, 2000) = %s, want 2000000", result.ToString()) - } - }) - - t.Run("large_valid_multiplication", func(t *testing.T) { - x := parseUint("340282366920938463463374607431768211455") // 2^128 - 1 - y := NewUint(2) - result := new(Uint).MustMul(x, y) - if result.ToString() != "680564733841876926926749214863536422910" { - t.Errorf("MustMul large valid failed") - } - }) - - t.Run("overflow_panics", func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Errorf("MustMul should panic on overflow") - } - }() - - max := MustFromDecimal(MAX_UINT256) - new(Uint).MustMul(max, NewUint(2)) - }) -} - -// TestIsOverflow verifies overflow bit detection -func TestIsOverflow(t *testing.T) { - tests := []struct { - name string - input *Uint - expected bool - }{ - { - name: "zero_not_overflow", - input: &Uint{arr: [4]uint64{0, 0, 0, 0}}, - expected: false, - }, - { - name: "max_value_not_overflow", - input: &Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0) >> 1}}, - expected: false, - }, - { - name: "bit_255_set_is_overflow", - input: &Uint{arr: [4]uint64{0, 0, 0, uint64(1) << 63}}, - expected: true, - }, - { - name: "all_bits_set_is_overflow", - input: &Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - expected: true, - }, - { - name: "max_plus_one_is_overflow", - input: &Uint{arr: [4]uint64{0, 0, 0, uint64(1) << 63}}, - expected: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if got := tc.input.IsOverflow(); got != tc.expected { - t.Errorf("IsOverflow() = %v, want %v", got, tc.expected) - } - }) - } -} - -// TestDivisionByZero verifies all division by zero cases return error -func TestDivisionByZero(t *testing.T) { - testCases := []struct { - name string - fn func() *Uint - }{ - { - name: "div_by_zero", - fn: func() *Uint { - x := MustFromDecimal("12345") - y := MustFromDecimal("0") - return new(Uint).Div(x, y) - }, - }, - { - name: "mod_by_zero", - fn: func() *Uint { - x := MustFromDecimal("12345") - y := MustFromDecimal("0") - return new(Uint).Mod(x, y) - }, - }, - { - name: "mulmod_by_zero", - fn: func() *Uint { - x := MustFromDecimal("12345") - y := MustFromDecimal("67890") - m := MustFromDecimal("0") - return new(Uint).MulMod(x, y, m) - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := tc.fn() - if !result.IsZero() { - t.Errorf("%s should return zero but returned %s", tc.name, result.ToString()) - } - }) - } -} - -// TestMustOperations verifies that Must* functions panic appropriately -func TestMustOperations(t *testing.T) { - t.Run("MustDiv_panics_on_zero", func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustDiv should panic on division by zero") - } - }() - x := NewUint(100) - y := Zero() - new(Uint).MustDiv(x, y) - }) - - t.Run("MustMod_panics_on_zero", func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustMod should panic on modulo by zero") - } - }() - x := NewUint(100) - y := Zero() - new(Uint).MustMod(x, y) - }) - - t.Run("MustMulMod_panics_on_zero", func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustMulMod should panic on modulo by zero") - } - }() - x := NewUint(10) - y := NewUint(20) - m := Zero() - new(Uint).MustMulMod(x, y, m) - }) - - t.Run("MustDivMod_panics_on_zero", func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustDivMod should panic on division by zero") - } - }() - x := NewUint(100) - y := Zero() - new(Uint).MustDivMod(x, y, new(Uint)) - }) - - t.Run("MustMul_panics_on_overflow", func(t *testing.T) { - defer func() { - if r := recover(); r == nil { - t.Error("MustMul should panic on overflow") - } - }() - x := MustFromDecimal("340282366920938463463374607431768211456") // 2^128 - y := MustFromDecimal("340282366920938463463374607431768211456") - new(Uint).MustMul(x, y) - }) - - // Test successful Must operations - t.Run("MustDiv_success", func(t *testing.T) { - x := NewUint(100) - y := NewUint(10) - result := new(Uint).MustDiv(x, y) - if !result.Eq(NewUint(10)) { - t.Error("MustDiv failed") - } - }) - - t.Run("MustMul_success", func(t *testing.T) { - x := NewUint(100) - y := NewUint(200) - result := new(Uint).MustMul(x, y) - if !result.Eq(NewUint(20000)) { - t.Error("MustMul failed") - } - }) -} - -// TestMulModPaths verifies both fast and reduce4 paths in MulMod -func TestMulModPaths(t *testing.T) { - tests := []struct { - name string - x string - y string - m string - want string - path string // "fast" or "reduce4" - }{ - // Fast path: x*y fits in 256 bits, m has no high word - { - name: "fast_path_small_product", - x: "1000000", - y: "2000000", - m: "999999", - want: "2", - path: "fast", - }, - { - name: "fast_path_exact_256_bits", - x: "0x1FFFFFFFFFFFFFFFF", // Just over 2^64 - y: "2", - m: "0xFFFFFFFFFFFFFFFF", // 2^64 - 1 - want: "2", - path: "fast", - }, - { - name: "boundary_case_m_arr3_zero", - x: "340282366920938463463374607431768211456", // Q128 - y: "340282366920938463463374607431768211456", - m: "340282366920938463463374607431768211455", // m.arr[3] != 0 - want: "1", - path: "fast", // Takes fast path because m.arr[3] == 0 - }, - - // Reduce4 path: x*y > 2^256 and m.arr[3] != 0 - { - name: "reduce4_path_max_inputs", - x: MAX_UINT256, - y: MAX_UINT256, - m: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 - want: "1", - path: "reduce4", - }, - { - name: "reduce4_path_exact_division", - x: "340282366920938463463374607431768211456", - y: "340282366920938463463374607431768211456", - m: "115792089237316195423570985008687907852589419931798687112530834793049593217024", - want: "680564733841876926926749214863536422912", - path: "reduce4", - }, - { - name: "reduce4_path_q96_calculation", - x: "79228162514264337593543950336", // 2^96 - y: "79228162514264337593543950336", - m: "6277101735386680763835789423207666416102355444464034512895", // Just under result - want: "1", - path: "reduce4", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - m := parseUint(tc.m) - want := parseUint(tc.want) - - got := new(Uint).MulMod(x, y, m) - - if !got.Eq(want) { - t.Errorf("MulMod(%s, %s, %s) = %s, want %s", - tc.x, tc.y, tc.m, got.ToString(), tc.want) - } - }) - } -} - -// TestDivModPowerOfTwo verifies optimized division by powers of two -func TestDivModPowerOfTwo(t *testing.T) { - tests := []struct { - name string - x string - y string - wantDiv string - wantMod string - }{ - { - name: "divmod_by_2", - x: "31337", - y: "2", - wantDiv: "15668", - wantMod: "1", - }, - { - name: "divmod_by_4", - x: "31337", - y: "4", - wantDiv: "7834", - wantMod: "1", - }, - { - name: "divmod_by_8", - x: "31337", - y: "8", - wantDiv: "3917", - wantMod: "1", - }, - { - name: "divmod_by_256", - x: "0xABCDEF1234567890", - y: "0x100", - wantDiv: "0xABCDEF12345678", - wantMod: "0x90", - }, - { - name: "divmod_by_2_32", - x: "0xFFFFFFFFFFFFFFFF", - y: "0x100000000", // 2^32 - wantDiv: "0xFFFFFFFF", - wantMod: "0xFFFFFFFF", - }, - { - name: "divmod_by_2_64", - x: "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", - y: "0x10000000000000000", // 2^64 - wantDiv: "0xFFFFFFFFFFFFFFFF", - wantMod: "0xFFFFFFFFFFFFFFFF", - }, - { - name: "max_divmod_power_of_two", - x: MAX_UINT256, - y: "0x10000000000000000", // 2^64 - wantDiv: "6277101735386680763835789423207666416102355444464034512895", - wantMod: "0xFFFFFFFFFFFFFFFF", - }, - { - name: "divmod_by_2_96", - x: MAX_UINT256, - y: "79228162514264337593543950336", // 2^96 - wantDiv: "1461501637330902918203684832716283019655932542975", - wantMod: "79228162514264337593543950335", - }, - { - name: "divmod_by_2_128", - x: MAX_UINT256, - y: "340282366920938463463374607431768211456", // 2^128 - wantDiv: "340282366920938463463374607431768211455", - wantMod: "340282366920938463463374607431768211455", - }, - { - name: "divmod_by_2_192", - x: MAX_UINT256, - y: "6277101735386680763835789423207666416102355444464034512896", // 2^192 - wantDiv: "18446744073709551615", - wantMod: "6277101735386680763835789423207666416102355444464034512895", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := parseUint(tc.x) - y := parseUint(tc.y) - wantDiv := parseUint(tc.wantDiv) - wantMod := parseUint(tc.wantMod) - - gotDiv := new(Uint) - gotMod := new(Uint) - gotDiv, gotMod = gotDiv.DivMod(x, y, gotMod) - - if !gotDiv.Eq(wantDiv) { - t.Errorf("DivMod div = %s, want %s", gotDiv.ToString(), tc.wantDiv) - } - if !gotMod.Eq(wantMod) { - t.Errorf("DivMod mod = %s, want %s", gotMod.ToString(), tc.wantMod) - } - }) - } -} - -// TestChainedOperations verifies DEX-like operation chains -func TestChainedOperations(t *testing.T) { - tests := []struct { - name string - ops []struct { - op string - arg string - } - start string - want string - }{ - { - name: "swap_fee_calculation", - start: "1000000000000000000", // 1 token - ops: []struct { - op string - arg string - }{ - {op: "mul", arg: "997"}, // 0.3% fee - {op: "div", arg: "1000"}, // fee calculation - }, - want: "997000000000000000", // 0.997 tokens after fee - }, - { - name: "liquidity_calculation", - start: "1000000000000000000000", // 1000 tokens - ops: []struct { - op string - arg string - }{ - {op: "mul", arg: "79228162514264337593543950336"}, // Multiply by Q96 - {op: "div", arg: "1000000000000000000"}, // Normalize - }, - want: "79228162514264337593543950336000", // Result in Q96 format - }, - { - name: "complex_price_impact", - start: "1000000000000000000", // 1 token - ops: []struct { - op string - arg string - }{ - {op: "mul", arg: "997"}, // Fee - {op: "div", arg: "1000"}, - {op: "mul", arg: "79228162514264337593543950336"}, // Price in Q96 - {op: "div", arg: "79228162514264337593543950336"}, // Normalize back - }, - want: "997000000000000000", // Same as fee calculation - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - result := parseUint(tc.start) - - for _, op := range tc.ops { - arg := parseUint(op.arg) - switch op.op { - case "add": - result = new(Uint).Add(result, arg) - case "sub": - result = new(Uint).Sub(result, arg) - case "mul": - result = new(Uint).Mul(result, arg) - case "div": - result = new(Uint).Div(result, arg) - } - } - - want := parseUint(tc.want) - if !result.Eq(want) { - t.Errorf("Chained operations = %s, want %s", result.ToString(), tc.want) - } - }) - } -} - -// Division rounding consistency test -func TestDivisionRoundingConsistency(t *testing.T) { - tests := []struct { - name string - x string - y string - wantDiv string - wantMod string - }{ - { - name: "rounds_down_small", - x: "10", - y: "3", - wantDiv: "3", - wantMod: "1", - }, - { - name: "rounds_down_large", - x: "999999999999999999", - y: "1000000000000000000", - wantDiv: "0", - wantMod: "999999999999999999", - }, - { - name: "exact_division", - x: "1000000000000000000", - y: "1000000000000000", - wantDiv: "1000", - wantMod: "0", - }, - { - name: "almost_two", - x: "1999999999999999999", - y: "1000000000000000000", - wantDiv: "1", - wantMod: "999999999999999999", - }, - { - name: "large_division_rounds_down", - x: MAX_UINT256, - y: "1000000000000000001", - wantDiv: "115792089237316195307778895771371712545491088894268851493966", - wantMod: "495113644278145969", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - x := parseUint(tt.x) - y := parseUint(tt.y) - - gotDiv := new(Uint).Div(x, y) - gotMod := new(Uint).Mod(x, y) - - wantDiv := parseUint(tt.wantDiv) - wantMod := parseUint(tt.wantMod) - - if !gotDiv.Eq(wantDiv) { - t.Errorf("Div(%s, %s) = %s, want %s", tt.x, tt.y, gotDiv.ToString(), tt.wantDiv) - } - if !gotMod.Eq(wantMod) { - t.Errorf("Mod(%s, %s) = %s, want %s", tt.x, tt.y, gotMod.ToString(), tt.wantMod) - } - - // Verify division always rounds down - // x = y * div + mod, and 0 <= mod < y - reconstructed := new(Uint).Mul(y, gotDiv) - reconstructed = new(Uint).Add(reconstructed, gotMod) - if !reconstructed.Eq(x) { - t.Errorf("Division property violated: y*div+mod != x") - } - if gotMod.Gte(y) { - t.Errorf("Modulo >= divisor: mod=%s, y=%s", gotMod.ToString(), tt.y) - } - }) - } -} - -// Test extreme values combinations -func TestExtremeValues(t *testing.T) { - extremeValues := []struct { - name string - value string - }{ - {"one", "1"}, - {"two", "2"}, - {"max_minus_two", "115792089237316195423570985008687907853269984665640564039457584007913129639933"}, - {"max_minus_one", "115792089237316195423570985008687907853269984665640564039457584007913129639934"}, - {"max", MAX_UINT256}, - } - - for _, x := range extremeValues { - for _, y := range extremeValues { - t.Run(x.name+"_with_"+y.name, func(t *testing.T) { - xu := parseUint(x.value) - yu := parseUint(y.value) - - // Test all operations don't panic unexpectedly - _ = new(Uint).Add(xu, yu) - _ = new(Uint).Sub(xu, yu) - _ = new(Uint).Mul(xu, yu) - - if !yu.IsZero() { - _ = new(Uint).Div(xu, yu) - _ = new(Uint).Mod(xu, yu) - - // DivMod - quotient := new(Uint) - remainder := new(Uint) - quotient, remainder = quotient.DivMod(xu, yu, remainder) - - // Verify DivMod consistency - divResult := new(Uint).Div(xu, yu) - modResult := new(Uint).Mod(xu, yu) - if !quotient.Eq(divResult) || !remainder.Eq(modResult) { - t.Errorf("DivMod inconsistent with Div/Mod") - } - } - }) - } - } -} - -// Benchmark functions -func BenchmarkAdd(b *testing.B) { - x := MustFromDecimal("123456789012345678901234567890123456789") - y := MustFromDecimal("987654321098765432109876543210987654321") - z := new(Uint) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - z.Add(x, y) - } -} - -func BenchmarkMul(b *testing.B) { - x := MustFromDecimal("123456789012345678901234567890") - y := MustFromDecimal("987654321098765432109876543210") - z := new(Uint) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - z.Mul(x, y) - } -} - -func BenchmarkDiv(b *testing.B) { - x := MustFromDecimal("123456789012345678901234567890123456789012345678901234567890") - y := MustFromDecimal("123456789012345678901234567890") - z := new(Uint) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - z.Div(x, y) - } -} - -func BenchmarkExp(b *testing.B) { - base := NewUint(2) - exp := NewUint(100) - z := new(Uint) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - z.Exp(base, exp) - } -} - -func BenchmarkMulMod(b *testing.B) { - x := MustFromDecimal("123456789012345678901234567890") - y := MustFromDecimal("987654321098765432109876543210") - m := MustFromDecimal("1000000000000000000000000000000") - z := new(Uint) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - z.MulMod(x, y, m) - } -} diff --git a/contract/p/gnoswap/uint256/bits_table.gno b/contract/p/gnoswap/uint256/bits_table.gno deleted file mode 100644 index aaaef7e..0000000 --- a/contract/p/gnoswap/uint256/bits_table.gno +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. -package uint256 - -// ntz8tab: A lookup table for 8-bit values (0-255) that shows -// the number of trailing zeros (zeros from the rightmost/LSB position). -// -// Example) 0x28 (binary 00101000) -// -// Binary: [ 0 0 1 0 1 0 0 0 ] -// ^^^^^^^^ -// 3 consecutive zeros -// -// ntz8tab[0x28] = 3 -const ntz8tab = "" + - "\x08\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x05\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x06\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x05\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x07\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x05\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x06\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x05\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" + - "\x04\x00\x01\x00\x02\x00\x01\x00\x03\x00\x01\x00\x02\x00\x01\x00" - -// pop8tab: A lookup table for 8-bit values (0-255) that allows -// quick lookup of the number of set bits (bits set to 1). -// -// Example) 0xB2 (binary 10110010) -// -// Binary: [ 1 0 1 1 0 0 1 0 ] -// Total of 4 set bits -// -// pop8tab[0xB2] = 4 -const pop8tab = "" + - "\x00\x01\x01\x02\x01\x02\x02\x03\x01\x02\x02\x03\x02\x03\x03\x04" + - "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + - "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + - "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + - "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + - "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + - "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + - "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + - "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" + - "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + - "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + - "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + - "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" + - "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + - "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" + - "\x04\x05\x05\x06\x05\x06\x06\x07\x05\x06\x06\x07\x06\x07\x07\x08" - -// rev8tab: A lookup table that pre-calculates bit-reversed results -// for 8-bit values (0-255). -// -// Example) 0x16 (binary 00010110) -// -// Binary: [ 0 0 0 1 0 1 1 0 ] -// Reversed: [ 0 1 1 0 1 0 0 0 ] -> 0x68 (104 in decimal) -// -// rev8tab[0x16] = 0x68 -const rev8tab = "" + - "\x00\x80\x40\xc0\x20\xa0\x60\xe0\x10\x90\x50\xd0\x30\xb0\x70\xf0" + - "\x08\x88\x48\xc8\x28\xa8\x68\xe8\x18\x98\x58\xd8\x38\xb8\x78\xf8" + - "\x04\x84\x44\xc4\x24\xa4\x64\xe4\x14\x94\x54\xd4\x34\xb4\x74\xf4" + - "\x0c\x8c\x4c\xcc\x2c\xac\x6c\xec\x1c\x9c\x5c\xdc\x3c\xbc\x7c\xfc" + - "\x02\x82\x42\xc2\x22\xa2\x62\xe2\x12\x92\x52\xd2\x32\xb2\x72\xf2" + - "\x0a\x8a\x4a\xca\x2a\xaa\x6a\xea\x1a\x9a\x5a\xda\x3a\xba\x7a\xfa" + - "\x06\x86\x46\xc6\x26\xa6\x66\xe6\x16\x96\x56\xd6\x36\xb6\x76\xf6" + - "\x0e\x8e\x4e\xce\x2e\xae\x6e\xee\x1e\x9e\x5e\xde\x3e\xbe\x7e\xfe" + - "\x01\x81\x41\xc1\x21\xa1\x61\xe1\x11\x91\x51\xd1\x31\xb1\x71\xf1" + - "\x09\x89\x49\xc9\x29\xa9\x69\xe9\x19\x99\x59\xd9\x39\xb9\x79\xf9" + - "\x05\x85\x45\xc5\x25\xa5\x65\xe5\x15\x95\x55\xd5\x35\xb5\x75\xf5" + - "\x0d\x8d\x4d\xcd\x2d\xad\x6d\xed\x1d\x9d\x5d\xdd\x3d\xbd\x7d\xfd" + - "\x03\x83\x43\xc3\x23\xa3\x63\xe3\x13\x93\x53\xd3\x33\xb3\x73\xf3" + - "\x0b\x8b\x4b\xcb\x2b\xab\x6b\xeb\x1b\x9b\x5b\xdb\x3b\xbb\x7b\xfb" + - "\x07\x87\x47\xc7\x27\xa7\x67\xe7\x17\x97\x57\xd7\x37\xb7\x77\xf7" + - "\x0f\x8f\x4f\xcf\x2f\xaf\x6f\xef\x1f\x9f\x5f\xdf\x3f\xbf\x7f\xff" - -// len8tab: A lookup table that pre-calculates the "bit length" -// of 8-bit values (0-255). -// (Bit length: position of the most significant bit + 1) -// -// Examples) -// -// 0x00 (binary 00000000) → No MSB → length 0 -// 0x01 (binary 00000001) → MSB at rightmost position → length 1 -// 0x02 (binary 00000010) ~ 0x03 (00000011) → length 2 -// 0x04 (binary 00000100) ~ 0x07 (00000111) → length 3 -// ... -const len8tab = "" + - "\x00\x01\x02\x02\x03\x03\x03\x03\x04\x04\x04\x04\x04\x04\x04\x04" + - "\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05\x05" + - "\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06" + - "\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06\x06" + - "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + - "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + - "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + - "\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07\x07" + - "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + - "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + - "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + - "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + - "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + - "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + - "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" + - "\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08" diff --git a/contract/p/gnoswap/uint256/bitwise.gno b/contract/p/gnoswap/uint256/bitwise.gno deleted file mode 100644 index e231af1..0000000 --- a/contract/p/gnoswap/uint256/bitwise.gno +++ /dev/null @@ -1,266 +0,0 @@ -// bitwise contains bitwise operations for Uint instances. -// This file includes functions to perform bitwise AND, OR, XOR, and NOT operations, as well as bit shifting. -// These operations are crucial for manipulating individual bits within a 256-bit unsigned integer. -package uint256 - -// Or sets z to the bitwise OR of x and y and returns z. -func (z *Uint) Or(x, y *Uint) *Uint { - z.arr[0] = x.arr[0] | y.arr[0] - z.arr[1] = x.arr[1] | y.arr[1] - z.arr[2] = x.arr[2] | y.arr[2] - z.arr[3] = x.arr[3] | y.arr[3] - return z -} - -// And sets z to the bitwise AND of x and y and returns z. -func (z *Uint) And(x, y *Uint) *Uint { - z.arr[0] = x.arr[0] & y.arr[0] - z.arr[1] = x.arr[1] & y.arr[1] - z.arr[2] = x.arr[2] & y.arr[2] - z.arr[3] = x.arr[3] & y.arr[3] - return z -} - -// Not sets z to the bitwise NOT of x and returns z. -func (z *Uint) Not(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = ^x.arr[3], ^x.arr[2], ^x.arr[1], ^x.arr[0] - return z -} - -// AndNot sets z to x AND NOT y and returns z. -func (z *Uint) AndNot(x, y *Uint) *Uint { - z.arr[0] = x.arr[0] &^ y.arr[0] - z.arr[1] = x.arr[1] &^ y.arr[1] - z.arr[2] = x.arr[2] &^ y.arr[2] - z.arr[3] = x.arr[3] &^ y.arr[3] - return z -} - -// Xor sets z to the bitwise XOR of x and y and returns z. -func (z *Uint) Xor(x, y *Uint) *Uint { - z.arr[0] = x.arr[0] ^ y.arr[0] - z.arr[1] = x.arr[1] ^ y.arr[1] - z.arr[2] = x.arr[2] ^ y.arr[2] - z.arr[3] = x.arr[3] ^ y.arr[3] - return z -} - -// Lsh sets z to x left-shifted by n bits and returns z. -// If n >= 256, z is set to 0. -func (z *Uint) Lsh(x *Uint, n uint) *Uint { - // n % 64 == 0 - if n&0x3f == 0 { - switch n { - case 0: - return z.Set(x) - case 64: - return z.lsh64(x) - case 128: - return z.lsh128(x) - case 192: - return z.lsh192(x) - default: - return z.Clear() - } - } - var a, b uint64 - // Big swaps first - switch { - case n > 192: - if n > 256 { - return z.Clear() - } - z.lsh192(x) - n -= 192 - goto sh192 - case n > 128: - z.lsh128(x) - n -= 128 - goto sh128 - case n > 64: - z.lsh64(x) - n -= 64 - goto sh64 - default: - z.Set(x) - } - - // remaining shifts - a = z.arr[0] >> (64 - n) - z.arr[0] = z.arr[0] << n - -sh64: - b = z.arr[1] >> (64 - n) - z.arr[1] = (z.arr[1] << n) | a - -sh128: - a = z.arr[2] >> (64 - n) - z.arr[2] = (z.arr[2] << n) | b - -sh192: - z.arr[3] = (z.arr[3] << n) | a - - return z -} - -// Rsh sets z to x right-shifted by n bits and returns z. -// If n >= 256, z is set to 0. -func (z *Uint) Rsh(x *Uint, n uint) *Uint { - // n % 64 == 0 - if n&0x3f == 0 { - switch n { - case 0: - return z.Set(x) - case 64: - return z.rsh64(x) - case 128: - return z.rsh128(x) - case 192: - return z.rsh192(x) - default: - return z.Clear() - } - } - var a, b uint64 - // Big swaps first - switch { - case n > 192: - if n > 256 { - return z.Clear() - } - z.rsh192(x) - n -= 192 - goto sh192 - case n > 128: - z.rsh128(x) - n -= 128 - goto sh128 - case n > 64: - z.rsh64(x) - n -= 64 - goto sh64 - default: - z.Set(x) - } - - // remaining shifts - a = z.arr[3] << (64 - n) - z.arr[3] = z.arr[3] >> n - -sh64: - b = z.arr[2] << (64 - n) - z.arr[2] = (z.arr[2] >> n) | a - -sh128: - a = z.arr[1] << (64 - n) - z.arr[1] = (z.arr[1] >> n) | b - -sh192: - z.arr[0] = (z.arr[0] >> n) | a - - return z -} - -// SRsh sets z to x arithmetically right-shifted by n bits and returns z. -// It treats x as a signed two's complement integer. For negative values, -// high-order bits are filled with 1s instead of 0s. If n >= 256 and x is negative, z is set to all 1s. -func (z *Uint) SRsh(x *Uint, n uint) *Uint { - // If the MSB is 0, SRsh is same as Rsh. - if !x.isBitSet(255) { - return z.Rsh(x, n) - } - if n%64 == 0 { - switch n { - case 0: - return z.Set(x) - case 64: - return z.srsh64(x) - case 128: - return z.srsh128(x) - case 192: - return z.srsh192(x) - default: - return z.SetAllOne() - } - } - var a uint64 = MaxUint64 << (64 - n%64) - // Big swaps first - switch { - case n > 192: - if n > 256 { - return z.SetAllOne() - } - z.srsh192(x) - n -= 192 - goto sh192 - case n > 128: - z.srsh128(x) - n -= 128 - goto sh128 - case n > 64: - z.srsh64(x) - n -= 64 - goto sh64 - default: - z.Set(x) - } - - // remaining shifts - z.arr[3], a = (z.arr[3]>>n)|a, z.arr[3]<<(64-n) - -sh64: - z.arr[2], a = (z.arr[2]>>n)|a, z.arr[2]<<(64-n) - -sh128: - z.arr[1], a = (z.arr[1]>>n)|a, z.arr[1]<<(64-n) - -sh192: - z.arr[0] = (z.arr[0] >> n) | a - - return z -} - -func (z *Uint) lsh64(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[2], x.arr[1], x.arr[0], 0 - return z -} - -func (z *Uint) lsh128(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[1], x.arr[0], 0, 0 - return z -} - -func (z *Uint) lsh192(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = x.arr[0], 0, 0, 0 - return z -} - -func (z *Uint) rsh64(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, x.arr[3], x.arr[2], x.arr[1] - return z -} - -func (z *Uint) rsh128(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, x.arr[3], x.arr[2] - return z -} - -func (z *Uint) rsh192(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x.arr[3] - return z -} - -func (z *Uint) srsh64(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, x.arr[3], x.arr[2], x.arr[1] - return z -} - -func (z *Uint) srsh128(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, x.arr[3], x.arr[2] - return z -} - -func (z *Uint) srsh192(x *Uint) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, x.arr[3] - return z -} diff --git a/contract/p/gnoswap/uint256/bitwise_test.gno b/contract/p/gnoswap/uint256/bitwise_test.gno deleted file mode 100644 index 4437e7d..0000000 --- a/contract/p/gnoswap/uint256/bitwise_test.gno +++ /dev/null @@ -1,346 +0,0 @@ -package uint256 - -import ( - "testing" -) - -type logicOpTest struct { - name string - x Uint - y Uint - want Uint -} - -func TestOr(t *testing.T) { - tests := []logicOpTest{ - { - name: "all zeros", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - { - name: "mixed", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, - y: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - { - name: "one operand all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).Or(&tc.x, &tc.y) - if *res != tc.want { - t.Errorf("Or(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) - } - }) - } -} - -func TestAnd(t *testing.T) { - tests := []logicOpTest{ - { - name: "all zeros", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - { - name: "mixed", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "mixed 2", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "mixed 3", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, - y: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "one operand zero", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "one operand all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, - want: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).And(&tc.x, &tc.y) - if *res != tc.want { - t.Errorf("And(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) - } - }) - } -} - -func TestNot(t *testing.T) { - tests := []struct { - name string - x Uint - want Uint - }{ - { - name: "all zeros", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - { - name: "all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "mixed", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, - want: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).Not(&tc.x) - if *res != tc.want { - t.Errorf("Not(%s) = %s, want %s", tc.x.ToString(), res.ToString(), (tc.want).ToString()) - } - }) - } -} - -func TestAndNot(t *testing.T) { - tests := []logicOpTest{ - { - name: "all zeros", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "mixed", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "mixed 2", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - { - name: "mixed 3", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, - y: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, - }, - { - name: "one operand zero", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "one operand all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, - want: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(0)}}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).AndNot(&tc.x, &tc.y) - if *res != tc.want { - t.Errorf("AndNot(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) - } - }) - } -} - -func TestXor(t *testing.T) { - tests := []logicOpTest{ - { - name: "all zeros", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{0, 0, 0, 0}}, - }, - { - name: "mixed", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - { - name: "mixed 2", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - { - name: "mixed 3", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), 0, 0}}, - y: Uint{arr: [4]uint64{0, 0, ^uint64(0), ^uint64(0)}}, - want: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - }, - { - name: "one operand zero", - x: Uint{arr: [4]uint64{0, 0, 0, 0}}, - y: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}}, - want: Uint{arr: [4]uint64{0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF}}, - }, - { - name: "one operand all ones", - x: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - y: Uint{arr: [4]uint64{0x5555555555555555, 0xAAAAAAAAAAAAAAAA, 0xFFFFFFFFFFFFFFFF, 0x0000000000000000}}, - want: Uint{arr: [4]uint64{0xAAAAAAAAAAAAAAAA, 0x5555555555555555, 0x0000000000000000, ^uint64(0)}}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - res := new(Uint).Xor(&tc.x, &tc.y) - if *res != tc.want { - t.Errorf("Xor(%s, %s) = %s, want %s", tc.x.ToString(), tc.y.ToString(), res.ToString(), (tc.want).ToString()) - } - }) - } -} - -func TestLsh(t *testing.T) { - tests := []struct { - x string - y uint - want string - }{ - {"0", 0, "0"}, - {"0", 1, "0"}, - {"0", 64, "0"}, - {"1", 0, "1"}, - {"1", 1, "2"}, - {"1", 64, "18446744073709551616"}, - {"1", 128, "340282366920938463463374607431768211456"}, - {"1", 192, "6277101735386680763835789423207666416102355444464034512896"}, - {"1", 255, "57896044618658097711785492504343953926634992332820282019728792003956564819968"}, - {"1", 256, "0"}, - {"31337", 0, "31337"}, - {"31337", 1, "62674"}, - {"31337", 64, "578065619037836218990592"}, - {"31337", 128, "10663428532201448629551770073089320442396672"}, - {"31337", 192, "196705537081812415096322133155058642481399512563169449530621952"}, - {"31337", 193, "393411074163624830192644266310117284962799025126338899061243904"}, - {"31337", 255, "57896044618658097711785492504343953926634992332820282019728792003956564819968"}, - {"31337", 256, "0"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := &Uint{} - got.Lsh(x, tc.y) - - if got.Neq(want) { - t.Errorf("Lsh(%s, %d) = %s, want %s", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} - -func TestRsh(t *testing.T) { - tests := []struct { - x string - y uint - want string - }{ - {"0", 0, "0"}, - {"0", 1, "0"}, - {"0", 64, "0"}, - {"1", 0, "1"}, - {"1", 1, "0"}, - {"1", 64, "0"}, - {"1", 128, "0"}, - {"1", 192, "0"}, - {"1", 255, "0"}, - {"57896044618658097711785492504343953926634992332820282019728792003956564819968", 255, "1"}, - {"6277101735386680763835789423207666416102355444464034512896", 192, "1"}, - {"340282366920938463463374607431768211456", 128, "1"}, - {"18446744073709551616", 64, "1"}, - {"393411074163624830192644266310117284962799025126338899061243904", 193, "31337"}, - {"196705537081812415096322133155058642481399512563169449530621952", 192, "31337"}, - {"10663428532201448629551770073089320442396672", 128, "31337"}, - {"578065619037836218990592", 64, "31337"}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - want, err := FromDecimal(tc.want) - if err != nil { - t.Error(err) - continue - } - - got := &Uint{} - got.Rsh(x, tc.y) - - if got.Neq(want) { - t.Errorf("Rsh(%s, %d) = %s, want %s", tc.x, tc.y, got.ToString(), want.ToString()) - } - } -} diff --git a/contract/p/gnoswap/uint256/cmp.gno b/contract/p/gnoswap/uint256/cmp.gno deleted file mode 100644 index 961c598..0000000 --- a/contract/p/gnoswap/uint256/cmp.gno +++ /dev/null @@ -1,103 +0,0 @@ -// cmp (or, comparisons) includes methods for comparing Uint instances. -// These comparison functions cover a range of operations including equality checks, less than/greater than -// evaluations, and specialized comparisons such as signed greater than. These are fundamental for logical -// decision making based on Uint values. -package uint256 - -import ( - "math/bits" -) - -// Cmp compares z and x and returns -1 if z < x, 0 if z == x, or +1 if z > x. -func (z *Uint) Cmp(x *Uint) (r int) { - // z < x <=> z - x < 0 i.e. when subtraction overflows. - d0, carry := bits.Sub64(z.arr[0], x.arr[0], 0) - d1, carry := bits.Sub64(z.arr[1], x.arr[1], carry) - d2, carry := bits.Sub64(z.arr[2], x.arr[2], carry) - d3, carry := bits.Sub64(z.arr[3], x.arr[3], carry) - if carry == 1 { - return -1 - } - if d0|d1|d2|d3 == 0 { - return 0 - } - return 1 -} - -// IsZero returns true if z equals 0. -func (z *Uint) IsZero() bool { - return (z.arr[0] | z.arr[1] | z.arr[2] | z.arr[3]) == 0 -} - -// Sign returns the sign of z interpreted as a two's complement signed number. -// It returns -1 if z < 0, 0 if z == 0, or +1 if z > 0. -func (z *Uint) Sign() int { - if z.IsZero() { - return 0 - } - if z.arr[3] < 0x8000000000000000 { - return 1 - } - return -1 -} - -// LtUint64 returns true if z is less than the uint64 value n. -func (z *Uint) LtUint64(n uint64) bool { - return z.arr[0] < n && (z.arr[1]|z.arr[2]|z.arr[3]) == 0 -} - -// GtUint64 returns true if z is greater than the uint64 value n. -func (z *Uint) GtUint64(n uint64) bool { - return z.arr[0] > n || (z.arr[1]|z.arr[2]|z.arr[3]) != 0 -} - -// Lt returns true if z is less than x. -func (z *Uint) Lt(x *Uint) bool { - // z < x <=> z - x < 0 i.e. when subtraction overflows. - _, carry := bits.Sub64(z.arr[0], x.arr[0], 0) - _, carry = bits.Sub64(z.arr[1], x.arr[1], carry) - _, carry = bits.Sub64(z.arr[2], x.arr[2], carry) - _, carry = bits.Sub64(z.arr[3], x.arr[3], carry) - - return carry != 0 -} - -// Gt returns true if z is greater than x. -func (z *Uint) Gt(x *Uint) bool { - return x.Lt(z) -} - -// Lte returns true if z is less than or equal to x. -func (z *Uint) Lte(x *Uint) bool { - return !x.Lt(z) -} - -// Gte returns true if z is greater than or equal to x. -func (z *Uint) Gte(x *Uint) bool { - return !z.Lt(x) -} - -// Eq returns true if z equals x. -func (z *Uint) Eq(x *Uint) bool { - return (z.arr[0] == x.arr[0]) && (z.arr[1] == x.arr[1]) && (z.arr[2] == x.arr[2]) && (z.arr[3] == x.arr[3]) -} - -// Neq returns true if z does not equal x. -func (z *Uint) Neq(x *Uint) bool { - return !z.Eq(x) -} - -// Sgt returns true if z > x when both are interpreted as two's complement signed integers. -func (z *Uint) Sgt(x *Uint) bool { - zSign := z.Sign() - xSign := x.Sign() - - switch { - case zSign >= 0 && xSign < 0: - return true - case zSign < 0 && xSign >= 0: - return false - default: - return z.Gt(x) - } -} diff --git a/contract/p/gnoswap/uint256/cmp_test.gno b/contract/p/gnoswap/uint256/cmp_test.gno deleted file mode 100644 index d536eb8..0000000 --- a/contract/p/gnoswap/uint256/cmp_test.gno +++ /dev/null @@ -1,229 +0,0 @@ -package uint256 - -import ( - "strings" - "testing" -) - -func TestCmp(t *testing.T) { - tests := []struct { - x, y string - want int - }{ - {"0", "0", 0}, - {"0", "1", -1}, - {"1", "0", 1}, - {"1", "1", 0}, - {"10", "10", 0}, - {"10", "11", -1}, - {"11", "10", 1}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Cmp(y) - if got != tc.want { - t.Errorf("Cmp(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestIsZero(t *testing.T) { - tests := []struct { - x string - want bool - }{ - {"0", true}, - {"1", false}, - {"10", false}, - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - got := x.IsZero() - if got != tc.want { - t.Errorf("IsZero(%s) = %v, want %v", tc.x, got, tc.want) - } - } -} - -func TestLtUint64(t *testing.T) { - tests := []struct { - x string - y uint64 - want bool - }{ - {"0", 1, true}, - {"1", 0, false}, - {"10", 10, false}, - {"0xffffffffffffffff", 0, false}, - {"0x10000000000000000", 10000000000000000, false}, - } - - for _, tc := range tests { - var x *Uint - var err error - - if strings.HasPrefix(tc.x, "0x") { - x, err = FromHex(tc.x) - if err != nil { - t.Error(err) - continue - } - } else { - x, err = FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - } - - got := x.LtUint64(tc.y) - - if got != tc.want { - t.Errorf("LtUint64(%s, %d) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestSGT(t *testing.T) { - x := MustFromHex("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe") - y := MustFromHex("0x0") - actual := x.Sgt(y) - if actual { - t.Fatalf("Expected %v false", actual) - } - - x = MustFromHex("0x0") - y = MustFromHex("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe") - actual = x.Sgt(y) - if !actual { - t.Fatalf("Expected %v true", actual) - } -} - -func TestLte(t *testing.T) { - tests := []struct { - x, y string - want bool - }{ - {"0", "0", true}, // equal - {"0", "1", true}, // less than - {"1", "0", false}, // greater than - {"10", "10", true}, // equal - {"10", "11", true}, // less than - {"11", "10", false}, // greater than - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Lte(y) - if got != tc.want { - t.Errorf("Lte(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestGte(t *testing.T) { - tests := []struct { - x, y string - want bool - }{ - {"0", "0", true}, // equal - {"0", "1", false}, // less than - {"1", "0", true}, // greater than - {"10", "10", true}, // equal - {"10", "11", false}, // less than - {"11", "10", true}, // greater than - } - - for _, tc := range tests { - x, err := FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Gte(y) - if got != tc.want { - t.Errorf("Gte(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} - -func TestEq(t *testing.T) { - tests := []struct { - x string - y string - want bool - }{ - {"0xffffffffffffffff", "18446744073709551615", true}, - {"0x10000000000000000", "18446744073709551616", true}, - {"0", "0", true}, - {"115792089237316195423570985008687907853269984665640564039457584007913129639935", "115792089237316195423570985008687907853269984665640564039457584007913129639935", true}, - } - - for _, tc := range tests { - var x *Uint - var err error - - if strings.HasPrefix(tc.x, "0x") { - x, err = FromHex(tc.x) - if err != nil { - t.Error(err) - continue - } - } else { - x, err = FromDecimal(tc.x) - if err != nil { - t.Error(err) - continue - } - } - - y, err := FromDecimal(tc.y) - if err != nil { - t.Error(err) - continue - } - - got := x.Eq(y) - - if got != tc.want { - t.Errorf("Eq(%s, %s) = %v, want %v", tc.x, tc.y, got, tc.want) - } - } -} diff --git a/contract/p/gnoswap/uint256/conversion.gno b/contract/p/gnoswap/uint256/conversion.gno deleted file mode 100644 index bfa8aa4..0000000 --- a/contract/p/gnoswap/uint256/conversion.gno +++ /dev/null @@ -1,602 +0,0 @@ -// conversions contains methods for converting Uint instances to other types and vice versa. -// This includes conversions to and from basic types such as uint64 and int32, as well as string representations -// and byte slices. Additionally, it covers marshaling and unmarshaling for JSON and other text formats. -package uint256 - -import ( - "encoding/binary" - "errors" - "strconv" - "strings" -) - -// Uint64 returns the lower 64 bits of z as a uint64. -func (z *Uint) Uint64() uint64 { - return z.arr[0] -} - -// Int64 returns the lower 64 bits of z as an int64. -func (z *Uint) Int64() int64 { - return int64(z.Uint64()) -} - -// Uint64WithOverflow returns the lower 64 bits of z and true if overflow occurred. -func (z *Uint) Uint64WithOverflow() (uint64, bool) { - return z.arr[0], (z.arr[1] | z.arr[2] | z.arr[3]) != 0 -} - -// SetUint64 sets z to the value of x and returns z. -func (z *Uint) SetUint64(x uint64) *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, x - return z -} - -// IsUint64 reports whether z can be represented as a uint64. -func (z *Uint) IsUint64() bool { - return (z.arr[1] | z.arr[2] | z.arr[3]) == 0 -} - -// Dec returns the decimal representation of z. -func (z *Uint) Dec() string { - if z.IsZero() { - return "0" - } - if z.IsUint64() { - return strconv.FormatUint(z.Uint64(), 10) - } - - // The max uint64 value being 18446744073709551615, the largest - // power-of-ten below that is 10000000000000000000. - // When we do a DivMod using that number, the remainder that we - // get back is the lower part of the output. - // - // The ascii-output of remainder will never exceed 19 bytes (since it will be - // below 10000000000000000000). - // - // Algorithm example using 100 as divisor - // - // 12345 % 100 = 45 (rem) - // 12345 / 100 = 123 (quo) - // -> output '45', continue iterate on 123 - var ( - // out is 98 bytes long: 78 (max size of a string without leading zeroes, - // plus slack so we can copy 19 bytes every iteration). - // We init it with zeroes, because when strconv appends the ascii representations, - // it will omit leading zeroes. - out = []byte("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") - divisor = NewUint(10000000000000000000) // 20 digits - y = new(Uint).Set(z) // copy to avoid modifying z - pos = len(out) // position to write to - buf = make([]byte, 0, 19) // buffer to write uint64:s to - ) - for { - // Obtain Q and R for divisor - var quot Uint - rem := udivrem(quot.arr[:], y.arr[:], divisor) - y.Set(") // Set Q for next loop - // Convert the R to ascii representation - buf = strconv.AppendUint(buf[:0], rem.Uint64(), 10) - // Copy in the ascii digits - copy(out[pos-len(buf):], buf) - if y.IsZero() { - break - } - // Move 19 digits left - pos -= 19 - } - // skip leading zeroes by only using the 'used size' of buf - return string(out[pos-len(buf):]) -} - -// Scan implements the database/sql Scanner interface. -// Supports scanning from strings, byte slices, and numeric types. Sets z to 0 if src is nil. -func (z *Uint) Scan(src any) error { - if src == nil { - z.Clear() - return nil - } - - switch src := src.(type) { - case string: - return z.scanScientificFromString(src) - case []byte: - return z.scanScientificFromString(string(src)) - } - return errors.New("default // unsupported type: can't convert to uint256.Uint") -} - -func (z *Uint) scanScientificFromString(src string) error { - if len(src) == 0 { - z.Clear() - return nil - } - - idx := strings.IndexByte(src, 'e') - if idx == -1 { - return z.SetFromDecimal(src) - } - if err := z.SetFromDecimal(src[:idx]); err != nil { - return err - } - if src[(idx+1):] == "0" { - return nil - } - exp := new(Uint) - if err := exp.SetFromDecimal(src[(idx + 1):]); err != nil { - return err - } - if exp.GtUint64(77) { // 10**78 is larger than 2**256 - return ErrBig256Range - } - exp.Exp(NewUint(10), exp) - if _, overflow := z.MulOverflow(z, exp); overflow { - return ErrBig256Range - } - return nil -} - -// ToString returns the decimal string representation of z. -// Returns an empty string if z is nil. This method doesn't exist in holiman's uint256. -func (z *Uint) ToString() string { - if z == nil { - return "" - } - - return z.Dec() -} - -// MarshalJSON implements json.Marshaler by marshaling z as a decimal string. -// This differs from big.Int which uses JSON's native numeric format. -// Uses string format to avoid JavaScript's 53-bit integer precision limitations. -func (z *Uint) MarshalJSON() ([]byte, error) { - return []byte(`"` + z.Dec() + `"`), nil -} - -// UnmarshalJSON implements json.Unmarshaler. -// Accepts quoted strings (hexadecimal or decimal) or unquoted strings (decimal only). -func (z *Uint) UnmarshalJSON(input []byte) error { - if len(input) < 2 || input[0] != '"' || input[len(input)-1] != '"' { - // if not quoted, it must be decimal - return z.fromDecimal(string(input)) - } - return z.UnmarshalText(input[1 : len(input)-1]) -} - -// MarshalText implements encoding.TextMarshaler by marshaling z as a decimal string. -// Compatible with big.Int's text marshaling format. -func (z *Uint) MarshalText() ([]byte, error) { - return []byte(z.Dec()), nil -} - -// UnmarshalText implements encoding.TextUnmarshaler. -// Accepts decimal strings or hexadecimal strings prefixed with 0x or 0X. -func (z *Uint) UnmarshalText(input []byte) error { - if len(input) >= 2 && input[0] == '0' && (input[1] == 'x' || input[1] == 'X') { - return z.fromHex(string(input)) - } - return z.fromDecimal(string(input)) -} - -// SetBytes interprets buf as a big-endian unsigned integer and sets z to that value. -// If buf is larger than 32 bytes, uses only the last 32 bytes. Returns z. -func (z *Uint) SetBytes(buf []byte) *Uint { - switch l := len(buf); l { - case 0: - z.Clear() - case 1: - z.SetBytes1(buf) - case 2: - z.SetBytes2(buf) - case 3: - z.SetBytes3(buf) - case 4: - z.SetBytes4(buf) - case 5: - z.SetBytes5(buf) - case 6: - z.SetBytes6(buf) - case 7: - z.SetBytes7(buf) - case 8: - z.SetBytes8(buf) - case 9: - z.SetBytes9(buf) - case 10: - z.SetBytes10(buf) - case 11: - z.SetBytes11(buf) - case 12: - z.SetBytes12(buf) - case 13: - z.SetBytes13(buf) - case 14: - z.SetBytes14(buf) - case 15: - z.SetBytes15(buf) - case 16: - z.SetBytes16(buf) - case 17: - z.SetBytes17(buf) - case 18: - z.SetBytes18(buf) - case 19: - z.SetBytes19(buf) - case 20: - z.SetBytes20(buf) - case 21: - z.SetBytes21(buf) - case 22: - z.SetBytes22(buf) - case 23: - z.SetBytes23(buf) - case 24: - z.SetBytes24(buf) - case 25: - z.SetBytes25(buf) - case 26: - z.SetBytes26(buf) - case 27: - z.SetBytes27(buf) - case 28: - z.SetBytes28(buf) - case 29: - z.SetBytes29(buf) - case 30: - z.SetBytes30(buf) - case 31: - z.SetBytes31(buf) - default: - z.SetBytes32(buf[l-32:]) - } - return z -} - -// SetBytes1 sets z from a 1-byte big-endian slice and returns z. -// Panics if input is shorter than 1 byte. -func (z *Uint) SetBytes1(in []byte) *Uint { - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - z.arr[0] = uint64(in[0]) - return z -} - -// SetBytes2 sets z from a 2-byte big-endian slice and returns z. -// Panics if input is shorter than 2 bytes. -func (z *Uint) SetBytes2(in []byte) *Uint { - _ = in[1] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - z.arr[0] = uint64(binary.BigEndian.Uint16(in[0:2])) - return z -} - -// SetBytes3 sets z from a 3-byte big-endian slice and returns z. -// Panics if input is shorter than 3 bytes. -func (z *Uint) SetBytes3(in []byte) *Uint { - _ = in[2] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - z.arr[0] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])<<16 - return z -} - -// SetBytes4 sets z from a 4-byte big-endian slice and returns z. -// Panics if input is shorter than 4 bytes. -func (z *Uint) SetBytes4(in []byte) *Uint { - _ = in[3] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - z.arr[0] = uint64(binary.BigEndian.Uint32(in[0:4])) - return z -} - -// SetBytes5 sets z from a 5-byte big-endian slice and returns z. -// Panics if input is shorter than 5 bytes. -func (z *Uint) SetBytes5(in []byte) *Uint { - _ = in[4] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - z.arr[0] = bigEndianUint40(in[0:5]) - return z -} - -// SetBytes6 sets z from a 6-byte big-endian slice and returns z. -// Panics if input is shorter than 6 bytes. -func (z *Uint) SetBytes6(in []byte) *Uint { - _ = in[5] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - z.arr[0] = bigEndianUint48(in[0:6]) - return z -} - -// SetBytes7 sets z from a 7-byte big-endian slice and returns z. -// Panics if input is shorter than 7 bytes. -func (z *Uint) SetBytes7(in []byte) *Uint { - _ = in[6] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - z.arr[0] = bigEndianUint56(in[0:7]) - return z -} - -// SetBytes8 sets z from an 8-byte big-endian slice and returns z. -// Panics if input is shorter than 8 bytes. -func (z *Uint) SetBytes8(in []byte) *Uint { - _ = in[7] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - z.arr[0] = binary.BigEndian.Uint64(in[0:8]) - return z -} - -// SetBytes9 sets z from a 9-byte big-endian slice and returns z. -// Panics if input is shorter than 9 bytes. -func (z *Uint) SetBytes9(in []byte) *Uint { - _ = in[8] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2] = 0, 0 - z.arr[1] = uint64(in[0]) - z.arr[0] = binary.BigEndian.Uint64(in[1:9]) - return z -} - -// SetBytes10 sets z from a 10-byte big-endian slice and returns z. -// Panics if input is shorter than 10 bytes. -func (z *Uint) SetBytes10(in []byte) *Uint { - _ = in[9] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2] = 0, 0 - z.arr[1] = uint64(binary.BigEndian.Uint16(in[0:2])) - z.arr[0] = binary.BigEndian.Uint64(in[2:10]) - return z -} - -// SetBytes11 sets z from an 11-byte big-endian slice and returns z. -// Panics if input is shorter than 11 bytes. -func (z *Uint) SetBytes11(in []byte) *Uint { - _ = in[10] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2] = 0, 0 - z.arr[1] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])<<16 - z.arr[0] = binary.BigEndian.Uint64(in[3:11]) - return z -} - -// SetBytes12 sets z from a 12-byte big-endian slice and returns z. -// Panics if input is shorter than 12 bytes. -func (z *Uint) SetBytes12(in []byte) *Uint { - _ = in[11] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2] = 0, 0 - z.arr[1] = uint64(binary.BigEndian.Uint32(in[0:4])) - z.arr[0] = binary.BigEndian.Uint64(in[4:12]) - return z -} - -// SetBytes13 sets z from a 13-byte big-endian slice and returns z. -// Panics if input is shorter than 13 bytes. -func (z *Uint) SetBytes13(in []byte) *Uint { - _ = in[12] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2] = 0, 0 - z.arr[1] = bigEndianUint40(in[0:5]) - z.arr[0] = binary.BigEndian.Uint64(in[5:13]) - return z -} - -// SetBytes14 sets z from a 14-byte big-endian slice and returns z. -// Panics if input is shorter than 14 bytes. -func (z *Uint) SetBytes14(in []byte) *Uint { - _ = in[13] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2] = 0, 0 - z.arr[1] = bigEndianUint48(in[0:6]) - z.arr[0] = binary.BigEndian.Uint64(in[6:14]) - return z -} - -// SetBytes15 sets z from a 15-byte big-endian slice and returns z. -// Panics if input is shorter than 15 bytes. -func (z *Uint) SetBytes15(in []byte) *Uint { - _ = in[14] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2] = 0, 0 - z.arr[1] = bigEndianUint56(in[0:7]) - z.arr[0] = binary.BigEndian.Uint64(in[7:15]) - return z -} - -// SetBytes16 sets z from a 16-byte big-endian slice and returns z. -// Panics if input is shorter than 16 bytes. -func (z *Uint) SetBytes16(in []byte) *Uint { - _ = in[15] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3], z.arr[2] = 0, 0 - z.arr[1] = binary.BigEndian.Uint64(in[0:8]) - z.arr[0] = binary.BigEndian.Uint64(in[8:16]) - return z -} - -// SetBytes17 sets z from a 17-byte big-endian slice and returns z. -// Panics if input is shorter than 17 bytes. -func (z *Uint) SetBytes17(in []byte) *Uint { - _ = in[16] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = 0 - z.arr[2] = uint64(in[0]) - z.arr[1] = binary.BigEndian.Uint64(in[1:9]) - z.arr[0] = binary.BigEndian.Uint64(in[9:17]) - return z -} - -// SetBytes18 sets z from an 18-byte big-endian slice and returns z. -// Panics if input is shorter than 18 bytes. -func (z *Uint) SetBytes18(in []byte) *Uint { - _ = in[17] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = 0 - z.arr[2] = uint64(binary.BigEndian.Uint16(in[0:2])) - z.arr[1] = binary.BigEndian.Uint64(in[2:10]) - z.arr[0] = binary.BigEndian.Uint64(in[10:18]) - return z -} - -// SetBytes19 sets z from a 19-byte big-endian slice and returns z. -// Panics if input is shorter than 19 bytes. -func (z *Uint) SetBytes19(in []byte) *Uint { - _ = in[18] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = 0 - z.arr[2] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])<<16 - z.arr[1] = binary.BigEndian.Uint64(in[3:11]) - z.arr[0] = binary.BigEndian.Uint64(in[11:19]) - return z -} - -// SetBytes20 sets z from a 20-byte big-endian slice and returns z. -// Panics if input is shorter than 20 bytes. -func (z *Uint) SetBytes20(in []byte) *Uint { - _ = in[19] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = 0 - z.arr[2] = uint64(binary.BigEndian.Uint32(in[0:4])) - z.arr[1] = binary.BigEndian.Uint64(in[4:12]) - z.arr[0] = binary.BigEndian.Uint64(in[12:20]) - return z -} - -// SetBytes21 sets z from a 21-byte big-endian slice and returns z. -// Panics if input is shorter than 21 bytes. -func (z *Uint) SetBytes21(in []byte) *Uint { - _ = in[20] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = 0 - z.arr[2] = bigEndianUint40(in[0:5]) - z.arr[1] = binary.BigEndian.Uint64(in[5:13]) - z.arr[0] = binary.BigEndian.Uint64(in[13:21]) - return z -} - -// SetBytes22 sets z from a 22-byte big-endian slice and returns z. -// Panics if input is shorter than 22 bytes. -func (z *Uint) SetBytes22(in []byte) *Uint { - _ = in[21] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = 0 - z.arr[2] = bigEndianUint48(in[0:6]) - z.arr[1] = binary.BigEndian.Uint64(in[6:14]) - z.arr[0] = binary.BigEndian.Uint64(in[14:22]) - return z -} - -// SetBytes23 sets z from a 23-byte big-endian slice and returns z. -// Panics if input is shorter than 23 bytes. -func (z *Uint) SetBytes23(in []byte) *Uint { - _ = in[22] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = 0 - z.arr[2] = bigEndianUint56(in[0:7]) - z.arr[1] = binary.BigEndian.Uint64(in[7:15]) - z.arr[0] = binary.BigEndian.Uint64(in[15:23]) - return z -} - -// SetBytes24 sets z from a 24-byte big-endian slice and returns z. -// Panics if input is shorter than 24 bytes. -func (z *Uint) SetBytes24(in []byte) *Uint { - _ = in[23] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = 0 - z.arr[2] = binary.BigEndian.Uint64(in[0:8]) - z.arr[1] = binary.BigEndian.Uint64(in[8:16]) - z.arr[0] = binary.BigEndian.Uint64(in[16:24]) - return z -} - -// SetBytes25 sets z from a 25-byte big-endian slice and returns z. -// Panics if input is shorter than 25 bytes. -func (z *Uint) SetBytes25(in []byte) *Uint { - _ = in[24] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = uint64(in[0]) - z.arr[2] = binary.BigEndian.Uint64(in[1:9]) - z.arr[1] = binary.BigEndian.Uint64(in[9:17]) - z.arr[0] = binary.BigEndian.Uint64(in[17:25]) - return z -} - -// SetBytes26 sets z from a 26-byte big-endian slice and returns z. -// Panics if input is shorter than 26 bytes. -func (z *Uint) SetBytes26(in []byte) *Uint { - _ = in[25] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = uint64(binary.BigEndian.Uint16(in[0:2])) - z.arr[2] = binary.BigEndian.Uint64(in[2:10]) - z.arr[1] = binary.BigEndian.Uint64(in[10:18]) - z.arr[0] = binary.BigEndian.Uint64(in[18:26]) - return z -} - -// SetBytes27 sets z from a 27-byte big-endian slice and returns z. -// Panics if input is shorter than 27 bytes. -func (z *Uint) SetBytes27(in []byte) *Uint { - _ = in[26] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = uint64(binary.BigEndian.Uint16(in[1:3])) | uint64(in[0])<<16 - z.arr[2] = binary.BigEndian.Uint64(in[3:11]) - z.arr[1] = binary.BigEndian.Uint64(in[11:19]) - z.arr[0] = binary.BigEndian.Uint64(in[19:27]) - return z -} - -// SetBytes28 sets z from a 28-byte big-endian slice and returns z. -// Panics if input is shorter than 28 bytes. -func (z *Uint) SetBytes28(in []byte) *Uint { - _ = in[27] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = uint64(binary.BigEndian.Uint32(in[0:4])) - z.arr[2] = binary.BigEndian.Uint64(in[4:12]) - z.arr[1] = binary.BigEndian.Uint64(in[12:20]) - z.arr[0] = binary.BigEndian.Uint64(in[20:28]) - return z -} - -// SetBytes29 sets z from a 29-byte big-endian slice and returns z. -// Panics if input is shorter than 29 bytes. -func (z *Uint) SetBytes29(in []byte) *Uint { - _ = in[28] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = bigEndianUint40(in[0:5]) - z.arr[2] = binary.BigEndian.Uint64(in[5:13]) - z.arr[1] = binary.BigEndian.Uint64(in[13:21]) - z.arr[0] = binary.BigEndian.Uint64(in[21:29]) - return z -} - -// SetBytes30 sets z from a 30-byte big-endian slice and returns z. -// Panics if input is shorter than 30 bytes. -func (z *Uint) SetBytes30(in []byte) *Uint { - _ = in[29] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = bigEndianUint48(in[0:6]) - z.arr[2] = binary.BigEndian.Uint64(in[6:14]) - z.arr[1] = binary.BigEndian.Uint64(in[14:22]) - z.arr[0] = binary.BigEndian.Uint64(in[22:30]) - return z -} - -// SetBytes31 sets z from a 31-byte big-endian slice and returns z. -// Panics if input is shorter than 31 bytes. -func (z *Uint) SetBytes31(in []byte) *Uint { - _ = in[30] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = bigEndianUint56(in[0:7]) - z.arr[2] = binary.BigEndian.Uint64(in[7:15]) - z.arr[1] = binary.BigEndian.Uint64(in[15:23]) - z.arr[0] = binary.BigEndian.Uint64(in[23:31]) - return z -} - -// SetBytes32 sets z from a 32-byte big-endian slice and returns z. -// Panics if input is shorter than 32 bytes. -func (z *Uint) SetBytes32(in []byte) *Uint { - _ = in[31] // bounds check hint to compiler; see golang.org/issue/14808 - z.arr[3] = binary.BigEndian.Uint64(in[0:8]) - z.arr[2] = binary.BigEndian.Uint64(in[8:16]) - z.arr[1] = binary.BigEndian.Uint64(in[16:24]) - z.arr[0] = binary.BigEndian.Uint64(in[24:32]) - return z -} - -// Utility methods that are "missing" among the bigEndian.UintXX methods. - -// bigEndianUint40 returns the uint64 value represented by the 5 bytes in big-endian order. -func bigEndianUint40(b []byte) uint64 { - _ = b[4] // bounds check hint to compiler; see golang.org/issue/14808 - return uint64(b[4]) | uint64(b[3])<<8 | uint64(b[2])<<16 | uint64(b[1])<<24 | - uint64(b[0])<<32 -} - -// bigEndianUint56 returns the uint64 value represented by the 7 bytes in big-endian order. -func bigEndianUint56(b []byte) uint64 { - _ = b[6] // bounds check hint to compiler; see golang.org/issue/14808 - return uint64(b[6]) | uint64(b[5])<<8 | uint64(b[4])<<16 | uint64(b[3])<<24 | - uint64(b[2])<<32 | uint64(b[1])<<40 | uint64(b[0])<<48 -} - -// bigEndianUint48 returns the uint64 value represented by the 6 bytes in big-endian order. -func bigEndianUint48(b []byte) uint64 { - _ = b[5] // bounds check hint to compiler; see golang.org/issue/14808 - return uint64(b[5]) | uint64(b[4])<<8 | uint64(b[3])<<16 | uint64(b[2])<<24 | - uint64(b[1])<<32 | uint64(b[0])<<40 -} diff --git a/contract/p/gnoswap/uint256/conversion_test.gno b/contract/p/gnoswap/uint256/conversion_test.gno deleted file mode 100644 index 12ae99c..0000000 --- a/contract/p/gnoswap/uint256/conversion_test.gno +++ /dev/null @@ -1,60 +0,0 @@ -package uint256 - -import ( - "testing" -) - -func TestIsUint64(t *testing.T) { - tests := []struct { - x string - want bool - }{ - {"0x0", true}, - {"0x1", true}, - {"0x10", true}, - {"0xffffffffffffffff", true}, - {"0x10000000000000000", false}, - } - - for _, tc := range tests { - x := MustFromHex(tc.x) - got := x.IsUint64() - - if got != tc.want { - t.Errorf("IsUint64(%s) = %v, want %v", tc.x, got, tc.want) - } - } -} - -func TestDec(t *testing.T) { - testCases := []struct { - name string - z Uint - want string - }{ - { - name: "zero", - z: Uint{arr: [4]uint64{0, 0, 0, 0}}, - want: "0", - }, - { - name: "less than 20 digits", - z: Uint{arr: [4]uint64{1234567890, 0, 0, 0}}, - want: "1234567890", - }, - { - name: "max possible value", - z: Uint{arr: [4]uint64{^uint64(0), ^uint64(0), ^uint64(0), ^uint64(0)}}, - want: "115792089237316195423570985008687907853269984665640564039457584007913129639935", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - result := tc.z.Dec() - if result != tc.want { - t.Errorf("Dec(%v) = %s, want %s", tc.z, result, tc.want) - } - }) - } -} diff --git a/contract/p/gnoswap/uint256/doc.gno b/contract/p/gnoswap/uint256/doc.gno deleted file mode 100644 index e2d721b..0000000 --- a/contract/p/gnoswap/uint256/doc.gno +++ /dev/null @@ -1,14 +0,0 @@ -// Package uint256 implements 256-bit unsigned integer arithmetic for GnoSwap. -// -// This package provides a Uint type that represents a 256-bit unsigned integer, -// stored as four uint64 values in little-endian order. It includes arithmetic -// operations with overflow detection, which are essential for safe token -// calculations in DeFi protocols. -// -// The implementation is optimized for gas efficiency while maintaining -// compatibility with Ethereum's uint256 semantics, ensuring consistent -// behavior across different blockchain environments. -// -// All operations that may overflow return both the result and an overflow flag, -// allowing calling code to handle overflow conditions appropriately. -package uint256 diff --git a/contract/p/gnoswap/uint256/error.gno b/contract/p/gnoswap/uint256/error.gno deleted file mode 100644 index d200bb9..0000000 --- a/contract/p/gnoswap/uint256/error.gno +++ /dev/null @@ -1,73 +0,0 @@ -package uint256 - -import ( - "errors" -) - -var ( - ErrEmptyString = errors.New("empty hex string") - ErrSyntax = errors.New("invalid hex string") - ErrRange = errors.New("number out of range") - ErrMissingPrefix = errors.New("hex string without 0x prefix") - ErrEmptyNumber = errors.New("hex string \"0x\"") - ErrLeadingZero = errors.New("hex number with leading zero digits") - ErrBig256Range = errors.New("hex number > 256 bits") - ErrBadBufferLength = errors.New("bad ssz buffer length") - ErrBadEncodedLength = errors.New("bad ssz encoded length") - ErrInvalidBase = errors.New("invalid base") - ErrInvalidBitSize = errors.New("invalid bit size") -) - -type u256Error struct { - fn string // function name - input string - err error -} - -func (e *u256Error) Error() string { - return e.fn + ": " + e.input + ": " + e.err.Error() -} - -func (e *u256Error) Unwrap() error { - return e.err -} - -func errEmptyString(fn, input string) error { - return &u256Error{fn: fn, input: input, err: ErrEmptyString} -} - -func errSyntax(fn, input string) error { - return &u256Error{fn: fn, input: input, err: ErrSyntax} -} - -func errMissingPrefix(fn, input string) error { - return &u256Error{fn: fn, input: input, err: ErrMissingPrefix} -} - -func errEmptyNumber(fn, input string) error { - return &u256Error{fn: fn, input: input, err: ErrEmptyNumber} -} - -func errLeadingZero(fn, input string) error { - return &u256Error{fn: fn, input: input, err: ErrLeadingZero} -} - -func errRange(fn, input string) error { - return &u256Error{fn: fn, input: input, err: ErrRange} -} - -func errBig256Range(fn, input string) error { - return &u256Error{fn: fn, input: input, err: ErrBig256Range} -} - -func errBadBufferLength(fn, input string) error { - return &u256Error{fn: fn, input: input, err: ErrBadBufferLength} -} - -func errInvalidBase(fn string, base int) error { - return &u256Error{fn: fn, input: string(base), err: ErrInvalidBase} -} - -func errInvalidBitSize(fn string, bitSize int) error { - return &u256Error{fn: fn, input: string(bitSize), err: ErrInvalidBitSize} -} diff --git a/contract/p/gnoswap/uint256/fullmath.gno b/contract/p/gnoswap/uint256/fullmath.gno deleted file mode 100644 index fc86f95..0000000 --- a/contract/p/gnoswap/uint256/fullmath.gno +++ /dev/null @@ -1,106 +0,0 @@ -// REF: https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol - -// fullmath implements Uniswap V3's FullMath library. -// -// This library provides advanced fixed-point math operations that are essential -// for Uniswap V3's tick math and liquidity calculations. It enables precise -// calculations of (a * b / denominator) with full 512-bit intermediate precision. -// -// NOTE: Unlike other arithmetic functions in the uint256 package that return errors, -// functions in this file panic on invalid inputs to maintain behavioral compatibility -// with the original Solidity implementation which uses require() statements. -// -// This design choice is intentional because: -// 1. These functions are typically used in hot paths where error handling would add overhead -// 2. Invalid inputs (like zero denominator) represent programming errors, not runtime conditions -// 3. Staying close to the Solidity implementation makes protocol porting more reliable -// -// If you need error-returning versions, wrap these functions with appropriate error handling. -package uint256 - -import ( - "gno.land/p/nt/ufmt" -) - -// MulDiv calculates (a * b) / denominator with full 512-bit intermediate precision. -// Panics if denominator is zero or if the result overflows 256 bits. -func MulDiv(a, b, denominator *Uint) *Uint { - if denominator.IsZero() { - panic("denominator must be greater than 0") - } - - // 512-bit product (8 limbs of 64 bits) - p := umul(a, b) - - if (p[4] | p[5] | p[6] | p[7]) == 0 { - var lo Uint - lo.arr = [4]uint64{p[0], p[1], p[2], p[3]} - return new(Uint).Div(&lo, denominator) - } - - // optional early overflow check: - // If hi >= denominator then floor((hi*2^256 + lo) / denominator) >= 2^256, which is overflow. - { - var hi Uint - hi.arr = [4]uint64{p[4], p[5], p[6], p[7]} - if denominator.Lte(&hi) { - panic(ufmt.Sprintf("overflow: denominator(%s) must be greater than hi(%s)", denominator.ToString(), hi.ToString())) - } - } - - // perform 512 / 256 division - // udivrem stores quotient into `quot` (len(u) - len(d) + 1 words) - // we pass 8 words to be safe. - var quot [8]uint64 - udivrem(quot[:], p[:], denominator) // ignore remainder - - if (quot[4] | quot[5] | quot[6] | quot[7]) != 0 { - panic("uint256: MulDiv overflow (high quotient words non-zero)") - } - - // return lower 256 bits of quotient - var z Uint - copy(z.arr[:], quot[:4]) - return &z -} - -// MulDivRoundingUp calculates ceil((a * b) / denominator) with full 512-bit intermediate precision. -// Panics if denominator is zero or if the result overflows 256 bits. -func MulDivRoundingUp(a, b, denominator *Uint) *Uint { - result := MulDiv(a, b, denominator) - - // Check if there's a remainder - mulModResult := new(Uint).MulMod(a, b, denominator) - - // If there's no remainder, return the result as-is - if mulModResult.IsZero() { - return result - } - - // Add 1 to round up, but check for overflow - if result.Eq(MustFromDecimal(MAX_UINT256)) { - panic(ufmt.Sprintf("overflow: result(%s) + 1 would exceed MAX_UINT256", result.ToString())) - } - - return new(Uint).Add(result, one) -} - -// DivRoundingUp calculates ceil(x / y) and returns the result. -// Panics if y is zero. -func DivRoundingUp(x, y *Uint) *Uint { - if y.IsZero() { - panic("division by zero") - } - div := new(Uint).Div(x, y) - mod := new(Uint).Mod(x, y) - z := new(Uint).Add(div, gt(mod, Zero())) - return z -} - -// gt returns One() if x > y, otherwise returns Zero(). -func gt(x, y *Uint) *Uint { - if x.Gt(y) { - return one - } - return Zero() -} diff --git a/contract/p/gnoswap/uint256/fullmath_test.gno b/contract/p/gnoswap/uint256/fullmath_test.gno deleted file mode 100644 index fe3b4a5..0000000 --- a/contract/p/gnoswap/uint256/fullmath_test.gno +++ /dev/null @@ -1,844 +0,0 @@ -package uint256 - -import ( - "testing" - - "gno.land/p/nt/ufmt" -) - -func TestFullMathMulDiv(t *testing.T) { - tests := []struct { - name string - x string - y string - denom string - want string - wantPanic bool - }{ - // Basic functionality - { - name: "simple_multiplication_division", - x: "100", - y: "200", - denom: "50", - want: "400", // (100 * 200) / 50 = 20000 / 50 = 400 - wantPanic: false, - }, - { - name: "exact_division", - x: "1000", - y: "3000", - denom: "100", - want: "30000", // (1000 * 3000) / 100 = 3000000 / 100 = 30000 - wantPanic: false, - }, - - // Zero inputs - { - name: "zero_first_operand", - x: "0", - y: "1000", - denom: "100", - want: "0", - wantPanic: false, - }, - { - name: "zero_second_operand", - x: "123456789", - y: "0", - denom: "100", - want: "0", - wantPanic: false, - }, - - // Identity operations (denom = 1) - { - name: "identity_small_numbers", - x: "123", - y: "456", - denom: "1", - want: "56088", // 123 * 456 = 56088 - wantPanic: false, - }, - { - name: "identity_max_value", - x: MAX_UINT256, - y: "1", - denom: "1", - want: MAX_UINT256, - wantPanic: false, - }, - - // Essential panic cases - { - name: "panic_denominator_zero", - x: "100", - y: "200", - denom: "0", - want: "", - wantPanic: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - denom := MustFromDecimal(tc.denom) - - if tc.wantPanic { - defer func() { - if r := recover(); r == nil { - t.Errorf("MulDiv(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) - } - }() - MulDiv(x, y, denom) - return - } - - got := MulDiv(x, y, denom) - want := MustFromDecimal(tc.want) - - if !got.Eq(want) { - t.Errorf("MulDiv(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) - } - }) - } -} - -func TestMulDivBoundary(t *testing.T) { - tests := []struct { - name string - x string - y string - denom string - want string - wantPanic bool - }{ - // Core boundaries - { - name: "all_max_inputs", - x: MAX_UINT256, - y: MAX_UINT256, - denom: MAX_UINT256, - want: MAX_UINT256, - wantPanic: false, - }, - - // Overflow detection (hi >= denominator) - { - name: "overflow_hi_equals_denom", - x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 - y: "2", - denom: "1", // hiProduct = 1, denom = 1 - want: "", - wantPanic: true, - }, - { - name: "overflow_hi_exceeds_denom", - x: MAX_UINT256, - y: MAX_UINT256, - denom: "340282366920938463463374607431768211455", // 2^128 - 1 - want: "", - wantPanic: true, - }, - { - name: "overflow_q128_squared", - x: "340282366920938463463374607431768211456", // Q128 (2^128) - y: "340282366920938463463374607431768211456", // Q128 - denom: "1", - want: "", - wantPanic: true, - }, - - // Just below overflow boundary - { - name: "boundary_hi_lt_denom_no_overflow", - x: "340282366920938463463374607431768211456", // Q128 - y: "340282366920938463463374607431768211456", // Q128 - denom: "340282366920938463463374607431768211457", // Q128 + 1 - want: "340282366920938463463374607431768211455", // Q128 - 1 - wantPanic: false, - }, - - // Borrow conditions (remainder > lo) - { - name: "borrow_needed_remainder_gt_lo", - x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 - y: "2", - denom: "57896044618658097711785492504343953926634992332820282019728792003956564819967", // 2^255 - 1 - want: "2", - wantPanic: false, - }, - { - name: "no_borrow_remainder_lt_lo", - x: "100", - y: "100", - denom: "101", // remainder = 1, lo = 10000 - want: "99", // 10000 / 101 = 99 - wantPanic: false, - }, - - // Powers of 2 optimization path - { - name: "power_of_2_denominator", - x: "340282366920938463463374607431768211456", // Q128 - y: "1", - denom: "1024", // 2^10 - want: "332306998946228968225951765070086144", // Q128 / 1024 - wantPanic: false, - }, - { - name: "mixed_power_of_2_factorization", - x: "1000000000000000000", - y: "1000000000000000000", - denom: "9223372036854775808", // 2^63 (single high bit) - want: "108420217248550443", - wantPanic: false, - }, - { - name: "power_of_2_division_large", - x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 - y: "2", - denom: "4", - want: "28948022309329048855892746252171976963317496166410141009864396001978282409984", // 2^254 - wantPanic: false, - }, - - // Newton-Raphson precision (odd denominators) - { - name: "odd_denominator_prime", - x: "1000000000000000000", - y: "1000000000000000000", - denom: "999999999999999989", // Large prime - want: "1000000000000000011", - wantPanic: false, - }, - { - name: "odd_denominator_alternating_bits", - x: "1000000000000000000", - y: "1000000000000000000", - denom: "6148914691236517205", // 0x5555555555555555 (alternating bits) - want: "162630325872825665", - wantPanic: false, - }, - - // Q128 special cases - { - name: "q128_divided_by_small_odd", - x: "340282366920938463463374607431768211456", // Q128 - y: "1", - denom: "3", - want: "113427455640312821154458202477256070485", // Q128 / 3 - wantPanic: false, - }, - { - name: "q128_divided_by_q128_minus_1", - x: "340282366920938463463374607431768211456", // Q128 - y: "1", - denom: "340282366920938463463374607431768211455", // Q128 - 1 - want: "1", - wantPanic: false, - }, - - // Edge cases with zero - { - name: "zero_numerator", - x: "0", - y: "1000000000000000000", - denom: "999999999999999999", - want: "0", - wantPanic: false, - }, - - // Combined optimization paths - { - name: "combined_optimization_paths", - x: "1606938044258990275541962092341162602522202993782792835301376", // 2^200 - y: "36028797018963968", // 2^55 - denom: "1809251394333065553493296640760748560207343510400633813116524750123642650624", // 2^250 - want: "32", // 2^(200+55-250) = 2^5 = 32 - wantPanic: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - denom := MustFromDecimal(tc.denom) - - if tc.wantPanic { - defer func() { - if r := recover(); r == nil { - t.Errorf("MulDiv(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) - } - }() - MulDiv(x, y, denom) - return - } - - got := MulDiv(x, y, denom) - want := MustFromDecimal(tc.want) - - if !got.Eq(want) { - t.Errorf("MulDiv(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) - } - }) - } -} - -// This test verifies that the MulDiv function does not modify its input parameters -func TestMulDivInputPreservation(t *testing.T) { - tests := []struct { - name string - x string - y string - denom string - }{ - { - name: "normal_inputs", - x: "12345678901234567890", - y: "98765432109876543210", - denom: "123456789", - }, - { - name: "power_of_2_boundary", - x: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 - y: "2", - denom: "4", - }, - { - name: "phantom_overflow_case", - x: "340282366920938463463374607431768211456", // Q128 - y: "11930464781601263584560605149792510336", // 35 * Q128 / 1000 - denom: "2722258935367507707706996859454145691648", // 8 * Q128 - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - denom := MustFromDecimal(tc.denom) - - // Create copies of input values - xCopy := new(Uint).Set(x) - yCopy := new(Uint).Set(y) - denomCopy := new(Uint).Set(denom) - - // Call MulDiv - MulDiv(x, y, denom) - - // Verify that input values were not modified - if !x.Eq(xCopy) { - t.Errorf("Input 'x' was modified: original=%s, modified=%s", - xCopy.ToString(), x.ToString()) - } - - if !y.Eq(yCopy) { - t.Errorf("Input 'y' was modified: original=%s, modified=%s", - yCopy.ToString(), y.ToString()) - } - - if !denom.Eq(denomCopy) { - t.Errorf("Input 'denom' was modified: original=%s, modified=%s", - denomCopy.ToString(), denom.ToString()) - } - }) - } -} - -// Phantom overflow cases (product > 256 bits but result fits) -func TestMulDivPhantomOverflow(t *testing.T) { - tests := []struct { - name string - x string - y string - denom string - want string - wantPanic bool - }{ - { - name: "phantom_overflow_case_1", - x: "340282366920938463463374607431768211456", // Q128 - y: "11930464781601263584560605149792510336", // 35 * Q128 / 1000 - denom: "2722258935367507707706996859454145691648", // 8 * Q128 - want: "1491308097700157948070075643724063792", // (35/1000) / 8 * Q128 = 4.375/1000 * Q128 - wantPanic: false, - }, - { - name: "phantom_overflow_repeating_decimal", - x: "340282366920938463463374607431768211456", // Q128 - y: "340282366920938463463374607431768211456000", // 1000 * Q128 - denom: "1020847100762815390390123822295304634368000", // 3000 * Q128 - want: "113427455640312821154458202477256070485", // Q128 / 3 - wantPanic: false, - }, - { - name: "accurate_without_phantom_overflow", - x: "340282366920938463463374607431768211456", // Q128 - y: "170141183460469231731687303715884105728", // 0.5 * Q128 - denom: "510423550381407695195061911147652317184", // 1.5 * Q128 - want: "113427455640312821154458202477256070485", // Q128 / 3 - wantPanic: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - denom := MustFromDecimal(tc.denom) - - if tc.wantPanic { - defer func() { - if r := recover(); r == nil { - t.Errorf("MulDiv(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) - } - }() - MulDiv(x, y, denom) - return - } - - got := MulDiv(x, y, denom) - want := MustFromDecimal(tc.want) - - if !got.Eq(want) { - t.Errorf("MulDiv(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) - } - }) - } -} - -func TestMulDivRoundingUp(t *testing.T) { - tests := []struct { - name string - x string - y string - denom string - want string - wantPanic bool - }{ - // Basic rounding functionality - { - name: "no_rounding_needed", - x: "100", - y: "200", - denom: "50", - want: "400", // 20000 / 50 = 400 (exact) - wantPanic: false, - }, - { - name: "rounding_up_needed", - x: "100", - y: "201", - denom: "50", - want: "402", // 20100 / 50 = 402 (exact) - wantPanic: false, - }, - { - name: "rounding_up_with_remainder", - x: "101", - y: "199", - denom: "50", - want: "402", // 1101 * 199 = 20099, 20099 / 50 = 401.98 → 402 - wantPanic: false, - }, - - // Identity operations - { - name: "identity_operation", - x: "789", - y: "123", - denom: "1", - want: "97047", // 789 * 123 = 97047 - wantPanic: false, - }, - - // Essential panic cases - { - name: "panic_denominator_zero", - x: "100", - y: "200", - denom: "0", - want: "", - wantPanic: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - denom := MustFromDecimal(tc.denom) - - if tc.wantPanic { - defer func() { - if r := recover(); r == nil { - t.Errorf("MulDivRoundingUp(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) - } - }() - MulDivRoundingUp(x, y, denom) - return - } - - got := MulDivRoundingUp(x, y, denom) - want := MustFromDecimal(tc.want) - - if !got.Eq(want) { - t.Errorf("MulDivRoundingUp(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) - } - }) - } -} - -func TestMulDivRoundingUpBoundary(t *testing.T) { - tests := []struct { - name string - x string - y string - denom string - want string - wantPanic bool - }{ - // Boundary rounding cases - { - name: "max_result_no_overflow", - x: MAX_UINT256, - y: "1", - denom: "1", - want: MAX_UINT256, - wantPanic: false, - }, - { - name: "q128_divided_by_3_rounded_up", - x: "340282366920938463463374607431768211456", // Q128 - y: "1", - denom: "3", - want: "113427455640312821154458202477256070486", // (Q128 / 3) + 1 - wantPanic: false, - }, - { - name: "overflow_after_rounding", - x: "340282366920938463463374607431768211456", // Q128 - y: "340282366920938463463374607431768211456", // Q128 - denom: "1", - want: "", - wantPanic: true, - }, - { - name: "rounding_at_max_boundary", - x: MAX_UINT256, - y: "3", - denom: "3", - want: MAX_UINT256, // No rounding needed (exact division) - wantPanic: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - denom := MustFromDecimal(tc.denom) - - if tc.wantPanic { - defer func() { - if r := recover(); r == nil { - t.Errorf("MulDivRoundingUp(%s, %s, %s) expected panic but got none", tc.x, tc.y, tc.denom) - } - }() - MulDivRoundingUp(x, y, denom) - return - } - - got := MulDivRoundingUp(x, y, denom) - want := MustFromDecimal(tc.want) - - if !got.Eq(want) { - t.Errorf("MulDivRoundingUp(%s, %s, %s) = %s, want %s", tc.x, tc.y, tc.denom, got.ToString(), tc.want) - } - }) - } -} - -func TestDivRoundingUp(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - wantPanic bool - }{ - // Basic functionality - { - name: "exact_division", - x: "100", - y: "10", - want: "10", - wantPanic: false, - }, - { - name: "division_with_remainder", - x: "101", - y: "10", - want: "11", // 10 + 1 - wantPanic: false, - }, - { - name: "zero_dividend", - x: "0", - y: "10", - want: "0", - wantPanic: false, - }, - { - name: "one_divided_by_two", - x: "1", - y: "2", - want: "1", // 0 + 1 (rounded up) - wantPanic: false, - }, - - // Identity operations - { - name: "identity_operation", - x: "12345", - y: "1", - want: "12345", - wantPanic: false, - }, - - // Essential panic cases - { - name: "panic_division_by_zero", - x: "100", - y: "0", - want: "", - wantPanic: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - - if tc.wantPanic { - defer func() { - if r := recover(); r == nil { - t.Errorf("DivRoundingUp(%s, %s) expected panic but got none", tc.x, tc.y) - } - }() - DivRoundingUp(x, y) - return - } - - got := DivRoundingUp(x, y) - want := MustFromDecimal(tc.want) - - if !got.Eq(want) { - t.Errorf("DivRoundingUp(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - }) - } -} - -func TestDivRoundingUpBoundary(t *testing.T) { - tests := []struct { - name string - x string - y string - want string - wantPanic bool - }{ - // Boundary value cases - { - name: "max_divided_by_max", - x: MAX_UINT256, - y: MAX_UINT256, - want: "1", - wantPanic: false, - }, - { - name: "max_minus_1_divided_by_max_rounded_up", - x: "115792089237316195423570985008687907853269984665640564039457584007913129639934", // MAX - 1 - y: MAX_UINT256, - want: "1", // 0 + 1 (rounded up) - wantPanic: false, - }, - { - name: "large_number_with_remainder", - x: "1000000000000000000000000000000000000001", - y: "1000000000000000000", - want: "1000000000000000000001", // 1000000000000000000 + 1 - wantPanic: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - - if tc.wantPanic { - defer func() { - if r := recover(); r == nil { - t.Errorf("DivRoundingUp(%s, %s) expected panic but got none", tc.x, tc.y) - } - }() - DivRoundingUp(x, y) - return - } - - got := DivRoundingUp(x, y) - want := MustFromDecimal(tc.want) - - if !got.Eq(want) { - t.Errorf("DivRoundingUp(%s, %s) = %s, want %s", tc.x, tc.y, got.ToString(), tc.want) - } - }) - } -} - -// Floor vs Ceil comparison -func TestMulDivFloorVsCeil(t *testing.T) { - tests := []struct { - name string - x string - y string - denom string - expectSame bool // true if floor == ceil (exact division) - }{ - { - name: "exact_division", - x: "1000", - y: "2000", - denom: "100", - expectSame: true, // 2000000/100 = 20000 (exact) - }, - { - name: "inexact_division", - x: "1234", - y: "5678", - denom: "100", - expectSame: false, // 7006652/100 = 70066.52 (inexact) - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - x := MustFromDecimal(tc.x) - y := MustFromDecimal(tc.y) - denom := MustFromDecimal(tc.denom) - - floor := MulDiv(x, y, denom) - ceil := MulDivRoundingUp(x, y, denom) - - if tc.expectSame { - if !floor.Eq(ceil) { - t.Errorf("Expected floor == ceil for exact division, got floor=%s, ceil=%s", - floor.ToString(), ceil.ToString()) - } - } else { - expected := new(Uint).Add(floor, one) - if !ceil.Eq(expected) { - t.Errorf("Expected ceil == floor + 1 for inexact division, got floor=%s, ceil=%s", - floor.ToString(), ceil.ToString()) - } - } - }) - } -} - -// Property-based testing for DivRoundingUp -func TestDivRoundingUpProperty(t *testing.T) { - // Property: for any x,y>0: ceil = DivRoundingUp(x,y); floor = x.Div(y) - // assert ceil == floor || ceil == floor+1 - - testCases := []struct { - x, y uint64 - }{ - {100, 3}, // 33.33... -> floor=33, ceil=34 - {100, 10}, // 10.0 -> floor=10, ceil=10 (exact) - {1000, 7}, // 142.857... -> floor=142, ceil=143 - {999, 333}, // 3.0 -> floor=3, ceil=3 (exact) - {1, 2}, // 0.5 -> floor=0, ceil=1 - {0, 999}, // 0.0 -> floor=0, ceil=0 (exact) - } - - for i, tc := range testCases { - t.Run(ufmt.Sprintf("property_test_%d", i), func(t *testing.T) { - if tc.y == 0 { - return // Skip division by zero - } - - xUint := MustFromDecimal(ufmt.Sprintf("%d", tc.x)) - yUint := MustFromDecimal(ufmt.Sprintf("%d", tc.y)) - - ceil := DivRoundingUp(xUint, yUint) - floor := new(Uint).Div(xUint, yUint) - floorPlusOne := new(Uint).Add(floor, one) - - if !ceil.Eq(floor) && !ceil.Eq(floorPlusOne) { - t.Errorf("Property failed: ceil(%s) != floor(%s) && ceil != floor+1(%s)", - ceil.ToString(), floor.ToString(), floorPlusOne.ToString()) - } - }) - } -} - -// Mathematical property verification for MulDiv -func TestMulDivMathematicalProperty(t *testing.T) { - // Property: q = MulDiv(x, y, denom), r = (x * y) % denom - // Then: q * denom + r == x * y AND r < denom - - testCases := []struct { - x, y, denom uint64 - }{ - {123, 456, 789}, - {1000, 2000, 500}, - {999, 777, 333}, - {1, 1, 1}, - {0, 999, 123}, - {100, 7, 3}, - } - - for i, tc := range testCases { - t.Run(ufmt.Sprintf("math_property_%d", i), func(t *testing.T) { - if tc.denom == 0 { - return // Skip division by zero - } - - xUint := MustFromDecimal(ufmt.Sprintf("%d", tc.x)) - yUint := MustFromDecimal(ufmt.Sprintf("%d", tc.y)) - denomUint := MustFromDecimal(ufmt.Sprintf("%d", tc.denom)) - - // Skip if this would cause overflow - defer func() { - if r := recover(); r != nil { - // Overflow is expected for some cases - return - } - }() - - product := new(Uint).Mul(xUint, yUint) - quotient := MulDiv(xUint, yUint, denomUint) - remainder := new(Uint).MulMod(xUint, yUint, denomUint) - - // Property 1: remainder < denom - if remainder.Gte(denomUint) { - t.Errorf("Property 1 failed: remainder(%s) >= denom(%s)", - remainder.ToString(), denomUint.ToString()) - } - - // Property 2: quotient * denom + remainder == x * y - reconstructed := new(Uint).Add(new(Uint).Mul(quotient, denomUint), remainder) - if !reconstructed.Eq(product) { - t.Errorf("Property 2 failed: q*d+r(%s) != x*y(%s)", - reconstructed.ToString(), product.ToString()) - } - }) - } -} diff --git a/contract/p/gnoswap/uint256/gnomod.toml b/contract/p/gnoswap/uint256/gnomod.toml deleted file mode 100644 index 1267ec7..0000000 --- a/contract/p/gnoswap/uint256/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/p/gnoswap/uint256" -gno = "0.9" diff --git a/contract/p/gnoswap/uint256/gs_pointer.gno b/contract/p/gnoswap/uint256/gs_pointer.gno deleted file mode 100644 index e24e6bf..0000000 --- a/contract/p/gnoswap/uint256/gs_pointer.gno +++ /dev/null @@ -1,8 +0,0 @@ -package uint256 - -func (z *Uint) NilToZero() *Uint { - if z == nil { - z = NewUint(0) - } - return z -} diff --git a/contract/p/gnoswap/uint256/mod.gno b/contract/p/gnoswap/uint256/mod.gno deleted file mode 100644 index f6ff096..0000000 --- a/contract/p/gnoswap/uint256/mod.gno +++ /dev/null @@ -1,605 +0,0 @@ -package uint256 - -import ( - "math/bits" -) - -// Some utility functions - -// Reciprocal computes a 320-bit value representing 1/m -// -// Notes: -// - specialized for m.arr[3] != 0, hence limited to 2^192 <= m < 2^256 -// - returns zero if m.arr[3] == 0 -// - starts with a 32-bit division, refines with newton-raphson iterations -func Reciprocal(m *Uint) (mu [5]uint64) { - if m.arr[3] == 0 { - return mu - } - - s := bits.LeadingZeros64(m.arr[3]) // Replace with leadingZeros(m) for general case - p := 255 - s // floor(log_2(m)), m>0 - - // 0 or a power of 2? - - // Check if at least one bit is set in m.arr[2], m.arr[1] or m.arr[0], - // or at least two bits in m.arr[3] - - if m.arr[0]|m.arr[1]|m.arr[2]|(m.arr[3]&(m.arr[3]-1)) == 0 { - - mu[4] = ^uint64(0) >> uint(p&63) - mu[3] = ^uint64(0) - mu[2] = ^uint64(0) - mu[1] = ^uint64(0) - mu[0] = ^uint64(0) - - return mu - } - - // Maximise division precision by left-aligning divisor - - var ( - y Uint // left-aligned copy of m - r0 uint32 // estimate of 2^31/y - ) - - y.Lsh(m, uint(s)) // 1/2 < y < 1 - - // Extract most significant 32 bits - - yh := uint32(y.arr[3] >> 32) - - if yh == 0x80000000 { // Avoid overflow in division - r0 = 0xffffffff - } else { - r0, _ = bits.Div32(0x80000000, 0, yh) - } - - // First iteration: 32 -> 64 - - t1 := uint64(r0) // 2^31/y - t1 *= t1 // 2^62/y^2 - t1, _ = bits.Mul64(t1, y.arr[3]) // 2^62/y^2 * 2^64/y / 2^64 = 2^62/y - - r1 := uint64(r0) << 32 // 2^63/y - r1 -= t1 // 2^63/y - 2^62/y = 2^62/y - r1 *= 2 // 2^63/y - - if (r1 | (y.arr[3] << 1)) == 0 { - r1 = ^uint64(0) - } - - // Second iteration: 64 -> 128 - - // square: 2^126/y^2 - a2h, a2l := bits.Mul64(r1, r1) - - // multiply by y: e2h:e2l:b2h = 2^126/y^2 * 2^128/y / 2^128 = 2^126/y - b2h, _ := bits.Mul64(a2l, y.arr[2]) - c2h, c2l := bits.Mul64(a2l, y.arr[3]) - d2h, d2l := bits.Mul64(a2h, y.arr[2]) - e2h, e2l := bits.Mul64(a2h, y.arr[3]) - - b2h, c := bits.Add64(b2h, c2l, 0) - e2l, c = bits.Add64(e2l, c2h, c) - e2h, _ = bits.Add64(e2h, 0, c) - - _, c = bits.Add64(b2h, d2l, 0) - e2l, c = bits.Add64(e2l, d2h, c) - e2h, _ = bits.Add64(e2h, 0, c) - - // subtract: t2h:t2l = 2^127/y - 2^126/y = 2^126/y - t2l, b := bits.Sub64(0, e2l, 0) - t2h, _ := bits.Sub64(r1, e2h, b) - - // double: r2h:r2l = 2^127/y - r2l, c := bits.Add64(t2l, t2l, 0) - r2h, _ := bits.Add64(t2h, t2h, c) - - if (r2h | r2l | (y.arr[3] << 1)) == 0 { - r2h = ^uint64(0) - r2l = ^uint64(0) - } - - // Third iteration: 128 -> 192 - - // square r2 (keep 256 bits): 2^190/y^2 - a3h, a3l := bits.Mul64(r2l, r2l) - b3h, b3l := bits.Mul64(r2l, r2h) - c3h, c3l := bits.Mul64(r2h, r2h) - - a3h, c = bits.Add64(a3h, b3l, 0) - c3l, c = bits.Add64(c3l, b3h, c) - c3h, _ = bits.Add64(c3h, 0, c) - - a3h, c = bits.Add64(a3h, b3l, 0) - c3l, c = bits.Add64(c3l, b3h, c) - c3h, _ = bits.Add64(c3h, 0, c) - - // multiply by y: q = 2^190/y^2 * 2^192/y / 2^192 = 2^190/y - - x0 := a3l - x1 := a3h - x2 := c3l - x3 := c3h - - var q0, q1, q2, q3, q4, t0 uint64 - - q0, _ = bits.Mul64(x2, y.arr[0]) - q1, t0 = bits.Mul64(x3, y.arr[0]) - q0, c = bits.Add64(q0, t0, 0) - q1, _ = bits.Add64(q1, 0, c) - - t1, _ = bits.Mul64(x1, y.arr[1]) - q0, c = bits.Add64(q0, t1, 0) - q2, t0 = bits.Mul64(x3, y.arr[1]) - q1, c = bits.Add64(q1, t0, c) - q2, _ = bits.Add64(q2, 0, c) - - t1, t0 = bits.Mul64(x2, y.arr[1]) - q0, c = bits.Add64(q0, t0, 0) - q1, c = bits.Add64(q1, t1, c) - q2, _ = bits.Add64(q2, 0, c) - - t1, t0 = bits.Mul64(x1, y.arr[2]) - q0, c = bits.Add64(q0, t0, 0) - q1, c = bits.Add64(q1, t1, c) - q3, t0 = bits.Mul64(x3, y.arr[2]) - q2, c = bits.Add64(q2, t0, c) - q3, _ = bits.Add64(q3, 0, c) - - t1, _ = bits.Mul64(x0, y.arr[2]) - q0, c = bits.Add64(q0, t1, 0) - t1, t0 = bits.Mul64(x2, y.arr[2]) - q1, c = bits.Add64(q1, t0, c) - q2, c = bits.Add64(q2, t1, c) - q3, _ = bits.Add64(q3, 0, c) - - t1, t0 = bits.Mul64(x1, y.arr[3]) - q1, c = bits.Add64(q1, t0, 0) - q2, c = bits.Add64(q2, t1, c) - q4, t0 = bits.Mul64(x3, y.arr[3]) - q3, c = bits.Add64(q3, t0, c) - q4, _ = bits.Add64(q4, 0, c) - - t1, t0 = bits.Mul64(x0, y.arr[3]) - q0, c = bits.Add64(q0, t0, 0) - q1, c = bits.Add64(q1, t1, c) - t1, t0 = bits.Mul64(x2, y.arr[3]) - q2, c = bits.Add64(q2, t0, c) - q3, c = bits.Add64(q3, t1, c) - q4, _ = bits.Add64(q4, 0, c) - - // subtract: t3 = 2^191/y - 2^190/y = 2^190/y - _, b = bits.Sub64(0, q0, 0) - _, b = bits.Sub64(0, q1, b) - t3l, b := bits.Sub64(0, q2, b) - t3m, b := bits.Sub64(r2l, q3, b) - t3h, _ := bits.Sub64(r2h, q4, b) - - // double: r3 = 2^191/y - r3l, c := bits.Add64(t3l, t3l, 0) - r3m, c := bits.Add64(t3m, t3m, c) - r3h, _ := bits.Add64(t3h, t3h, c) - - // Fourth iteration: 192 -> 320 - - // square r3 - - a4h, a4l := bits.Mul64(r3l, r3l) - b4h, b4l := bits.Mul64(r3l, r3m) - c4h, c4l := bits.Mul64(r3l, r3h) - d4h, d4l := bits.Mul64(r3m, r3m) - e4h, e4l := bits.Mul64(r3m, r3h) - f4h, f4l := bits.Mul64(r3h, r3h) - - b4h, c = bits.Add64(b4h, c4l, 0) - e4l, c = bits.Add64(e4l, c4h, c) - e4h, _ = bits.Add64(e4h, 0, c) - - a4h, c = bits.Add64(a4h, b4l, 0) - d4l, c = bits.Add64(d4l, b4h, c) - d4h, c = bits.Add64(d4h, e4l, c) - f4l, c = bits.Add64(f4l, e4h, c) - f4h, _ = bits.Add64(f4h, 0, c) - - a4h, c = bits.Add64(a4h, b4l, 0) - d4l, c = bits.Add64(d4l, b4h, c) - d4h, c = bits.Add64(d4h, e4l, c) - f4l, c = bits.Add64(f4l, e4h, c) - f4h, _ = bits.Add64(f4h, 0, c) - - // multiply by y - - x1, x0 = bits.Mul64(d4h, y.arr[0]) - x3, x2 = bits.Mul64(f4h, y.arr[0]) - t1, t0 = bits.Mul64(f4l, y.arr[0]) - x1, c = bits.Add64(x1, t0, 0) - x2, c = bits.Add64(x2, t1, c) - x3, _ = bits.Add64(x3, 0, c) - - t1, t0 = bits.Mul64(d4h, y.arr[1]) - x1, c = bits.Add64(x1, t0, 0) - x2, c = bits.Add64(x2, t1, c) - x4, t0 := bits.Mul64(f4h, y.arr[1]) - x3, c = bits.Add64(x3, t0, c) - x4, _ = bits.Add64(x4, 0, c) - t1, t0 = bits.Mul64(d4l, y.arr[1]) - x0, c = bits.Add64(x0, t0, 0) - x1, c = bits.Add64(x1, t1, c) - t1, t0 = bits.Mul64(f4l, y.arr[1]) - x2, c = bits.Add64(x2, t0, c) - x3, c = bits.Add64(x3, t1, c) - x4, _ = bits.Add64(x4, 0, c) - - t1, t0 = bits.Mul64(a4h, y.arr[2]) - x0, c = bits.Add64(x0, t0, 0) - x1, c = bits.Add64(x1, t1, c) - t1, t0 = bits.Mul64(d4h, y.arr[2]) - x2, c = bits.Add64(x2, t0, c) - x3, c = bits.Add64(x3, t1, c) - x5, t0 := bits.Mul64(f4h, y.arr[2]) - x4, c = bits.Add64(x4, t0, c) - x5, _ = bits.Add64(x5, 0, c) - t1, t0 = bits.Mul64(d4l, y.arr[2]) - x1, c = bits.Add64(x1, t0, 0) - x2, c = bits.Add64(x2, t1, c) - t1, t0 = bits.Mul64(f4l, y.arr[2]) - x3, c = bits.Add64(x3, t0, c) - x4, c = bits.Add64(x4, t1, c) - x5, _ = bits.Add64(x5, 0, c) - - t1, t0 = bits.Mul64(a4h, y.arr[3]) - x1, c = bits.Add64(x1, t0, 0) - x2, c = bits.Add64(x2, t1, c) - t1, t0 = bits.Mul64(d4h, y.arr[3]) - x3, c = bits.Add64(x3, t0, c) - x4, c = bits.Add64(x4, t1, c) - x6, t0 := bits.Mul64(f4h, y.arr[3]) - x5, c = bits.Add64(x5, t0, c) - x6, _ = bits.Add64(x6, 0, c) - t1, t0 = bits.Mul64(a4l, y.arr[3]) - x0, c = bits.Add64(x0, t0, 0) - x1, c = bits.Add64(x1, t1, c) - t1, t0 = bits.Mul64(d4l, y.arr[3]) - x2, c = bits.Add64(x2, t0, c) - x3, c = bits.Add64(x3, t1, c) - t1, t0 = bits.Mul64(f4l, y.arr[3]) - x4, c = bits.Add64(x4, t0, c) - x5, c = bits.Add64(x5, t1, c) - x6, _ = bits.Add64(x6, 0, c) - - // subtract - _, b = bits.Sub64(0, x0, 0) - _, b = bits.Sub64(0, x1, b) - r4l, b := bits.Sub64(0, x2, b) - r4k, b := bits.Sub64(0, x3, b) - r4j, b := bits.Sub64(r3l, x4, b) - r4i, b := bits.Sub64(r3m, x5, b) - r4h, _ := bits.Sub64(r3h, x6, b) - - // Multiply candidate for 1/4y by y, with full precision - - x0 = r4l - x1 = r4k - x2 = r4j - x3 = r4i - x4 = r4h - - q1, q0 = bits.Mul64(x0, y.arr[0]) - q3, q2 = bits.Mul64(x2, y.arr[0]) - q5, q4 := bits.Mul64(x4, y.arr[0]) - - t1, t0 = bits.Mul64(x1, y.arr[0]) - q1, c = bits.Add64(q1, t0, 0) - q2, c = bits.Add64(q2, t1, c) - t1, t0 = bits.Mul64(x3, y.arr[0]) - q3, c = bits.Add64(q3, t0, c) - q4, c = bits.Add64(q4, t1, c) - q5, _ = bits.Add64(q5, 0, c) - - t1, t0 = bits.Mul64(x0, y.arr[1]) - q1, c = bits.Add64(q1, t0, 0) - q2, c = bits.Add64(q2, t1, c) - t1, t0 = bits.Mul64(x2, y.arr[1]) - q3, c = bits.Add64(q3, t0, c) - q4, c = bits.Add64(q4, t1, c) - q6, t0 := bits.Mul64(x4, y.arr[1]) - q5, c = bits.Add64(q5, t0, c) - q6, _ = bits.Add64(q6, 0, c) - - t1, t0 = bits.Mul64(x1, y.arr[1]) - q2, c = bits.Add64(q2, t0, 0) - q3, c = bits.Add64(q3, t1, c) - t1, t0 = bits.Mul64(x3, y.arr[1]) - q4, c = bits.Add64(q4, t0, c) - q5, c = bits.Add64(q5, t1, c) - q6, _ = bits.Add64(q6, 0, c) - - t1, t0 = bits.Mul64(x0, y.arr[2]) - q2, c = bits.Add64(q2, t0, 0) - q3, c = bits.Add64(q3, t1, c) - t1, t0 = bits.Mul64(x2, y.arr[2]) - q4, c = bits.Add64(q4, t0, c) - q5, c = bits.Add64(q5, t1, c) - q7, t0 := bits.Mul64(x4, y.arr[2]) - q6, c = bits.Add64(q6, t0, c) - q7, _ = bits.Add64(q7, 0, c) - - t1, t0 = bits.Mul64(x1, y.arr[2]) - q3, c = bits.Add64(q3, t0, 0) - q4, c = bits.Add64(q4, t1, c) - t1, t0 = bits.Mul64(x3, y.arr[2]) - q5, c = bits.Add64(q5, t0, c) - q6, c = bits.Add64(q6, t1, c) - q7, _ = bits.Add64(q7, 0, c) - - t1, t0 = bits.Mul64(x0, y.arr[3]) - q3, c = bits.Add64(q3, t0, 0) - q4, c = bits.Add64(q4, t1, c) - t1, t0 = bits.Mul64(x2, y.arr[3]) - q5, c = bits.Add64(q5, t0, c) - q6, c = bits.Add64(q6, t1, c) - q8, t0 := bits.Mul64(x4, y.arr[3]) - q7, c = bits.Add64(q7, t0, c) - q8, _ = bits.Add64(q8, 0, c) - - t1, t0 = bits.Mul64(x1, y.arr[3]) - q4, c = bits.Add64(q4, t0, 0) - q5, c = bits.Add64(q5, t1, c) - t1, t0 = bits.Mul64(x3, y.arr[3]) - q6, c = bits.Add64(q6, t0, c) - q7, c = bits.Add64(q7, t1, c) - q8, _ = bits.Add64(q8, 0, c) - - // Final adjustment - - // subtract q from 1/4 - _, b = bits.Sub64(0, q0, 0) - _, b = bits.Sub64(0, q1, b) - _, b = bits.Sub64(0, q2, b) - _, b = bits.Sub64(0, q3, b) - _, b = bits.Sub64(0, q4, b) - _, b = bits.Sub64(0, q5, b) - _, b = bits.Sub64(0, q6, b) - _, b = bits.Sub64(0, q7, b) - _, b = bits.Sub64(uint64(1)<<62, q8, b) - - // decrement the result - x0, t := bits.Sub64(r4l, 1, 0) - x1, t = bits.Sub64(r4k, 0, t) - x2, t = bits.Sub64(r4j, 0, t) - x3, t = bits.Sub64(r4i, 0, t) - x4, _ = bits.Sub64(r4h, 0, t) - - // commit the decrement if the subtraction underflowed (reciprocal was too large) - if b != 0 { - r4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0 - } - - // Shift to correct bit alignment, truncating excess bits - - p = (p & 63) - 1 - - x0, c = bits.Add64(r4l, r4l, 0) - x1, c = bits.Add64(r4k, r4k, c) - x2, c = bits.Add64(r4j, r4j, c) - x3, c = bits.Add64(r4i, r4i, c) - x4, _ = bits.Add64(r4h, r4h, c) - - if p < 0 { - r4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0 - p = 0 // avoid negative shift below - } - - { - r := uint(p) // right shift - l := uint(64 - r) // left shift - - x0 = (r4l >> r) | (r4k << l) - x1 = (r4k >> r) | (r4j << l) - x2 = (r4j >> r) | (r4i << l) - x3 = (r4i >> r) | (r4h << l) - x4 = (r4h >> r) - } - - if p > 0 { - r4h, r4i, r4j, r4k, r4l = x4, x3, x2, x1, x0 - } - - mu[0] = r4l - mu[1] = r4k - mu[2] = r4j - mu[3] = r4i - mu[4] = r4h - - return mu -} - -// reduce4 computes the least non-negative residue of x modulo m -// -// requires a four-word modulus (m.arr[3] > 1) and its inverse (mu) -func reduce4(x [8]uint64, m *Uint, mu [5]uint64) (z Uint) { - // NB: Most variable names in the comments match the pseudocode for - // Barrett reduction in the Handbook of Applied Cryptography. - - // q1 = x/2^192 - - x0 := x[3] - x1 := x[4] - x2 := x[5] - x3 := x[6] - x4 := x[7] - - // q2 = q1 * mu; q3 = q2 / 2^320 - - var q0, q1, q2, q3, q4, q5, t0, t1, c uint64 - - q0, _ = bits.Mul64(x3, mu[0]) - q1, t0 = bits.Mul64(x4, mu[0]) - q0, c = bits.Add64(q0, t0, 0) - q1, _ = bits.Add64(q1, 0, c) - - t1, _ = bits.Mul64(x2, mu[1]) - q0, c = bits.Add64(q0, t1, 0) - q2, t0 = bits.Mul64(x4, mu[1]) - q1, c = bits.Add64(q1, t0, c) - q2, _ = bits.Add64(q2, 0, c) - - t1, t0 = bits.Mul64(x3, mu[1]) - q0, c = bits.Add64(q0, t0, 0) - q1, c = bits.Add64(q1, t1, c) - q2, _ = bits.Add64(q2, 0, c) - - t1, t0 = bits.Mul64(x2, mu[2]) - q0, c = bits.Add64(q0, t0, 0) - q1, c = bits.Add64(q1, t1, c) - q3, t0 = bits.Mul64(x4, mu[2]) - q2, c = bits.Add64(q2, t0, c) - q3, _ = bits.Add64(q3, 0, c) - - t1, _ = bits.Mul64(x1, mu[2]) - q0, c = bits.Add64(q0, t1, 0) - t1, t0 = bits.Mul64(x3, mu[2]) - q1, c = bits.Add64(q1, t0, c) - q2, c = bits.Add64(q2, t1, c) - q3, _ = bits.Add64(q3, 0, c) - - t1, _ = bits.Mul64(x0, mu[3]) - q0, c = bits.Add64(q0, t1, 0) - t1, t0 = bits.Mul64(x2, mu[3]) - q1, c = bits.Add64(q1, t0, c) - q2, c = bits.Add64(q2, t1, c) - q4, t0 = bits.Mul64(x4, mu[3]) - q3, c = bits.Add64(q3, t0, c) - q4, _ = bits.Add64(q4, 0, c) - - t1, t0 = bits.Mul64(x1, mu[3]) - q0, c = bits.Add64(q0, t0, 0) - q1, c = bits.Add64(q1, t1, c) - t1, t0 = bits.Mul64(x3, mu[3]) - q2, c = bits.Add64(q2, t0, c) - q3, c = bits.Add64(q3, t1, c) - q4, _ = bits.Add64(q4, 0, c) - - t1, t0 = bits.Mul64(x0, mu[4]) - _, c = bits.Add64(q0, t0, 0) - q1, c = bits.Add64(q1, t1, c) - t1, t0 = bits.Mul64(x2, mu[4]) - q2, c = bits.Add64(q2, t0, c) - q3, c = bits.Add64(q3, t1, c) - q5, t0 = bits.Mul64(x4, mu[4]) - q4, c = bits.Add64(q4, t0, c) - q5, _ = bits.Add64(q5, 0, c) - - t1, t0 = bits.Mul64(x1, mu[4]) - q1, c = bits.Add64(q1, t0, 0) - q2, c = bits.Add64(q2, t1, c) - t1, t0 = bits.Mul64(x3, mu[4]) - q3, c = bits.Add64(q3, t0, c) - q4, c = bits.Add64(q4, t1, c) - q5, _ = bits.Add64(q5, 0, c) - - // Drop the fractional part of q3 - - q0 = q1 - q1 = q2 - q2 = q3 - q3 = q4 - q4 = q5 - - // r1 = x mod 2^320 - - x0 = x[0] - x1 = x[1] - x2 = x[2] - x3 = x[3] - x4 = x[4] - - // r2 = q3 * m mod 2^320 - - var r0, r1, r2, r3, r4 uint64 - - r4, r3 = bits.Mul64(q0, m.arr[3]) - _, t0 = bits.Mul64(q1, m.arr[3]) - r4, _ = bits.Add64(r4, t0, 0) - - t1, r2 = bits.Mul64(q0, m.arr[2]) - r3, c = bits.Add64(r3, t1, 0) - _, t0 = bits.Mul64(q2, m.arr[2]) - r4, _ = bits.Add64(r4, t0, c) - - t1, t0 = bits.Mul64(q1, m.arr[2]) - r3, c = bits.Add64(r3, t0, 0) - r4, _ = bits.Add64(r4, t1, c) - - t1, r1 = bits.Mul64(q0, m.arr[1]) - r2, c = bits.Add64(r2, t1, 0) - t1, t0 = bits.Mul64(q2, m.arr[1]) - r3, c = bits.Add64(r3, t0, c) - r4, _ = bits.Add64(r4, t1, c) - - t1, t0 = bits.Mul64(q1, m.arr[1]) - r2, c = bits.Add64(r2, t0, 0) - r3, c = bits.Add64(r3, t1, c) - _, t0 = bits.Mul64(q3, m.arr[1]) - r4, _ = bits.Add64(r4, t0, c) - - t1, r0 = bits.Mul64(q0, m.arr[0]) - r1, c = bits.Add64(r1, t1, 0) - t1, t0 = bits.Mul64(q2, m.arr[0]) - r2, c = bits.Add64(r2, t0, c) - r3, c = bits.Add64(r3, t1, c) - _, t0 = bits.Mul64(q4, m.arr[0]) - r4, _ = bits.Add64(r4, t0, c) - - t1, t0 = bits.Mul64(q1, m.arr[0]) - r1, c = bits.Add64(r1, t0, 0) - r2, c = bits.Add64(r2, t1, c) - t1, t0 = bits.Mul64(q3, m.arr[0]) - r3, c = bits.Add64(r3, t0, c) - r4, _ = bits.Add64(r4, t1, c) - - // r = r1 - r2 - - var b uint64 - - r0, b = bits.Sub64(x0, r0, 0) - r1, b = bits.Sub64(x1, r1, b) - r2, b = bits.Sub64(x2, r2, b) - r3, b = bits.Sub64(x3, r3, b) - r4, b = bits.Sub64(x4, r4, b) - - // if r<0 then r+=m - - if b != 0 { - r0, c = bits.Add64(r0, m.arr[0], 0) - r1, c = bits.Add64(r1, m.arr[1], c) - r2, c = bits.Add64(r2, m.arr[2], c) - r3, c = bits.Add64(r3, m.arr[3], c) - r4, _ = bits.Add64(r4, 0, c) - } - - // while (r>=m) r-=m - - for { - // q = r - m - q0, b = bits.Sub64(r0, m.arr[0], 0) - q1, b = bits.Sub64(r1, m.arr[1], b) - q2, b = bits.Sub64(r2, m.arr[2], b) - q3, b = bits.Sub64(r3, m.arr[3], b) - q4, b = bits.Sub64(r4, 0, b) - - // if borrow break - if b != 0 { - break - } - - // r = q - r4, r3, r2, r1, r0 = q4, q3, q2, q1, q0 - } - - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = r3, r2, r1, r0 - - return z -} diff --git a/contract/p/gnoswap/uint256/uint256.gno b/contract/p/gnoswap/uint256/uint256.gno deleted file mode 100644 index b2f72b1..0000000 --- a/contract/p/gnoswap/uint256/uint256.gno +++ /dev/null @@ -1,303 +0,0 @@ -package uint256 - -import ( - "errors" - "math/bits" - "strconv" -) - -const ( - MaxUint64 = 1<<64 - 1 - MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935" -) - -var ( - zero = Zero() - one = One() - two = NewUint(2) - three = NewUint(3) -) - -// Uint represents a 256-bit unsigned integer. -// It is stored as an array of 4 uint64 in little-endian order, -// where arr[0] is the least significant and arr[3] is the most significant. -type Uint struct { - arr [4]uint64 -} - -// NewUint returns a new Uint initialized with the given uint64 value. -func NewUint(val uint64) *Uint { - return &Uint{arr: [4]uint64{val, 0, 0, 0}} -} - -// NewUintFromInt64 returns a new Uint initialized with the given int64 value. -// Panics if val is negative. -func NewUintFromInt64(val int64) *Uint { - if val < 0 { - panic("val is negative") - } - return NewUint(uint64(val)) -} - -// Zero returns a new Uint with value 0. -func Zero() *Uint { - return NewUint(0) -} - -// One returns a new Uint with value 1. -func One() *Uint { - return NewUint(1) -} - -// SetAllOne sets z to the maximum 256-bit value (all bits set to 1) and returns z. -func (z *Uint) SetAllOne() *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = MaxUint64, MaxUint64, MaxUint64, MaxUint64 - return z -} - -// Set sets z to x and returns z. -func (z *Uint) Set(x *Uint) *Uint { - *z = *x - return z -} - -// SetOne sets z to 1 and returns z. -func (z *Uint) SetOne() *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 1 - return z -} - -// SetFromDecimal sets z from a decimal string and returns an error if invalid. -// Accepts an optional leading "+" sign but rejects underscores and negative values. -// Returns ErrBig256Range if the number exceeds 256 bits. -func (z *Uint) SetFromDecimal(s string) (err error) { - // Remove max one leading + - if len(s) > 0 && s[0] == '+' { - s = s[1:] - } - // Remove any number of leading zeroes - if len(s) > 0 && s[0] == '0' { - var i int - var c rune - for i, c = range s { - if c != '0' { - break - } - } - s = s[i:] - } - if len(s) < len(MAX_UINT256) { - return z.fromDecimal(s) - } - if len(s) == len(MAX_UINT256) { - if s > MAX_UINT256 { - return ErrBig256Range - } - return z.fromDecimal(s) - } - return ErrBig256Range -} - -// FromDecimal creates a new Uint from a decimal string. -// Returns an error if the number exceeds 256 bits or is invalid. -func FromDecimal(decimal string) (*Uint, error) { - var z Uint - if err := z.SetFromDecimal(decimal); err != nil { - return nil, err - } - return &z, nil -} - -// MustFromDecimal creates a new Uint from a decimal string. -// Panics if the string is invalid or the number exceeds 256 bits. -func MustFromDecimal(decimal string) *Uint { - var z Uint - if err := z.SetFromDecimal(decimal); err != nil { - panic(err) - } - return &z -} - -// multipliers holds the values that are needed for fromDecimal -var multipliers = [5]*Uint{ - nil, // represents first round, no multiplication needed - {[4]uint64{10000000000000000000, 0, 0, 0}}, // 10 ^ 19 - {[4]uint64{687399551400673280, 5421010862427522170, 0, 0}}, // 10 ^ 38 - {[4]uint64{5332261958806667264, 17004971331911604867, 2938735877055718769, 0}}, // 10 ^ 57 - {[4]uint64{0, 8607968719199866880, 532749306367912313, 1593091911132452277}}, // 10 ^ 76 -} - -// fromDecimal parses a decimal string by processing it in 19-character chunks. -// Each chunk is multiplied by the appropriate power of 10 and accumulated. -func (z *Uint) fromDecimal(bs string) error { - // first clear the input - z.Clear() - // the maximum value of uint64 is 18446744073709551615, which is 20 characters - // one less means that a string of 19 9's is always within the uint64 limit - var ( - num uint64 - err error - remaining = len(bs) - ) - if remaining == 0 { - return errors.New("EOF") - } - // We proceed in steps of 19 characters (nibbles), from least significant to most significant. - // This means that the first (up to) 19 characters do not need to be multiplied. - // In the second iteration, our slice of 19 characters needs to be multipleied - // by a factor of 10^19. Et cetera. - for i, mult := range multipliers { - if remaining <= 0 { - return nil // Done - } - if remaining > 19 { - num, err = strconv.ParseUint(bs[remaining-19:remaining], 10, 64) - } else { - // Final round - num, err = strconv.ParseUint(bs, 10, 64) - } - if err != nil { - return err - } - // add that number to our running total - if i == 0 { - z.SetUint64(num) - } else { - base := NewUint(num) - // Check for overflow in multiplication - mulResult, overflow := new(Uint).MulOverflow(base, mult) - if overflow { - return ErrBig256Range - } - // Check for overflow in addition - addResult, overflow := new(Uint).AddOverflow(z, mulResult) - if overflow { - return ErrBig256Range - } - z.Set(addResult) - } - // Chop off another 19 characters - if remaining > 19 { - bs = bs[0 : remaining-19] - } - remaining -= 19 - } - return nil -} - -// Byte returns the value of the byte at position n as a Uint. -// Position n is counted from the right (0 = least significant byte). -// Returns 0 if n >= 32. -func (z *Uint) Byte(n *Uint) *Uint { - // in z, z.arr[0] is the least significant - if number, overflow := n.Uint64WithOverflow(); !overflow { - if number < 32 { - number := z.arr[4-1-number/8] - offset := (n.arr[0] & 0x7) << 3 // 8*(n.d % 8) - z.arr[0] = (number & (0xff00000000000000 >> offset)) >> (56 - offset) - z.arr[3], z.arr[2], z.arr[1] = 0, 0, 0 - return z - } - } - - return z.Clear() -} - -// BitLen returns the number of bits required to represent z. -// BitLen(0) returns 0. -func (z *Uint) BitLen() int { - switch { - case z.arr[3] != 0: - return 192 + bits.Len64(z.arr[3]) - case z.arr[2] != 0: - return 128 + bits.Len64(z.arr[2]) - case z.arr[1] != 0: - return 64 + bits.Len64(z.arr[1]) - default: - return bits.Len64(z.arr[0]) - } -} - -// ByteLen returns the number of bytes required to represent z. -// ByteLen(0) returns 0. -func (z *Uint) ByteLen() int { - return (z.BitLen() + 7) / 8 -} - -// Clear sets z to 0 and returns z. -func (z *Uint) Clear() *Uint { - z.arr[3], z.arr[2], z.arr[1], z.arr[0] = 0, 0, 0, 0 - return z -} - -const ( - // hextable = "0123456789abcdef" - bintable = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x01\x02\x03\x04\x05\x06\a\b\t\xff\xff\xff\xff\xff\xff\xff\n\v\f\r\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\n\v\f\r\x0e\x0f\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" - badNibble = 0xff -) - -// SetFromHex sets z from a hexadecimal string and returns an error if invalid. -// Requires "0x" or "0X" prefix and rejects leading zeros after prefix, underscores, and negative values. -// Returns ErrBig256Range if the number exceeds 256 bits. -func (z *Uint) SetFromHex(hex string) error { - return z.fromHex(hex) -} - -// fromHex parses a hex-string into z. -func (z *Uint) fromHex(hex string) error { - if err := checkNumberS(hex); err != nil { - return err - } - if len(hex) > 66 { - return ErrBig256Range - } - z.Clear() - end := len(hex) - for i := 0; i < 4; i++ { - start := end - 16 - if start < 2 { - start = 2 - } - for ri := start; ri < end; ri++ { - nib := bintable[hex[ri]] - if nib == badNibble { - return ErrSyntax - } - z.arr[i] = z.arr[i] << 4 - z.arr[i] += uint64(nib) - } - end = start - } - return nil -} - -// FromHex creates a new Uint from a hexadecimal string. -// The string must be 0x-prefixed and represent a value within 256 bits. -func FromHex(hex string) (*Uint, error) { - var z Uint - if err := z.fromHex(hex); err != nil { - return nil, err - } - return &z, nil -} - -// MustFromHex creates a new Uint from a hexadecimal string. -// Panics if the string is invalid or the number exceeds 256 bits. -func MustFromHex(hex string) *Uint { - var z Uint - if err := z.fromHex(hex); err != nil { - panic(err) - } - return &z -} - -// Clone returns a new Uint with the same value as z. -func (z *Uint) Clone() *Uint { - var x Uint - x.arr[0] = z.arr[0] - x.arr[1] = z.arr[1] - x.arr[2] = z.arr[2] - x.arr[3] = z.arr[3] - - return &x -} diff --git a/contract/p/gnoswap/uint256/uint256_test.gno b/contract/p/gnoswap/uint256/uint256_test.gno deleted file mode 100644 index 2468b0a..0000000 --- a/contract/p/gnoswap/uint256/uint256_test.gno +++ /dev/null @@ -1,825 +0,0 @@ -package uint256 - -import ( - "testing" - - "gno.land/p/nt/uassert" -) - -func TestFromDecimal(t *testing.T) { - tests := []struct { - name string - input string - expected string - shouldPanic bool - panicMsg string - }{ - // Basic cases - { - name: "zero", - input: "0", - expected: "0", - }, - { - name: "one", - input: "1", - expected: "1", - }, - { - name: "max_uint64", - input: "18446744073709551615", - expected: "18446744073709551615", - }, - { - name: "max_uint128", - input: "340282366920938463463374607431768211455", - expected: "340282366920938463463374607431768211455", - }, - { - name: "max_uint256", - input: MAX_UINT256, - expected: MAX_UINT256, - }, - - // Format cases - { - name: "leading_zeros", - input: "00000000000000000000000001234567890", - expected: "1234567890", - }, - { - name: "plus_sign", - input: "+12345", - expected: "12345", - }, - - // Error cases - { - name: "max_uint256_plus_one", - input: "115792089237316195423570985008687907853269984665640564039457584007913129639936", - shouldPanic: true, - panicMsg: "hex number > 256 bits", - }, - { - name: "multiple_plus_signs", - input: "++12345", - shouldPanic: true, - }, - { - name: "negative_number", - input: "-12345", - shouldPanic: true, - }, - { - name: "empty_string", - input: "", - shouldPanic: true, - panicMsg: "EOF", - }, - { - name: "invalid_characters", - input: "123abc456", - shouldPanic: true, - }, - { - name: "spaces_in_number", - input: "123 456", - shouldPanic: true, - }, - { - name: "decimal_point", - input: "123.456", - shouldPanic: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - if tt.panicMsg != "" { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - MustFromDecimal(tt.input) - }) - } else { - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected panic but got none") - } - }() - MustFromDecimal(tt.input) - } - } else { - result := MustFromDecimal(tt.input) - uassert.Equal(t, tt.expected, result.ToString()) - } - }) - } -} - -func TestFromHex(t *testing.T) { - tests := []struct { - name string - input string - expected string - shouldPanic bool - panicMsg string - }{ - // Basic cases - { - name: "zero", - input: "0x0", - expected: "0", - }, - { - name: "one", - input: "0x1", - expected: "1", - }, - { - name: "max_uint256", - input: "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", - expected: MAX_UINT256, - }, - - // Format cases - { - name: "uppercase_0X", - input: "0Xff", - expected: "255", - }, - { - name: "mixed_case", - input: "0xAbCdEf", - expected: "11259375", - }, - - // Error cases - { - name: "hex_overflow_67_chars", - input: "0x10000000000000000000000000000000000000000000000000000000000000000", - shouldPanic: true, - panicMsg: "hex number > 256 bits", - }, - { - name: "no_0x_prefix", - input: "ffffffff", - shouldPanic: true, - panicMsg: "UnmarshalText: ffffffff: hex string without 0x prefix", - }, - { - name: "empty_string", - input: "", - shouldPanic: true, - panicMsg: "UnmarshalText: : empty hex string", - }, - { - name: "only_0x", - input: "0x", - shouldPanic: true, - panicMsg: "UnmarshalText: 0x: hex string \"0x\"", - }, - { - name: "invalid_hex_chars", - input: "0xgg", - shouldPanic: true, - panicMsg: "invalid hex string", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.shouldPanic { - if tt.panicMsg != "" { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - MustFromHex(tt.input) - }) - } else { - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected panic but got none") - } - }() - MustFromHex(tt.input) - } - } else { - result := MustFromHex(tt.input) - uassert.Equal(t, tt.expected, result.ToString()) - } - }) - } -} - -func TestComparisonOperations(t *testing.T) { - tests := []struct { - name string - operation string - x string - y string - expected bool - }{ - // Equality tests - { - name: "eq_true", - operation: "eq", - x: "12345", - y: "12345", - expected: true, - }, - { - name: "eq_false", - operation: "eq", - x: "12345", - y: "12346", - expected: false, - }, - - // Less than tests - { - name: "lt_true", - operation: "lt", - x: "12345", - y: "12346", - expected: true, - }, - { - name: "lt_false_greater", - operation: "lt", - x: "12346", - y: "12345", - expected: false, - }, - { - name: "lt_false_equal", - operation: "lt", - x: "12345", - y: "12345", - expected: false, - }, - - // Greater than tests - { - name: "gt_true", - operation: "gt", - x: "12346", - y: "12345", - expected: true, - }, - { - name: "gt_false_smaller", - operation: "gt", - x: "12345", - y: "12346", - expected: false, - }, - { - name: "gt_false_equal", - operation: "gt", - x: "12345", - y: "12345", - expected: false, - }, - - // Less than or equal tests - { - name: "lte_true_less", - operation: "lte", - x: "12345", - y: "12346", - expected: true, - }, - { - name: "lte_true_equal", - operation: "lte", - x: "12345", - y: "12345", - expected: true, - }, - { - name: "lte_false", - operation: "lte", - x: "12346", - y: "12345", - expected: false, - }, - - // Greater than or equal tests - { - name: "gte_true_greater", - operation: "gte", - x: "12346", - y: "12345", - expected: true, - }, - { - name: "gte_true_equal", - operation: "gte", - x: "12345", - y: "12345", - expected: true, - }, - { - name: "gte_false", - operation: "gte", - x: "12345", - y: "12346", - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - x := MustFromDecimal(tt.x) - y := MustFromDecimal(tt.y) - - var result bool - switch tt.operation { - case "eq": - result = x.Eq(y) - case "lt": - result = x.Lt(y) - case "gt": - result = x.Gt(y) - case "lte": - result = x.Lte(y) - case "gte": - result = x.Gte(y) - } - - uassert.Equal(t, tt.expected, result) - }) - } -} - -func TestBitOperations(t *testing.T) { - tests := []struct { - name string - operation string - x string - n uint - expected string - }{ - // Left shift tests - { - name: "lsh_zero", - operation: "lsh", - x: "0", - n: 100, - expected: "0", - }, - { - name: "lsh_one_by_zero", - operation: "lsh", - x: "1", - n: 0, - expected: "1", - }, - { - name: "lsh_one_by_one", - operation: "lsh", - x: "1", - n: 1, - expected: "2", - }, - { - name: "lsh_one_by_64", - operation: "lsh", - x: "1", - n: 64, - expected: "18446744073709551616", - }, - { - name: "lsh_one_by_255", - operation: "lsh", - x: "1", - n: 255, - expected: "57896044618658097711785492504343953926634992332820282019728792003956564819968", - }, - { - name: "lsh_one_by_256", - operation: "lsh", - x: "1", - n: 256, - expected: "0", // Shifts out of range - }, - - // Right shift tests - { - name: "rsh_zero", - operation: "rsh", - x: "0", - n: 100, - expected: "0", - }, - { - name: "rsh_by_zero", - operation: "rsh", - x: "123456", - n: 0, - expected: "123456", - }, - { - name: "rsh_by_one", - operation: "rsh", - x: "123456", - n: 1, - expected: "61728", - }, - { - name: "rsh_by_64", - operation: "rsh", - x: "340282366920938463463374607431768211456", // 2^128 - n: 64, - expected: "18446744073709551616", // 2^64 - }, - { - name: "rsh_max_by_255", - operation: "rsh", - x: MAX_UINT256, - n: 255, - expected: "1", - }, - { - name: "rsh_max_by_256", - operation: "rsh", - x: MAX_UINT256, - n: 256, - expected: "0", // Shifts out of range - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - x := MustFromDecimal(tt.x) - var result *Uint - - switch tt.operation { - case "lsh": - result = new(Uint).Lsh(x, tt.n) - case "rsh": - result = new(Uint).Rsh(x, tt.n) - } - - uassert.Equal(t, tt.expected, result.ToString()) - }) - } -} - -func TestByteOperation(t *testing.T) { - tests := []struct { - name string - x string - n uint64 - expected string - }{ - { - name: "byte_0_of_max", - x: MAX_UINT256, - n: 0, - expected: "255", - }, - { - name: "byte_31_of_max", - x: MAX_UINT256, - n: 31, - expected: "255", - }, - { - name: "byte_32_out_of_range", - x: MAX_UINT256, - n: 32, - expected: "0", - }, - { - name: "byte_0_of_256", - x: "256", - n: 30, - expected: "1", - }, - { - name: "byte_31_of_single_byte", - x: "255", - n: 31, - expected: "255", - }, - { - name: "byte_at_boundary", - x: "18446744073709551616", // 2^64 - n: 23, - expected: "1", - }, - { - name: "byte_in_middle", - x: "0xff00ff00ff00ff00ff00", // Hexadecimal format without leading zeros - n: 15, - expected: "0", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var x *Uint - if len(tt.x) >= 2 && tt.x[:2] == "0x" { - x = MustFromHex(tt.x) - } else { - x = MustFromDecimal(tt.x) - } - - n := NewUint(tt.n) - result := x.Byte(n) - - uassert.Equal(t, tt.expected, result.ToString()) - }) - } -} - -func TestMulDiv(t *testing.T) { - tests := []struct { - name string - x string - y string - denominator string - expected string - shouldPanic bool - panicMsg string - }{ - { - name: "reverts_if_denominator_is_0", - x: "340282366920938463463374607431768211456", // Q128 - y: "5", - denominator: "0", - shouldPanic: true, - panicMsg: "denominator must be greater than 0", - }, - { - name: "reverts_if_output_overflows_uint256", - x: "340282366920938463463374607431768211456", // Q128 - y: "340282366920938463463374607431768211456", // Q128 - denominator: "1", - shouldPanic: true, - panicMsg: "overflow: denominator(1) must be greater than hi(1)", - }, - { - name: "all_max_inputs", - x: MAX_UINT256, - y: MAX_UINT256, - denominator: MAX_UINT256, - expected: MAX_UINT256, - }, - { - name: "simple_case_no_remainder", - x: "1000000", - y: "1000000", - denominator: "1000", - expected: "1000000000", - }, - { - name: "accurate_without_phantom_overflow", - x: "340282366920938463463374607431768211456", // Q128 - y: "170141183460469231731687303715884105728", // 50 * Q128 / 100 - denominator: "510423550381407695195061911147652317184", // 150 * Q128 / 100 - expected: "113427455640312821154458202477256070485", // Q128 / 3 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - x := MustFromDecimal(tt.x) - y := MustFromDecimal(tt.y) - denominator := MustFromDecimal(tt.denominator) - - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - MulDiv(x, y, denominator) - }) - } else { - result := MulDiv(x, y, denominator) - uassert.Equal(t, tt.expected, result.ToString()) - } - }) - } -} - -func TestMulDivRoundingUpSimple(t *testing.T) { - tests := []struct { - name string - x string - y string - denominator string - expected string - shouldPanic bool - panicMsg string - }{ - { - name: "reverts_if_denominator_is_0", - x: "340282366920938463463374607431768211456", // Q128 - y: "5", - denominator: "0", - shouldPanic: true, - panicMsg: "denominator must be greater than 0", - }, - { - name: "rounds_up_when_remainder", - x: "5", - y: "2", - denominator: "3", - expected: "4", // (5*2)/3 = 3.33, rounded up to 4 - }, - { - name: "no_rounding_when_exact", - x: "6", - y: "3", - denominator: "2", - expected: "9", // (6*3)/2 = 9, no rounding needed - }, - { - name: "accurate_with_rounding", - x: "340282366920938463463374607431768211456", // Q128 - y: "17014118346046923173168730371588410572800", // 50 * Q128 - denominator: "51042355038140769519506191114765231718400", // 150 * Q128 - expected: "113427455640312821154458202477256070486", // Q128/3 + 1 - }, - { - name: "max_result_no_remainder", - x: MAX_UINT256, - y: "1", - denominator: "1", - expected: MAX_UINT256, - }, - { - name: "rounding_overflow", - x: MAX_UINT256, - y: "2", - denominator: "1", - shouldPanic: true, - panicMsg: "overflow: denominator(1) must be greater than hi(1)", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - x := MustFromDecimal(tt.x) - y := MustFromDecimal(tt.y) - denominator := MustFromDecimal(tt.denominator) - - if tt.shouldPanic { - uassert.PanicsWithMessage(t, tt.panicMsg, func() { - MulDivRoundingUp(x, y, denominator) - }) - } else { - result := MulDivRoundingUp(x, y, denominator) - uassert.Equal(t, tt.expected, result.ToString()) - } - }) - } -} - -func TestBitLenAndByteLen(t *testing.T) { - tests := []struct { - name string - input string - expectedBit int - expectedByte int - }{ - { - name: "zero", - input: "0", - expectedBit: 0, - expectedByte: 0, - }, - { - name: "one", - input: "1", - expectedBit: 1, - expectedByte: 1, - }, - { - name: "byte_boundary", - input: "255", // 2^8 - 1 - expectedBit: 8, - expectedByte: 1, - }, - { - name: "word_boundary", - input: "18446744073709551615", // 2^64 - 1 - expectedBit: 64, - expectedByte: 8, - }, - { - name: "max_uint256", - input: MAX_UINT256, - expectedBit: 256, - expectedByte: 32, - }, - { - name: "half_max", - input: "57896044618658097711785492504343953926634992332820282019728792003956564819968", // 2^255 - expectedBit: 256, - expectedByte: 32, - }, - { - name: "uneven_bits", - input: "123456789", - expectedBit: 27, - expectedByte: 4, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - x := MustFromDecimal(tt.input) - bitLen := x.BitLen() - byteLen := x.ByteLen() - - uassert.Equal(t, tt.expectedBit, bitLen) - uassert.Equal(t, tt.expectedByte, byteLen) - }) - } -} - -func TestInPlaceSafety(t *testing.T) { - // Testing that operations don't mutate their inputs - t.Run("original_values_preserved", func(t *testing.T) { - a := MustFromDecimal("1234567890") - b := MustFromDecimal("9876543210") - - original_a := a.Clone() - original_b := b.Clone() - - _ = new(Uint).Add(a, b) - - uassert.True(t, a.Eq(original_a)) - uassert.True(t, b.Eq(original_b)) - }) - - // Testing that chained operations work correctly - t.Run("chained_operations", func(t *testing.T) { - a := MustFromDecimal("1000000") - b := MustFromDecimal("2000000") - c := MustFromDecimal("3000000") - - // (a + b) * c - temp := new(Uint).Add(a, b) - result := new(Uint).Mul(temp, c) - - expected := MustFromDecimal("9000000000000") - uassert.True(t, result.Eq(expected)) - }) -} - -func TestOperationConsistency(t *testing.T) { - // Testing mathematical properties - t.Run("addition_associativity", func(t *testing.T) { - a := NewUint(12345) - b := NewUint(67890) - c := NewUint(11111) - - // (a + b) + c - path1 := new(Uint).Add(a, b) - path1 = new(Uint).Add(path1, c) - - // a + (b + c) - path2 := new(Uint).Add(b, c) - path2 = new(Uint).Add(a, path2) - - uassert.True(t, path1.Eq(path2)) - }) - - t.Run("distributive_property", func(t *testing.T) { - a := NewUint(12345) - b := NewUint(67) - c := NewUint(89) - - // a * (b + c) - sum := new(Uint).Add(b, c) - dist1 := new(Uint).Mul(a, sum) - - // (a * b) + (a * c) - prod1 := new(Uint).Mul(a, b) - prod2 := new(Uint).Mul(a, c) - dist2 := new(Uint).Add(prod1, prod2) - - uassert.True(t, dist1.Eq(dist2)) - }) - - t.Run("inverse_operations", func(t *testing.T) { - x := MustFromDecimal("123456789012345678901234567890") - - // x + y - y = x - y := MustFromDecimal("999999999999999999999999999999") - result := new(Uint).Add(x, y) - result = new(Uint).Sub(result, y) - uassert.True(t, result.Eq(x)) - - // x * y / y = x (when no remainder) - y = NewUint(12345) - result = new(Uint).Mul(x, y) - result = new(Uint).Div(result, y) - uassert.True(t, result.Eq(x)) - - // x << n >> n = x (when n < bitlen(x)) - n := uint(10) - result = new(Uint).Lsh(x, n) - result = new(Uint).Rsh(result, n) - uassert.True(t, result.Eq(x)) - }) -} diff --git a/contract/p/gnoswap/uint256/utils.gno b/contract/p/gnoswap/uint256/utils.gno deleted file mode 100644 index 284e301..0000000 --- a/contract/p/gnoswap/uint256/utils.gno +++ /dev/null @@ -1,22 +0,0 @@ -package uint256 - -// checkNumberS validates that input represents a valid hexadecimal number. -// Requires "0x" or "0X" prefix and disallows leading zeros after the prefix. -func checkNumberS(input string) error { - const fn = "UnmarshalText" - l := len(input) - if l == 0 { - return errEmptyString(fn, input) - } - if l < 2 || input[0] != '0' || - (input[1] != 'x' && input[1] != 'X') { - return errMissingPrefix(fn, input) - } - if l == 2 { - return errEmptyNumber(fn, input) - } - if len(input) > 3 && input[2] == '0' { - return errLeadingZero(fn, input) - } - return nil -} diff --git a/contract/r/gnoswap/access/README.md b/contract/r/gnoswap/access/README.md deleted file mode 100644 index a79b7a4..0000000 --- a/contract/r/gnoswap/access/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Access - -Role-based access control for GnoSwap contracts. - -## Overview - -Access control system manages permissions across all protocol contracts using role-based authorization. - -## Roles - -- **admin**: Protocol administrator -- **governance**: Governance contract -- **router**: Swap router -- **pool**: Pool management -- **position**: Position NFT -- **staker**: Liquidity staking -- **emission**: GNS emission -- **protocol_fee**: Fee collector -- **launchpad**: Token launchpad -- **gov_staker**: Governance staking -- **gov_xgns**: xGNS token - -## Key Functions - -### `GetAddress` -Returns address for a role. - -### `SetRoleAddresses` -Updates all role addresses (RBAC only). - -### `IsAuthorized` -Checks if address has role. - -### Assert Functions -- `AssertIsAdmin` - Require admin role -- `AssertIsGovernance` - Require governance -- `AssertIsAdminOrGovernance` - Admin or governance -- `AssertIsRouter`, `AssertIsPool`, etc. - -### Swap Whitelist -- `UpdateSwapWhiteList` - Add to whitelist -- `RemoveFromSwapWhiteList` - Remove from whitelist -- `IsSwapWhitelisted` - Check whitelist status - -## Usage - -```go -// Check permission -if !access.IsAuthorized("admin", caller) { - panic("unauthorized") -} - -// Assert permission (panics if unauthorized) -access.AssertIsAdminOrGovernance(caller) - -// Get role address -addr, exists := access.GetAddress("router") - -// Manage whitelist -access.UpdateSwapWhiteList(routerAddr) -``` - -## Security - -- Centralized permission management -- Role-based authorization -- Swap whitelist for approved routers -- RBAC-only role updates \ No newline at end of file diff --git a/contract/r/gnoswap/access/access.gno b/contract/r/gnoswap/access/access.gno deleted file mode 100644 index 044a855..0000000 --- a/contract/r/gnoswap/access/access.gno +++ /dev/null @@ -1,67 +0,0 @@ -package access - -import ( - "std" - - "gno.land/p/nt/ufmt" -) - -var roleAddresses map[string]std.Address - -func init() { - roleAddresses = make(map[string]std.Address) -} - -// GetAddress returns the address for a role and whether it exists. -func GetAddress(role string) (std.Address, bool) { - addr, ok := roleAddresses[role] - - return addr, ok -} - -// GetRoleAddresses returns a copy of all role addresses. -func GetRoleAddresses() map[string]std.Address { - addresses := make(map[string]std.Address) - - for role, addr := range roleAddresses { - addresses[role] = addr - } - - return addresses -} - -// SetRoleAddresses updates all role addresses. -// -// Parameters: -// - newRoleAddresses: map of role names to addresses -// -// Only callable by RBAC contract. -func SetRoleAddresses(cur realm, newRoleAddresses map[string]std.Address) { - caller := std.PreviousRealm().Address() - assertIsRBAC(caller) - - // Validate all addresses before applying updates - for role, addr := range newRoleAddresses { - if !addr.IsValid() || addr == std.Address("") { - panic(ufmt.Errorf("invalid address for role %s: %s", role, addr)) - } - } - - roleAddresses = newRoleAddresses -} - -// IsAuthorized checks if caller has the specified role. -// -// Parameters: -// - role: role name to check -// - caller: address to verify -// -// Returns true if authorized, false otherwise. -func IsAuthorized(role string, caller std.Address) bool { - roleAddr, ok := roleAddresses[role] - if !ok { - return false - } - - return caller == roleAddr -} diff --git a/contract/r/gnoswap/access/assert.gno b/contract/r/gnoswap/access/assert.gno deleted file mode 100644 index 1ad15bf..0000000 --- a/contract/r/gnoswap/access/assert.gno +++ /dev/null @@ -1,145 +0,0 @@ -package access - -import ( - "std" - - "gno.land/p/nt/ufmt" - prbac "gno.land/p/gnoswap/rbac" -) - -// AssertIsAdminOrGovernance panics if the caller is not admin or governance. -// Used for functions that require elevated privileges. -func AssertIsAdminOrGovernance(caller std.Address) { - if !IsAuthorized(prbac.ROLE_ADMIN.String(), caller) && !IsAuthorized(prbac.ROLE_GOVERNANCE.String(), caller) { - panic(ufmt.Errorf("unauthorized: caller %s is not admin or governance", caller)) - } -} - -// AssertIsAdmin panics if the caller is not admin. -// Used for admin-only functions. -func AssertIsAdmin(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_ADMIN.String(), caller) -} - -// AssertIsGovernance panics if the caller is not governance. -// Used for governance-only functions. -func AssertIsGovernance(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_GOVERNANCE.String(), caller) -} - -// AssertIsGovStaker panics if the caller is not governance staker. -// Used for governance staking functions. -func AssertIsGovStaker(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_GOV_STAKER.String(), caller) -} - -// AssertIsRouter panics if the caller is not router. -// Used for router-only functions. -func AssertIsRouter(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_ROUTER.String(), caller) -} - -// AssertIsPool panics if the caller is not pool. -// Used for pool-only functions. -func AssertIsPool(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_POOL.String(), caller) -} - -// AssertIsPosition panics if the caller is not position. -// Used for position-only functions. -func AssertIsPosition(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_POSITION.String(), caller) -} - -// AssertIsStaker panics if the caller is not staker. -// Used for staker-only functions. -func AssertIsStaker(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_STAKER.String(), caller) -} - -// AssertIsLaunchpad panics if the caller is not launchpad. -// Used for launchpad-only functions. -func AssertIsLaunchpad(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_LAUNCHPAD.String(), caller) -} - -// AssertIsEmission panics if the caller is not emission. -// Used for emission-only functions. -func AssertIsEmission(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_EMISSION.String(), caller) -} - -// AssertIsProtocolFee panics if the caller is not protocol fee. -// Used for protocol fee management functions. -func AssertIsProtocolFee(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_PROTOCOL_FEE.String(), caller) -} - -// AssertIsGovXGNS panics if the caller is not xGNS governance. -// Used for xGNS governance functions. -func AssertIsGovXGNS(caller std.Address) { - AssertIsAuthorized(prbac.ROLE_XGNS.String(), caller) -} - -// AssertIsAuthorized panics if the caller does not have the specified role. -// Also panics if the role does not exist. -func AssertIsAuthorized(roleName string, caller std.Address) { - roleAddr, ok := GetAddress(roleName) - if !ok { - panic(ufmt.Errorf("role %s does not exist", roleName)) - } - - if caller != roleAddr { - panic(ufmt.Errorf("unauthorized: caller %s is not %s", caller, roleName)) - } -} - -// AssertHasAnyRole panics if the caller does not have any of the specified roles. -// Also panics if any of the roles do not exist. -func AssertHasAnyRole(caller std.Address, roleNames ...string) { - for _, roleName := range roleNames { - roleAddr, ok := GetAddress(roleName) - if !ok { - panic(ufmt.Errorf("role %s does not exist", roleName)) - } - - if caller == roleAddr { - return - } - } - - panic(ufmt.Errorf("unauthorized: caller %s is not any of the roles %v", caller, roleNames)) -} - -// AssertIsValidAddress panics if the provided address is invalid. -func AssertIsValidAddress(addr std.Address) { - if !addr.IsValid() { - panic(ufmt.Errorf("invalid address: %s", addr)) - } -} - -// AssertIsUser panics if the caller is not a user realm. -// Used to ensure calls come from user accounts, not other contracts. -func AssertIsUser(r std.Realm) { - if !r.IsUser() { - panic(ufmt.Errorf("caller is not user")) - } -} - -// AssertIsSwapWhitelisted panics if the caller is not on the swap whitelist. -// Used to restrict swap operations to authorized routers only. -func AssertIsSwapWhitelisted(caller std.Address) { - if !IsSwapWhitelisted(caller) { - panic(ufmt.Errorf("unauthorized: caller %s is not a whitelisted router", caller)) - } -} - -// assertIsRBAC panics if the caller is not the RBAC contract. -// Used internally to protect role management functions. -func assertIsRBAC(caller std.Address) { - rbacAddress := std.DerivePkgAddr(rbacPackagePath) - - if caller != rbacAddress { - panic(ufmt.Errorf("unauthorized: caller %s is not rbac", caller)) - } -} diff --git a/contract/r/gnoswap/access/consts.gno b/contract/r/gnoswap/access/consts.gno deleted file mode 100644 index 036c6b0..0000000 --- a/contract/r/gnoswap/access/consts.gno +++ /dev/null @@ -1,5 +0,0 @@ -package access - -const ( - rbacPackagePath = "gno.land/r/gnoswap/rbac" -) diff --git a/contract/r/gnoswap/access/errors.gno b/contract/r/gnoswap/access/errors.gno deleted file mode 100644 index 07a6dba..0000000 --- a/contract/r/gnoswap/access/errors.gno +++ /dev/null @@ -1,10 +0,0 @@ -package access - -const ( - errConfigNil = "config cannot be nil" - errNotInitialized = "access control not initialized" - errEmptyRole = "role name cannot be empty" - errRoleExists = "role %s already exists" - errDeclareRole = "failed to declare role %s: %v" - errUnauthorized = "caller(%s) is not authorized for role %s" -) diff --git a/contract/r/gnoswap/access/gnomod.toml b/contract/r/gnoswap/access/gnomod.toml deleted file mode 100644 index 38289c9..0000000 --- a/contract/r/gnoswap/access/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/access" -gno = "0.9" diff --git a/contract/r/gnoswap/access/swap_whitelist.gno b/contract/r/gnoswap/access/swap_whitelist.gno deleted file mode 100644 index 0c5f864..0000000 --- a/contract/r/gnoswap/access/swap_whitelist.gno +++ /dev/null @@ -1,77 +0,0 @@ -package access - -import ( - "std" - - "gno.land/p/nt/ufmt" - prbac "gno.land/p/gnoswap/rbac" -) - -// Router whitelist storage -var swapWhitelist map[std.Address]bool - -func init() { - swapWhitelist = make(map[std.Address]bool) -} - -// UpdateSwapWhiteList adds a router address to the swap whitelist. -// Panics if router address is invalid. -// Only admin or governance can call this function. -func UpdateSwapWhiteList(cur realm, router std.Address) { - caller := std.PreviousRealm().Address() - AssertIsAdminOrGovernance(caller) - - if !router.IsValid() { - panic(ufmt.Errorf("invalid router address: %s", router)) - } - - // Add or update the router in the whitelist - swapWhitelist[router] = true -} - -// RemoveFromSwapWhiteList removes a router address from the swap whitelist. -// Does nothing if the router is not in the whitelist. -// Only admin or governance can call this function. -func RemoveFromSwapWhiteList(cur realm, router std.Address) { - caller := std.PreviousRealm().Address() - AssertIsAdminOrGovernance(caller) - - delete(swapWhitelist, router) -} - -// IsSwapWhitelisted returns true if the address is either the official router -// or is in the swap whitelist. Returns false otherwise. -func IsSwapWhitelisted(addr std.Address) bool { - // Check if it's the official router first - // - // Note: While it's a common pattern to store the router's address - // in a global variable to prevent unnecessary function calls, - // this function is called infrequently and retrieves the address internally - // to respond to address changes. - officialRouter, ok := GetAddress(prbac.ROLE_ROUTER.String()) - if ok && addr == officialRouter { - return true - } - - // Then check whitelist - return swapWhitelist[addr] -} - -// GetWhitelistedSwaps returns all whitelisted router addresses including -// the official router address if it exists. -func GetWhitelistedSwaps() []std.Address { - routers := make([]std.Address, 0, len(swapWhitelist)+1) - - // Include official router - officialRouter, ok := GetAddress(prbac.ROLE_ROUTER.String()) - if ok { - routers = append(routers, officialRouter) - } - - // Add whitelisted routers - for router := range swapWhitelist { - routers = append(routers, router) - } - - return routers -} diff --git a/contract/r/gnoswap/emission/README.md b/contract/r/gnoswap/emission/README.md deleted file mode 100644 index 2142078..0000000 --- a/contract/r/gnoswap/emission/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# Emission - -GNS token emission and distribution system. - -## Overview - -The emission system controls creation and distribution of new GNS tokens with a deflationary model featuring periodic halvings, ensuring predictable and decreasing supply growth over 12 years. For more details, check out [docs](https://docs.gnoswap.io/gnoswap-token/emission). - -## Token Economics - -- **Total Supply Cap**: 1,000,000,000 GNS -- **Initial Minted**: 100,000,000 GNS -- **To Be Minted**: 900,000,000 GNS over 12 years -- **Halving Period**: Every 2 years (63,072,000 seconds) -- **Halving Reduction**: 50% decrease in emission rate -- **Distribution**: Automatic during protocol activity - -## Configuration - -- **Distribution Ratios** (modifiable by governance): - - Liquidity Staker: 75% (default) - - DevOps: 20% (default) - - Community Pool: 5% (default) - - Governance Staker: 0% (default) -- **Start Time**: Unix timestamp (immutable once set) - -## Core Features - -### Emission Schedule -Implements Bitcoin-style halving model: -- Year 0-2: 100% emission rate -- Year 2-4: 50% emission rate -- Year 4-6: 25% emission rate -- Year 6-8: 12.5% emission rate -- Year 8-10: 6.25% emission rate -- Year 10-12: 3.125% emission rate - -### Distribution Mechanism -When triggered by protocol activity: -1. Calculates elapsed time since last distribution -2. Mints GNS based on current emission rate -3. Distributes to targets per configured ratios -4. Carries forward any undistributed amounts - -## Key Functions - -### `MintAndDistributeGns` -Mints and distributes GNS tokens automatically. - -### `SetDistributionStartTime` -One-time setup of emission start timestamp. - -### `SetDistributionRatio` -Updates distribution percentages (governance only). - -### `GetDistributionRatio` -Returns current distribution ratios. - -## Technical Details - -### Timestamp-Based Emission -``` -emissionPerSecond = baseEmission / (2^halvingCount) -amountToMint = emissionPerSecond * timeSinceLastMint -``` - -### Halving Calculation -``` -halvingCount = floor(timeSinceStart / halvingPeriod) -``` - -### Distribution Targets -1. **Liquidity Staker**: Rewards for LP providers -2. **DevOps**: Development and operations fund -3. **Community Pool**: Community-governed treasury -4. **Governance Staker**: GNS staking rewards (currently 0%) - -## Usage - -```go -// Set emission start (one-time by admin) -SetDistributionStartTime(1704067200) // Jan 1, 2024 - -// Trigger emission (called automatically) -amount := MintAndDistributeGns() - -// Update distribution ratios -ChangeDistributionPct( - 1, 7000, // 70% to liquidity stakers - 2, 2000, // 20% to devops - 3, 1000, // 10% to community pool - 4, 0 // 0% to governance stakers -) - -// Query distribution info -stakerPct := GetDistributionBpsPct(LIQUIDITY_STAKER) -accumulated := GetAccuDistributedToStaker() -rate := GetStakerEmissionAmountPerSecond() -``` - -## Security - -- Start time immutable once set and passed -- Distribution percentages must sum to 10000 (100%) -- Automatic triggers prevent manipulation -- Leftover tracking ensures no token loss -- Halving enforced at protocol level \ No newline at end of file diff --git a/contract/r/gnoswap/emission/assert.gno b/contract/r/gnoswap/emission/assert.gno deleted file mode 100644 index 490e9bf..0000000 --- a/contract/r/gnoswap/emission/assert.gno +++ /dev/null @@ -1,82 +0,0 @@ -package emission - -import ( - "gno.land/p/nt/ufmt" -) - -// assertValidDistributionTargets panics if any of the four distribution targets is invalid -// or if there are duplicate targets. All four distribution targets must be unique and valid. -func assertValidDistributionTargets(target01, target02, target03, target04 int) { - validTargets := map[int]bool{ - LIQUIDITY_STAKER: false, - DEVOPS: false, - COMMUNITY_POOL: false, - GOV_STAKER: false, - } - - currentTargets := []int{target01, target02, target03, target04} - - for _, target := range currentTargets { - if _, ok := validTargets[target]; !ok { - panic(makeErrorWithDetails( - errInvalidEmissionTarget, - ufmt.Sprintf("invalid target(%d)", target), - )) - } - - validTargets[target] = true - } - - for _, valid := range validTargets { - if !valid { - panic(errDuplicateTarget) - } - } -} - -// assertValidDistributionTarget panics if the given distribution target is invalid. -func assertValidDistributionTarget(target int) { - validTargets := map[int]bool{ - LIQUIDITY_STAKER: false, - DEVOPS: false, - COMMUNITY_POOL: false, - GOV_STAKER: false, - } - - if _, ok := validTargets[target]; !ok { - panic(makeErrorWithDetails( - errInvalidEmissionTarget, - ufmt.Sprintf("invalid target(%d)", target), - )) - } -} - -// assertValidDistributionPct ensures the sum of all distribution percentages equals 10000 (100%). -// Panics if the sum does not equal exactly 10000 basis points. -func assertValidDistributionPct(pct01, pct02, pct03, pct04 int64) { - // Validate individual percentages are non-negative and reasonable - percentages := []int64{pct01, pct02, pct03, pct04} - for i, pct := range percentages { - if pct < 0 { - panic(makeErrorWithDetails( - errInvalidEmissionPct, - ufmt.Sprintf("percentage %d cannot be negative: %d", i+1, pct), - )) - } - - if pct > 10000 { - panic(makeErrorWithDetails( - errInvalidEmissionPct, - ufmt.Sprintf("percentage %d cannot exceed 100%%: %d", i+1, pct), - )) - } - } - - sum := pct01 + pct02 + pct03 + pct04 - if sum != 10000 { - panic(makeErrorWithDetails( - errInvalidEmissionPct, - ufmt.Sprintf("sum of percentages must be 10000, got %d", sum), - )) - } -} diff --git a/contract/r/gnoswap/emission/distribution.gno b/contract/r/gnoswap/emission/distribution.gno deleted file mode 100644 index f091066..0000000 --- a/contract/r/gnoswap/emission/distribution.gno +++ /dev/null @@ -1,389 +0,0 @@ -package emission - -import ( - "std" - "strconv" - "time" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - prbac "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/gns" - "gno.land/r/gnoswap/halt" -) - -const ( - LIQUIDITY_STAKER int = iota + 1 - DEVOPS - COMMUNITY_POOL - GOV_STAKER -) - -var ( - // Stores the percentage (in basis points) for each distribution target - // 1 basis point = 0.01% - // These percentages can be modified through governance. - distributionBpsPct *avl.Tree - - distributedToStaker int64 // can be cleared by staker contract - distributedToDevOps int64 - distributedToCommunityPool int64 - distributedToGovStaker int64 // can be cleared by governance staker - - // Historical total distributions (never reset) - accuDistributedToStaker int64 - accuDistributedToDevOps int64 - accuDistributedToCommunityPool int64 - accuDistributedToGovStaker int64 -) - -// Initialize default distribution percentages: -// - Liquidity Stakers: 75% -// - DevOps: 20% -// - Community Pool: 5% -// - Governance Stakers: 0% -// -// ref: https://docs.gnoswap.io/gnoswap-token/emission -func init() { - distributionBpsPct = avl.NewTree() - distributionBpsPct.Set(strconv.Itoa(LIQUIDITY_STAKER), int64(7500)) - distributionBpsPct.Set(strconv.Itoa(DEVOPS), int64(2000)) - distributionBpsPct.Set(strconv.Itoa(COMMUNITY_POOL), int64(500)) - distributionBpsPct.Set(strconv.Itoa(GOV_STAKER), int64(0)) -} - -// ChangeDistributionPct changes distribution percentages for emission targets. -// -// This function redistributes how newly minted GNS tokens are allocated across -// protocol components. Changes take effect immediately for future emissions. -// Historical distributions are not affected. -// -// Parameters: -// - target01-04: Target identifiers (1=LIQUIDITY_STAKER, 2=DEVOPS, 3=COMMUNITY_POOL, 4=GOV_STAKER) -// - pct01-04: Percentage in basis points (100 = 1%, 10000 = 100%) -// -// Requirements: -// - All four targets must be specified (use current values if unchanged) -// - Percentages must sum to exactly 10000 (100%) -// - Each percentage must be 0-10000 -// - Targets must be unique (no duplicates) -// -// Example: -// -// ChangeDistributionPct( -// 1, 7000, // 70% to liquidity stakers -// 2, 2000, // 20% to devops -// 3, 1000, // 10% to community pool -// 4, 0 // 0% to governance stakers -// ) -// -// Only callable by admin or governance. -func ChangeDistributionPct( - cur realm, - target01 int, pct01 int64, - target02 int, pct02 int64, - target03 int, pct03 int64, - target04 int, pct04 int64, -) { - halt.AssertIsNotHaltedEmission() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertValidDistributionTargets(target01, target02, target03, target04) - assertValidDistributionPct(pct01, pct02, pct03, pct04) - - changeDistributionPcts( - target01, pct01, - target02, pct02, - target03, pct03, - target04, pct04, - ) - - previousRealm := std.PreviousRealm() - std.Emit( - "ChangeDistributionPct", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "target01", targetToStr(target01), - "pct01", formatInt(pct01), - "target02", targetToStr(target02), - "pct02", formatInt(pct02), - "target03", targetToStr(target03), - "pct03", formatInt(pct03), - "target04", targetToStr(target04), - "pct04", formatInt(pct04), - ) -} - -// changeDistributionPcts updates the distribution percentages in the AVL tree. -func changeDistributionPcts( - target01 int, pct01 int64, - target02 int, pct02 int64, - target03 int, pct03 int64, - target04 int, pct04 int64, -) { - // First, cache the percentage of the staker just before it changes Callback if needed - // (check if the LIQUIDITY_STAKER was located between target01 and 04) - setDistributionBpsPct(target01, pct01) - setDistributionBpsPct(target02, pct02) - setDistributionBpsPct(target03, pct03) - setDistributionBpsPct(target04, pct04) -} - -// distributeToTarget distributes tokens according to configured percentages. -// -// Returns total amount distributed and any error. -func distributeToTarget(amount int64) (int64, error) { - totalSent := int64(0) - - var err error - - distributionBpsPct.Iterate("", "", func(targetStr string, iPct any) bool { - targetInt, distErr := strconv.Atoi(targetStr) - if distErr != nil { - err = distErr - return true - } - - pct, ok := iPct.(int64) - if !ok { - panic("failed to cast distributionBpsPct's element to int64") - } - - distAmount := calculateAmount(amount, pct) - - // Skip zero amounts to avoid unnecessary transfers - if distAmount == 0 { - return false - } - - totalSent += distAmount - - distErr = transferToTarget(targetInt, distAmount) - if distErr != nil { - err = distErr - - return true - } - - return false - }) - - return totalSent, err -} - -// calculateAmount converts basis points to actual token amount. -func calculateAmount(amount, bptPct int64) int64 { - if amount < 0 || bptPct < 0 || bptPct > 10000 { - panic("invalid amount or bptPct") - } - - // More precise overflow prevention - const maxInt64 = 9223372036854775807 - if amount > maxInt64/10000 { - panic("amount too large, would cause overflow") - } - - // Additional safety check for zero division - if bptPct == 0 { - return 0 - } - - return amount * bptPct / 10000 -} - -// transferToTarget sends tokens to the appropriate target address. -// -// Returns error if target address not found. -func transferToTarget(target int, amount int64) error { - switch target { - case LIQUIDITY_STAKER: - stakerAddr, ok := access.GetAddress(prbac.ROLE_STAKER.String()) - if !ok { - return makeErrorWithDetails( - errDistributionAddressNotFound, - ufmt.Sprintf("%s not found", prbac.ROLE_STAKER.String()), - ) - } - - gns.Transfer(cross, stakerAddr, amount) - distributedToStaker += amount - accuDistributedToStaker += amount - - case DEVOPS: - devOpsAddr, ok := access.GetAddress(prbac.ROLE_DEVOPS.String()) - if !ok { - return makeErrorWithDetails( - errDistributionAddressNotFound, - ufmt.Sprintf("%s not found", prbac.ROLE_DEVOPS.String()), - ) - } - - gns.Transfer(cross, devOpsAddr, amount) - distributedToDevOps += amount - accuDistributedToDevOps += amount - - case COMMUNITY_POOL: - communityPoolAddr, ok := access.GetAddress(prbac.ROLE_COMMUNITY_POOL.String()) - if !ok { - return makeErrorWithDetails( - errDistributionAddressNotFound, - ufmt.Sprintf("%s not found", prbac.ROLE_COMMUNITY_POOL.String()), - ) - } - - gns.Transfer(cross, communityPoolAddr, amount) - distributedToCommunityPool += amount - accuDistributedToCommunityPool += amount - - case GOV_STAKER: - govStakerAddr, ok := access.GetAddress(prbac.ROLE_GOV_STAKER.String()) - if !ok { - return makeErrorWithDetails( - errDistributionAddressNotFound, - ufmt.Sprintf("%s not found", prbac.ROLE_GOV_STAKER.String()), - ) - } - - gns.Transfer(cross, govStakerAddr, amount) - distributedToGovStaker += amount - accuDistributedToGovStaker += amount - - default: - return makeErrorWithDetails( - errInvalidEmissionTarget, - ufmt.Sprintf("invalid target(%d)", target), - ) - } - - return nil -} - -// GetDistributionBpsPct returns the distribution percentage in basis points for a specific target. -func GetDistributionBpsPct(target int) int64 { - assertValidDistributionTarget(target) - if distributionBpsPct == nil { - panic("distributionBpsPct is nil") - } - - iInt64, exist := distributionBpsPct.Get(strconv.Itoa(target)) - if !exist { - panic(makeErrorWithDetails( - errInvalidEmissionTarget, - ufmt.Sprintf("invalid target(%d)", target), - )) - } - - pct, ok := iInt64.(int64) - if !ok { - panic("failed to cast distributionBpsPct's element to int64") - } - - return pct -} - -// GetDistributedToStaker returns pending GNS for liquidity stakers. -func GetDistributedToStaker() int64 { - return distributedToStaker -} - -// GetDistributedToDevOps returns accumulated GNS for DevOps. -func GetDistributedToDevOps() int64 { - return distributedToDevOps -} - -// GetDistributedToCommunityPool returns the amount of GNS distributed to Community Pool. -func GetDistributedToCommunityPool() int64 { - return distributedToCommunityPool -} - -// GetDistributedToGovStaker returns the amount of GNS distributed to governance stakers since last clear. -func GetDistributedToGovStaker() int64 { - return distributedToGovStaker -} - -// GetAccuDistributedToStaker returns the total historical GNS distributed to liquidity stakers. -func GetAccuDistributedToStaker() int64 { - return accuDistributedToStaker -} - -// GetAccuDistributedToDevOps returns the total historical GNS distributed to DevOps. -func GetAccuDistributedToDevOps() int64 { - return accuDistributedToDevOps -} - -// GetAccuDistributedToCommunityPool returns the total historical GNS distributed to Community Pool. -func GetAccuDistributedToCommunityPool() int64 { - return accuDistributedToCommunityPool -} - -// GetAccuDistributedToGovStaker returns the total historical GNS distributed to governance stakers. -func GetAccuDistributedToGovStaker() int64 { - return accuDistributedToGovStaker -} - -// GetStakerEmissionAmountPerSecond returns the current per-second emission amount allocated to liquidity stakers. -func GetStakerEmissionAmountPerSecond() int64 { - currentTimestamp := time.Now().Unix() - return calculateAmount(gns.GetEmissionAmountPerSecondByTimestamp(currentTimestamp), GetDistributionBpsPct(LIQUIDITY_STAKER)) -} - -// GetStakerEmissionAmountPerSecondInRange returns emission amounts allocated to liquidity stakers for a time range. -func GetStakerEmissionAmountPerSecondInRange(start, end int64) ([]int64, []int64) { - halvingBlocks, halvingEmissions := gns.GetEmissionAmountPerSecondInRange(start, end) - for i := range halvingBlocks { - // Applying staker ratio for past halving blocks - halvingEmissions[i] = calculateAmount(halvingEmissions[i], GetDistributionBpsPct(LIQUIDITY_STAKER)) - } - - return halvingBlocks, halvingEmissions -} - -// ClearDistributedToStaker resets the pending distribution amount for liquidity stakers. -// -// Only callable by staker contract. -func ClearDistributedToStaker(cur realm) { - caller := std.PreviousRealm().Address() - access.AssertIsStaker(caller) - - distributedToStaker = 0 -} - -// ClearDistributedToGovStaker resets the pending distribution amount for governance stakers. -// -// Only callable by governance staker contract. -func ClearDistributedToGovStaker(cur realm) { - caller := std.PreviousRealm().Address() - access.AssertIsGovStaker(caller) - distributedToGovStaker = 0 -} - -// setDistributionBpsPct changes percentage of each target for how much GNS it will get by emission. -// Creates new AVL tree if nil. -func setDistributionBpsPct(target int, pct int64) { - if distributionBpsPct == nil { - distributionBpsPct = avl.NewTree() - } - - distributionBpsPct.Set(strconv.Itoa(target), pct) -} - -// targetToStr converts target constant to string representation. -func targetToStr(target int) string { - switch target { - case LIQUIDITY_STAKER: - return "LIQUIDITY_STAKER" - case DEVOPS: - return "DEVOPS" - case COMMUNITY_POOL: - return "COMMUNITY_POOL" - case GOV_STAKER: - return "GOV_STAKER" - default: - return "UNKNOWN" - } -} diff --git a/contract/r/gnoswap/emission/emission.gno b/contract/r/gnoswap/emission/emission.gno deleted file mode 100644 index 244fcab..0000000 --- a/contract/r/gnoswap/emission/emission.gno +++ /dev/null @@ -1,216 +0,0 @@ -package emission - -import ( - "math" - "std" - "time" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/gns" - "gno.land/r/gnoswap/halt" -) - -var ( - // leftGNSAmount tracks undistributed GNS tokens from previous distributions - leftGNSAmount int64 - // lastExecutedTimestamp stores the last timestamp when distribution was executed - lastExecutedTimestamp int64 - - // emissionAddr is the address of the emission realm - emissionAddr = std.CurrentRealm().Address() - - // distributionStartTimestamp is the timestamp from which emission distribution starts - // Default is 0, meaning distribution is not started until explicitly set - distributionStartTimestamp int64 -) - -// GetLeftGNSAmount returns the amount of undistributed GNS tokens from previous distributions. -func GetLeftGNSAmount() int64 { return leftGNSAmount } - -// GetDistributionStartTimestamp returns the timestamp when emission distribution started. -// Returns 0 if distribution has not been started yet. -func GetDistributionStartTimestamp() int64 { return distributionStartTimestamp } - -// setLeftGNSAmount updates the undistributed GNS token amount -func setLeftGNSAmount(amount int64) { - if amount < 0 { - panic("left GNS amount cannot be negative") - } - - leftGNSAmount = amount -} - -// GetLastExecutedTimestamp returns the timestamp of the last emission distribution execution. -func GetLastExecutedTimestamp() int64 { return lastExecutedTimestamp } - -// setLastExecutedTimestamp updates the timestamp of the last emission distribution execution. -func setLastExecutedTimestamp(timestamp int64) { - if timestamp < 0 { - panic("last executed timestamp cannot be negative") - } - - lastExecutedTimestamp = timestamp -} - -// MintAndDistributeGns mints and distributes GNS tokens according to the emission schedule. -// -// This function is called automatically by protocol contracts during user interactions -// to trigger periodic GNS emission. It mints new tokens based on elapsed time since -// last distribution and distributes them to predefined targets (staker, devops, etc.). -// -// Returns: -// - int64: Total amount of GNS distributed in this call -// -// Note: Distribution only occurs if start timestamp is set and reached. -// Any undistributed tokens from previous calls are carried forward. -func MintAndDistributeGns(cur realm) int64 { - halt.AssertIsNotHaltedEmission() - - currentHeight := std.ChainHeight() - currentTimestamp := time.Now().Unix() - - // Check if distribution start timestamp is set and if current timestamp has reached it - // If distributionStartTimestamp is 0 (default), skip distribution to prevent immediate start - // If current timestamp is below start timestamp, skip distribution - if distributionStartTimestamp == 0 || currentTimestamp < distributionStartTimestamp { - return 0 - } - - // Skip if we've already minted tokens at this timestamp - lastMintedTimestamp := gns.LastMintedTimestamp() - if currentTimestamp <= lastMintedTimestamp { - return 0 - } - - // Additional check to prevent re-entrancy - if lastExecutedTimestamp >= currentTimestamp { - // Skip if we've already processed this height in emission - return 0 - } - - // Mint new tokens and add any leftover amounts from previous distribution - mintedEmissionRewardAmount := gns.MintGns(cross, emissionAddr) - - // Validate minted amount - if mintedEmissionRewardAmount < 0 { - panic("minted emission reward amount cannot be negative") - } - - distributableAmount := mintedEmissionRewardAmount - prevLeftAmount := GetLeftGNSAmount() - - if leftGNSAmount > 0 { - // Check for overflow before addition - if distributableAmount > math.MaxInt64-prevLeftAmount { - panic("distributable amount would overflow") - } - - distributableAmount += prevLeftAmount - setLeftGNSAmount(0) - } - - // Distribute tokens and track any undistributed amount - distributedGNSAmount, err := distributeToTarget(distributableAmount) - if err != nil { - panic(err) - } - - // Validate distribution arithmetic - if distributedGNSAmount < 0 { - panic("distributed amount cannot be negative") - } - - if distributedGNSAmount > distributableAmount { - panic("distributed amount cannot exceed distributable amount") - } - - if distributableAmount != distributedGNSAmount { - remainder := distributableAmount - distributedGNSAmount - if remainder < 0 { - panic("remainder calculation error") - } - setLeftGNSAmount(remainder) - } - - setLastExecutedTimestamp(currentTimestamp) - - previousRealm := std.PreviousRealm() - std.Emit( - "MintAndDistributeGns", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "lastTimestamp", formatInt(lastExecutedTimestamp), - "currentTimestamp", formatInt(currentTimestamp), - "currentHeight", formatInt(currentHeight), - "mintedAmount", formatInt(mintedEmissionRewardAmount), - "prevLeftAmount", formatInt(prevLeftAmount), - "distributedAmount", formatInt(distributedGNSAmount), - "currentLeftAmount", formatInt(GetLeftGNSAmount()), - "gnsTotalSupply", formatInt(gns.TotalSupply()), - ) - - return distributedGNSAmount -} - -// SetDistributionStartTime sets the timestamp when emission distribution starts. -// -// This function controls when GNS emission begins. Once set and reached, the protocol -// starts minting GNS tokens according to the emission schedule. The timestamp can only -// be set before distribution starts - it becomes immutable once active. -// -// Parameters: -// - startTimestamp: Unix timestamp when emission should begin -// -// Requirements: -// - Must be called before distribution starts (one-time setup) -// - Timestamp must be in the future -// - Cannot be negative -// -// Effects: -// - Sets global distribution start time -// - Initializes GNS emission state if not already started -// - Emission begins automatically when timestamp is reached -// -// Only callable by admin or governance. -func SetDistributionStartTime(cur realm, startTimestamp int64) { - halt.AssertIsNotHaltedEmission() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - // Prevent negative timestamps. - if startTimestamp < 0 { - panic("distribution start timestamp cannot be negative") - } - - currentTimestamp := time.Now().Unix() - - // Cannot change after distribution started. - if distributionStartTimestamp != 0 && distributionStartTimestamp <= currentTimestamp { - panic("distribution has already started, cannot change start timestamp") - } - - // Must be in the future. - if startTimestamp > 0 && startTimestamp <= currentTimestamp { - panic("distribution start timestamp must be greater than current timestamp") - } - - prevStartTimestamp := distributionStartTimestamp - - // Reinitialize emission state if not started yet. - if startTimestamp > currentTimestamp && gns.MintedEmissionAmount() == 0 { - gns.InitEmissionState(cross, startTimestamp, startTimestamp) - } - - distributionStartTimestamp = startTimestamp - - std.Emit( - "SetDistributionStartTime", - "caller", caller.String(), - "prevStartTimestamp", formatInt(prevStartTimestamp), - "newStartTimestamp", formatInt(startTimestamp), - "height", formatInt(std.ChainHeight()), - "timestamp", formatInt(time.Now().Unix()), - ) -} diff --git a/contract/r/gnoswap/emission/errors.gno b/contract/r/gnoswap/emission/errors.gno deleted file mode 100644 index a20da21..0000000 --- a/contract/r/gnoswap/emission/errors.gno +++ /dev/null @@ -1,21 +0,0 @@ -package emission - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errCallbackIsNil = errors.New("[GNOSWAP-EMISSION-001] callback func is nil") - errInvalidEmissionTarget = errors.New("[GNOSWAP-EMISSION-002] invalid emission target") - errInvalidEmissionPct = errors.New("[GNOSWAP-EMISSION-003] invalid emission percentage") - errDuplicateTarget = errors.New("[GNOSWAP-EMISSION-004] duplicate emission target") - errEmissionAddressNotFound = errors.New("[GNOSWAP-EMISSION-005] emission address not found") - errDistributionAddressNotFound = errors.New("[GNOSWAP-EMISSION-006] distribution address not found") -) - -// makeErrorWithDetails creates a new error by combining a base error with additional details. -func makeErrorWithDetails(err error, detail string) error { - return ufmt.Errorf("%s || %s", err.Error(), detail) -} diff --git a/contract/r/gnoswap/emission/gnomod.toml b/contract/r/gnoswap/emission/gnomod.toml deleted file mode 100644 index e719176..0000000 --- a/contract/r/gnoswap/emission/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/emission" -gno = "0.9" diff --git a/contract/r/gnoswap/emission/utils.gno b/contract/r/gnoswap/emission/utils.gno deleted file mode 100644 index e415b9b..0000000 --- a/contract/r/gnoswap/emission/utils.gno +++ /dev/null @@ -1,37 +0,0 @@ -package emission - -import ( - "strconv" - - "gno.land/p/nt/ufmt" -) - -// formatUint converts various unsigned integer types to string representation. -// Panics if the type is not supported. -func formatUint(v any) string { - switch v := v.(type) { - case uint8: - return strconv.FormatUint(uint64(v), 10) - case uint32: - return strconv.FormatUint(uint64(v), 10) - case uint64: - return strconv.FormatUint(v, 10) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} - -// formatInt converts various signed integer types to string representation. -// Panics if the type is not supported. -func formatInt(v any) string { - switch v := v.(type) { - case int32: - return strconv.FormatInt(int64(v), 10) - case int64: - return strconv.FormatInt(v, 10) - case int: - return strconv.Itoa(v) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} diff --git a/contract/r/gnoswap/gns/README.md b/contract/r/gnoswap/gns/README.md deleted file mode 100644 index 7ffc1e9..0000000 --- a/contract/r/gnoswap/gns/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# GNS - -GnoSwap governance and utility token. - -## Overview - -GNS is the native governance token of GnoSwap, featuring a deflationary emission schedule with halvings every 2 years over 12 years total. - -## Token Economics - -- **Symbol**: GNS -- **Decimals**: 6 -- **Max Supply**: 1,000,000,000 GNS -- **Initial Mint**: 100,000,000 GNS -- **Total Emission**: 900,000,000 GNS over 12 years - -## Emission Schedule - -| Years | Annual Emission | Rate | -|-------|----------------|------| -| 1-2 | 225,000,000 | 100% | -| 3-4 | 112,500,000 | 50% | -| 5-6 | 56,250,000 | 25% | -| 7-8 | 28,125,000 | 12.5%| -| 9-12 | 14,062,500 | 6.25%| - -## Core Functions - -### `Transfer` -Transfers tokens between addresses. - -### `TransferFrom` -Transfers with allowance. - -### `Approve` -Approves spending allowance. - -### `MintGns` -Mints new tokens per emission schedule. - -### `Burn` -Burns tokens from supply. - -## Usage - -```go -// Transfer tokens -Transfer(to, amount) - -// Approve and transfer -Approve(spender, amount) -TransferFrom(from, to, amount) - -// Mint per emission schedule -MintGns() -``` - -## Distribution - -See [emission contract](../emission) for distribution details. \ No newline at end of file diff --git a/contract/r/gnoswap/gns/assert.gno b/contract/r/gnoswap/gns/assert.gno deleted file mode 100644 index f07c8b0..0000000 --- a/contract/r/gnoswap/gns/assert.gno +++ /dev/null @@ -1,14 +0,0 @@ -package gns - -import ( - "std" - - "gno.land/p/nt/ufmt" -) - -func assertAddressIsPreviousRealm(addr std.Address) { - previousRealm := std.PreviousRealm() - if addr != previousRealm.Address() { - panic(ufmt.Errorf("address(%s) is not previous realm", addr.String())) - } -} diff --git a/contract/r/gnoswap/gns/consts.gno b/contract/r/gnoswap/gns/consts.gno deleted file mode 100644 index 9db42d0..0000000 --- a/contract/r/gnoswap/gns/consts.gno +++ /dev/null @@ -1,29 +0,0 @@ -package gns - -const ( - DAY_PER_YEAR = 365 - SECONDS_PER_DAY = 86400 - SECONDS_IN_YEAR = 31536000 - - HALVING_START_YEAR = int64(1) - HALVING_END_YEAR = int64(12) - - // Maximum allowed block time in milliseconds (1 second) - MAX_BLOCK_TIME_MS = 1e9 -) - -// Annual halving amount - maximum issuance per year -var halvingAmountsPerYear = [HALVING_END_YEAR]int64{ - 18_750_000_000_000 * 12, // Year 1: 225000000000000 - 18_750_000_000_000 * 12, // Year 2: 225000000000000 - 9_375_000_000_000 * 12, // Year 3: 112500000000000 - 9_375_000_000_000 * 12, // Year 4: 112500000000000 - 4_687_500_000_000 * 12, // Year 5: 56250000000000 - 4_687_500_000_000 * 12, // Year 6: 56250000000000 - 2_343_750_000_000 * 12, // Year 7: 28125000000000 - 2_343_750_000_000 * 12, // Year 8: 28125000000000 - 1_171_875_000_000 * 12, // Year 9: 14062500000000 - 1_171_875_000_000 * 12, // Year 10: 14062500000000 - 1_171_875_000_000 * 12, // Year 11: 14062500000000 - 1_171_875_000_000 * 12, // Year 12: 14062500000000 -} diff --git a/contract/r/gnoswap/gns/emission_state.gno b/contract/r/gnoswap/gns/emission_state.gno deleted file mode 100644 index 4029a4e..0000000 --- a/contract/r/gnoswap/gns/emission_state.gno +++ /dev/null @@ -1,166 +0,0 @@ -package gns - -import ( - "std" - "time" - - "gno.land/p/nt/ufmt" -) - -var emissionState *EmissionState - -func init() { - emissionState = NewEmissionState(0, 0) -} - -// EmissionState manages emission state and halving data. -// Tracks emission timing, status, and halving year information for 12-year schedule. -type EmissionState struct { - startHeight int64 - startTimestamp int64 - endTimestamp int64 - halvingData *HalvingData -} - -// isInitialized returns true if emission state has been initialized with valid height and timestamp. -func (e *EmissionState) isInitialized() bool { - return e.startHeight != 0 && e.startTimestamp != 0 -} - -// isActive returns true if emission is currently active at the given timestamp. -// Returns false if not initialized or timestamp is outside emission period. -func (e *EmissionState) isActive(timestamp int64) bool { - if !e.isInitialized() { - return false - } - - if e.startTimestamp > timestamp { - return false - } - - if e.endTimestamp < timestamp { - return false - } - - return true -} - -// isEnded returns true if emission has ended at the given timestamp. -func (e *EmissionState) isEnded(timestamp int64) bool { - return e.endTimestamp < timestamp -} - -// getCurrentYear returns the halving year (1-12) for the given timestamp, or 0 if outside emission period. -func (e *EmissionState) getCurrentYear(timestamp int64) int64 { - if timestamp < e.startTimestamp { - return 0 - } - - if timestamp > e.endTimestamp { - return 0 - } - - year := (timestamp - e.startTimestamp) / SECONDS_IN_YEAR - return year + 1 -} - -// getStartHeight returns the blockchain height when emission started. -func (e *EmissionState) getStartHeight() int64 { - return e.startHeight -} - -// getStartTimestamp returns the timestamp when emission started. -func (e *EmissionState) getStartTimestamp() int64 { - return e.startTimestamp -} - -// getEndTimestamp returns the timestamp when emission ends. -func (e *EmissionState) getEndTimestamp() int64 { - return e.endTimestamp -} - -// getHalvingData returns the halving data containing emission schedule details. -func (e *EmissionState) getHalvingData() *HalvingData { - return e.halvingData -} - -// getHalvingYearStartTimestamp returns the start timestamp for the specified halving year. -func (e *EmissionState) getHalvingYearStartTimestamp(year int64) int64 { - return e.halvingData.getStartTimestamp(year) -} - -// getHalvingYearEndTimestamp returns the end timestamp for the specified halving year. -func (e *EmissionState) getHalvingYearEndTimestamp(year int64) int64 { - return e.halvingData.getEndTimestamp(year) -} - -// getHalvingYearAmountPerSecond returns the emission rate per second for the specified halving year. -func (e *EmissionState) getHalvingYearAmountPerSecond(year int64) int64 { - return e.halvingData.getAmountPerSecond(year) -} - -// getHalvingYearAccumulatedAmount returns the accumulated emission amount for the specified halving year. -func (e *EmissionState) getHalvingYearAccumulatedAmount(year int64) int64 { - return e.halvingData.getAccumAmount(year) -} - -// getHalvingYearLeftAmount returns the remaining emission amount for the specified halving year. -func (e *EmissionState) getHalvingYearLeftAmount(year int64) int64 { - return e.halvingData.getLeftAmount(year) -} - -// addHalvingYearAccumulatedAmount adds to the accumulated emission amount for the specified halving year. -// Returns error if year is invalid (0 or outside 1-12 range). -func (e *EmissionState) addHalvingYearAccumulatedAmount(year int64, amount int64) error { - if year == 0 { - return makeErrorWithDetails(errInvalidYear, ufmt.Sprintf("year: %d", year)) - } - - accumulatedAmount := e.halvingData.getAccumAmount(year) - accumulatedAmount += amount - - e.halvingData.setAccumAmount(year, accumulatedAmount) - return nil -} - -// subHalvingYearLeftAmount subtracts from the remaining emission amount for the specified halving year. -// Returns error if year is invalid (0 or outside 1-12 range). -func (e *EmissionState) subHalvingYearLeftAmount(year int64, amount int64) error { - if year == 0 { - return makeErrorWithDetails(errInvalidYear, ufmt.Sprintf("year: %d", year)) - } - - leftAmount := e.halvingData.getLeftAmount(year) - leftAmount -= amount - - e.halvingData.setLeftAmount(year, leftAmount) - return nil -} - -// updateHalvingData initializes halving data with the given start timestamp. -func (e *EmissionState) updateHalvingData(startTimestamp int64) { - e.halvingData = NewHalvingData(startTimestamp) -} - -// NewEmissionState creates a new EmissionState with specified start height and timestamp. -// Calculates emission end time based on 12-year schedule and initializes halving data. -func NewEmissionState(startHeight int64, startTimestamp int64) *EmissionState { - emissionEndTime := startTimestamp + SECONDS_IN_YEAR*HALVING_END_YEAR - 1 - - return &EmissionState{ - startHeight: startHeight, - startTimestamp: startTimestamp, - endTimestamp: emissionEndTime, - halvingData: NewHalvingData(startTimestamp), - } -} - -// getEmissionState returns the singleton emission state instance. -// Creates a new instance with current height and timestamp if one doesn't exist. -func getEmissionState() *EmissionState { - if emissionState == nil { - emissionState = NewEmissionState(std.ChainHeight(), time.Now().Unix()) - } - - return emissionState -} diff --git a/contract/r/gnoswap/gns/errors.gno b/contract/r/gnoswap/gns/errors.gno deleted file mode 100644 index 4ebb218..0000000 --- a/contract/r/gnoswap/gns/errors.gno +++ /dev/null @@ -1,18 +0,0 @@ -package gns - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errInvalidYear = errors.New("[GNOSWAP-GNS-001] invalid year") - errTooManyEmission = errors.New("[GNOSWAP-GNS-002] too many emission reward") - errEmissionChangeIsNilCallback = errors.New("[GNOSWAP-GNS-003] callback emission change is nil") - errInvalidAvgBlockTimeInMs = errors.New("[GNOSWAP-GNS-004] invalid avg block time in ms") -) - -func makeErrorWithDetails(err error, details string) error { - return ufmt.Errorf("%s || %s", err.Error(), details) -} diff --git a/contract/r/gnoswap/gns/getter.gno b/contract/r/gnoswap/gns/getter.gno deleted file mode 100644 index 5b7aae8..0000000 --- a/contract/r/gnoswap/gns/getter.gno +++ /dev/null @@ -1,185 +0,0 @@ -package gns - -import ( - "std" - "strconv" - "time" - - "gno.land/p/onbloc/json" -) - -// IsEmissionInitialized returns true if emission schedule has been initialized. -func IsEmissionInitialized() bool { - return getEmissionState().isInitialized() -} - -// IsEmissionActive returns true if emission is currently active based on current time. -func IsEmissionActive() bool { - return getEmissionState().isActive(time.Now().Unix()) -} - -// IsEmissionEnded returns true if emission schedule has completed. -func IsEmissionEnded() bool { - return getEmissionState().isEnded(time.Now().Unix()) -} - -// GetHalvingYear returns the halving year (1-12) for a given timestamp. -func GetHalvingYear(timestamp int64) int64 { - return getEmissionState().getCurrentYear(timestamp) -} - -// GetCurrentYear returns the current halving year (1-12) or 0 if emission is not active. -func GetCurrentYear() int64 { - return getEmissionState().getCurrentYear(time.Now().Unix()) -} - -// GetEmissionAmountPerSecondInRange returns halving timestamps and emission rates for the given time range. -// Returns two slices: timestamps when halving periods start and corresponding emission rates per second. -func GetEmissionAmountPerSecondInRange(fromTime, toTime int64) ([]int64, []int64) { - halvingData := getEmissionState().getHalvingData() - halvingTimes := make([]int64, 0) - halvingEmissions := make([]int64, 0) - - for year := HALVING_START_YEAR; year <= HALVING_END_YEAR; year++ { - startTimestamp := halvingData.getStartTimestamp(year) - if startTimestamp < fromTime { - continue - } - - if toTime < startTimestamp { - break - } - - halvingTimes = append(halvingTimes, startTimestamp) - halvingEmissions = append(halvingEmissions, halvingData.getAmountPerSecond(year)) - } - - return halvingTimes, halvingEmissions -} - -// GetEmissionAmountPerSecondByTimestamp returns the emission rate per second for a given timestamp. -// Returns 0 if timestamp is outside emission period. -func GetEmissionAmountPerSecondByTimestamp(timestamp int64) int64 { - year := getEmissionState().getCurrentYear(timestamp) - return getEmissionState().getHalvingYearAmountPerSecond(year) -} - -// GetEmissionLeftAmountByTimestamp returns the remaining emission amount for the halving year at given timestamp. -// Returns 0 if timestamp is outside emission period. -func GetEmissionLeftAmountByTimestamp(timestamp int64) int64 { - year := getEmissionState().getCurrentYear(timestamp) - return getEmissionState().getHalvingYearLeftAmount(year) -} - -// GetEmissionAccumulatedAmountByTimestamp returns the accumulated emission amount for the halving year at given timestamp. -// Returns 0 if timestamp is outside emission period. -func GetEmissionAccumulatedAmountByTimestamp(timestamp int64) int64 { - year := getEmissionState().getCurrentYear(timestamp) - return getEmissionState().getHalvingYearAccumulatedAmount(year) -} - -// GetHalvingYearStartTimestamp returns the start timestamp for the specified halving year. -func GetHalvingYearStartTimestamp(year int64) int64 { - halvingData := getEmissionState().getHalvingData() - return halvingData.getStartTimestamp(year) -} - -// GetHalvingYearEndTimestamp returns the end timestamp for the specified halving year. -func GetHalvingYearEndTimestamp(year int64) int64 { - halvingData := getEmissionState().getHalvingData() - return halvingData.getEndTimestamp(year) -} - -// GetHalvingYearTimestamp returns the start timestamp for the specified halving year. -func GetHalvingYearTimestamp(year int64) int64 { - halvingData := getEmissionState().getHalvingData() - return halvingData.getStartTimestamp(year) -} - -// GetHalvingYearMaxAmount returns the maximum token issuance for the specified halving year. -func GetHalvingYearMaxAmount(year int64) int64 { - halvingData := getEmissionState().getHalvingData() - return halvingData.getMaxAmount(year) -} - -// GetHalvingYearMintAmount returns the amount of tokens minted for the specified halving year. -func GetHalvingYearMintAmount(year int64) int64 { - halvingData := getEmissionState().getHalvingData() - return halvingData.getMintedAmount(year) -} - -// GetHalvingYearLeftAmount returns the remaining token issuance for the specified halving year. -func GetHalvingYearLeftAmount(year int64) int64 { - halvingData := getEmissionState().getHalvingData() - return halvingData.getLeftAmount(year) -} - -// GetHalvingYearAccuAmount returns the accumulated token issuance for the specified halving year. -func GetHalvingYearAccuAmount(year int64) int64 { - halvingData := getEmissionState().getHalvingData() - return halvingData.getAccumAmount(year) -} - -// GetAmountPerSecondPerHalvingYear returns the emission rate per second for the specified halving year. -func GetAmountPerSecondPerHalvingYear(year int64) int64 { - halvingData := getEmissionState().getHalvingData() - return halvingData.getAmountPerSecond(year) -} - -// GetHalvingAmountsPerYear returns the total emission amount allocated for the specified year. -func GetHalvingAmountsPerYear(year int64) int64 { - return halvingAmountsPerYear[year-1] -} - -// GetEmissionStartTimestamp returns the timestamp when emission schedule begins. -func GetEmissionStartTimestamp() int64 { - return getEmissionState().getStartTimestamp() -} - -// GetEmissionEndTimestamp returns the timestamp when emission schedule ends. -func GetEmissionEndTimestamp() int64 { - return getEmissionState().getEndTimestamp() -} - -// GetHalvingYearInfo returns the halving year, start timestamp, and end timestamp for a given timestamp. -// Returns (year, startTimestamp, endTimestamp). Year is 0 if outside emission period. -func GetHalvingYearInfo(timestamp int64) (int64, int64, int64) { - state := getEmissionState() - - endTimestamp := state.getEndTimestamp() - startTimestamp := state.getStartTimestamp() - - year := getEmissionState().getCurrentYear(timestamp) - - return year, startTimestamp + (SECONDS_IN_YEAR * year), endTimestamp -} - -// GetHalvingInfo returns comprehensive halving information as JSON string. -// Includes current height, timestamp, and details for all halving years (1-12). Panics on JSON marshal error. -func GetHalvingInfo() string { - currentTime := time.Now().Unix() - currentHeight := std.ChainHeight() - - halvings := make([]*json.Node, 0) - - for year := HALVING_START_YEAR; year <= HALVING_END_YEAR; year++ { - halvings = append(halvings, json.ObjectNode("", map[string]*json.Node{ - "year": json.StringNode("year", strconv.FormatInt(year, 10)), - "timestamp": json.NumberNode("timestamp", float64(GetHalvingYearTimestamp(year))), - "amount": json.NumberNode("amount", float64(GetAmountPerSecondPerHalvingYear(year))), - })) - } - - node := json.ObjectNode("", map[string]*json.Node{ - "height": json.NumberNode("height", float64(currentHeight)), - "currentTime": json.NumberNode("timestamp", float64(currentTime)), - "halvings": json.ArrayNode("", halvings), - }) - - b, err := json.Marshal(node) - if err != nil { - panic(err.Error()) - } - - return string(b) -} diff --git a/contract/r/gnoswap/gns/gnomod.toml b/contract/r/gnoswap/gns/gnomod.toml deleted file mode 100644 index c2d2ffa..0000000 --- a/contract/r/gnoswap/gns/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/gns" -gno = "0.9" diff --git a/contract/r/gnoswap/gns/gns.gno b/contract/r/gnoswap/gns/gns.gno deleted file mode 100644 index f72f870..0000000 --- a/contract/r/gnoswap/gns/gns.gno +++ /dev/null @@ -1,309 +0,0 @@ -package gns - -import ( - "std" - "strings" - "time" - - "gno.land/p/demo/tokens/grc20" - "gno.land/p/nt/ownable" - "gno.land/p/nt/ufmt" - - "gno.land/r/demo/defi/grc20reg" - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" -) - -const ( - MAXIMUM_SUPPLY = int64(1_000_000_000_000_000) - INITIAL_MINT_AMOUNT = int64(100_000_000_000_000) - MAX_EMISSION_AMOUNT = int64(900_000_000_000_000) // MAXIMUM_SUPPLY - INITIAL_MINT_AMOUNT -) - -var ( - adminAddr = getAdminAddress() - token, privateLedger = grc20.NewToken("Gnoswap", "GNS", 6) - UserTeller = token.CallerTeller() - owner = ownable.NewWithAddress(adminAddr) - - leftEmissionAmount int64 // amount of GNS can be minted for emission - mintedEmissionAmount int64 // amount of GNS that has been minted for emission - lastMintedTimestamp int64 // last block time that gns was minted for emission - - burnAmount int64 // amount of GNS that has been burned -) - -func init() { - privateLedger.Mint(owner.Owner(), INITIAL_MINT_AMOUNT) - grc20reg.Register(cross, token, "") - - // Initial amount set to 900_000_000_000_000 (MAXIMUM_SUPPLY - INITIAL_MINT_AMOUNT). - // leftEmissionAmount will decrease as tokens are minted. - setLeftEmissionAmount(MAX_EMISSION_AMOUNT) - setMintedEmissionAmount(0) - setLastMintedTimestamp(std.ChainHeight()) - burnAmount = 0 -} - -// Name returns the name of the GNS token. -func Name() string { return token.GetName() } - -// Symbol returns the symbol of the GNS token. -func Symbol() string { return token.GetSymbol() } - -// Decimals returns the number of decimal places for GNS token. -func Decimals() int { return token.GetDecimals() } - -// TotalSupply returns the total supply of GNS tokens in circulation. -func TotalSupply() int64 { return token.TotalSupply() } - -// KnownAccounts returns the number of addresses that have held GNS. -func KnownAccounts() int { return token.KnownAccounts() } - -// BalanceOf returns the GNS balance of a specific address. -func BalanceOf(owner std.Address) int64 { return token.BalanceOf(owner) } - -// Allowance returns the amount of GNS that a spender is allowed to transfer from an owner. -func Allowance(owner, spender std.Address) int64 { return token.Allowance(owner, spender) } - -// MintGns mints new GNS tokens according to the emission schedule. -// -// Parameters: -// - address: recipient address for minted tokens -// -// Returns amount minted. -// Only callable by emission contract. -func MintGns(cur realm, address std.Address) int64 { - halt.AssertIsNotHaltedEmission() - - caller := std.PreviousRealm().Address() - access.AssertIsEmission(caller) - - lastGNSMintedTimestamp := LastMintedTimestamp() - currentTime := time.Now().Unix() - - // Skip if already minted this timestamp or emission ended. - if lastGNSMintedTimestamp == currentTime || lastGNSMintedTimestamp >= GetEmissionEndTimestamp() { - return 0 - } - - amountToMint := calculateAmountToMint(lastGNSMintedTimestamp+1, currentTime) - err := validEmissionAmount(amountToMint) - if err != nil { - panic(err) - } - - setLastMintedTimestamp(currentTime) - setMintedEmissionAmount(MintedEmissionAmount() + amountToMint) - setLeftEmissionAmount(LeftEmissionAmount() - amountToMint) - - err = privateLedger.Mint(address, amountToMint) - if err != nil { - panic(err.Error()) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "MintGNS", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "mintedBlockTime", formatInt(currentTime), - "mintedGNSAmount", formatInt(amountToMint), - "accumMintedGNSAmount", formatInt(MintedEmissionAmount()), - "accumLeftMintGNSAmount", formatInt(LeftEmissionAmount()), - ) - - return amountToMint -} - -// Burn permanently removes GNS tokens from circulation. -// -// Parameters: -// - from: address to burn tokens from -// - amount: amount to burn -// -// Only callable by contract owner. -func Burn(cur realm, from std.Address, amount int64) { - assertAddressIsPreviousRealm(from) - - checkErr(privateLedger.Burn(from, amount)) - - burnAmount += amount - - previousRealm := std.PreviousRealm() - std.Emit( - "Burn", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "burnedBlockHeight", formatInt(std.ChainHeight()), - "burnFrom", from.String(), - "burnedGNSAmount", formatInt(amount), - "accumBurnedGNSAmount", formatInt(BurnAmount()), - ) -} - -// Transfer transfers GNS tokens from caller to recipient. -// -// Parameters: -// - to: recipient address -// - amount: amount to transfer -func Transfer(cur realm, to std.Address, amount int64) { - userTeller := token.CallerTeller() - checkErr(userTeller.Transfer(to, amount)) -} - -// Approve allows spender to transfer GNS tokens from caller's account. -// -// Parameters: -// - spender: address authorized to spend -// - amount: maximum amount spender can transfer -func Approve(cur realm, spender std.Address, amount int64) { - userTeller := token.CallerTeller() - checkErr(userTeller.Approve(spender, amount)) -} - -// TransferFrom transfers GNS tokens on behalf of owner. -// -// Parameters: -// - from: token owner address -// - to: recipient address -// - amount: amount to transfer -func TransferFrom(cur realm, from, to std.Address, amount int64) { - userTeller := token.CallerTeller() - checkErr(userTeller.TransferFrom(from, to, amount)) -} - -// Render returns token information for web interface. -func Render(path string) string { - parts := strings.Split(path, "/") - c := len(parts) - - switch { - case path == "": - return token.RenderHome() - case c == 2 && parts[0] == "balance": - owner := std.Address(parts[1]) - balance := token.BalanceOf(owner) - return ufmt.Sprintf("%d\n", balance) - default: - return "404\n" - } -} - -// checkErr panics if error is not nil. -func checkErr(err error) { - if err != nil { - panic(err.Error()) - } -} - -// calculateAmountToMint calculates GNS tokens to mint for given timestamp range. -func calculateAmountToMint(fromTimestamp, toTimestamp int64) int64 { - endTimestamp := GetEmissionEndTimestamp() - if toTimestamp > endTimestamp { - toTimestamp = endTimestamp - } - - if fromTimestamp > toTimestamp { - return 0 - } - - startTimestamp := getEmissionState().getStartTimestamp() - if fromTimestamp < startTimestamp { - fromTimestamp = startTimestamp - } - - if toTimestamp < startTimestamp { - return 0 - } - - fromYear := getEmissionState().getCurrentYear(fromTimestamp) - toYear := getEmissionState().getCurrentYear(toTimestamp) - - if fromYear == 0 || toYear == 0 { - return 0 - } - - totalAmountToMint := int64(0) - - for year := fromYear; year <= toYear; year++ { - yearEndTimestamp := GetHalvingYearEndTimestamp(year) - currentToTimestamp := i64Min(toTimestamp, yearEndTimestamp) - - seconds := currentToTimestamp - fromTimestamp + 1 - if seconds <= 0 { - break - } - - amountPerSecond := GetAmountPerSecondPerHalvingYear(year) - - yearAmountToMint := amountPerSecond * seconds - - if currentToTimestamp >= yearEndTimestamp { - leftover := handleLeftEmissionAmount(year, yearAmountToMint) - yearAmountToMint += leftover - } - - totalAmountToMint += yearAmountToMint - - getEmissionState().addHalvingYearAccumulatedAmount(year, yearAmountToMint) - getEmissionState().subHalvingYearLeftAmount(year, yearAmountToMint) - - std.Emit( - "CalculateAmountToMint", - "fromTimestamp", formatInt(fromTimestamp), - "toTimestamp", formatInt(currentToTimestamp), - "year", formatInt(year), - "amountPerSecond", formatInt(amountPerSecond), - ) - - fromTimestamp = currentToTimestamp + 1 - - if fromTimestamp > toTimestamp { - break - } - } - - return totalAmountToMint -} - -// isLastSecondOfHalvingYear returns true if timestamp is the last second of a halving year. -func isLastSecondOfHalvingYear(timestamp int64) bool { - year := getEmissionState().getCurrentYear(timestamp) - lastSecond := GetHalvingYearEndTimestamp(year) - - return timestamp == lastSecond -} - -// handleLeftEmissionAmount returns the remaining emission amount for a halving year. -func handleLeftEmissionAmount(year, amount int64) int64 { - return GetHalvingYearLeftAmount(year) - amount -} - -// skipIfSameHeight returns true if last minted height equals current height, -// preventing multiple mints in the same block. -func skipIfSameHeight(lastMintedHeight, currentHeight int64) bool { - return lastMintedHeight == currentHeight -} - -// BurnAmount returns the total amount of GNS tokens burned. -func BurnAmount() int64 { return burnAmount } - -// LastMintedTimestamp returns the timestamp of the last GNS emission mint. -func LastMintedTimestamp() int64 { return lastMintedTimestamp } - -// LeftEmissionAmount returns the remaining GNS tokens available for emission. -func LeftEmissionAmount() int64 { return leftEmissionAmount } - -// MintedEmissionAmount returns the total GNS tokens minted through emission, -// excluding the initial mint amount. -func MintedEmissionAmount() int64 { return mintedEmissionAmount } - -// setLastMintedTimestamp sets the timestamp of the last emission mint. -func setLastMintedTimestamp(timestamp int64) { lastMintedTimestamp = timestamp } - -// setLeftEmissionAmount sets the remaining emission amount. -func setLeftEmissionAmount(amount int64) { leftEmissionAmount = amount } - -// setMintedEmissionAmount sets the total minted emission amount. -func setMintedEmissionAmount(amount int64) { mintedEmissionAmount = amount } diff --git a/contract/r/gnoswap/gns/gns_emission.gno b/contract/r/gnoswap/gns/gns_emission.gno deleted file mode 100644 index 3e41c4c..0000000 --- a/contract/r/gnoswap/gns/gns_emission.gno +++ /dev/null @@ -1,28 +0,0 @@ -package gns - -import ( - "std" - - "gno.land/r/gnoswap/access" -) - -// InitEmissionState initializes emission schedule with start timestamp. -// Only callable by emission contract. Sets up 12-year emission schedule -// with halving every 2 years. Panics if caller is not emission contract. -func InitEmissionState(cur realm, height int64, timestamp int64) { - caller := std.PreviousRealm().Address() - access.AssertIsEmission(caller) - - emissionState = NewEmissionState(height, timestamp) - - previousRealm := std.PreviousRealm() - std.Emit( - "InitEmissionState", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "height", formatInt(height), - "timestamp", formatInt(timestamp), - "startTimestamp", formatInt(emissionState.getStartTimestamp()), - "endTimestamp", formatInt(emissionState.getEndTimestamp()), - ) -} diff --git a/contract/r/gnoswap/gns/halving.gno b/contract/r/gnoswap/gns/halving.gno deleted file mode 100644 index 593a464..0000000 --- a/contract/r/gnoswap/gns/halving.gno +++ /dev/null @@ -1,205 +0,0 @@ -package gns - -// HalvingData stores emission data for each halving period. -// Contains timestamps, amounts, and rates for the 12-year emission schedule. -type HalvingData struct { - startTimestamps []int64 - endTimestamps []int64 - maxAmount []int64 - mintedAmount []int64 - leftAmount []int64 - accumAmount []int64 - amountPerSecond []int64 -} - -// getStartTimestamp returns the start timestamp for the specified halving year. -// Returns 0 if year is invalid. -func (h *HalvingData) getStartTimestamp(year int64) int64 { - if validYear(year) != nil { - return 0 - } - - return h.startTimestamps[year-1] -} - -// getEndTimestamp returns the end timestamp for the specified halving year. -// Returns 0 if year is invalid. -func (h *HalvingData) getEndTimestamp(year int64) int64 { - if validYear(year) != nil { - return 0 - } - - return h.endTimestamps[year-1] -} - -// getMaxAmount returns the maximum emission amount for the specified halving year. -// Returns 0 if year is invalid. -func (h *HalvingData) getMaxAmount(year int64) int64 { - if validYear(year) != nil { - return 0 - } - - return h.maxAmount[year-1] -} - -// getMintedAmount returns the amount already minted for the specified halving year. -func (h *HalvingData) getMintedAmount(year int64) int64 { - if validYear(year) != nil { - return 0 - } - return h.mintedAmount[year-1] -} - -// getAccumAmount returns the accumulated emission amount for the specified halving year. -func (h *HalvingData) getAccumAmount(year int64) int64 { - if validYear(year) != nil { - return 0 - } - return h.accumAmount[year-1] -} - -// getLeftAmount returns the remaining emission amount for the specified halving year. -func (h *HalvingData) getLeftAmount(year int64) int64 { - if validYear(year) != nil { - return 0 - } - return h.leftAmount[year-1] -} - -// getAmountPerSecond returns the emission rate per second for the specified halving year. -// Returns 0 if year is invalid. -func (h *HalvingData) getAmountPerSecond(year int64) int64 { - if validYear(year) != nil { - return 0 - } - return h.amountPerSecond[year-1] -} - -// setStartTimestamp sets the start timestamp for the specified halving year. -// Returns error if year is invalid. -func (h *HalvingData) setStartTimestamp(year int64, timestamp int64) error { - err := validYear(year) - if err != nil { - return err - } - - h.startTimestamps[year-1] = timestamp - - return nil -} - -// setEndTimestamp sets the end timestamp for the specified halving year. -func (h *HalvingData) setEndTimestamp(year int64, timestamp int64) error { - err := validYear(year) - if err != nil { - return err - } - - h.endTimestamps[year-1] = timestamp - - return nil -} - -// setMaxAmount sets the maximum emission amount for the specified halving year. -func (h *HalvingData) setMaxAmount(year, amount int64) error { - err := validYear(year) - if err != nil { - return err - } - - h.maxAmount[year-1] = amount - - return nil -} - -// setMintedAmount sets the minted amount for the specified halving year. -func (h *HalvingData) setMintedAmount(year, amount int64) error { - err := validYear(year) - if err != nil { - return err - } - - h.mintedAmount[year-1] = amount - - return nil -} - -// setAccumAmount sets the accumulated amount for the specified halving year. -func (h *HalvingData) setAccumAmount(year, amount int64) error { - err := validYear(year) - if err != nil { - return err - } - - h.accumAmount[year-1] = amount - - return nil -} - -// setLeftAmount sets the remaining amount for the specified halving year. -func (h *HalvingData) setLeftAmount(year, amount int64) error { - err := validYear(year) - if err != nil { - return err - } - - h.leftAmount[year-1] = amount - - return nil -} - -// addAccumAmount adds to the accumulated amount for the specified halving year. -func (h *HalvingData) addAccumAmount(year, amount int64) error { - err := validYear(year) - if err != nil { - return err - } - - h.accumAmount[year-1] += amount - - return nil -} - -// setAmountPerSecond sets the emission rate per second for the specified halving year. -// Returns error if year is invalid. -func (h *HalvingData) setAmountPerSecond(year, amount int64) error { - err := validYear(year) - if err != nil { - return err - } - - h.amountPerSecond[year-1] = amount - - return nil -} - -// NewHalvingData creates a new HalvingData instance with emission schedule. -// Initializes 12 years of halving periods with timestamps, amounts, and rates based on startTimestamp. -func NewHalvingData(startTimestamp int64) *HalvingData { - halvingData := &HalvingData{ - startTimestamps: make([]int64, HALVING_END_YEAR), - endTimestamps: make([]int64, HALVING_END_YEAR), - maxAmount: make([]int64, HALVING_END_YEAR), - mintedAmount: make([]int64, HALVING_END_YEAR), - leftAmount: make([]int64, HALVING_END_YEAR), - accumAmount: make([]int64, HALVING_END_YEAR), - amountPerSecond: make([]int64, HALVING_END_YEAR), - } - - for year := HALVING_START_YEAR; year <= HALVING_END_YEAR; year++ { - yearStartTimestamp := startTimestamp + (SECONDS_IN_YEAR * (year - 1)) - yearEndTimestamp := yearStartTimestamp + SECONDS_IN_YEAR - 1 - yearDistributionAmount := GetHalvingAmountsPerYear(year) - yearAmountPerSecond := yearDistributionAmount / SECONDS_IN_YEAR - - halvingData.setStartTimestamp(year, yearStartTimestamp) - halvingData.setEndTimestamp(year, yearEndTimestamp) - halvingData.setMaxAmount(year, yearDistributionAmount) - halvingData.setMintedAmount(year, 0) - halvingData.setAccumAmount(year, 0) - halvingData.setAmountPerSecond(year, yearAmountPerSecond) - halvingData.setLeftAmount(year, yearDistributionAmount) - } - - return halvingData -} diff --git a/contract/r/gnoswap/gns/utils.gno b/contract/r/gnoswap/gns/utils.gno deleted file mode 100644 index 5557175..0000000 --- a/contract/r/gnoswap/gns/utils.gno +++ /dev/null @@ -1,91 +0,0 @@ -package gns - -import ( - "std" - "strconv" - - "gno.land/p/nt/ufmt" - - prabc "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/rbac" -) - -// validBlockTime validates that block time is within acceptable range. -// Returns error if block time is <= 0 or >= 1e9. -func validBlockTime(blockTime int64) error { - if blockTime <= 0 || blockTime >= 1e9 { - return errInvalidAvgBlockTimeInMs - } - - return nil -} - -// validYear validates that year is within halving period range (1-12). -// Returns error if year is outside the valid range. -func validYear(year int64) error { - if year < HALVING_START_YEAR || year > HALVING_END_YEAR { - return makeErrorWithDetails(errInvalidYear, ufmt.Sprintf("year: %d", year)) - } - - return nil -} - -// validEmissionAmount validates that the emission amount does not exceed maximum. -// Returns error if minting the amount would exceed MAX_EMISSION_AMOUNT. -func validEmissionAmount(amount int64) error { - if (amount + MintedEmissionAmount()) > MAX_EMISSION_AMOUNT { - return ufmt.Errorf("too many emission amount: %d", amount) - } - - return nil -} - -// getAdminAddress returns the admin address from access control or default role address. -func getAdminAddress() std.Address { - addr, exists := access.GetAddress(prabc.ROLE_ADMIN.String()) - if !exists { - return rbac.DefaultRoleAddresses[prabc.ROLE_ADMIN] - } - - return addr -} - -// i64Min returns the smaller of two int64 values. -func i64Min(x, y int64) int64 { - if x < y { - return x - } - return y -} - -// formatUint formats unsigned integer types to string. -// Supports uint8, uint32, and uint64. Panics for unsupported types. -func formatUint(v any) string { - switch v := v.(type) { - case uint8: - return strconv.FormatUint(uint64(v), 10) - case uint32: - return strconv.FormatUint(uint64(v), 10) - case uint64: - return strconv.FormatUint(v, 10) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} - -// formatInt formats signed integer types to string. -// Supports int32, int64, and int. Panics for unsupported types. -func formatInt(v any) string { - switch v := v.(type) { - case int32: - return strconv.FormatInt(int64(v), 10) - case int64: - return strconv.FormatInt(v, 10) - case int: - return strconv.Itoa(v) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} diff --git a/contract/r/gnoswap/halt/README.md b/contract/r/gnoswap/halt/README.md deleted file mode 100644 index 20e425b..0000000 --- a/contract/r/gnoswap/halt/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Halt - -Emergency pause mechanism for protocol safety. - -## Overview - -Halt system provides granular control over protocol operations for emergency response and beta safety mode. - -## Configuration - -### Halt Levels - -- **NONE**: All operations enabled (normal operation) -- **SAFE_MODE**: All operations enabled except withdrawals (beta mainnet default) -- **EMERGENCY**: Only governance and withdrawal operations enabled (crisis response) -- **COMPLETE**: All operations disabled (full system halt) - -### Controllable Operations (OpTypes) - -- **pool**: Pool creation and liquidity operations -- **position**: Position NFT minting and management -- **protocol_fee**: Fee collection and distribution -- **router**: Swap routing and execution -- **staker**: Liquidity staking and rewards -- **launchpad**: Token distribution projects -- **governance**: Proposal creation and voting -- **gov_staker**: GNS staking for xGNS -- **xgns**: xGNS token operations -- **community_pool**: Treasury management -- **emission**: GNS emission and distribution -- **withdraw**: Withdrawal operations (LP, rewards, etc.) - -## Key Functions - -### `SetHaltLevel` -Sets system-wide halt level. - -### `SetOperationStatus` -Controls individual operation types. - -### `IsHalted` -Checks if operation is halted. - -## Usage - -```go -// Set system to safe mode (beta mainnet) -SetHaltLevel(HaltLevelSafeMode) - -// Enable emergency mode -SetHaltLevel(HaltLevelEmergency) - -// Halt specific operation -SetOperationStatus(OpTypeRouter, true) - -// Resume specific operation -SetOperationStatus(OpTypeRouter, false) - -// Check before operation -if IsHalted(OpTypeWithdraw) { - panic("withdrawals halted") -} -``` - -## Halt Level Behaviors - -### NONE (Normal Operation) -- All contracts fully operational -- No restrictions applied - -### SAFE_MODE (Beta Mainnet) -- All operations enabled except withdrawals -- Used during initial mainnet launch -- Allows trading but prevents fund extraction - -### EMERGENCY (Crisis Response) -- Only governance and withdrawals enabled -- Allows users to exit positions -- Governance can still execute proposals - -### COMPLETE (Full Halt) -- All operations disabled -- Complete system freeze -- Recovery requires admin/governance action - -## Security - -- Admin/governance control only -- Beta mainnet starts in SAFE_MODE -- Granular operation control -- Event emission for transparency -- Emergency response capability \ No newline at end of file diff --git a/contract/r/gnoswap/halt/assert.gno b/contract/r/gnoswap/halt/assert.gno deleted file mode 100644 index da83f44..0000000 --- a/contract/r/gnoswap/halt/assert.gno +++ /dev/null @@ -1,82 +0,0 @@ -package halt - -// AssertIsNotHalted panics if any of the specified operation types are halted. -// Panics with error if operation type is invalid or any operation is halted. -func AssertIsNotHalted(opTypes ...OpType) { - halted, err := IsHalted(opTypes...) - if err != nil { - panic(err) - } - - if halted { - panic(halted) - } -} - -// AssertIsNotHaltedPool panics if pool operations are halted. -func AssertIsNotHaltedPool() { - AssertIsNotHaltedOperation(OpTypePool) -} - -// AssertIsNotHaltedPosition panics if position operations are halted. -func AssertIsNotHaltedPosition() { - AssertIsNotHaltedOperation(OpTypePosition) -} - -// AssertIsNotHaltedProtocolFee panics if protocol fee operations are halted. -func AssertIsNotHaltedProtocolFee() { - AssertIsNotHaltedOperation(OpTypeProtocolFee) -} - -// AssertIsNotHaltedRouter panics if router operations are halted. -func AssertIsNotHaltedRouter() { - AssertIsNotHaltedOperation(OpTypeRouter) -} - -// AssertIsNotHaltedStaker panics if staker operations are halted. -func AssertIsNotHaltedStaker() { - AssertIsNotHaltedOperation(OpTypeStaker) -} - -// AssertIsNotHaltedLaunchpad panics if launchpad operations are halted. -func AssertIsNotHaltedLaunchpad() { - AssertIsNotHaltedOperation(OpTypeLaunchpad) -} - -// AssertIsNotHaltedGovernance panics if governance operations are halted. -func AssertIsNotHaltedGovernance() { - AssertIsNotHaltedOperation(OpTypeGovernance) -} - -// AssertIsNotHaltedGovStaker panics if governance staker operations are halted. -func AssertIsNotHaltedGovStaker() { - AssertIsNotHaltedOperation(OpTypeGovStaker) -} - -// AssertIsNotHaltedXGns panics if xGNS operations are halted. -func AssertIsNotHaltedXGns() { - AssertIsNotHaltedOperation(OpTypeXGns) -} - -// AssertIsNotHaltedCommunityPool panics if community pool operations are halted. -func AssertIsNotHaltedCommunityPool() { - AssertIsNotHaltedOperation(OpTypeCommunityPool) -} - -// AssertIsNotHaltedEmission panics if emission operations are halted. -func AssertIsNotHaltedEmission() { - AssertIsNotHaltedOperation(OpTypeEmission) -} - -// AssertIsNotHaltedWithdraw panics if withdraw operations are halted. -func AssertIsNotHaltedWithdraw() { - AssertIsNotHaltedOperation(OpTypeWithdraw) -} - -// AssertIsNotHaltedOperation panics if the specified operation type is halted. -// Panics with error details including operation type name. -func AssertIsNotHaltedOperation(op OpType) { - if halted := isHaltedOperation(op); halted { - panic(makeErrorWithDetails(errHalted, op.String())) - } -} diff --git a/contract/r/gnoswap/halt/config.gno b/contract/r/gnoswap/halt/config.gno deleted file mode 100644 index 9b52fce..0000000 --- a/contract/r/gnoswap/halt/config.gno +++ /dev/null @@ -1,122 +0,0 @@ -package halt - -// HaltConfig stores halt state for each operation type. -type HaltConfig map[OpType]bool - -// IsHalted returns true if the specified operation is halted, false otherwise. -// Returns false if operation type is not found in configuration. -func (c HaltConfig) IsHalted(op OpType) bool { - halted, exists := c[op] - if !exists { - return false - } - - return halted -} - -// Clone creates a deep copy of the halt configuration and returns it. -func (c HaltConfig) Clone() HaltConfig { - clone := make(HaltConfig) - - for op, option := range c { - clone[op] = option - } - - return clone -} - -// get retrieves halt state for the specified operation type. -// Returns error if operation type is invalid. -func (c HaltConfig) get(op OpType) (bool, error) { - enabled, exists := c[op] - if !exists { - return false, makeErrorWithDetails(errInvalidOpType, op.String()) - } - - return enabled, nil -} - -// set updates halt state for the specified operation type. -// Returns error if operation type is invalid. -func (c HaltConfig) set(op OpType, enabled bool) error { - if !op.IsValid() { - return makeErrorWithDetails(errInvalidOpType, op.String()) - } - - c[op] = enabled - - return nil -} - -// newNoneConfig creates configuration with all operations enabled (no halts). -func newNoneConfig() HaltConfig { - return HaltConfig{ - OpTypePool: false, - OpTypePosition: false, - OpTypeProtocolFee: false, - OpTypeRouter: false, - OpTypeStaker: false, - OpTypeLaunchpad: false, - OpTypeGovernance: false, - OpTypeGovStaker: false, - OpTypeXGns: false, - OpTypeCommunityPool: false, - OpTypeEmission: false, - OpTypeWithdraw: false, - } -} - -// newSafeModeConfig creates configuration for safe mode with only withdrawals halted. -func newSafeModeConfig() HaltConfig { - return HaltConfig{ - OpTypePool: false, - OpTypePosition: false, - OpTypeProtocolFee: false, - OpTypeRouter: false, - OpTypeStaker: false, - OpTypeLaunchpad: false, - OpTypeGovernance: false, - OpTypeGovStaker: false, - OpTypeXGns: false, - OpTypeCommunityPool: false, - OpTypeEmission: false, - OpTypeWithdraw: true, - } -} - -// newEmergencyConfig creates configuration for emergency mode with most operations halted. -// Only governance and withdraw operations remain enabled for emergency recovery. -func newEmergencyConfig() HaltConfig { - return HaltConfig{ - OpTypePool: true, - OpTypePosition: true, - OpTypeProtocolFee: true, - OpTypeRouter: true, - OpTypeStaker: true, - OpTypeLaunchpad: true, - OpTypeGovernance: false, - OpTypeGovStaker: true, - OpTypeXGns: true, - OpTypeCommunityPool: true, - OpTypeEmission: true, - OpTypeWithdraw: false, - } -} - -// newCompleteConfig creates configuration with all operations halted for complete lockdown. -func newCompleteConfig() HaltConfig { - return HaltConfig{ - OpTypePool: true, - OpTypePosition: true, - OpTypeProtocolFee: true, - OpTypeRouter: true, - OpTypeStaker: true, - OpTypeLaunchpad: true, - OpTypeGovernance: true, - OpTypeGovStaker: true, - OpTypeXGns: true, - OpTypeCommunityPool: true, - OpTypeEmission: true, - OpTypeWithdraw: true, - } -} diff --git a/contract/r/gnoswap/halt/doc.gno b/contract/r/gnoswap/halt/doc.gno deleted file mode 100644 index a3a9f4f..0000000 --- a/contract/r/gnoswap/halt/doc.gno +++ /dev/null @@ -1,2 +0,0 @@ -// Package halt provides functionality for managing protocol halt levels and operations. -package halt diff --git a/contract/r/gnoswap/halt/errors.gno b/contract/r/gnoswap/halt/errors.gno deleted file mode 100644 index 83449dc..0000000 --- a/contract/r/gnoswap/halt/errors.gno +++ /dev/null @@ -1,18 +0,0 @@ -package halt - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errHalted = errors.New("halted") - errInvalidOpType = errors.New("invalid operation type") - errInvalidHaltLevel = errors.New("invalid halt level") -) - -// makeErrorWithDetails creates an error with additional details appended to the base error message. -func makeErrorWithDetails(err error, details string) error { - return ufmt.Errorf("%s: %s", err.Error(), details) -} diff --git a/contract/r/gnoswap/halt/getters.gno b/contract/r/gnoswap/halt/getters.gno deleted file mode 100644 index f68ecec..0000000 --- a/contract/r/gnoswap/halt/getters.gno +++ /dev/null @@ -1,117 +0,0 @@ -package halt - -import ( - "gno.land/p/onbloc/json" -) - -// IsHalted returns true if any of the specified operation types are halted. -// Returns error if any operation type is invalid. -func IsHalted(opTypes ...OpType) (bool, error) { - for _, op := range opTypes { - if !op.IsValid() { - return true, makeErrorWithDetails(errInvalidOpType, op.String()) - } - - halted, err := haltConfig.get(op) - if err != nil { - return true, err - } - - if halted { - return true, nil - } - } - - return false, nil -} - -// GetHaltConfig returns a copy of the current halt configuration. -func GetHaltConfig() HaltConfig { - return haltConfig.Clone() -} - -// GetHaltConfigJson returns the halt configuration as a JSON string. -func GetHaltConfigJson() string { - haltConfig := GetHaltConfig() - - statusNodes := make(map[string]*json.Node) - - for op, halted := range haltConfig { - statusNodes[op.String()] = json.BoolNode(op.String(), halted) - } - - objectNode := json.ObjectNode("status", statusNodes) - - return objectNode.String() -} - -// IsHaltedPool returns true if pool operations are halted. -func IsHaltedPool() bool { - return isHaltedOperation(OpTypePool) -} - -// IsHaltedPosition returns true if position operations are halted. -func IsHaltedPosition() bool { - return isHaltedOperation(OpTypePosition) -} - -// IsHaltedProtocolFee returns true if protocol fee operations are halted. -func IsHaltedProtocolFee() bool { - return isHaltedOperation(OpTypeProtocolFee) -} - -// IsHaltedRouter returns true if router operations are halted. -func IsHaltedRouter() bool { - return isHaltedOperation(OpTypeRouter) -} - -// IsHaltedStaker returns true if staker operations are halted. -func IsHaltedStaker() bool { - return isHaltedOperation(OpTypeStaker) -} - -// IsHaltedLaunchpad returns true if launchpad operations are halted. -func IsHaltedLaunchpad() bool { - return isHaltedOperation(OpTypeLaunchpad) -} - -// IsHaltedGovernance returns true if governance operations are halted. -func IsHaltedGovernance() bool { - return isHaltedOperation(OpTypeGovernance) -} - -// IsHaltedGovStaker returns true if governance staker operations are halted. -func IsHaltedGovStaker() bool { - return isHaltedOperation(OpTypeGovStaker) -} - -// IsHaltedXGns returns true if xGNS operations are halted. -func IsHaltedXGns() bool { - return isHaltedOperation(OpTypeXGns) -} - -// IsHaltedCommunityPool returns true if community pool operations are halted. -func IsHaltedCommunityPool() bool { - return isHaltedOperation(OpTypeCommunityPool) -} - -// IsHaltedEmission returns true if emission operations are halted. -func IsHaltedEmission() bool { - return isHaltedOperation(OpTypeEmission) -} - -// IsHaltedWithdraw returns true if withdraw operations are halted. -func IsHaltedWithdraw() bool { - return isHaltedOperation(OpTypeWithdraw) -} - -// isHaltedOperation returns halt status for the specified operation type. -// Panics if operation type is invalid. -func isHaltedOperation(op OpType) bool { - halted, err := haltConfig.get(op) - if err != nil { - panic(err) - } - - return halted -} diff --git a/contract/r/gnoswap/halt/gnomod.toml b/contract/r/gnoswap/halt/gnomod.toml deleted file mode 100644 index 176cb8a..0000000 --- a/contract/r/gnoswap/halt/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/halt" -gno = "0.9" diff --git a/contract/r/gnoswap/halt/halt.gno b/contract/r/gnoswap/halt/halt.gno deleted file mode 100644 index 9ef6675..0000000 --- a/contract/r/gnoswap/halt/halt.gno +++ /dev/null @@ -1,84 +0,0 @@ -package halt - -import ( - "std" - "strconv" - - "gno.land/r/gnoswap/access" -) - -var haltConfig HaltConfig - -func init() { - haltConfig = newNoneConfig() -} - -// SetHaltLevel sets the global halt level. -// -// Parameters: -// - level: halt level to apply (None, SafeMode, Emergency, Complete) -// -// Only callable by admin or governance. -func SetHaltLevel(cur realm, level HaltLevel) { - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - err := setHaltLevel(level) - if err != nil { - panic(err) - } - - std.Emit( - "SetHaltLevel", - "level", level.String(), - "description", level.Description(), - "caller", caller.String(), - ) -} - -// SetOperationStatus sets halt status for a specific operation. -// -// Parameters: -// - op: operation type -// - halted: true to halt, false to resume -// -// Only callable by admin or governance. -func SetOperationStatus(cur realm, op OpType, halted bool) { - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - if !op.IsValid() { - panic(makeErrorWithDetails(errInvalidOpType, op.String())) - } - - haltConfig.set(op, halted) - - std.Emit( - "SetOperationStatus", - "operation", string(op), - "halted", strconv.FormatBool(halted), - "caller", caller.String(), - ) -} - -// setHaltLevel applies predefined halt level configuration. -func setHaltLevel(level HaltLevel) error { - var config HaltConfig - - switch level { - case HaltLevelNone: - config = newNoneConfig() - case HaltLevelSafeMode: - config = newSafeModeConfig() - case HaltLevelEmergency: - config = newEmergencyConfig() - case HaltLevelComplete: - config = newCompleteConfig() - default: - return makeErrorWithDetails(errInvalidHaltLevel, level.String()) - } - - haltConfig = config - - return nil -} diff --git a/contract/r/gnoswap/halt/types.gno b/contract/r/gnoswap/halt/types.gno deleted file mode 100644 index 3a1a6b6..0000000 --- a/contract/r/gnoswap/halt/types.gno +++ /dev/null @@ -1,89 +0,0 @@ -package halt - -// Halt levels define different states of system operation restriction. -const ( - HaltLevelNone HaltLevel = "NONE" // All operations enabled. - HaltLevelSafeMode HaltLevel = "SAFE_MODE" // All operations enabled except withdrawals. - HaltLevelEmergency HaltLevel = "EMERGENCY" // Only governance and withdrawal operations enabled. - HaltLevelComplete HaltLevel = "COMPLETE" // All operations disabled. -) - -// Operation types representing individual contracts. -const ( - OpTypePool OpType = "pool" - OpTypePosition OpType = "position" - OpTypeProtocolFee OpType = "protocol_fee" - OpTypeRouter OpType = "router" - OpTypeStaker OpType = "staker" - OpTypeLaunchpad OpType = "launchpad" - OpTypeGovernance OpType = "governance" - OpTypeGovStaker OpType = "gov_staker" - OpTypeXGns OpType = "xgns" - OpTypeCommunityPool OpType = "community_pool" - OpTypeEmission OpType = "emission" - OpTypeWithdraw OpType = "withdraw" -) - -var haltLevelDescriptions = map[HaltLevel]string{ - HaltLevelNone: "All operations enabled", - HaltLevelSafeMode: "All operations enabled except withdrawals", - HaltLevelEmergency: "Only governance and withdrawal operations enabled", - HaltLevelComplete: "All operations disabled", -} - -// HaltLevel represents current system halt state. -type HaltLevel string - -// String returns the string representation of the halt level. -func (h HaltLevel) String() string { - return string(h) -} - -// Description returns a human-readable description of the halt level. -func (h HaltLevel) Description() string { - desc, ok := haltLevelDescriptions[h] - if !ok { - return "Unknown halt level" - } - - return desc -} - -// IsValid returns true if the halt level is valid. -func (h HaltLevel) IsValid() bool { - switch h { - case HaltLevelNone, HaltLevelSafeMode, HaltLevelEmergency, HaltLevelComplete: - return true - default: - return false - } -} - -// OpType represents operation types that can be controlled independently. -type OpType string - -// String returns the string representation of the operation type. -func (o OpType) String() string { - return string(o) -} - -// IsValid returns true if the operation type is valid. -func (o OpType) IsValid() bool { - switch o { - case OpTypePool, - OpTypePosition, - OpTypeProtocolFee, - OpTypeRouter, - OpTypeStaker, - OpTypeLaunchpad, - OpTypeGovernance, - OpTypeGovStaker, - OpTypeXGns, - OpTypeCommunityPool, - OpTypeEmission, - OpTypeWithdraw: - return true - default: - return false - } -} diff --git a/contract/r/gnoswap/rbac/README.md b/contract/r/gnoswap/rbac/README.md deleted file mode 100644 index aa52748..0000000 --- a/contract/r/gnoswap/rbac/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# RBAC - -Role-based access control management realm. - -## Overview - -RBAC realm manages role addresses and permissions for the GnoSwap protocol, integrating with the access package. - -## Configuration - -- **Admin Control**: Full role management -- **Dynamic Roles**: Add/remove at runtime -- **Access Integration**: Syncs with access package - -## Key Functions - -### `RegisterRole` - -Registers new role in system. - -### `RemoveRole` - -Removes existing role. - -### `UpdateRoleAddress` - -Updates address for role. - -### `GetRoleAddress` - -Returns address for role. - -### `TransferOwnership` - -Transfers admin role to new address. - -## Usage - -```go -// Register new role -RegisterRole("new_role") - -// Update role address -UpdateRoleAddress("staker", newAddress) - -// Get role address -addr, err := GetRoleAddress("router") - -// Transfer admin ownership -TransferOwnership(newAdmin) -``` - -## Contract Upgrade - -RBAC enables seamless contract upgrades through role address updates. Versioned contracts (with paths like `v1`) can be upgraded by deploying new versions and updating role addresses. - -### Upgrade Process - -1. **Deploy new contract version** (e.g., `v2` contracts) -2. **Update role addresses** to point to new contracts -3. **Verify distribution** flows to new contract addresses - -### Upgradeable Components - -All versioned contracts under `gno.land/r/gnoswap/{version}/` are upgradeable: - -- `pool` - Liquidity pool management -- `position` - Position management -- `router` - Swap routing engine -- `staker` - Staking and rewards -- `governance` - Governance system (governance, staker, xgns) -- `launchpad` - Token launch platform -- `protocol_fee` - Fee collection -- `community_pool` - Community treasury - -### Example: GNS Distribution Upgrade - -```go -// Before upgrade - GNS distributed to v1 contracts -mintAndDistribute() // → v1 staker, devops, community_pool - -// Upgrade process - update role addresses -rbac.UpdateRoleAddress("staker", newV2StakerAddr) -rbac.UpdateRoleAddress("devops", newV2DevOpsAddr) -rbac.UpdateRoleAddress("community_pool", newV2CommunityPoolAddr) - -// After upgrade - GNS distributed to v2 contracts -mintAndDistribute() // → v2 staker, devops, community_pool -``` - -This approach ensures zero-downtime upgrades with atomic role address switches, maintaining protocol continuity while enabling feature updates and bug fixes. - -### Test Example - -The upgrade mechanism is demonstrated in the test file: -[upgrade scenario test](./../../../../tests/scenario/upgrade/change_gns_distribution_target_filetest.gno) - -```go -// Test scenario steps: -// 1. Initialize emission and mint GNS to v1 contracts -// 2. Update role addresses to point to v2 contracts -// 3. Verify GNS now flows to v2 contracts - -func changeDistributionTarget() { - // Update all role addresses atomically - rbac.UpdateRoleAddress("staker", newStakerAddr) - rbac.UpdateRoleAddress("gov_staker", newGovStakerAddr) - rbac.UpdateRoleAddress("devops", newDevOpsAddr) - rbac.UpdateRoleAddress("community_pool", newCommunityPoolAddr) -} -``` - -The test validates that after role updates, GNS distribution switches from v1 to v2 contracts without any protocol downtime or loss of funds. - -## Security - -- Admin-only role management -- Synchronized with access package -- Ownership transfer capability -- Role validation before updates diff --git a/contract/r/gnoswap/rbac/assert.gno b/contract/r/gnoswap/rbac/assert.gno deleted file mode 100644 index 7d8c8a1..0000000 --- a/contract/r/gnoswap/rbac/assert.gno +++ /dev/null @@ -1,51 +0,0 @@ -package rbac - -import ( - "std" - - "gno.land/p/nt/ufmt" - - prbac "gno.land/p/gnoswap/rbac" -) - -// assertIsOwner panics if addr is not the current owner. -func assertIsOwner(addr std.Address) { - if manager.Owner() != addr { - panic(makeErrorWithDetails( - errCallerIsNotOwner, - ufmt.Sprintf("caller: %s", addr.String()), - )) - } -} - -// assertIsPendingOwner panics if addr is not the pending owner. -func assertIsPendingOwner(addr std.Address) { - if manager.PendingOwner() != addr { - panic(makeErrorWithDetails( - errCallerIsNotPendingOwner, - ufmt.Sprintf("caller: %s", addr.String()), - )) - } -} - -// assertIsAdmin panics if addr is not authorized for admin role. -func assertIsAdmin(addr std.Address) { - if !manager.IsAuthorized(prbac.ROLE_ADMIN.String(), addr) { - panic( - makeErrorWithDetails( - errCallerIsNotAdmin, - ufmt.Sprintf("caller: %s", addr.String()), - ), - ) - } -} - -// assertIsValidRoleName panics if roleName is invalid (empty). -func assertIsValidRoleName(roleName string) { - if roleName == "" { - panic(makeErrorWithDetails( - errInvalidRoleName, - ufmt.Sprintf("role name: %s", roleName), - )) - } -} diff --git a/contract/r/gnoswap/rbac/consts.gno b/contract/r/gnoswap/rbac/consts.gno deleted file mode 100644 index 2f4f54e..0000000 --- a/contract/r/gnoswap/rbac/consts.gno +++ /dev/null @@ -1,40 +0,0 @@ -package rbac - -import "std" - -// Initial addresses for protocol roles. -const ( - // ADMIN is the initial admin address for RBAC management. - ADMIN std.Address = "g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42" - // DEV_OPS is the initial DevOps address for operational tasks. - DEV_OPS std.Address = "g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42" -) - -// Derived addresses for GnoSwap protocol packages. -var ( - // GNS_ADDR is the derived address for the GNS token package. - GNS_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/gns") - // EMISSION_ADDR is the derived address for the emission package. - EMISSION_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/emission") - - // POOL_ADDR is the derived address for the pool package. - POOL_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/pool") - // POSITION_ADDR is the derived address for the position package. - POSITION_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/position") - // ROUTER_ADDR is the derived address for the router package. - ROUTER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/router") - // STAKER_ADDR is the derived address for the staker package. - STAKER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/staker") - // PROTOCOL_FEE_ADDR is the derived address for the protocol fee package. - PROTOCOL_FEE_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/protocol_fee") - // COMMUNITY_POOL_ADDR is the derived address for the community pool package. - COMMUNITY_POOL_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/community_pool") - // GOV_GOVERNANCE_ADDR is the derived address for the governance package. - GOV_GOVERNANCE_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/governance") - // GOV_STAKER_ADDR is the derived address for the governance staker package. - GOV_STAKER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/staker") - // GOV_XGNS_ADDR is the derived address for the xGNS governance package. - GOV_XGNS_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/xgns") - // LAUNCHPAD_ADDR is the derived address for the launchpad package. - LAUNCHPAD_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/launchpad") -) diff --git a/contract/r/gnoswap/rbac/emit.gno b/contract/r/gnoswap/rbac/emit.gno deleted file mode 100644 index 889f24e..0000000 --- a/contract/r/gnoswap/rbac/emit.gno +++ /dev/null @@ -1,39 +0,0 @@ -package rbac - -import "std" - -// emitRegisterRoleEvent emits a RegisterRole event with roleName information. -func emitRegisterRoleEvent(roleName string) { - prevRealm := std.PreviousRealm() - std.Emit( - "RegisterRole", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "roleName", roleName, - "roleAddress", "", - ) -} - -// emitRemoveRoleEvent emits a RemoveRole event with roleName information. -func emitRemoveRoleEvent(roleName string) { - prevRealm := std.PreviousRealm() - std.Emit( - "RemoveRole", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "roleName", roleName, - "roleAddress", "", - ) -} - -// emitUpdateRoleAddressEvent emits an UpdateRoleAddress event with roleName and address information. -func emitUpdateRoleAddressEvent(roleName string, address std.Address) { - prevRealm := std.PreviousRealm() - std.Emit( - "UpdateRoleAddress", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "roleName", roleName, - "roleAddress", address.String(), - ) -} diff --git a/contract/r/gnoswap/rbac/errors.gno b/contract/r/gnoswap/rbac/errors.gno deleted file mode 100644 index 6e9f5bb..0000000 --- a/contract/r/gnoswap/rbac/errors.gno +++ /dev/null @@ -1,20 +0,0 @@ -package rbac - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errCallerIsNotOwner = errors.New("caller is not owner") - errCallerIsNotAdmin = errors.New("caller is not admin") - errCallerIsNotPendingOwner = errors.New("caller is not pending owner") - errInvalidAddress = errors.New("invalid address") - errInvalidRoleName = errors.New("invalid role name") -) - -// makeErrorWithDetails combines an error with additional details. -func makeErrorWithDetails(err error, details string) error { - return ufmt.Errorf("%s || %s", err.Error(), details) -} diff --git a/contract/r/gnoswap/rbac/gnomod.toml b/contract/r/gnoswap/rbac/gnomod.toml deleted file mode 100644 index cf30f83..0000000 --- a/contract/r/gnoswap/rbac/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/rbac" -gno = "0.9" diff --git a/contract/r/gnoswap/rbac/ownership.gno b/contract/r/gnoswap/rbac/ownership.gno deleted file mode 100644 index 8239720..0000000 --- a/contract/r/gnoswap/rbac/ownership.gno +++ /dev/null @@ -1,45 +0,0 @@ -package rbac - -import "std" - -// IsOwner returns true if addr is the current owner. -func IsOwner(addr std.Address) bool { - return manager.Owner() == addr -} - -// IsPendingOwner returns true if addr is the pending owner. -func IsPendingOwner(addr std.Address) bool { - return manager.PendingOwner() == addr -} - -// GetOwner returns the current owner address. -func GetOwner() std.Address { - return manager.Owner() -} - -// GetPendingOwner returns the pending owner address. -func GetPendingOwner() std.Address { - return manager.PendingOwner() -} - -// AcceptOwnership completes the ownership transfer process. -// Only callable by pending owner. -func AcceptOwnership(cur realm) { - err := manager.AcceptOwnership() - if err != nil { - panic(err) - } -} - -// TransferOwnership initiates the ownership transfer process. -// -// Parameters: -// - addr: address to transfer ownership to -// -// Only callable by current owner. -func TransferOwnership(cur realm, addr std.Address) { - err := manager.TransferOwnership(addr) - if err != nil { - panic(err) - } -} diff --git a/contract/r/gnoswap/rbac/rbac.gno b/contract/r/gnoswap/rbac/rbac.gno deleted file mode 100644 index b6b846f..0000000 --- a/contract/r/gnoswap/rbac/rbac.gno +++ /dev/null @@ -1,112 +0,0 @@ -package rbac - -import ( - "std" - - "gno.land/r/gnoswap/access" - - "gno.land/p/nt/ufmt" - prbac "gno.land/p/gnoswap/rbac" -) - -var manager *prbac.RBAC - -func init() { - initRbac() -} - -// initRbac initializes RBAC manager with default admin and role mappings. -func initRbac() { - manager = prbac.NewRBACWithAddress(ADMIN) - - for role, addr := range DefaultRoleAddresses { - manager.RegisterRole(role.String()) - manager.UpdateRoleAddress(role.String(), addr) - } - - updateAccessRoleAddresses(manager.GetRoleAddresses()) -} - -// RegisterRole registers a new role in the RBAC system. -// -// Parameters: -// - roleName: name of the role to register -// -// Only callable by admin or governance. -func RegisterRole(cur realm, roleName string) { - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsValidRoleName(roleName) - - err := manager.RegisterRole(roleName) - if err != nil { - panic(makeErrorWithDetails( - err, - ufmt.Sprintf("role name: %s", roleName), - )) - } - - updateAccessRoleAddresses(manager.GetRoleAddresses()) - - emitRegisterRoleEvent(roleName) -} - -// RemoveRole removes a role from the RBAC system. -// -// Parameters: -// - roleName: name of the role to remove -// -// Only callable by admin or governance. -func RemoveRole(cur realm, roleName string) { - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsValidRoleName(roleName) - - err := manager.RemoveRole(roleName) - if err != nil { - panic(makeErrorWithDetails( - err, - ufmt.Sprintf("role name: %s", roleName), - )) - } - - updateAccessRoleAddresses(manager.GetRoleAddresses()) - - emitRemoveRoleEvent(roleName) -} - -// GetRoleAddress returns the address assigned to roleName. -func GetRoleAddress(roleName string) (std.Address, error) { - return manager.GetRoleAddress(roleName) -} - -// UpdateRoleAddress updates the address assigned to a role. -// -// Parameters: -// - roleName: name of the role -// - addr: new address for the role -// -// Only callable by admin or governance. -func UpdateRoleAddress(cur realm, roleName string, addr std.Address) { - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - err := manager.UpdateRoleAddress(roleName, addr) - if err != nil { - panic(makeErrorWithDetails( - err, - ufmt.Sprintf("role name: %s, address: %s", roleName, addr.String()), - )) - } - - updateAccessRoleAddresses(manager.GetRoleAddresses()) - - emitUpdateRoleAddressEvent(roleName, addr) -} - -// updateAccessRoleAddresses synchronizes role addresses with the access package. -func updateAccessRoleAddresses(newRoleAddresses map[string]std.Address) { - access.SetRoleAddresses(cross, newRoleAddresses) -} diff --git a/contract/r/gnoswap/rbac/role.gno b/contract/r/gnoswap/rbac/role.gno deleted file mode 100644 index a7586d2..0000000 --- a/contract/r/gnoswap/rbac/role.gno +++ /dev/null @@ -1,25 +0,0 @@ -package rbac - -import ( - "std" - - prbac "gno.land/p/gnoswap/rbac" -) - -// DefaultRoleAddresses maps system roles to their default addresses. -// Used during RBAC initialization to set up the protocol role structure. -var DefaultRoleAddresses = map[prbac.SystemRole]std.Address{ - prbac.ROLE_ADMIN: ADMIN, - prbac.ROLE_DEVOPS: DEV_OPS, - prbac.ROLE_COMMUNITY_POOL: COMMUNITY_POOL_ADDR, - prbac.ROLE_GOVERNANCE: GOV_GOVERNANCE_ADDR, - prbac.ROLE_GOV_STAKER: GOV_STAKER_ADDR, - prbac.ROLE_XGNS: GOV_XGNS_ADDR, - prbac.ROLE_POOL: POOL_ADDR, - prbac.ROLE_POSITION: POSITION_ADDR, - prbac.ROLE_ROUTER: ROUTER_ADDR, - prbac.ROLE_STAKER: STAKER_ADDR, - prbac.ROLE_EMISSION: EMISSION_ADDR, - prbac.ROLE_LAUNCHPAD: LAUNCHPAD_ADDR, - prbac.ROLE_PROTOCOL_FEE: PROTOCOL_FEE_ADDR, -} diff --git a/contract/r/gnoswap/referral/README.md b/contract/r/gnoswap/referral/README.md deleted file mode 100644 index 124415a..0000000 --- a/contract/r/gnoswap/referral/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Referral - -Referral system for tracking user relationships. - -## Overview - -Manages referral relationships between users with cooldown periods to prevent gaming. - -## Key Functions - -### `TryRegister` -Attempts to register referral relationship. - -### `Register` -Registers new referral (panics if exists). - -### `UpdateReferral` -Changes referral address after cooldown. - -### `DeleteReferral` -Removes referral relationship. - -### `GetReferral` -Returns referral for address. - -## Usage - -```go -// Register referral -success := TryRegister(user, referrer) - -// Update after cooldown -UpdateReferral(newReferrer) - -// Query referral -referrer := GetReferral(userAddress) - -// Remove referral -DeleteReferral() -``` - -## Security - -- One referral per address -- 24-hour change cooldown -- No self-referrals -- Immutable during cooldown \ No newline at end of file diff --git a/contract/r/gnoswap/referral/doc.gno b/contract/r/gnoswap/referral/doc.gno deleted file mode 100644 index 3f526af..0000000 --- a/contract/r/gnoswap/referral/doc.gno +++ /dev/null @@ -1,109 +0,0 @@ -// Package referral implements a referral system on Gno. It allows -// any authorized caller to register, update, or remove referral -// information. A referral link is defined as a mapping from one -// address (the "user") to another address (the "referrer"). -// -// ## Overview -// -// The referral package is composed of the following components: -// -// 1. **errors.gno**: Provides custom error types (ReferralError) with -// specific error codes and messages. -// 2. **utils.gno**: Contains utility functions for permission checks, -// especially isValidCaller, which ensures only specific, pre-authorized -// callers (e.g., governance or router addresses) can invoke the core -// functions. -// 3. **types.gno**: Defines core constants for event types, attributes, -// and the ReferralKeeper interface, which outlines the fundamental -// methods of the referral system (Register, Update, Remove, etc.). -// 4. **keeper.gno**: Implements the actual business logic behind the -// ReferralKeeper interface. It uses an AVL Tree (avl.Tree) to store -// referral data (address -> referrer). The keeper methods emit events -// when a new referral is registered, updated, or removed. -// 5. **referral.gno**: Exposes a public API (the Referral struct) -// that delegates to the keeper, providing external contracts or -// applications a straightforward way to interact with the system. -// -// ## Workflow -// -// Typical usage of this contract follows these steps: -// -// 1. A caller with valid permissions invokes Register, Update, or Remove -// through the Referral struct. -// 2. The Referral struct forwards the request to the internal keeper -// methods. -// 3. The keeper checks caller permission (via isValidCaller), validates -// addresses, and stores or removes data in the AVL Tree. -// 4. An event is emitted for off-chain or cross-module notifications. -// -// ## Integration with Other Contracts -// -// Other contracts can leverage the referral system in two major ways: -// -// 1. **Direct Calls**: If you wish to directly call this contract, -// instantiate the Referral object (via NewReferral) and invoke its -// methods, assuming you meet the authorized-caller criteria. -// -// 2. **Embedded or Extended**: If you have a complex module that includes -// referral features, import this package and embed a Referral instance -// in your own keeper. This way, you can handle additional validations -// or custom logic before delegating to the existing referral functions. -// -// ## Error Handling -// -// The package defines several error types through ReferralError: -// - `ErrInvalidAddress`: Returned when an address format is invalid -// - `ErrUnauthorized`: Returned when the caller lacks permission -// - `ErrNotFound`: Returned when attempting to get a non-existent referral -// - `ErrZeroAddress`: Returned when attempting operations with zero address -// -// ## Example: Integration with a Staking Contract -// -// Suppose you have a staking contract that wants to reward referrers -// when a new user stakes tokens: -// -// ```go -// -// import ( -// "std" -// "gno.land/r/gnoswap/referral" -// "gno.land/p/demo/mystaking" // example staking contract -// ) -// -// func rewardReferrerOnStake(user std.Address, amount int) { -// // 1) Access the referral system -// r := referral.NewReferral() -// -// // 2) Get the user's referrer -// refAddr, err := r.GetReferral(user) -// if err != nil { -// // handle error or skip if not found -// return -// } -// -// // 3) Reward the referrer -// mystaking.AddReward(refAddr, calculateReward(amount)) -// } -// -// ``` -// -// In this simple example, the staking contract checks if the user has -// a referrer by calling `GetReferral`. If a referrer is found, it then -// calculates a reward based on the staked amount. -// -// ## Limitations and Constraints -// -// - A user can have only one referrer at a time -// - Once a referral is removed, it cannot be automatically restored -// - Only authorized contracts can modify referral relationships -// - Address validation is strict and requires proper Bech32 format -// -// # Notes -// -// - The contract strictly enforces caller restrictions via isValidCaller. -// Make sure to configure it to permit only the addresses or roles that -// should be able to register or update referrals. -// - Zero addresses are treated as a trigger for removing a referral record. -// - The system emits events (register_referral, update_referral, remove_referral) -// which can be consumed by other on-chain or off-chain services. -package referral diff --git a/contract/r/gnoswap/referral/errors.gno b/contract/r/gnoswap/referral/errors.gno deleted file mode 100644 index 480886c..0000000 --- a/contract/r/gnoswap/referral/errors.gno +++ /dev/null @@ -1,16 +0,0 @@ -package referral - -import ( - "errors" -) - -var ( - ErrInvalidAddress = errors.New("invalid address format") - ErrZeroAddress = errors.New("zero address is not allowed") - ErrSelfReferral = errors.New("self referral is not allowed") - ErrUnauthorized = errors.New("unauthorized caller") - ErrInvalidCaller = errors.New("invalid caller") - ErrCyclicReference = errors.New("cyclic reference is not allowed") - ErrTooManyRequests = errors.New("too many requests: operations allowed once per 24 hours for each address") - ErrNotFound = errors.New("referral not found") -) diff --git a/contract/r/gnoswap/referral/global_keeper.gno b/contract/r/gnoswap/referral/global_keeper.gno deleted file mode 100644 index 73a27f9..0000000 --- a/contract/r/gnoswap/referral/global_keeper.gno +++ /dev/null @@ -1,72 +0,0 @@ -package referral - -import "std" - -var gReferralKeeper ReferralKeeper - -const ( - EventReferralInvalid = "ReferralInvalid" - EventRegisterFailed = "ReferralRegistrationFailed" - EventRegisterSuccess = "ReferralRegistrationSuccess" -) - -func init() { - if gReferralKeeper == nil { - gReferralKeeper = NewKeeper() - } -} - -// getKeeper returns the global referral keeper instance. -func getKeeper() ReferralKeeper { - return gReferralKeeper -} - -// GetReferral returns the referral address for the given address. -func GetReferral(addr string) string { - referral, err := gReferralKeeper.get(std.Address(addr)) - if err != nil { - return "" - } - return referral.String() -} - -// HasReferral returns true if the given address has a referral. -func HasReferral(addr string) bool { - referral, err := gReferralKeeper.get(std.Address(addr)) - if err != nil { - return false - } - return referral != zeroAddress -} - -// IsEmpty returns true if no referrals exist in the system. -func IsEmpty() bool { - return gReferralKeeper.isEmpty() -} - -// TryRegister attempts to register a new referral. -// -// Parameters: -// - addr: address to register -// - referral: referral address string -// -// Returns true on success, false on failure. -func TryRegister(cur realm, addr std.Address, referral string) bool { - refAddr := std.Address(referral) - err := gReferralKeeper.register(addr, refAddr) - if err != nil { - std.Emit( - EventRegisterFailed, - "address", addr.String(), - "error", err.Error(), - ) - return false - } - - std.Emit( - EventRegisterSuccess, - "address", addr.String(), - "referral", referral, - ) - return true -} diff --git a/contract/r/gnoswap/referral/gnomod.toml b/contract/r/gnoswap/referral/gnomod.toml deleted file mode 100644 index 93123cf..0000000 --- a/contract/r/gnoswap/referral/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/referral" -gno = "0.9" diff --git a/contract/r/gnoswap/referral/keeper.gno b/contract/r/gnoswap/referral/keeper.gno deleted file mode 100644 index 14ab417..0000000 --- a/contract/r/gnoswap/referral/keeper.gno +++ /dev/null @@ -1,176 +0,0 @@ -package referral - -import ( - "std" - "time" - - "gno.land/p/nt/avl" -) - -const ( - // MinTimeBetweenUpdates is minimum duration between operations (24 hours). - MinTimeBetweenUpdates int64 = 24 * 60 * 60 -) - -// keeper implements ReferralKeeper using AVL tree storage. -// It includes rate limiting to prevent abuse. -type keeper struct { - store *avl.Tree - lastOps map[string]int64 -} - -var _ ReferralKeeper = &keeper{} - -// NewKeeper creates a new ReferralKeeper instance. -func NewKeeper() ReferralKeeper { - return &keeper{ - store: avl.NewTree(), - lastOps: make(map[string]int64), - } -} - -// register creates a new referral relationship between addresses. -// Caller must have valid permissions. -func (k *keeper) register(addr, refAddr std.Address) error { - return k.setReferral(addr, refAddr, EventTypeRegister) -} - -// update modifies an existing referral address. -// Caller must have valid permissions. -func (k *keeper) update(addr, newRefAddr std.Address) error { - return k.setReferral(addr, newRefAddr, EventTypeUpdate) -} - -// setReferral handles common logic for registering and updating referrals. -// Setting refAddr to zero address removes the referral. -// Rate limiting applies to prevent abuse. -func (k *keeper) setReferral(addr, refAddr std.Address, eventType string) error { - if err := isValidCaller(std.PreviousRealm().Address()); err != nil { - return err - } - - if err := k.validateAddresses(addr, refAddr); err != nil { - return err - } - - addrStr := addr.String() - refAddrStr := refAddr.String() - - if refAddr == zeroAddress { - if k.has(addr) { - _, ok := k.store.Remove(addrStr) - if !ok { - return ErrNotFound - } - } - return nil - } - - if err := k.checkRateLimit(addrStr); err != nil { - return err - } - - k.store.Set(addrStr, refAddrStr) - k.lastOps[addrStr] = time.Now().Unix() - - std.Emit( - eventType, - "myAddress", addrStr, - "refAddress", refAddrStr, - ) - - return nil -} - -// validateAddresses validates that addresses are properly formatted and not self-referencing. -func (k *keeper) validateAddresses(addr, refAddr std.Address) error { - if !addr.IsValid() || (refAddr != zeroAddress && !refAddr.IsValid()) { - return ErrInvalidAddress - } - if addr == refAddr { - return ErrSelfReferral - } - return nil -} - -// remove deletes a referral relationship. -// Caller must have valid permissions and rate limiting applies. -func (k *keeper) remove(target std.Address) error { - if err := isValidCaller(std.PreviousRealm().Address()); err != nil { - return err - } - - if err := k.validateAddresses(target, zeroAddress); err != nil { - return err - } - - tgt := target.String() - - if err := k.checkRateLimit(tgt); err != nil { - return err - } - - _, ok := k.store.Remove(tgt) - if !ok { - return ErrNotFound - } - - std.Emit( - EventTypeRemove, - "removedAddress", tgt, - ) - - return nil -} - -// has returns true if a referral exists for the given address. -func (k *keeper) has(addr std.Address) bool { - _, exists := k.store.Get(addr.String()) - return exists -} - -// get retrieves the referral address for a given address. -// Returns ErrNotFound if no referral exists. -func (k *keeper) get(addr std.Address) (std.Address, error) { - if !addr.IsValid() { - return zeroAddress, ErrInvalidAddress - } - - val, ok := k.store.Get(addr.String()) - if !ok { - return zeroAddress, ErrNotFound - } - - refAddr, ok := val.(string) - if !ok { - return zeroAddress, ErrInvalidAddress - } - - return std.Address(refAddr), nil -} - -// isEmpty returns true if no referrals exist in the store. -func (k *keeper) isEmpty() bool { - empty := true - k.store.Iterate("", "", func(key string, value any) bool { - empty = false - return true // stop iteration on first item - }) - return empty -} - -// checkRateLimit verifies if enough time has passed since the last operation. -// Returns ErrTooManyRequests if rate limit is exceeded. -func (k *keeper) checkRateLimit(addr string) error { - now := time.Now().Unix() - - if lastOpTime, exists := k.lastOps[addr]; exists { - timeSinceLastOp := now - lastOpTime - - if timeSinceLastOp < MinTimeBetweenUpdates { - return ErrTooManyRequests - } - } - - return nil -} diff --git a/contract/r/gnoswap/referral/referral.gno b/contract/r/gnoswap/referral/referral.gno deleted file mode 100644 index a7f9763..0000000 --- a/contract/r/gnoswap/referral/referral.gno +++ /dev/null @@ -1,56 +0,0 @@ -package referral - -import ( - "std" -) - -// Referral manages referral relationships between addresses. -type Referral struct { - keeper ReferralKeeper -} - -// NewReferral creates a new Referral instance. -func NewReferral() *Referral { - if gReferralKeeper == nil { - gReferralKeeper = NewKeeper() - } - return &Referral{ - keeper: gReferralKeeper, - } -} - -// Register creates a new referral relationship. -// -// Parameters: -// - addr: address to register -// - refAddr: referral address -func (r *Referral) Register(addr, refAddr std.Address) error { - return r.keeper.register(addr, refAddr) -} - -// Update modifies an existing referral relationship. -// -// Parameters: -// - addr: address to update -// - newAddr: new referral address -func (r *Referral) Update(addr, newAddr std.Address) error { - return r.keeper.update(addr, newAddr) -} - -// Remove deletes a referral relationship. -// -// Parameters: -// - addr: address to remove -func (r *Referral) Remove(addr std.Address) error { - return r.keeper.remove(addr) -} - -// Has returns true if a referral exists for the given address. -func (r *Referral) Has(addr std.Address) bool { - return r.keeper.has(addr) -} - -// Get retrieves the referral address for the given address. -func (r *Referral) Get(addr std.Address) (std.Address, error) { - return r.keeper.get(addr) -} diff --git a/contract/r/gnoswap/referral/type.gno b/contract/r/gnoswap/referral/type.gno deleted file mode 100644 index 38b4db4..0000000 --- a/contract/r/gnoswap/referral/type.gno +++ /dev/null @@ -1,33 +0,0 @@ -package referral - -import "std" - -var zeroAddress = std.Address("") - -// Event types for referral actions. -const ( - EventTypeRegister = "RegisterReferral" - EventTypeUpdate = "UpdateReferral" - EventTypeRemove = "RemoveReferral" -) - -// ReferralKeeper defines the interface for managing referral relationships. -type ReferralKeeper interface { - // register creates a new referral relationship between addresses. - register(addr, refAddr std.Address) error - - // update modifies an existing referral address. - update(addr, newRefAddr std.Address) error - - // remove deletes a referral relationship. - remove(addr std.Address) error - - // has returns true if a referral exists for the given address. - has(addr std.Address) bool - - // get retrieves the referral address for a given address. - get(addr std.Address) (std.Address, error) - - // isEmpty returns true if no referrals exist in the system. - isEmpty() bool -} diff --git a/contract/r/gnoswap/referral/utils.gno b/contract/r/gnoswap/referral/utils.gno deleted file mode 100644 index 336ca28..0000000 --- a/contract/r/gnoswap/referral/utils.gno +++ /dev/null @@ -1,38 +0,0 @@ -package referral - -import ( - "std" - - "gno.land/p/nt/ufmt" - - prabc "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoswap/access" - _ "gno.land/r/gnoswap/rbac" -) - -// validCallerRoles is a list of roles that are authorized to modify referral data. -// This includes governance contracts, router, position manager, and staker contracts. -var validCallerRoles = []string{ - prabc.ROLE_GOVERNANCE.String(), - prabc.ROLE_GOV_STAKER.String(), - prabc.ROLE_ROUTER.String(), - prabc.ROLE_POSITION.String(), - prabc.ROLE_STAKER.String(), - prabc.ROLE_LAUNCHPAD.String(), -} - -// isValidCaller checks if the caller address has permission to modify referral data. -// Only addresses with specific roles defined in validCallerRoles are authorized. -// Returns an error if the caller is not authorized. -func isValidCaller(caller std.Address) error { - roleAddresses := access.GetRoleAddresses() - - for _, role := range validCallerRoles { - if roleAddresses[role] == caller { - return nil - } - } - - return ufmt.Errorf("unauthorized caller: %s", caller) -} diff --git a/contract/r/gnoswap/v1/common/consts.gno b/contract/r/gnoswap/v1/common/consts.gno deleted file mode 100644 index 55a64ee..0000000 --- a/contract/r/gnoswap/v1/common/consts.gno +++ /dev/null @@ -1,7 +0,0 @@ -package common - -// Tick bounds. -const ( - minTick = -887272 - maxTick = 887272 -) diff --git a/contract/r/gnoswap/v1/common/doc.gno b/contract/r/gnoswap/v1/common/doc.gno deleted file mode 100644 index 52e8c26..0000000 --- a/contract/r/gnoswap/v1/common/doc.gno +++ /dev/null @@ -1,6 +0,0 @@ -// Package common provides shared utilities for GnoSwap v1 contracts. -// -// This package contains core mathematical functions and helpers used across -// the protocol, including tick math for price calculations, liquidity math -// for position management, and GRC20 registry integration. -package common diff --git a/contract/r/gnoswap/v1/common/errors.gno b/contract/r/gnoswap/v1/common/errors.gno deleted file mode 100644 index 8c8783f..0000000 --- a/contract/r/gnoswap/v1/common/errors.gno +++ /dev/null @@ -1,25 +0,0 @@ -package common - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errNoPermission = errors.New("[GNOSWAP-COMMON-001] caller has no permission") - errHalted = errors.New("[GNOSWAP-COMMON-002] halted") - errOutOfRange = errors.New("[GNOSWAP-COMMON-003] value out of range") - errNotRegistered = errors.New("[GNOSWAP-COMMON-004] token is not registered") - errInvalidAddr = errors.New("[GNOSWAP-COMMON-005] invalid address") - errOverflow = errors.New("[GNOSWAP-COMMON-006] overflow") - errInvalidPositionId = errors.New("[GNOSWAP-COMMON-007] invalid positionId") - errInvalidInput = errors.New("[GNOSWAP-COMMON-008] invalid input data") - errOverFlow = errors.New("[GNOSWAP-COMMON-009] overflow") - errIdenticalTicks = errors.New("[GNOSWAP-COMMON-010] identical ticks") -) - -// newErrorWithDetail creates an error message with additional context in format " || ". -func newErrorWithDetail(err error, detail string) string { - return ufmt.Errorf("%s || %s", err.Error(), detail).Error() -} diff --git a/contract/r/gnoswap/v1/common/gnomod.toml b/contract/r/gnoswap/v1/common/gnomod.toml deleted file mode 100644 index 91da871..0000000 --- a/contract/r/gnoswap/v1/common/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/common" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/common/grc20reg_helper.gno b/contract/r/gnoswap/v1/common/grc20reg_helper.gno deleted file mode 100644 index 3235641..0000000 --- a/contract/r/gnoswap/v1/common/grc20reg_helper.gno +++ /dev/null @@ -1,86 +0,0 @@ -package common - -import ( - "regexp" - "std" - "strings" - - "gno.land/p/demo/tokens/grc20" - "gno.land/p/nt/ufmt" - "gno.land/r/demo/defi/grc20reg" -) - -var re = regexp.MustCompile(`\[gno\.land/r/[^\]]+\]`) - -// GetToken returns a grc20.Token instance for the specified path, panicking if not registered. -func GetToken(path string) *grc20.Token { - return grc20reg.MustGet(path) -} - -// GetTokenTeller returns a grc20.Teller instance for the specified path, panicking if not registered. -func GetTokenTeller(path string) grc20.Teller { - return GetToken(path).CallerTeller() -} - -// IsRegistered checks if a token is registered in grc20reg, returning nil if registered or error if not. -func IsRegistered(path string) error { - getter := grc20reg.Get(path) - if getter == nil { - return ufmt.Errorf("token(%s) is not registered to grc20reg", path) - } - return nil -} - -// MustRegistered checks if all provided tokens are registered, panicking if any is not registered. -func MustRegistered(paths ...string) { - for _, path := range paths { - if err := IsRegistered(path); err != nil { - panic(newErrorWithDetail( - errNotRegistered, - ufmt.Sprintf("token(%s)", path), - )) - } - } -} - -// extractTokenPathsFromRender extracts token paths from rendered grc20reg output. -func extractTokenPathsFromRender(render string) []string { - matches := re.FindAllString(render, -1) - - tokenPaths := make([]string, 0, len(matches)) - for _, match := range matches { - tokenPath := strings.Trim(match, "[]") // Remove the brackets - tokenPaths = append(tokenPaths, tokenPath) - } - return tokenPaths -} - -// TotalSupply returns the total supply of the specified token. -func TotalSupply(path string) int64 { - return GetToken(path).TotalSupply() -} - -// BalanceOf returns the token balance for the specified address. -func BalanceOf(path string, addr std.Address) int64 { - return GetToken(path).BalanceOf(addr) -} - -// Allowance returns the token allowance from owner to spender. -func Allowance(path string, owner, spender std.Address) int64 { - return GetToken(path).Allowance(owner, spender) -} - -// Transfer transfers tokens to the specified address using grc20.Teller.Transfer. -func Transfer(cur realm, path string, to std.Address, amount int64) error { - return GetTokenTeller(path).Transfer(to, amount) -} - -// TransferFrom transfers tokens from one address to another using grc20.Teller.TransferFrom. -func TransferFrom(cur realm, path string, from, to std.Address, amount int64) error { - return GetTokenTeller(path).TransferFrom(from, to, amount) -} - -// Approve approves tokens for the specified spender using grc20.Teller.Approve. -func Approve(cur realm, path string, spender std.Address, amount int64) error { - return GetTokenTeller(path).Approve(spender, amount) -} diff --git a/contract/r/gnoswap/v1/common/liquidity_amounts.gno b/contract/r/gnoswap/v1/common/liquidity_amounts.gno deleted file mode 100644 index cce5e6d..0000000 --- a/contract/r/gnoswap/v1/common/liquidity_amounts.gno +++ /dev/null @@ -1,332 +0,0 @@ -package common - -import ( - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" -) - -const ( - Q96_RESOLUTION = 96 - Q128_RESOLUTION = 128 - MAX_UINT128 = "340282366920938463463374607431768211455" // 2^128 - 1 - Q96 = "79228162514264337593543950336" // 2^96 -) - -var ( - maxUint128 = u256.MustFromDecimal(MAX_UINT128) - q96Uint = u256.MustFromDecimal(Q96) - q128Mask = func() *u256.Uint { - mask := u256.Zero().Lsh(u256.One(), Q128_RESOLUTION) - mask = u256.Zero().Sub(mask, u256.One()) - return mask - }() - // only used for return value - zero = u256.Zero() -) - -// toAscendingOrder returns the two values in ascending order. -func toAscendingOrder(a, b *u256.Uint) (*u256.Uint, *u256.Uint) { - if a.Gt(b) { - return b, a - } - - return a, b -} - -// toUint128 ensures the value fits within uint128 range. -// -// Validates and constrains a 256-bit unsigned integer to 128-bit range. -// Used for liquidity calculations where amounts must fit in compact storage. -// -// Parameters: -// - value: 256-bit unsigned integer to constrain -// -// Returns: -// - Masked value if exceeds MAX_UINT128 (2^128 - 1) -// - Original value if within range -// -// Panics if value is nil. -// Critical for preventing overflow in liquidity math. -func toUint128(value *u256.Uint) *u256.Uint { - if value == nil { - panic(newErrorWithDetail( - errInvalidInput, - "value is nil", - )) - } - - if value.Gt(maxUint128) { - return u256.Zero().And(value, q128Mask) - } - return value -} - -// safeConvertToUint128 safely ensures a *u256.Uint value fits within the uint128 range. -// -// This function verifies that the provided unsigned 256-bit integer does not exceed the maximum value for uint128 (`2^128 - 1`). -// If the value is within the uint128 range, it is returned as is; otherwise, the function triggers a panic. -// -// Parameters: -// - value (*u256.Uint): The unsigned 256-bit integer to be checked. -// -// Returns: -// - *u256.Uint: The same value if it is within the uint128 range. -// -// Panics: -// - If the value exceeds the maximum uint128 value (`2^128 - 1`), the function will panic with a descriptive error -// indicating the overflow and the original value. -// -// Notes: -// - The constant `MAX_UINT128` is defined as `340282366920938463463374607431768211455` (the largest uint128 value). -// - No actual conversion occurs since the function works directly with *u256.Uint types. -// -// Example: -// validUint128 := safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211455")) // Valid -// safeConvertToUint128(u256.MustFromDecimal("340282366920938463463374607431768211456")) // Panics due to overflow -func safeConvertToUint128(value *u256.Uint) *u256.Uint { - if value.Gt(maxUint128) { - panic(ufmt.Sprintf( - "%v: amount(%s) overflows uint128 range", - errOverFlow, value.ToString())) - } - return value -} - -// computeLiquidityForAmount0 calculates the liquidity for a given amount of token0. -// -// This function computes the maximum possible liquidity that can be provided for `token0` -// based on the provided price boundaries (sqrtRatioAX96 and sqrtRatioBX96) in Q64.96 format. -// -// Parameters: -// - sqrtRatioAX96: *u256.Uint - The square root price at the lower tick boundary (Q64.96). -// - sqrtRatioBX96: *u256.Uint - The square root price at the upper tick boundary (Q64.96). -// - amount0: *u256.Uint - The amount of token0 to be converted to liquidity. -// -// Returns: -// - *u256.Uint: The calculated liquidity, represented as an unsigned 128-bit integer (uint128). -// -// Panics: -// - If the resulting liquidity exceeds the uint128 range, `safeConvertToUint128` will trigger a panic. -func computeLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0 *u256.Uint) *u256.Uint { - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) - intermediate := u256.MulDiv(sqrtRatioAX96, sqrtRatioBX96, q96Uint) - - diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) - if diff.IsZero() { - panic(newErrorWithDetail( - errIdenticalTicks, - ufmt.Sprintf("sqrtRatioAX96 (%s) and sqrtRatioBX96 (%s) are identical", sqrtRatioAX96.ToString(), sqrtRatioBX96.ToString()), - )) - } - res := u256.MulDiv(amount0, intermediate, diff) - return safeConvertToUint128(res) -} - -// computeLiquidityForAmount1 calculates liquidity based on the provided token1 amount and price range. -// -// This function computes the liquidity for a given amount of token1 by using the difference -// between the upper and lower square root price ratios. The calculation uses Q96 fixed-point -// arithmetic to maintain precision. -// -// Parameters: -// - sqrtRatioAX96: *u256.Uint - The square root ratio of price at the lower tick, represented in Q96 format. -// - sqrtRatioBX96: *u256.Uint - The square root ratio of price at the upper tick, represented in Q96 format. -// - amount1: *u256.Uint - The amount of token1 to calculate liquidity for. -// -// Returns: -// - *u256.Uint: The calculated liquidity based on the provided amount of token1 and price range. -// -// Notes: -// - The result is not directly limited to uint128, as liquidity values can exceed uint128 bounds. -// - If `sqrtRatioAX96 == sqrtRatioBX96`, the function will panic due to division by zero. -// - Q96 is a constant representing `2^96`, ensuring that precision is maintained during division. -// -// Panics: -// - If the resulting liquidity exceeds the uint128 range, `safeConvertToUint128` will trigger a panic. -func computeLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1 *u256.Uint) *u256.Uint { - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) - - diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) - if diff.IsZero() { - panic(newErrorWithDetail( - errIdenticalTicks, - ufmt.Sprintf("sqrtRatioAX96 (%s) and sqrtRatioBX96 (%s) are identical", sqrtRatioAX96.ToString(), sqrtRatioBX96.ToString()), - )) - } - res := u256.MulDiv(amount1, q96Uint, diff) - return safeConvertToUint128(res) -} - -// GetLiquidityForAmounts calculates the maximum liquidity given the current price (sqrtRatioX96), -// upper and lower price bounds (sqrtRatioAX96 and sqrtRatioBX96), and token amounts (amount0, amount1). -// -// This function evaluates how much liquidity can be obtained for specified amounts of token0 and token1 -// within the provided price range. It returns the lesser liquidity based on available token0 or token1 -// to ensure the pool remains balanced. -// -// Parameters: -// - sqrtRatioX96: The current price as a square root ratio in Q64.96 format (*u256.Uint). -// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). -// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). -// - amount0: The amount of token0 available to provide liquidity (*u256.Uint). -// - amount1: The amount of token1 available to provide liquidity (*u256.Uint). -// -// Returns: -// - *u256.Uint: The maximum possible liquidity that can be minted. -// -// Notes: -// - The `Clone` method is used to prevent modification of the original values during computation. -// - The function ensures that liquidity calculations handle edge cases when the current price -// is outside the specified range by returning liquidity based on the dominant token. -// -// TODO: consider to reduce the number of clones (after confirmed this logic is correct) -func GetLiquidityForAmounts(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, amount0, amount1 *u256.Uint) (liquidity *u256.Uint) { - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone()) - - if sqrtRatioX96.Lte(sqrtRatioAX96) { - liquidity = computeLiquidityForAmount0(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone(), amount0.Clone()) - } else if sqrtRatioX96.Lt(sqrtRatioBX96) { - liquidity0 := computeLiquidityForAmount0(sqrtRatioX96.Clone(), sqrtRatioBX96.Clone(), amount0.Clone()) - liquidity1 := computeLiquidityForAmount1(sqrtRatioAX96.Clone(), sqrtRatioX96.Clone(), amount1.Clone()) - - if liquidity0.Lt(liquidity1) { - liquidity = liquidity0 - } else { - liquidity = liquidity1 - } - } else { - liquidity = computeLiquidityForAmount1(sqrtRatioAX96.Clone(), sqrtRatioBX96.Clone(), amount1.Clone()) - } - return liquidity -} - -// computeAmount0ForLiquidity calculates the required amount of token0 for a given liquidity level -// within a specified price range (represented by sqrt ratios). -// -// This function determines the amount of token0 needed to provide a specified amount of liquidity -// within a price range defined by sqrtRatioAX96 (lower bound) and sqrtRatioBX96 (upper bound). -// -// Parameters: -// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). -// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). -// - liquidity: The liquidity to be provided (*u256.Uint). -// -// Returns: -// - *u256.Uint: The amount of token0 required to achieve the specified liquidity level. -// -// Notes: -// - This function assumes the price bounds are expressed in Q64.96 fixed-point format. -// - The function returns 0 if the liquidity is 0 or the price bounds are invalid. -// - Handles edge cases where sqrtRatioAX96 equals sqrtRatioBX96 by returning 0 (to prevent division by zero). -func computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) - if sqrtRatioAX96.IsZero() || sqrtRatioBX96.IsZero() || liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { - return zero - } - - val1 := u256.Zero().Lsh(liquidity, Q96_RESOLUTION) - val2 := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) - - res := u256.MulDiv(val1, val2, sqrtRatioBX96) - res = res.Div(res, sqrtRatioAX96) - - return res -} - -// computeAmount1ForLiquidity calculates the required amount of token1 for a given liquidity level -// within a specified price range (represented by sqrt ratios). -// -// This function determines the amount of token1 needed to provide liquidity between the -// lower (sqrtRatioAX96) and upper (sqrtRatioBX96) price bounds. The calculation is performed -// in Q64.96 fixed-point format, which is standard for many liquidity calculations. -// -// Parameters: -// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). -// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). -// - liquidity: The liquidity amount to be used in the calculation (*u256.Uint). -// -// Returns: -// - *u256.Uint: The amount of token1 required to achieve the specified liquidity level. -// -// Notes: -// - This function handles edge cases where the liquidity is zero or when sqrtRatioAX96 equals sqrtRatioBX96 -// to prevent division by zero. -// - The calculation assumes sqrtRatioAX96 is always less than or equal to sqrtRatioBX96 after the initial -// ascending order sorting. -func computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) *u256.Uint { - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) - if liquidity.IsZero() || sqrtRatioAX96.Eq(sqrtRatioBX96) { - return zero - } - - diff := u256.Zero().Sub(sqrtRatioBX96, sqrtRatioAX96) - res := u256.MulDiv(liquidity, diff, q96Uint) - - return res -} - -// GetAmountsForLiquidity calculates the amounts of token0 and token1 required -// to provide a specified liquidity within a price range. -// -// This function determines the quantities of token0 and token1 necessary to achieve -// a given liquidity level, depending on the current price (sqrtRatioX96) and the -// bounds of the price range (sqrtRatioAX96 and sqrtRatioBX96). The function returns -// the calculated amounts of token0 and token1 as strings. -// -// If the current price is below the lower bound of the price range, only token0 is required. -// If the current price is above the upper bound, only token1 is required. When the -// price is within the range, both token0 and token1 are calculated. -// -// Parameters: -// - sqrtRatioX96: The current price represented as a square root ratio in Q64.96 format (*u256.Uint). -// - sqrtRatioAX96: The lower bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). -// - sqrtRatioBX96: The upper bound of the price range as a square root ratio in Q64.96 format (*u256.Uint). -// - liquidity: The amount of liquidity to be provided (*u256.Uint). -// -// Returns: -// - string: The calculated amount of token0 required to achieve the specified liquidity. -// - string: The calculated amount of token1 required to achieve the specified liquidity. -// -// Notes: -// - If liquidity is zero, the function returns "0" for both token0 and token1. -// - The function guarantees that sqrtRatioAX96 is always the lower bound and -// sqrtRatioBX96 is the upper bound by calling toAscendingOrder(). -// - Edge cases where the current price is exactly on the bounds are handled without division by zero. -// -// Example: -// ``` -// amount0, amount1 := GetAmountsForLiquidity( -// -// u256.MustFromDecimal("79228162514264337593543950336"), // sqrtRatioX96 (1.0 in Q64.96) -// u256.MustFromDecimal("39614081257132168796771975168"), // sqrtRatioAX96 (0.5 in Q64.96) -// u256.MustFromDecimal("158456325028528675187087900672"), // sqrtRatioBX96 (2.0 in Q64.96) -// u256.MustFromDecimal("1000000"), // Liquidity -// -// ) -// -// println("Token0:", amount0, "Token1:", amount1) -// -// // Output: -// Token0: 500000, Token1: 250000 -// ``` -func GetAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, liquidity *u256.Uint) (string, string) { - if liquidity.IsZero() { - return "0", "0" - } - - sqrtRatioAX96, sqrtRatioBX96 = toAscendingOrder(sqrtRatioAX96, sqrtRatioBX96) - - amount0 := u256.Zero() - amount1 := u256.Zero() - - if sqrtRatioX96.Lte(sqrtRatioAX96) { - amount0 = computeAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) - } else if sqrtRatioX96.Lt(sqrtRatioBX96) { - amount0 = computeAmount0ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity) - amount1 = computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity) - } else { - amount1 = computeAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity) - } - - return amount0.ToString(), amount1.ToString() -} diff --git a/contract/r/gnoswap/v1/common/tick_math.gno b/contract/r/gnoswap/v1/common/tick_math.gno deleted file mode 100644 index 02bb47d..0000000 --- a/contract/r/gnoswap/v1/common/tick_math.gno +++ /dev/null @@ -1,263 +0,0 @@ -package common - -import ( - "gno.land/p/nt/ufmt" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -// Pre-calculated ratio constants for performance optimization. -// These values are pre-computed to avoid runtime decimal parsing. -var ( - // Initial ratio constants - exactly matching Uniswap V3 - ratio0 = u256.MustFromDecimal("340265354078544963557816517032075149313") // 0xfffcb933bd6fad37aa2d162d1a594001 - ratio1 = u256.MustFromDecimal("340282366920938463463374607431768211456") // 0x100000000000000000000000000000000 (2^128) - - // Bit mask ratio constants in order (bit 1 to bit 19) - ratioConstants = []*u256.Uint{ - u256.MustFromDecimal("340248342086729790484326174814286782778"), // 0xfff97272373d413259a46990580e213a (bit 1) - u256.MustFromDecimal("340214320654664324051920982716015181260"), // 0xfff2e50f5f656932ef12357cf3c7fdcc (bit 2) - u256.MustFromDecimal("340146287995602323631171512101879684304"), // 0xffe5caca7e10e4e61c3624eaa0941cd0 (bit 3) - u256.MustFromDecimal("340010263488231146823593991679159461444"), // 0xffcb9843d60f6159c9db58835c926644 (bit 4) - u256.MustFromDecimal("339738377640345403697157401104375502016"), // 0xff973b41fa98c081472e6896dfb254c0 (bit 5) - u256.MustFromDecimal("339195258003219555707034227454543997025"), // 0xff2ea16466c96a3843ec78b326b52861 (bit 6) - u256.MustFromDecimal("338111622100601834656805679988414885971"), // 0xfe5dee046a99a2a811c461f1969c3053 (bit 7) - u256.MustFromDecimal("335954724994790223023589805789778977700"), // 0xfcbe86c7900a88aedcffc83b479aa3a4 (bit 8) - u256.MustFromDecimal("331682121138379247127172139078559817300"), // 0xf987a7253ac413176f2b074cf7815e54 (bit 9) - u256.MustFromDecimal("323299236684853023288211250268160618739"), // 0xf3392b0822b70005940c7a398e4b70f3 (bit 10) - u256.MustFromDecimal("307163716377032989948697243942600083929"), // 0xe7159475a2c29b7443b29c7fa6e889d9 (bit 11) - u256.MustFromDecimal("277268403626896220162999269216087595045"), // 0xd097f3bdfd2022b8845ad8f792aa5825 (bit 12) - u256.MustFromDecimal("225923453940442621947126027127485391333"), // 0xa9f746462d870fdf8a65dc1f90e061e5 (bit 13) - u256.MustFromDecimal("149997214084966997727330242082538205943"), // 0x70d869a156d2a1b890bb3df62baf32f7 (bit 14) - u256.MustFromDecimal("66119101136024775622716233608466517926"), // 0x31be135f97d08fd981231505542fcfa6 (bit 15) - u256.MustFromDecimal("12847376061809297530290974190478138313"), // 0x9aa508b5b7a84e1c677de54f3e99bc9 (bit 16) - u256.MustFromDecimal("485053260817066172746253684029974020"), // 0x5d6af8dedb81196699c329225ee604 (bit 17) - u256.MustFromDecimal("691415978906521570653435304214168"), // 0x2216e584f5fa1ea926041bedfe98 (bit 18) - u256.MustFromDecimal("1404880482679654955896180642"), // 0x48a170391f7dc42444e8fa2 (bit 19) - } - - // Pre-computed constants for optimization - maxUint256 = u256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935") // 2^256 - 1 - minSqrtRatio = u256.MustFromDecimal("4295128739") // same as TickMathGetSqrtRatioAtTick(minTick) - maxSqrtRatio = u256.MustFromDecimal("1461446703485210103287273052203988822378723970342") // same as TickMathGetSqrtRatioAtTick(maxTick) - - // MSB calculation thresholds - pre-computed for performance - msb128Threshold = u256.MustFromDecimal("340282366920938463463374607431768211455") // 2^128 - 1 - msb64Threshold = u256.MustFromDecimal("18446744073709551615") // 2^64 - 1 - msb32Threshold = u256.MustFromDecimal("4294967295") // 2^32 - 1 - msb16Threshold = u256.NewUint(65535) // 2^16 - 1 - msb8Threshold = u256.NewUint(255) // 2^8 - 1 - msb4Threshold = u256.NewUint(15) // 2^4 - 1 - msb2Threshold = u256.NewUint(3) // 2^2 - 1 - msb1Threshold = u256.One() // 1 - - // Pre-computed constants for tick calculation - log2Multiplier = i256.MustFromDecimal("255738958999603826347141") - tickLowOffset = i256.MustFromDecimal("3402992956809132418596140100660247210") - tickHiOffset = i256.MustFromDecimal("291339464771989622907027621153398088495") - - oneLsh32 = u256.One().Lsh(u256.One(), 32) // 1 << 32 -) - -// TickMathGetSqrtRatioAtTick calculates sqrt price ratio for given tick. -// -// Converts tick index to square root price in Q64.96 fixed-point format. -// Based on Uniswap V3's mathematical formula: price = 1.0001^tick. -// Uses bit manipulation for gas-efficient calculation. -// -// Parameters: -// - tick: Tick index in range [-887272, 887272] -// -// Returns: -// - Square root of price ratio as Q64.96 fixed-point -// - Result represents sqrt(token1/token0) price -// -// Mathematical formula: -// -// sqrtPriceX96 = sqrt(1.0001^tick) * 2^96 -// -// Panics if tick outside valid range. -// Critical for all price calculations in concentrated liquidity. -func TickMathGetSqrtRatioAtTick(tick int32) *u256.Uint { - assertValidTickRange(tick) - absTick := abs(tick) - - // Initialize ratio based on LSB - exactly like Uniswap V3 - var ratio *u256.Uint - - if absTick&0x1 != 0 { - ratio = ratio0.Clone() - } else { - ratio = ratio1.Clone() - } - - temp := u256.Zero() - - // Apply bit masks using optimized loop - maintains exact same logic - for i := 1; i < 20; i++ { - if absTick&(1< 0 { - ratio = temp.Div(maxUint256, ratio) - } - - // Convert from Q128.128 to Q128.96 with rounding up. - // This divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96 - upper := u256.Zero().Rsh(ratio, 32) // ratio >> 32 - remainder := u256.Zero().Mod(ratio, oneLsh32) // ratio % (1 << 32) - - // Round up: add 1 if remainder != 0 - if !remainder.IsZero() { - upper = u256.Zero().Add(upper, u256.One()) - } - - return upper -} - -// getMostSignificantBit returns the position of the most significant bit. -func getMostSignificantBit(r *u256.Uint) (msb uint64) { - temp := r.Clone() - - // Optimized MSB calculation using pre-computed thresholds - if temp.Gt(msb128Threshold) { - msb |= 128 - temp = temp.Rsh(temp, 128) - } - - if temp.Gt(msb64Threshold) { - msb |= 64 - temp = temp.Rsh(temp, 64) - } - - if temp.Gt(msb32Threshold) { - msb |= 32 - temp = temp.Rsh(temp, 32) - } - - if temp.Gt(msb16Threshold) { - msb |= 16 - temp = temp.Rsh(temp, 16) - } - - if temp.Gt(msb8Threshold) { - msb |= 8 - temp = temp.Rsh(temp, 8) - } - - if temp.Gt(msb4Threshold) { - msb |= 4 - temp = temp.Rsh(temp, 4) - } - - if temp.Gt(msb2Threshold) { - msb |= 2 - temp = temp.Rsh(temp, 2) - } - - if temp.Gt(msb1Threshold) { - msb |= 1 - } - - return -} - -// TickMathGetTickAtSqrtRatio calculates tick value for given sqrt price ratio, returning greatest tick where getSqrtRatioAtTick(tick) <= ratio, matching Uniswap V3 exactly. -func TickMathGetTickAtSqrtRatio(sqrtPriceX96 *u256.Uint) int32 { - if sqrtPriceX96.Lt(minSqrtRatio) || sqrtPriceX96.Gte(maxSqrtRatio) { - panic(newErrorWithDetail( - errOutOfRange, - ufmt.Sprintf("sqrtPriceX96(%s) is out of range", sqrtPriceX96.ToString()), - )) - } - - // Scale ratio by 32 bits to convert from Q64.96 to Q96.128 - ratio := u256.Zero().Lsh(sqrtPriceX96, 32) - - // Find MSB using optimized calculation - msb := getMostSignificantBit(ratio) - - // Adjust ratio based on MSB - var r *u256.Uint - - if msb >= 128 { - r = u256.Zero().Rsh(ratio, uint(msb-127)) - } else { - r = u256.Zero().Lsh(ratio, uint(127-msb)) - } - - // Calculate log_2 using fixed-point arithmetic - log2 := i256.NewInt(int64(msb) - 128) - log2 = i256.Zero().Lsh(log2, 64) - - // Define temporary variables for optimization - tempR := u256.Zero() - tempF := u256.Zero() - tempI256 := i256.Zero() - - // Optimized iterative calculation using loop - maintains exact same logic - for i := 0; i < 14; i++ { - tempR = tempR.Mul(r, r) - r = u256.Zero().Rsh(tempR, 127) - - tempF = tempF.Rsh(r, 128) - tempI256 = i256.FromUint256(tempF) - f := tempF - - tempI256 = tempI256.Lsh(tempI256, uint(63-i)) - log2 = log2.Or(log2, tempI256) - r = u256.Zero().Rsh(r, uint(f.Uint64())) - } - - // Calculate tick from log_sqrt10001 - logSqrt10001 := i256.Zero().Mul(log2, log2Multiplier) - - // Calculate tick bounds - tickLow := i256.Zero().Sub(logSqrt10001, tickLowOffset) - tickLow = tickLow.Rsh(tickLow, 128) - tickLowInt32 := int32(tickLow.Int64()) - - tickHi := i256.Zero().Add(logSqrt10001, tickHiOffset) - tickHi = tickHi.Rsh(tickHi, 128) - tickHiInt32 := int32(tickHi.Int64()) - - // Select the appropriate tick - if tickLowInt32 == tickHiInt32 { - return tickLowInt32 - } else if TickMathGetSqrtRatioAtTick(tickHiInt32).Lte(sqrtPriceX96) { - return tickHiInt32 - } - - return tickLowInt32 -} - -// abs returns the absolute value of x. -func abs(x int32) int32 { - if x < 0 { - return -x - } - - return x -} - -// assertValidTickRange panics if tick is outside valid range [-887272, 887272]. -func assertValidTickRange(tick int32) { - if tick > maxTick { - panic(newErrorWithDetail( - errOutOfRange, - ufmt.Sprintf("tick is out of range (larger than 887272), tick: %d", tick), - )) - } - if tick < minTick { - panic(newErrorWithDetail( - errOutOfRange, - ufmt.Sprintf("tick is out of range (smaller than -887272), tick: %d", tick), - )) - } -} diff --git a/contract/r/gnoswap/v1/community_pool/README.md b/contract/r/gnoswap/v1/community_pool/README.md deleted file mode 100644 index 170c362..0000000 --- a/contract/r/gnoswap/v1/community_pool/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# Community Pool - -GnoSwap community treasury for ecosystem development. - -## Overview - -Community-governed treasury that receives protocol emissions and fees for ecosystem growth initiatives. Also collects unclaimed internal staking rewards from warmup periods. - -## Configuration - -- **Emission Allocation**: 5% of GNS emissions (default) -- **Governance Control**: All disbursements require proposal -- **Fund Sources**: GNS emissions, unclaimed rewards (internal reward only), protocol fees - -## Governance Process - -- **Proposal Creation**: Submit funding request with justification -- **Voting Period**: Token holders vote on proposal -- **Execution**: Approved transfers execute automatically -- **Transparency**: All operations emit events - -## Key Functions - -### `TransferToken` -Transfers tokens to specified address (governance only). - -## Usage - -```go -// Transfer via governance proposal -TransferToken( - "gno.land/r/demo/usdc", - recipientAddr, - 1000000, -) -``` - -## Security - -- Governance-only transfers -- No emergency withdrawals -- Event emission for transparency -- Multi-token support \ No newline at end of file diff --git a/contract/r/gnoswap/v1/community_pool/community_pool.gno b/contract/r/gnoswap/v1/community_pool/community_pool.gno deleted file mode 100644 index d5dd6f4..0000000 --- a/contract/r/gnoswap/v1/community_pool/community_pool.gno +++ /dev/null @@ -1,51 +0,0 @@ -package community_pool - -import ( - "std" - "strconv" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" -) - -// TransferToken transfers tokens from the community pool. -// -// Parameters: -// - tokenPath: token contract path -// - to: recipient address -// - amount: transfer amount -// -// Only callable by admin or governance. -func TransferToken(cur realm, tokenPath string, to std.Address, amount int64) { - halt.AssertIsNotHaltedCommunityPool() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - err := transferToken(tokenPath, to, amount) - if err != nil { - panic(err) - } -} - -// transferToken performs actual token transfer. -func transferToken(tokenPath string, to std.Address, amount int64) error { - err := common.Transfer(cross, tokenPath, to, amount) - if err != nil { - return err - } - - prevRealm := std.PreviousRealm() - std.Emit( - "TransferToken", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "tokenPath", tokenPath, - "to", to.String(), - "amount", strconv.FormatInt(amount, 10), - ) - - return nil -} diff --git a/contract/r/gnoswap/v1/community_pool/doc.gno b/contract/r/gnoswap/v1/community_pool/doc.gno deleted file mode 100644 index 019f9f2..0000000 --- a/contract/r/gnoswap/v1/community_pool/doc.gno +++ /dev/null @@ -1,6 +0,0 @@ -// Package community_pool manages the GnoSwap community treasury. -// -// This contract holds protocol-owned assets that can be allocated through -// governance proposals. It receives a portion of GNS emissions and can be -// used for ecosystem development, grants, and protocol improvements. -package community_pool diff --git a/contract/r/gnoswap/v1/community_pool/errors.gno b/contract/r/gnoswap/v1/community_pool/errors.gno deleted file mode 100644 index e060c33..0000000 --- a/contract/r/gnoswap/v1/community_pool/errors.gno +++ /dev/null @@ -1,13 +0,0 @@ -package community_pool - -import ( - "errors" -) - -var ( - errNoPermission = errors.New("[GNOSWAP-COMMUNITY_POOL-001] caller has no permission") - errNotRegistered = errors.New("[GNOSWAP-COMMUNITY_POOL-002] not registered") - errAlreadyRegistered = errors.New("[GNOSWAP-COMMUNITY_POOL-003] already registered") - errLocked = errors.New("[GNOSWAP-COMMUNITY_POOL-004] can't transfer token while locked") - errHalted = errors.New("[GNOSWAP-COMMUNITY_POOL-005] halted") -) diff --git a/contract/r/gnoswap/v1/community_pool/gnomod.toml b/contract/r/gnoswap/v1/community_pool/gnomod.toml deleted file mode 100644 index d121d3d..0000000 --- a/contract/r/gnoswap/v1/community_pool/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/community_pool" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/gnft/assert.gno b/contract/r/gnoswap/v1/gnft/assert.gno deleted file mode 100644 index 252ef71..0000000 --- a/contract/r/gnoswap/v1/gnft/assert.gno +++ /dev/null @@ -1,37 +0,0 @@ -package gnft - -import ( - "std" - - "gno.land/p/demo/tokens/grc721" - "gno.land/p/nt/ufmt" -) - -// assertIsValidTokenURI panics if the token already has a URI set. -func assertIsValidTokenURI(tid grc721.TokenID) { - uri, _ := nft.TokenURI(tid) - if string(uri) != "" { - panic(makeErrorWithDetails(errCannotSetURI, ufmt.Sprintf("token id (%s) has already set URI", string(tid)))) - } -} - -// assertIsValidAddress panics if the address is invalid. -func assertIsValidAddress(addr std.Address) { - if !addr.IsValid() { - panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("address (%s)", addr.String()))) - } -} - -// assertFromIsValidAddress panics if the from address is invalid. -func assertFromIsValidAddress(from std.Address) { - if !from.IsValid() { - panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("from address (%s)", from.String()))) - } -} - -// assertToIsValidAddress panics if the to address is invalid. -func assertToIsValidAddress(to std.Address) { - if !to.IsValid() { - panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("to address (%s)", to.String()))) - } -} diff --git a/contract/r/gnoswap/v1/gnft/errors.gno b/contract/r/gnoswap/v1/gnft/errors.gno deleted file mode 100644 index bb1bc0a..0000000 --- a/contract/r/gnoswap/v1/gnft/errors.gno +++ /dev/null @@ -1,31 +0,0 @@ -package gnft - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errNoPermission = errors.New("[GNOSWAP-GNFT-001] caller has no permission") - errNotTokenOwner = errors.New("[GNOSWAP-GNFT-001] caller is not token owner") - - errCannotSetURI = errors.New("[GNOSWAP-GNFT-002] cannot set URI") - errTokenDoesNotExist = errors.New("[GNOSWAP-GNFT-002] cannot set URI || token does not exist") - errTokenBurned = errors.New("[GNOSWAP-GNFT-002] cannot set URI || token has been burned") - - errNoTokenForCaller = errors.New("[GNOSWAP-GNFT-003] no token for caller") - errInvalidAddress = errors.New("[GNOSWAP-GNFT-004] invalid addresss") - errInvalidTokenID = errors.New("[GNOSWAP-GNFT-005] invalid token ID") - - // Transfer errors - errNotOwnerOrApproved = errors.New("[GNOSWAP-GNFT-006] caller is not token owner or approved") - errTokenNotExists = errors.New("[GNOSWAP-GNFT-007] token does not exist") - errTransferToSelf = errors.New("[GNOSWAP-GNFT-008] cannot transfer to self") - errNotApprovedForAll = errors.New("[GNOSWAP-GNFT-009] not approved for all tokens") -) - -// makeErrorWithDetails creates an error with additional context. -func makeErrorWithDetails(err error, details string) error { - return ufmt.Errorf("%s || %s", err.Error(), details) -} diff --git a/contract/r/gnoswap/v1/gnft/gnft.gno b/contract/r/gnoswap/v1/gnft/gnft.gno deleted file mode 100644 index 0fd86a4..0000000 --- a/contract/r/gnoswap/v1/gnft/gnft.gno +++ /dev/null @@ -1,272 +0,0 @@ -package gnft - -import ( - "std" - - "gno.land/p/demo/tokens/grc721" - "gno.land/p/nt/ownable" - "gno.land/p/nt/ufmt" - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" -) - -var ( - nft = grc721.NewBasicNFT("GNOSWAP NFT", "GNFT") - owner = ownable.NewWithAddress(getPositionAddress()) -) - -// Name returns the NFT collection name. -func Name() string { - return nft.Name() -} - -// Symbol returns the NFT symbol. -func Symbol() string { - return nft.Symbol() -} - -// TotalSupply returns the total number of NFTs minted. -func TotalSupply() int64 { - return nft.TokenCount() -} - -// TokenURI returns the metadata URI for the specified token ID. -func TokenURI(tid grc721.TokenID) (string, error) { - uri, err := nft.TokenURI(tid) - if err != nil { - return "", err - } - - return string(uri), nil -} - -// BalanceOf returns the number of NFTs owned by the specified address. -func BalanceOf(owner std.Address) (int64, error) { - assertIsValidAddress(owner) - - balance, err := nft.BalanceOf(owner) - if err != nil { - return 0, err - } - return balance, nil -} - -// OwnerOf returns the owner address for the specified token ID. -func OwnerOf(tid grc721.TokenID) (std.Address, error) { - ownerAddr, err := nft.OwnerOf(tid) - if err != nil { - return "", err - } - - return ownerAddr, nil -} - -// MustOwnerOf returns the owner address for the specified token ID. -// It panics if the token ID is invalid. -func MustOwnerOf(tid grc721.TokenID) std.Address { - ownerAddr, err := OwnerOf(tid) - if err != nil { - panic(err.Error()) - } - - return ownerAddr -} - -// SetTokenURI sets the metadata URI for the specified token. -// -// Parameters: -// - tid: token ID -// - tURI: token URI -// -// Only callable by position contract. -func SetTokenURI(cur realm, tid grc721.TokenID, tURI grc721.TokenURI) (bool, error) { - halt.AssertIsNotHaltedPosition() - - assertIsValidTokenURI(tid) - - err := setTokenURI(tid, tURI) - if err != nil { - panic(err) - } - - return true, nil -} - -// SafeTransferFrom transfers token ownership with receiver validation. -// -// Parameters: -// - from: current owner address -// - to: recipient address -// - tid: token ID to transfer -// -// Returns error if transfer fails. -// Only callable by staker contract. -func SafeTransferFrom(cur realm, from, to std.Address, tid grc721.TokenID) error { - halt.AssertIsNotHaltedPosition() - - caller := std.PreviousRealm().Address() - access.AssertIsStaker(caller) - - assertFromIsValidAddress(from) - assertToIsValidAddress(to) - - err := nft.SafeTransferFrom(from, to, tid) - checkTransferErr(err, from, to, tid) - return nil -} - -// TransferFrom transfers a token from one address to another. -// -// Parameters: -// - from: current owner address -// - to: recipient address -// - tid: token ID -// -// Returns error if transfer fails. -// Only callable by staker contract. -func TransferFrom(cur realm, from, to std.Address, tid grc721.TokenID) error { - halt.AssertIsNotHaltedPosition() - - caller := std.PreviousRealm().Address() - access.AssertIsStaker(caller) - - assertFromIsValidAddress(from) - assertToIsValidAddress(to) - - err := nft.TransferFrom(from, to, tid) - checkTransferErr(err, from, to, tid) - return nil -} - -// Approve grants permission to transfer a specific token ID to another address. -// -// Parameters: -// - approved: address to approve -// - tid: token ID to approve for transfer -// -// Returns error if approval fails. -// Only callable when not halted. -func Approve(cur realm, approved std.Address, tid grc721.TokenID) error { - halt.AssertIsNotHaltedPosition() - assertIsValidAddress(approved) - - err := nft.Approve(approved, tid) - checkApproveErr(err, approved, tid) - return nil -} - -// SetApprovalForAll enables/disables operator approval for all tokens. -// -// Parameters: -// - operator: address to set approval for -// - approved: true to approve, false to revoke -// -// Returns error if operation fails. -// Only callable when not halted. -func SetApprovalForAll(cur realm, operator std.Address, approved bool) error { - halt.AssertIsNotHaltedPosition() - assertIsValidAddress(operator) - - checkErr(nft.SetApprovalForAll(operator, approved)) - return nil -} - -// GetApproved returns approved address for token ID. -// -// Parameters: -// - tid: token ID to check -// -// Returns approved address and error if token doesn't exist. -func GetApproved(tid grc721.TokenID) (std.Address, error) { - return nft.GetApproved(tid) -} - -// IsApprovedForAll checks if operator can manage all owner's tokens. -// -// Parameters: -// - owner: token owner address -// - operator: operator address to check -// -// Returns true if operator is approved for all owner's tokens. -func IsApprovedForAll(owner, operator std.Address) bool { - return nft.IsApprovedForAll(owner, operator) -} - -// Mint creates new NFT and transfers to address. -// -// Parameters: -// - to: recipient address -// - tid: token ID -// -// Returns minted token ID. -// Only callable by position contract. -func Mint(cur realm, to std.Address, tid grc721.TokenID) grc721.TokenID { - halt.AssertIsNotHaltedPosition() - owner.AssertOwnedByPrevious() - - ownerAddress := owner.Owner() - - checkErr(nft.Mint(ownerAddress, tid)) - - tokenURI := genImageURI(generateRandInstance()) - err := setTokenURI(tid, grc721.TokenURI(tokenURI)) - if err != nil { - panic(err) - } - - checkErr(nft.TransferFrom(ownerAddress, to, tid)) - - return tid -} - -// Exists checks if token ID exists. -func Exists(tid grc721.TokenID) bool { - _, err := nft.OwnerOf(tid) - if err != nil { - return false - } - - return true -} - -// Burn removes a specific token ID. -// -// Parameters: -// - tid: token ID to burn -// -// Only callable by owner. -func Burn(cur realm, tid grc721.TokenID) { - halt.AssertIsNotHaltedPosition() - owner.AssertOwnedByPrevious() - - checkErr(nft.Burn(tid)) -} - -// Render returns the HTML representation of the NFT. -func Render(path string) string { - switch { - case path == "": - return nft.RenderHome() - default: - return "404\n" - } -} - -// setTokenURI sets the metadata URI for a specific token ID. -func setTokenURI(tid grc721.TokenID, tURI grc721.TokenURI) error { - _, err := nft.SetTokenURI(tid, tURI) - if err != nil { - return makeErrorWithDetails(err, ufmt.Sprintf("token id (%s)", tid)) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "SetTokenURI", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "tokenId", string(tid), - "tokenURI", string(tURI), - ) - - return nil -} diff --git a/contract/r/gnoswap/v1/gnft/gnomod.toml b/contract/r/gnoswap/v1/gnft/gnomod.toml deleted file mode 100644 index 4291218..0000000 --- a/contract/r/gnoswap/v1/gnft/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/gnft" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/gnft/svg_generator.gno b/contract/r/gnoswap/v1/gnft/svg_generator.gno deleted file mode 100644 index e0c8936..0000000 --- a/contract/r/gnoswap/v1/gnft/svg_generator.gno +++ /dev/null @@ -1,69 +0,0 @@ -package gnft - -import ( - b64 "encoding/base64" - "math/rand" - "strings" - - "gno.land/p/nt/ufmt" -) - -var baseTempalte = ` - - - - - - - - - - - - - - - - - - - - - - - -` - -// charset contains valid hex digits for color generation. -const charset = "0123456789ABCDEF" - -// genImageURI generates a base64-encoded SVG image URI with random gradient colors. -func genImageURI(r *rand.Rand) string { - imageRaw := genImageRaw(r) - sEnc := b64.StdEncoding.EncodeToString([]byte(imageRaw)) - - return "data:image/svg+xml;base64," + sEnc -} - -// genImageRaw generates an SVG image with random gradient parameters. -func genImageRaw(r *rand.Rand) string { - x1 := 7 + r.Uint64N(7) - y1 := 7 + r.Uint64N(7) - - x2 := 121 + r.Uint64N(6) - y2 := 121 + r.Uint64N(6) - - var color1, color2 strings.Builder - color1.Grow(7) - color2.Grow(7) - color1.WriteByte('#') - color2.WriteByte('#') - - for i := 0; i < 6; i++ { - color1.WriteByte(charset[r.IntN(16)]) - color2.WriteByte(charset[r.IntN(16)]) - } - - randImage := ufmt.Sprintf(baseTempalte, x1, y1, x2, y2, color1.String(), color2.String()) - return randImage -} diff --git a/contract/r/gnoswap/v1/gnft/utils.gno b/contract/r/gnoswap/v1/gnft/utils.gno deleted file mode 100644 index 366c002..0000000 --- a/contract/r/gnoswap/v1/gnft/utils.gno +++ /dev/null @@ -1,114 +0,0 @@ -package gnft - -import ( - "math/rand" - "std" - "time" - - "gno.land/p/demo/tokens/grc721" - "gno.land/p/nt/ufmt" - prabc "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/rbac" -) - -func getPositionAddress() std.Address { - addr, exists := access.GetAddress(prabc.ROLE_POSITION.String()) - if !exists { - return rbac.DefaultRoleAddresses[prabc.ROLE_POSITION] - } - - return addr -} - -// tid converts uint64 to grc721.TokenID. -func tid(id uint64) grc721.TokenID { - return grc721.TokenID(ufmt.Sprintf("%d", id)) -} - -// generateRandInstance generates a new random instance. -func generateRandInstance() *rand.Rand { - seed1 := time.Now().Unix() + TotalSupply() - seed2 := time.Now().UnixNano() + TotalSupply() - pcg := rand.NewPCG(uint64(seed1), uint64(seed2)) - return rand.New(pcg) -} - -// checkErr panics if an error occurs. -func checkErr(err error) { - if err != nil { - panic(err.Error()) - } -} - -// checkTransferErr wraps transfer errors with more specific context. -func checkTransferErr(err error, from, to std.Address, tid grc721.TokenID) { - if err == nil { - return - } - - caller := std.PreviousRealm().Address() - - // Check if token exists - owner, ownerErr := nft.OwnerOf(tid) - if ownerErr != nil { - panic(ownerErr) - } - - switch err { - case grc721.ErrCallerIsNotOwnerOrApproved: - // Check if caller is the owner - if caller == owner { - panic(makeErrorWithDetails(grc721.ErrTransferFromIncorrectOwner, ufmt.Sprintf("owner mismatch - from: %s, actual owner: %s, token: %s", from, owner, string(tid)))) - } - - // Check if caller is approved for this specific token - approved, _ := nft.GetApproved(tid) - if approved != caller { - // Check if caller is approved for all tokens - if !nft.IsApprovedForAll(owner, caller) { - panic(makeErrorWithDetails(grc721.ErrCallerIsNotOwnerOrApproved, ufmt.Sprintf("caller %s is not owner %s or approved for token %s", caller, owner, string(tid)))) - } - } - - case grc721.ErrInvalidAddress: - panic(makeErrorWithDetails(grc721.ErrInvalidAddress, ufmt.Sprintf("to address (%s)", to))) - - case grc721.ErrTransferFromIncorrectOwner: - panic(makeErrorWithDetails(grc721.ErrTransferFromIncorrectOwner, ufmt.Sprintf("from %s is not the owner %s of token %s", from, owner, string(tid)))) - - case grc721.ErrInvalidTokenId: - panic(makeErrorWithDetails(grc721.ErrInvalidTokenId, ufmt.Sprintf("token %s", string(tid)))) - - default: - panic(err.Error()) - } -} - -// checkApproveErr wraps approve errors with more specific context. -func checkApproveErr(err error, approved std.Address, tid grc721.TokenID) { - if err == nil { - return - } - - errMsg := err.Error() - caller := std.PreviousRealm().Address() - - // Check if token exists - owner, ownerErr := nft.OwnerOf(tid) - if ownerErr != nil { - panic(makeErrorWithDetails(errTokenNotExists, ufmt.Sprintf("token %s", string(tid)))) - } - - switch { - case errMsg == "caller is not token owner or approved": - panic(makeErrorWithDetails(errNotOwnerOrApproved, ufmt.Sprintf("caller %s cannot approve for token %s owned by %s", caller, string(tid), owner))) - - case errMsg == "approval to current owner": - panic(makeErrorWithDetails(errTransferToSelf, ufmt.Sprintf("cannot approve to current owner %s for token %s", approved, string(tid)))) - - default: - panic(err.Error()) - } -} diff --git a/contract/r/gnoswap/v1/gov/README.md b/contract/r/gnoswap/v1/gov/README.md deleted file mode 100644 index 2c5deb5..0000000 --- a/contract/r/gnoswap/v1/gov/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# Governance - -Decentralized protocol governance via GNS staking and voting. - -## Overview - -Governance system enables GNS holders to stake for xGNS voting power, create proposals, and vote on protocol changes. For more details, check out [docs](https://docs.gnoswap.io/core-concepts/governance). - -## Configuration - -- **Voting Period**: 7 days -- **Quorum**: 50% of xGNS supply -- **Proposal Threshold**: 1,000 GNS -- **Execution Delay**: 1 day timelock -- **Execution Window**: 30 days -- **Undelegation Lockup**: 7 days -- **Vote Weight Smoothing**: 24 hours - -## Core Mechanics - -### Staking Flow -``` -GNS → Stake → xGNS (voting power) → Delegate → Vote -``` -1. Stake GNS to receive equal xGNS -2. Delegate voting power (can be self) -3. Vote on proposals with delegated power -4. 7-day lockup for undelegation - -### Proposal Types -- **Text**: Signal proposals without execution -- **CommunityPoolSpend**: Treasury disbursements -- **ParameterChange**: Protocol parameter updates - -## Proposal Lifecycle - -### Creation -- Requires 1,000 GNS balance -- One active proposal per address -- Valid type and parameters required - -### Voting -- 1 day delay before voting starts -- 7 days voting period -- Weight = 24hr average delegation (prevents flash loans) - -### Execution -- Requires quorum (50%) and majority (>50%) -- 1 day timelock after voting -- 30 day execution window -- Anyone can trigger execution - -## Technical Details - -### Vote Weight Calculation -```go -// 24-hour average prevents manipulation -snapshot1 = getDelegationAt(proposalTime - 24hr) -snapshot2 = getDelegationAt(proposalTime) -voteWeight = (snapshot1 + snapshot2) / 2 -``` - -### Dynamic Quorum -```go -activeXGNS = totalXGNS - launchpadXGNS -requiredVotes = activeXGNS * 0.5 -``` - -### Rewards Distribution -xGNS holders earn protocol fees: -``` -userShare = (userXGNS / totalXGNS) * protocolFees -``` - -## Usage - -```go -// Stake GNS for xGNS -Delegate(amount, delegateTo) - -// Create proposal -ProposeText(title, description, body) -ProposeCommunityPoolSpend(recipient, amount) -ProposeParameterChange(params) - -// Vote on proposal -Vote(proposalId, true) // YES -Vote(proposalId, false) // NO - -// Execute after timelock -Execute(proposalId) - -// Undelegate (7-day lockup) -Undelegate() -``` - -## Security - -- Flash loan protection via vote smoothing -- Sybil resistance through stake weighting -- Timelock prevents rushed execution -- Single proposal limit per address -- Dynamic quorum excludes inactive xGNS \ No newline at end of file diff --git a/contract/r/gnoswap/v1/gov/doc.gno b/contract/r/gnoswap/v1/gov/doc.gno deleted file mode 100644 index 94cf398..0000000 --- a/contract/r/gnoswap/v1/gov/doc.gno +++ /dev/null @@ -1,5 +0,0 @@ -// Package gov provides Gnoswap's governance system through three packages: -// 1. governance: Handles proposal creation, voting, and execution -// 2. staker: Manages GNS staking, delegation, and reward distribution -// 3. xgns: Implements the xGNS token representing staked GNS -package gov diff --git a/contract/r/gnoswap/v1/gov/gnomod.toml b/contract/r/gnoswap/v1/gov/gnomod.toml deleted file mode 100644 index 15b44f5..0000000 --- a/contract/r/gnoswap/v1/gov/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/gov" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/gov/governance/api.gno b/contract/r/gnoswap/v1/gov/governance/api.gno deleted file mode 100644 index e19f41f..0000000 --- a/contract/r/gnoswap/v1/gov/governance/api.gno +++ /dev/null @@ -1,195 +0,0 @@ -package governance - -import ( - "std" - "strconv" - "strings" - "time" - - "gno.land/p/onbloc/json" -) - -func createProposalJsonNode(id int64, proposal *Proposal) *json.Node { - return json.Builder(). - WriteString("id", formatInt(id)). - WriteString("configVersion", formatInt(proposal.ConfigVersion)). - WriteString("proposer", proposal.Proposer().String()). - WriteString("status", b64Encode(getProposalStatus(id))). - WriteString("type", proposal.Type().String()). - WriteString("title", proposal.Title()). - WriteString("description", proposal.Description()). - WriteString("vote", b64Encode(getProposalVotes(id))). - WriteString("extra", b64Encode(getProposalExtraData(id))). - Node() -} - -// GetProposalById returns a single proposal with necessary information. -func GetProposalById(id int64) string { - _, exists := getProposal(id) - if !exists { - return "" - } - - proposalsObj := metaNode() - proposalArr := json.ArrayNode("", nil) - proposalObj := getProposalById(id) - proposalArr.AppendArray(proposalObj) - proposalsObj.AppendObject("proposals", proposalArr) - - return marshal(proposalsObj) -} - -// GetVoteStatusFromProposalById returns the vote status(max, yes, no) of a proposal. -func GetVoteStatusFromProposalById(id int64) string { - _, exists := getProposal(id) - if !exists { - return "" - } - - votesObj := metaNode() - votesObj.AppendObject("proposalId", json.StringNode("proposalId", formatInt(id))) - votesObj.AppendObject("votes", json.StringNode("votes", b64Encode(getProposalVotes(id)))) // max, yes, no - - return marshal(votesObj) -} - -// GetVoteByAddressFromProposalById returns the vote of an address from a certain proposal. -func GetVoteByAddressFromProposalById(addr std.Address, id int64) string { - vote, exists := getProposalUserVotingInfo(id, addr) - if !exists { - return "" - } - - votesObj := metaNode() - voteArr := json.ArrayNode("", nil) - voteObj := createVoteJsonNode(addr, id, vote) - voteArr.AppendArray(voteObj) - votesObj.AppendObject("votes", voteArr) - - return marshal(votesObj) -} - -// getProposalById is a helper function for GetProposals and GetProposalById. -func getProposalById(id int64) *json.Node { - proposal := mustGetProposal(id) - return createProposalJsonNode(id, proposal) -} - -func createVoteJsonNode(addr std.Address, id int64, vote *VotingInfo) *json.Node { - return json.Builder(). - WriteString("proposalId", formatInt(id)). - WriteString("voteYes", formatBool(vote.votedYes)). - WriteString("voteWeight", formatInt(vote.votedWeight)). - WriteString("voteHeight", formatInt(vote.votedHeight)). - WriteString("voteTimestamp", formatInt(vote.votedAt)). - Node() -} - -// getProposalExtraData returns extra data of a proposal based on its type. -func getProposalExtraData(proposalId int64) string { - proposal, exist := getProposal(proposalId) - if !exist { - return "" - } - - switch proposal.Type() { - case Text: - return "" - case CommunityPoolSpend: - return getCommunityPoolSpendProposalData(proposalId) - case ParameterChange: - return getParameterChangeProposalData(proposalId) - } - - return "" -} - -// getCommunityPoolSpendProposalData returns community pool spending proposal data including recipient address, token path, and amount. -func getCommunityPoolSpendProposalData(proposalId int64) string { - proposal := mustGetProposal(proposalId) - spend := proposal.data.CommunityPoolSpend() - - proposalObj := json.Builder(). - WriteString("to", spend.to.String()). - WriteString("tokenPath", spend.tokenPath). - WriteString("amount", formatInt(spend.amount)). - Node() - - return marshal(proposalObj) -} - -// getParameterChangeProposalData returns parameter change proposal data as a joined string of messages. -func getParameterChangeProposalData(proposalId int64) string { - proposal := mustGetProposal(proposalId) - - msgs := proposal.data.Execution().msgs - msgsStr := strings.Join(msgs, "*GOV*") - - return msgsStr -} - -// getProposalStatus returns status of a proposal. -func getProposalStatus(id int64) string { - proposal, exist := getProposal(id) - if !exist { - return "" - } - - // Get current status dynamically - status := proposal.Status(time.Now().Unix()) - - schedule := proposal.status.schedule - // Create status node with schedule and current status - node := json.Builder(). - WriteString("status", status). - WriteString("createTime", formatInt(schedule.createTime)). - WriteString("activeTime", formatInt(schedule.activeTime)). - WriteString("votingEndTime", formatInt(schedule.votingEndTime)). - WriteString("executableTime", formatInt(schedule.executableTime)). - WriteString("expiredTime", formatInt(schedule.expiredTime)) - - // Add action state if applicable - if proposal.status.IsCanceled(time.Now().Unix()) { - node. - WriteString("canceled", formatBool(true)). - WriteString("canceledAt", formatInt(proposal.status.actionStatus.canceledAt)). - WriteString("canceledBy", proposal.status.actionStatus.canceledBy.String()) - } - if proposal.status.IsExecuted(time.Now().Unix()) { - node. - WriteString("executed", formatBool(true)). - WriteString("executedAt", formatInt(proposal.status.actionStatus.executedAt)). - WriteString("executedBy", proposal.status.actionStatus.executedBy.String()) - } - - return marshal(node.Node()) -} - -// getProposalVotes returns votes of a proposal. -func getProposalVotes(id int64) string { - proposal, exist := getProposal(id) - if !exist { - return "" - } - voting := proposal.status.voteStatus - maxVoting := formatInt(voting.maxVotingWeight) - - proposalObj := json.Builder(). - WriteString("quorum", formatInt(voting.quorumAmount)). - WriteString("max", maxVoting). - WriteString("yes", formatInt(voting.yea)). - WriteString("no", formatInt(voting.nay)). - Node() - - return marshal(proposalObj) -} - -func metaNode() *json.Node { - height := std.ChainHeight() - now := time.Now().Unix() - - return json.Builder(). - WriteString("height", strconv.FormatInt(height, 10)). - WriteString("now", strconv.FormatInt(now, 10)). - Node() -} diff --git a/contract/r/gnoswap/v1/gov/governance/assert.gno b/contract/r/gnoswap/v1/gov/governance/assert.gno deleted file mode 100644 index 920780e..0000000 --- a/contract/r/gnoswap/v1/gov/governance/assert.gno +++ /dev/null @@ -1,15 +0,0 @@ -package governance - -import "std" - -// assertCallerIsProposer panics if the caller is not the proposer of the given proposal. -func assertCallerIsProposer(proposalID int64, caller std.Address) { - proposal, exists := getProposal(proposalID) - if !exists { - panic(errProposalNotFound) - } - - if !proposal.IsProposer(caller) { - panic(errNotProposer) - } -} diff --git a/contract/r/gnoswap/v1/gov/governance/config.gno b/contract/r/gnoswap/v1/gov/governance/config.gno deleted file mode 100644 index 6527982..0000000 --- a/contract/r/gnoswap/v1/gov/governance/config.gno +++ /dev/null @@ -1,116 +0,0 @@ -package governance - -import ( - "std" - - "gno.land/r/gnoswap/access" - en "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" -) - -// Config represents the configuration of the governor contract -// All parameters in this struct can be modified through governance. -type Config struct { - // VotingStartDelay is the delay before voting starts after proposal creation (in seconds) - VotingStartDelay int64 - // VotingPeriod is the duration during which votes are collected (in seconds) - VotingPeriod int64 - // VotingWeightSmoothingDuration is the period over which voting weight is averaged - // for proposal creation and cancellation threshold calculations (in seconds) - VotingWeightSmoothingDuration int64 - // Quorum is the percentage of total GNS supply required for proposal approval - Quorum int64 - // ProposalCreationThreshold is the minimum average voting weight required to create a proposal - ProposalCreationThreshold int64 - // ExecutionDelay is the waiting period after voting ends before a proposal can be executed (in seconds) - ExecutionDelay int64 - // ExecutionWindow is the time window during which an approved proposal can be executed (in seconds) - ExecutionWindow int64 -} - -// Reconfigure updates governance configuration. -// Only admin or governance contract can call this function. -// Updates all governance parameters and emits a "Reconfigure" event. -func Reconfigure( - cur realm, - votingStartDelay int64, - votingPeriod int64, - votingWeightSmoothingDuration int64, - quorum int64, - proposalCreationThreshold int64, - executionDelay int64, - executionWindow int64, -) int64 { - // Check if system is halted before proceeding - halt.AssertIsNotHaltedGovernance() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - // Mint and distribute GNS tokens as part of the process - en.MintAndDistributeGns(cross) - - // Store previous version for event emission - previousVersion := getCurrentConfigVersion() - - // Apply the new configuration - nextVersion, newCfg := reconfigure( - votingStartDelay, - votingPeriod, - votingWeightSmoothingDuration, - quorum, - proposalCreationThreshold, - executionDelay, - executionWindow, - ) - - // Emit configuration change event with all parameters - previousRealm := std.PreviousRealm() - std.Emit( - "Reconfigure", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "votingStartDelay", formatInt(newCfg.VotingStartDelay), - "votingPeriod", formatInt(newCfg.VotingPeriod), - "votingWeightSmoothingDuration", formatInt(newCfg.VotingWeightSmoothingDuration), - "quorum", formatInt(newCfg.Quorum), - "proposalCreationThreshold", formatInt(newCfg.ProposalCreationThreshold), - "executionDelay", formatInt(newCfg.ExecutionDelay), - "executionPeriod", formatInt(newCfg.ExecutionWindow), - "newConfigVersion", formatInt(nextVersion), - "prevConfigVersion", formatInt(previousVersion), - ) - - return nextVersion -} - -// reconfigure updates the Governor's configuration. -// Creates new configuration and stores it with incremented version number. -func reconfigure( - votingStartDelay int64, - votingPeriod int64, - votingWeightSmoothingDuration int64, - quorum int64, - proposalCreationThreshold int64, - executionDelay int64, - executionWindow int64, -) (int64, Config) { - // Create new configuration with provided parameters - cfg := Config{ - VotingStartDelay: votingStartDelay, - VotingPeriod: votingPeriod, - VotingWeightSmoothingDuration: votingWeightSmoothingDuration, - Quorum: quorum, - ProposalCreationThreshold: proposalCreationThreshold, - ExecutionDelay: executionDelay, - ExecutionWindow: executionWindow, - } - - // Generate next version number - nextVersion := nextConfigVersion() - - // Store the new configuration with version - setConfig(nextVersion, cfg) - - return nextVersion, cfg -} diff --git a/contract/r/gnoswap/v1/gov/governance/consts.gno b/contract/r/gnoswap/v1/gov/governance/consts.gno deleted file mode 100644 index 9c248a7..0000000 --- a/contract/r/gnoswap/v1/gov/governance/consts.gno +++ /dev/null @@ -1,16 +0,0 @@ -package governance - -const ( - // Governance can execute multiple messages in a single proposal - // each message is a string with the following format: - // *EXE**EXE* - // To execute a message, we need to parse the message and call the corresponding function - // with the given parameters - parameterSeparator = "*EXE*" - - messageSeparator = "*GOV*" - - maxTitleLength = 255 - maxDescriptionLength = 10_000 - maxNumberOfExecution = 10 -) diff --git a/contract/r/gnoswap/v1/gov/governance/counter.gno b/contract/r/gnoswap/v1/gov/governance/counter.gno deleted file mode 100644 index 6c9fae8..0000000 --- a/contract/r/gnoswap/v1/gov/governance/counter.gno +++ /dev/null @@ -1,25 +0,0 @@ -package governance - -// Counter manages unique incrementing IDs. -type Counter struct { - id int64 -} - -// NewCounter creates a new Counter starting at 0. -func NewCounter() *Counter { - return &Counter{ - id: 0, - } -} - -// next increments and returns the next ID. -func (c *Counter) next() int64 { - c.id++ - - return c.id -} - -// Get returns the current ID without incrementing. -func (c *Counter) Get() int64 { - return c.id -} diff --git a/contract/r/gnoswap/v1/gov/governance/doc.gno b/contract/r/gnoswap/v1/gov/governance/doc.gno deleted file mode 100644 index 3fe7f3d..0000000 --- a/contract/r/gnoswap/v1/gov/governance/doc.gno +++ /dev/null @@ -1,5 +0,0 @@ -// Package governance implements proposal lifecycle management and voting. -// It supports text proposals, parameter changes, and community pool spending. -// Proposals go through creation, voting, and execution phases with configurable -// parameters for voting delays, periods, and thresholds. -package governance diff --git a/contract/r/gnoswap/v1/gov/governance/errors.gno b/contract/r/gnoswap/v1/gov/governance/errors.gno deleted file mode 100644 index b99d038..0000000 --- a/contract/r/gnoswap/v1/gov/governance/errors.gno +++ /dev/null @@ -1,37 +0,0 @@ -package governance - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errOutOfRange = errors.New("[GNOSWAP-GOVERNANCE-001] out of range for numeric value") - errInvalidInput = errors.New("[GNOSWAP-GOVERNANCE-002] invalid input") - errDataNotFound = errors.New("[GNOSWAP-GOVERNANCE-003] requested data not found") - errNotEnoughBalance = errors.New("[GNOSWAP-GOVERNANCE-004] not enough balance") - errUnableToVoteCanceledProposal = errors.New("[GNOSWAP-GOVERNANCE-005] unable to vote for canceled proposal") - errAlreadyVoted = errors.New("[GNOSWAP-GOVERNANCE-006] can not vote twice") - errNotEnoughVotingWeight = errors.New("[GNOSWAP-GOVERNANCE-007] not enough voting power") - errAlreadyCanceledProposal = errors.New("[GNOSWAP-GOVERNANCE-008] can not cancel already canceled proposal") - errUnableToCancleVotingProposal = errors.New("[GNOSWAP-GOVERNANCE-009] unable to cancel voting proposal") - errUnableToCancelProposalWithVoterEnoughDelegated = errors.New("[GNOSWAP-GOVERNANCE-010] unable to cancel proposal with voter has enough delegation") - errTextProposalNotExecutable = errors.New("[GNOSWAP-GOVERNANCE-011] can not execute text proposal") - errUnsupportedProposalType = errors.New("[GNOSWAP-GOVERNANCE-012] unsupported proposal type") - errInvalidProposalType = errors.New("[GNOSWAP-GOVERNANCE-013] invalid proposal type") - errUnableToVoteOutOfPeriod = errors.New("[GNOSWAP-GOVERNANCE-014] unable to vote out of voting period") - errInvalidMessageFormat = errors.New("[GNOSWAP-GOVERNANCE-015] invalid message format") - errProposalNotPassed = errors.New("[GNOSWAP-GOVERNANCE-016] proposal not passed") - errInvalidAddress = errors.New("[GNOSWAP-GOVERNANCE-017] invalid address") - errExecutionWindowNotStarted = errors.New("[GNOSWAP-GOVERNANCE-018] execution window not started") - errAlreadyActiveProposal = errors.New("[GNOSWAP-GOVERNANCE-019] already active proposal") - errProposalNotFound = errors.New("[GNOSWAP-GOVERNANCE-020] proposal not found") - errProposalNotExecutable = errors.New("[GNOSWAP-GOVERNANCE-021] proposal not executable") - errNotProposer = errors.New("[GNOSWAP-GOVERNANCE-022] not proposer") -) - -// makeErrorWithDetails creates an error with additional context. -func makeErrorWithDetails(err error, detail string) error { - return ufmt.Errorf("%s || %s", err.Error(), detail) -} diff --git a/contract/r/gnoswap/v1/gov/governance/getter_proposal.gno b/contract/r/gnoswap/v1/gov/governance/getter_proposal.gno deleted file mode 100644 index 10fff0b..0000000 --- a/contract/r/gnoswap/v1/gov/governance/getter_proposal.gno +++ /dev/null @@ -1,71 +0,0 @@ -package governance - -import "time" - -func mustGetProposal(proposalId int64) *Proposal { - proposal, ok := getProposal(proposalId) - if !ok { - panic(errDataNotFound) - } - - return proposal -} - -func GetProposerByProposalId(proposalId int64) string { - return mustGetProposal(proposalId).proposer.String() -} - -func GetProposalTypeByProposalId(proposalId int64) string { - return mustGetProposal(proposalId).data.proposalType.String() -} - -func GetYeaByProposalId(proposalId int64) int64 { - return mustGetProposal(proposalId).status.YesWeight() -} - -func GetNayByProposalId(proposalId int64) int64 { - return mustGetProposal(proposalId).status.NoWeight() -} - -func GetConfigVersionByProposalId(proposalId int64) int64 { - return mustGetProposal(proposalId).configVersion -} - -func GetQuorumAmountByProposalId(proposalId int64) int64 { - return mustGetProposal(proposalId).status.voteStatus.quorumAmount -} - -func GetTitleByProposalId(proposalId int64) string { - return mustGetProposal(proposalId).metadata.title -} - -func GetDescriptionByProposalId(proposalId int64) string { - return mustGetProposal(proposalId).metadata.description -} - -// GetExecutionStateByProposalId is deprecated. Use GetProposalStatusById instead. -// This function is kept for backward compatibility. -func GetExecutionStateByProposalId(proposalId int64) string { - currentAt := time.Now().Unix() - proposal := mustGetProposal(proposalId) - - return proposal.Status(currentAt) -} - -func GetLatestConfig() Config { - config, ok := getCurrentConfig() - if !ok { - panic(errDataNotFound) - } - - return config -} - -func GetConfig(configVersion int64) Config { - config, ok := getConfig(configVersion) - if !ok { - panic(errDataNotFound) - } - - return config -} diff --git a/contract/r/gnoswap/v1/gov/governance/getter_vote.gno b/contract/r/gnoswap/v1/gov/governance/getter_vote.gno deleted file mode 100644 index d470d28..0000000 --- a/contract/r/gnoswap/v1/gov/governance/getter_vote.gno +++ /dev/null @@ -1,32 +0,0 @@ -package governance - -import ( - "std" -) - -func GetVoteWeight(proposalID int64, address std.Address) int64 { - proposalUserVotingInfo, ok := getProposalUserVotingInfo(proposalID, address) - if !ok { - panic(errDataNotFound) - } - - return proposalUserVotingInfo.VotedWeight() -} - -func GetVotedHeight(proposalID int64, address std.Address) int64 { - proposalUserVotingInfo, ok := getProposalUserVotingInfo(proposalID, address) - if !ok { - panic(errDataNotFound) - } - - return proposalUserVotingInfo.votedHeight -} - -func GetVotedAt(proposalID int64, address std.Address) int64 { - proposalUserVotingInfo, ok := getProposalUserVotingInfo(proposalID, address) - if !ok { - panic(errDataNotFound) - } - - return proposalUserVotingInfo.votedAt -} diff --git a/contract/r/gnoswap/v1/gov/governance/gnomod.toml b/contract/r/gnoswap/v1/gov/governance/gnomod.toml deleted file mode 100644 index 50d0119..0000000 --- a/contract/r/gnoswap/v1/gov/governance/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/gov/governance" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/gov/governance/governance_execute.gno b/contract/r/gnoswap/v1/gov/governance/governance_execute.gno deleted file mode 100644 index d23d45e..0000000 --- a/contract/r/gnoswap/v1/gov/governance/governance_execute.gno +++ /dev/null @@ -1,253 +0,0 @@ -package governance - -import ( - "std" - "time" - - en "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" -) - -// Execute executes an approved proposal. -// -// Processes and implements governance decisions after successful voting. -// Enforces timelock delays and execution windows for security. -// Anyone can trigger execution to ensure decentralization. -// -// Parameters: -// - proposalID: ID of the proposal to execute -// -// Requirements: -// - Proposal must have passed (majority yes votes) -// - Quorum must be reached (50% of xGNS supply) -// - Timelock period must have elapsed (1 day default) -// - Must be within execution window (30 days default) -// - Proposal not already executed or cancelled -// -// Effects: -// - Executes proposal actions (parameter changes, treasury transfers) -// - Marks proposal as executed -// - Emits execution event -// - Refunds gas costs from treasury -// -// Returns executed proposal ID. -// Callable by anyone once proposal is executable. -func Execute(cur realm, proposalID int64) int64 { - // Check if execution is allowed (system not halted for execution) - halt.AssertIsNotHaltedGovernance() - - // Get caller information and current blockchain state - caller := std.PreviousRealm().Address() - currentHeight := std.ChainHeight() - currentAt := time.Now().Unix() - - // Mint and distribute GNS tokens as part of the execution process - en.MintAndDistributeGns(cross) - - // Attempt to execute the proposal with current context - proposal, err := executeProposal( - proposalID, - currentAt, - currentHeight, - caller, - ) - if err != nil { - panic(err) - } - - // Emit execution event for tracking and auditing - previousRealm := std.PreviousRealm() - std.Emit( - "Execute", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "proposalId", formatInt(proposalID), - ) - - return proposal.ID() -} - -// executeProposal handles core logic of proposal execution. -func executeProposal( - proposalID int64, - executedAt int64, - executedHeight int64, - executedBy std.Address, -) (*Proposal, error) { - // Retrieve the proposal from storage - proposal, ok := getProposal(proposalID) - if !ok { - return nil, errDataNotFound - } - - // Text proposals cannot be executed (they are informational only) - if proposal.IsTextType() { - return nil, errTextProposalNotExecutable - } - - // Verify proposal is in executable state (timing and voting requirements met) - if !proposal.IsExecutable(executedAt) { - return nil, errProposalNotExecutable - } - - // Mark proposal as executed in its status - err := proposal.execute(executedAt, executedHeight, executedBy) - if err != nil { - return nil, err - } - - // Create parameter registry for handling execution actions - parameterRegistry := createParameterHandlers() - - // Execute proposal based on its type - switch proposal.Type() { - case CommunityPoolSpend: - // Execute community pool spending (token transfers) - err = executeCommunityPoolSpend(proposal, parameterRegistry, executedAt, executedHeight, executedBy) - if err != nil { - return nil, err - } - case ParameterChange: - // Execute parameter changes (governance configuration updates) - err = executeParameterChange(proposal, parameterRegistry, executedAt, executedHeight, executedBy) - if err != nil { - return nil, err - } - } - - return proposal, nil -} - -// Cancel cancels a proposal in upcoming status. -// -// Allows proposers to withdraw their proposals before voting begins. -// Prevents accidental or malicious proposals from reaching vote. -// Safety mechanism for proposal errors or changed circumstances. -// -// Parameters: -// - proposalID: ID of the proposal to cancel -// -// Requirements: -// - Must be called by original proposer -// - Proposal must be in "upcoming" status -// - Voting must not have started yet -// - Proposal not already cancelled or executed -// -// Effects: -// - Sets proposal status to "cancelled" -// - Prevents future voting or execution -// - Emits cancellation event -// - Frees up proposer's proposal slot -// -// Returns cancelled proposal ID. -// Only callable by original proposer before voting begins. -func Cancel(cur realm, proposalID int64) int64 { - halt.AssertIsNotHaltedGovernance() - - caller := std.PreviousRealm().Address() - assertCallerIsProposer(proposalID, caller) - - // Get current blockchain state and caller information - currentHeight := std.ChainHeight() - currentAt := time.Now().Unix() - - // Mint and distribute GNS tokens as part of the process - en.MintAndDistributeGns(cross) - - // Attempt to cancel the proposal - proposal, err := cancel(proposalID, currentAt, currentHeight, caller) - if err != nil { - panic(err) - } - - // Emit cancellation event for tracking - previousRealm := std.PreviousRealm() - std.Emit( - "Cancel", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "proposalId", formatInt(proposalID), - ) - - return proposal.ID() -} - -// cancel handles core logic of proposal cancellation. -// Validates proposal state and updates status to canceled. -func cancel(proposalID, canceledAt, canceledHeight int64, canceledBy std.Address) (proposal *Proposal, err error) { - // Retrieve the proposal from storage - proposal, ok := getProposal(proposalID) - if !ok { - return nil, errDataNotFound - } - - // Attempt to cancel the proposal (this validates cancellation conditions) - err = proposal.cancel(canceledAt, canceledHeight, canceledBy) - if err != nil { - return nil, err - } - - return proposal, nil -} - -// executeCommunityPoolSpend executes community pool spending proposals. -// Handles token transfers from community pool to specified recipients. -func executeCommunityPoolSpend( - proposal *Proposal, - parameterRegistry *ParameterRegistry, - executedAt int64, - executedHeight int64, - executedBy std.Address, -) error { - // Verify token registration for community pool spending - if proposal.IsCommunityPoolSpendType() { - common.MustRegistered(proposal.CommunityPoolSpendTokenPath()) - } - - // Execute all parameter changes defined in the proposal - parameterChangesInfos := proposal.data.execution.ParameterChangesInfos() - for _, parameterChangeInfo := range parameterChangesInfos { - // Get the appropriate handler for this parameter change - handler, err := parameterRegistry.handler(parameterChangeInfo.pkgPath, parameterChangeInfo.function) - if err != nil { - return err - } - - // Execute the parameter change with provided parameters - err = handler.Execute(parameterChangeInfo.params) - if err != nil { - return err - } - } - - return nil -} - -// executeParameterChange executes parameter change proposals. -// Handles governance configuration updates and system parameter modifications. -func executeParameterChange( - proposal *Proposal, - parameterRegistry *ParameterRegistry, - executedAt int64, - executedHeight int64, - executedBy std.Address, -) error { - // Execute all parameter changes defined in the proposal - parameterChangesInfos := proposal.data.execution.ParameterChangesInfos() - for _, parameterChangeInfo := range parameterChangesInfos { - // Get the appropriate handler for this parameter change - handler, err := parameterRegistry.handler(parameterChangeInfo.pkgPath, parameterChangeInfo.function) - if err != nil { - return err - } - - // Execute the parameter change with provided parameters - err = handler.Execute(parameterChangeInfo.params) - if err != nil { - return err - } - } - - return nil -} diff --git a/contract/r/gnoswap/v1/gov/governance/governance_propose.gno b/contract/r/gnoswap/v1/gov/governance/governance_propose.gno deleted file mode 100644 index 3dafd6a..0000000 --- a/contract/r/gnoswap/v1/gov/governance/governance_propose.gno +++ /dev/null @@ -1,461 +0,0 @@ -package governance - -import ( - "std" - "time" - - "gno.land/r/gnoswap/gns" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/gov/staker" -) - -// ProposeText creates a text proposal for community discussion. -// -// Signal proposals for non-binding community sentiment. -// Used for policy discussions, roadmap planning, and community feedback. -// No on-chain execution, serves as formal governance record. -// -// Parameters: -// - title: Short, descriptive proposal title (max 100 chars recommended) -// - description: Full proposal content with rationale and context -// -// Requirements: -// - Caller must hold minimum 1,000 GNS tokens -// - No other active proposal from same address -// - Title and description must be non-empty -// -// Process: -// - 1 day delay before voting starts -// - 7 days voting period -// - Simple majority decides outcome -// - No execution phase (signal only) -// -// Returns new proposal ID. -func ProposeText( - cur realm, - title string, - description string, -) (newProposalId int64) { - halt.AssertIsNotHaltedGovernance() - - callerAddress := std.PreviousRealm().Address() - - createdAt := time.Now().Unix() - createdHeight := std.ChainHeight() - gnsBalance := gns.BalanceOf(callerAddress) - - config, ok := getCurrentConfig() - if !ok { - panic(errDataNotFound) - } - - // Check if caller already has an active proposal (one proposal per address) - if hasActiveProposal(callerAddress, createdAt) { - panic(errAlreadyActiveProposal) - } - - // Get snapshot of voting weights for proposal creation - userVotes, maxVotingWeight, err := getUserVotingInfoSnapshot( - createdAt, - config.VotingWeightSmoothingDuration, - ) - if err != nil { - panic(err) - } - - // Create the text proposal with metadata - proposal, err := createProposal( - Text, - config, - maxVotingWeight, - NewProposalMetadata(title, description), - NewProposalTextData(), - callerAddress, - gnsBalance, - createdAt, - createdHeight, - ) - if err != nil { - panic(err) - } - - // Store voting information for the proposal - success := updateProposalUserVotes(proposal, userVotes) - if !success { - panic(errDataNotFound) - } - - // Emit proposal creation event for indexing and tracking - previousRealm := std.PreviousRealm() - std.Emit( - "ProposeText", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "title", title, - "description", description, - "proposalId", formatInt(proposal.ID()), - "quorumAmount", formatInt(proposal.VotingQuorumAmount()), - "maxVotingWeight", formatInt(proposal.VotingMaxWeight()), - "configVersion", formatInt(proposal.ConfigVersion()), - "createdAt", formatInt(proposal.CreatedAt()), - ) - - return proposal.ID() -} - -// ProposeCommunityPoolSpend creates a treasury disbursement proposal. -// -// Allocates community pool funds for approved purposes. -// Supports grants, development funding, and protocol incentives. -// Automatic transfer on execution if approved. -// -// Parameters: -// - title: Proposal title describing purpose -// - description: Detailed justification and budget breakdown -// - to: Recipient address for funds -// - tokenPath: Token contract path (e.g., "gno.land/r/gnoswap/gns") -// - amount: Amount to transfer (in smallest unit) -// -// Requirements: -// - Caller must hold minimum 1,000 GNS tokens -// - Sufficient balance in community pool -// - Valid recipient address -// - Supported token type -// -// Security: -// - Enforces timelock after approval -// - Single transfer per proposal -// - Tracks all disbursements on-chain -// -// Returns new proposal ID. -func ProposeCommunityPoolSpend( - cur realm, - title string, - description string, - to std.Address, - tokenPath string, - amount int64, -) (newProposalId int64) { - halt.AssertIsNotHaltedGovernance() - halt.AssertIsNotHaltedWithdraw() - - callerAddress := std.PreviousRealm().Address() - - createdAt := time.Now().Unix() - createdHeight := std.ChainHeight() - gnsBalance := gns.BalanceOf(callerAddress) - - config, ok := getCurrentConfig() - if !ok { - panic(errDataNotFound) - } - - // Check if caller already has an active proposal (one proposal per address) - if hasActiveProposal(callerAddress, createdAt) { - panic(errAlreadyActiveProposal) - } - - // Get snapshot of voting weights for proposal creation - userVotes, maxVotingWeight, err := getUserVotingInfoSnapshot( - createdAt, - config.VotingWeightSmoothingDuration, - ) - if err != nil { - panic(err) - } - - // Create the community pool spend proposal with execution data - proposal, err := createProposal( - CommunityPoolSpend, - config, - maxVotingWeight, - NewProposalMetadata(title, description), - NewProposalCommunityPoolSpendData(tokenPath, to, amount, COMMUNITY_POOL_PATH), - callerAddress, - gnsBalance, - createdAt, - createdHeight, - ) - if err != nil { - panic(err) - } - - // Store voting information for the proposal - success := updateProposalUserVotes(proposal, userVotes) - if !success { - panic(errDataNotFound) - } - - // Emit proposal creation event for indexing and tracking - previousRealm := std.PreviousRealm() - std.Emit( - "ProposeCommunityPoolSpend", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "title", title, - "description", description, - "to", to.String(), - "tokenPath", tokenPath, - "amount", formatInt(amount), - "proposalId", formatInt(proposal.ID()), - "quorumAmount", formatInt(proposal.VotingQuorumAmount()), - "maxVotingWeight", formatInt(proposal.VotingMaxWeight()), - "configVersion", formatInt(proposal.ConfigVersion()), - "createdAt", formatInt(proposal.CreatedAt()), - ) - - return proposal.ID() -} - -// ProposeParameterChange creates a protocol parameter update proposal. -// -// Modifies system parameters through governance. -// Supports multiple parameter changes in single proposal. -// Changes apply atomically on execution. -// -// Parameters: -// - title: Clear description of changes -// - description: Rationale and impact analysis -// - numToExecute: Number of parameter changes -// - executions: JSON array of changes, each containing: -// - target: Contract address to modify -// - function: Function name to call -// - params: Parameters for the function -// -// Example executions format: -// -// [{ -// "target": "gno.land/r/gnoswap/v1/gov", -// "function": "SetVotingPeriod", -// "params": ["604800"] -// }] -// -// Requirements: -// - Valid JSON format for executions -// - Target contracts must exist -// - Functions must be governance-callable -// - Parameters must match function signatures -// -// Returns new proposal ID. -func ProposeParameterChange( - cur realm, - title string, - description string, - numToExecute int64, - executions string, -) (newProposalId int64) { - halt.AssertIsNotHaltedGovernance() - - callerAddress := std.PreviousRealm().Address() - - createdAt := time.Now().Unix() - createdHeight := std.ChainHeight() - gnsBalance := gns.BalanceOf(callerAddress) - - config, ok := getCurrentConfig() - if !ok { - panic(errDataNotFound) - } - - // Check if caller already has an active proposal (one proposal per address) - if hasActiveProposal(callerAddress, createdAt) { - panic(errAlreadyActiveProposal) - } - - // Get snapshot of voting weights for proposal creation - userVotes, maxVotingWeight, err := getUserVotingInfoSnapshot( - createdAt, - config.VotingWeightSmoothingDuration, - ) - if err != nil { - panic(err) - } - - // Create the parameter change proposal with execution data - proposal, err := createProposal( - ParameterChange, - config, - maxVotingWeight, - NewProposalMetadata(title, description), - NewProposalExecutionData(numToExecute, executions), - callerAddress, - gnsBalance, - createdAt, - createdHeight, - ) - if err != nil { - panic(err) - } - - // Store voting information for the proposal - success := updateProposalUserVotes(proposal, userVotes) - if !success { - panic(errDataNotFound) - } - - // Emit proposal creation event for indexing and tracking - previousRealm := std.PreviousRealm() - std.Emit( - "ProposeParameterChange", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "title", title, - "description", description, - "numToExecute", formatInt(numToExecute), - "executions", executions, - "proposalId", formatInt(proposal.ID()), - "quorumAmount", formatInt(proposal.VotingQuorumAmount()), - "maxVotingWeight", formatInt(proposal.VotingMaxWeight()), - "configVersion", formatInt(proposal.ConfigVersion()), - "createdAt", formatInt(proposal.CreatedAt()), - ) - - return proposal.ID() -} - -// createProposal handles proposal creation logic. -// Validates input data, checks proposer eligibility, and creates proposal object. -func createProposal( - proposalType ProposalType, - config Config, - maxVotingWeight int64, - proposalMetadata *ProposalMetadata, - proposalData *ProposalData, - proposerAddress std.Address, - proposerGnsBalance int64, - createdAt int64, - createdHeight int64, -) (*Proposal, error) { - // Validate proposal metadata (title and description) - err := proposalMetadata.Validate() - if err != nil { - return nil, err - } - - // Validate proposal data (type-specific validation) - err = proposalData.Validate() - if err != nil { - return nil, err - } - - // Check if proposer has enough GNS balance to create proposal - if proposerGnsBalance < config.ProposalCreationThreshold { - return nil, errNotEnoughBalance - } - - // Generate unique proposal ID - proposalID := nextProposalID() - - // Create proposal status with voting schedule and requirements - proposalStatus := NewProposalStatus( - config, - maxVotingWeight, - proposalType.IsExecutable(), - createdAt, - ) - - // Get current configuration version for tracking - configVersion := getCurrentConfigVersion() - - // Create the proposal object - proposal := NewProposal( - proposalID, - proposalStatus, - proposalMetadata, - proposalData, - proposerAddress, - configVersion, - createdAt, - createdHeight, - ) - - // Store the proposal in state - success := addProposal(proposal) - if !success { - return nil, errDataNotFound - } - - return proposal, nil -} - -// getUserVotingInfoSnapshot retrieves voting information snapshot for proposal creation. -// Calculates voting weights at specific time point for fair voting. -func getUserVotingInfoSnapshot( - current, - smoothingPeriod int64, -) (map[string]*VotingInfo, int64, error) { - // Calculate snapshot time by going back by smoothing period - snapshotTime := current - smoothingPeriod - - var votingInfos map[string]*VotingInfo - var maxVotingWeight int64 - var ok bool - - // Use custom snapshot function if available - if getUserVotingInfoSnapshotFn != nil { - votingInfos, maxVotingWeight, ok = getUserVotingInfoSnapshotFn(snapshotTime) - } else { - votingInfos, maxVotingWeight, ok = getUserVotingInfotWithDelegationSnapshots(snapshotTime) - } - - if !ok || maxVotingWeight <= 0 { - return votingInfos, maxVotingWeight, errNotEnoughVotingWeight - } - - return votingInfos, maxVotingWeight, nil -} - -// getUserVotingInfotWithDelegationSnapshots retrieves voting info from staker delegation snapshots. -// Integrates with staker contract to get actual delegation amounts. -func getUserVotingInfotWithDelegationSnapshots( - snapshotTime int64, -) (map[string]*VotingInfo, int64, bool) { - // Get delegation snapshots from staker contract - delegationSnapshots, ok := staker.GetDelegationSnapshots(snapshotTime) - if !ok { - return nil, 0, false - } - - maxVotingWeight := int64(0) - userVotes := make(map[string]*VotingInfo) - - // Process each delegation snapshot - for _, snapshot := range delegationSnapshots { - delegatorAddress := snapshot.DelegatorAddress() - delegationAmount := snapshot.DelegationAmount() - - // Create voting info for each delegator - userVotes[delegatorAddress.String()] = NewVotingInfo(delegationAmount, delegatorAddress) - maxVotingWeight += delegationAmount - } - - return userVotes, maxVotingWeight, true -} - -// updateProposalUserVotes stores voting information for specific proposal. -// Links voting eligibility data to proposal for later use during voting. -func updateProposalUserVotes( - proposal *Proposal, - userVotingInfos map[string]*VotingInfo, -) bool { - // Store the voting information mapping for this proposal - proposalUserVotingInfos.Set(formatInt(proposal.ID()), userVotingInfos) - - return true -} - -// hasActiveProposal checks if address already has active proposal. -// Enforces one-proposal-per-address rule to prevent spam. -func hasActiveProposal(proposerAddress std.Address, current int64) bool { - // Get all proposals for this address - proposals := getUserProposals(proposerAddress) - - // Check if any proposal is still active - for _, proposal := range proposals { - if proposal.IsActive(current) { - return true - } - } - - return false -} diff --git a/contract/r/gnoswap/v1/gov/governance/governance_vote.gno b/contract/r/gnoswap/v1/gov/governance/governance_vote.gno deleted file mode 100644 index cedd8eb..0000000 --- a/contract/r/gnoswap/v1/gov/governance/governance_vote.gno +++ /dev/null @@ -1,121 +0,0 @@ -package governance - -import ( - "std" - "time" - - en "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" -) - -// Vote casts a vote on a proposal. -// -// Records on-chain vote with weight based on delegated xGNS. -// Uses 24-hour average voting power to prevent manipulation. -// Votes are final and cannot be changed. -// -// Parameters: -// - proposalID: ID of the proposal to vote on -// - yes: true for yes vote, false for no vote -// -// Vote Weight Calculation: -// - Based on delegated xGNS amount -// - 24-hour average before proposal creation -// - Prevents flash loan attacks -// - Includes both self-stake and delegations received -// -// Requirements: -// - Proposal must be in voting period -// - Voter must have xGNS delegated -// - Cannot vote twice on same proposal -// - Voting period typically 7 days -// -// Returns voting weight used as string. -func Vote(cur realm, proposalID int64, yes bool) string { - halt.AssertIsNotHaltedGovernance() - - // Get current blockchain state and caller information - currentHeight := std.ChainHeight() - currentAt := time.Now() - - // Mint and distribute GNS tokens as part of the voting process - en.MintAndDistributeGns(cross) - - // Extract voter address from realm context - voter := std.PreviousRealm().Address() - - // Process the vote and get updated vote tallies - userVote, totalYesVoteWeight, totalNoVoteWeight, err := vote( - proposalID, - voter, - yes, - currentHeight, - currentAt.Unix(), - ) - if err != nil { - panic(err) - } - - // Emit voting event for tracking and transparency - previousRealm := std.PreviousRealm() - std.Emit( - "Vote", - "prevAddr", previousRealm.Address().String(), - "prevPkgPath", previousRealm.PkgPath(), - "proposalId", formatInt(proposalID), - "voter", voter.String(), - "yes", userVote.VotingType(), - "voteWeight", formatInt(userVote.VotedWeight()), - "voteYes", formatInt(totalYesVoteWeight), - "voteNo", formatInt(totalNoVoteWeight), - ) - - return formatInt(userVote.VotedWeight()) -} - -// vote handles core voting logic. -func vote( - proposalID int64, - voterAddress std.Address, - votedYes bool, - votedHeight, - votedAt int64, -) (*VotingInfo, int64, int64, error) { - // Retrieve the proposal from storage - proposal, ok := getProposal(proposalID) - if !ok { - return nil, 0, 0, makeErrorWithDetails(errDataNotFound, "not found proposal") - } - - // Check if current time is within voting period - if !proposal.IsVotingPeriod(votedAt) { - return nil, 0, 0, makeErrorWithDetails(errUnableToVoteOutOfPeriod, "can not vote out of voting period") - } - - // Get user's voting information for this proposal - userVote, ok := getProposalUserVotingInfo(proposalID, voterAddress) - if !ok { - return nil, 0, 0, makeErrorWithDetails(errDataNotFound, "not found user's voting info") - } - - // Check if user has voting weight available - votingWeight := userVote.AvailableVoteWeight() - if votingWeight <= 0 { - return nil, 0, 0, makeErrorWithDetails(errNotEnoughVotingWeight, "no voting weight") - } - - // Record the vote in user's voting info (this also prevents double voting) - err := userVote.vote(votedYes, votingWeight, votedHeight, votedAt) - if err != nil { - return nil, 0, 0, err - } - - // Update proposal vote tallies - err = proposal.vote(votedYes, votingWeight) - if err != nil { - return nil, 0, 0, err - } - - // Return updated vote information and current tallies - return userVote, proposal.VotingYesWeight(), proposal.VotingNoWeight(), nil -} diff --git a/contract/r/gnoswap/v1/gov/governance/parameter_registry.gno b/contract/r/gnoswap/v1/gov/governance/parameter_registry.gno deleted file mode 100644 index 61d4558..0000000 --- a/contract/r/gnoswap/v1/gov/governance/parameter_registry.gno +++ /dev/null @@ -1,529 +0,0 @@ -package governance - -import ( - "std" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - en "gno.land/r/gnoswap/emission" - cp "gno.land/r/gnoswap/v1/community_pool" - - pl "gno.land/r/gnoswap/v1/pool" - pf "gno.land/r/gnoswap/v1/protocol_fee" - rr "gno.land/r/gnoswap/v1/router" - sr "gno.land/r/gnoswap/v1/staker" - - "gno.land/r/gnoswap/halt" -) - -// Package paths -const ( - GNS_PATH = "gno.land/r/gnoswap/gns" - HALT_PATH = "gno.land/r/gnoswap/halt" - ACCESS_PATH = "gno.land/r/gnoswap/access" - EMISSION_PATH = "gno.land/r/gnoswap/emission" - COMMON_PATH = "gno.land/r/gnoswap/v1/common" - POOL_PATH = "gno.land/r/gnoswap/v1/pool" - ROUTER_PATH = "gno.land/r/gnoswap/v1/router" - STAKER_PATH = "gno.land/r/gnoswap/v1/staker" - PROTOCOL_FEE_PATH = "gno.land/r/gnoswap/v1/protocol_fee" - COMMUNITY_POOL_PATH = "gno.land/r/gnoswap/v1/community_pool" - GOV_GOVERNANCE_PATH = "gno.land/r/gnoswap/v1/gov/governance" -) - -// ParameterHandler interface defines the contract for parameter execution handlers. -// Each handler is responsible for executing specific parameter changes in the system. -type ParameterHandler interface { - // Execute processes the parameters and applies the changes to the system - Execute(params []string) error -} - -// ParameterHandlerOptions contains the configuration and execution logic for a parameter handler. -// This struct encapsulates all information needed to identify and execute a parameter change. -type ParameterHandlerOptions struct { - pkgPath string // Package path of the target contract - function string // Function name to be called - paramCount int // Expected number of parameters - handlerFunc func([]string) error // Function that executes the parameter change -} - -// HandlerKey generates a unique key for this handler based on package path and function name. -// -// Returns: -// - string: unique identifier for the handler -func (h *ParameterHandlerOptions) HandlerKey() string { - return makeHandlerKey(h.pkgPath, h.function) -} - -// Execute validates parameter count and executes the handler function. -// This method ensures the correct number of parameters are provided before execution. -// -// Parameters: -// - params: slice of string parameters to pass to the handler -// -// Returns: -// - error: execution error if parameter count mismatch or handler execution fails -func (h *ParameterHandlerOptions) Execute(params []string) error { - // Validate parameter count matches expected count - if len(params) != h.paramCount { - return ufmt.Errorf("expected %d parameters, got %d", h.paramCount, len(params)) - } - - // Create realm context function and execute handler - fn := func(cur realm) error { - return h.handlerFunc(params) - } - - return fn(cross) -} - -// NewParameterHandlerOptions creates a new parameter handler with the specified configuration. -// -// Parameters: -// - pkgPath: package path of the target contract -// - function: function name to be called -// - paramCount: expected number of parameters -// - handlerFunc: function that executes the parameter change -// -// Returns: -// - ParameterHandler: configured parameter handler interface -func NewParameterHandlerOptions( - pkgPath, - function string, - paramCount int, - handlerFunc func([]string) error, -) ParameterHandler { - return &ParameterHandlerOptions{ - pkgPath: pkgPath, - function: function, - paramCount: paramCount, - handlerFunc: handlerFunc, - } -} - -// ParameterRegistry manages the collection of parameter handlers for governance execution. -// This registry allows proposals to execute parameter changes across different system contracts. -type ParameterRegistry struct { - handlers *avl.Tree // Tree storing handler configurations keyed by package:function -} - -// register adds a new parameter handler to the registry. -// Each handler is identified by a unique combination of package path and function name. -// -// Parameters: -// - handler: parameter handler configuration to register -func (r *ParameterRegistry) register(handler ParameterHandlerOptions) { - r.handlers.Set(handler.HandlerKey(), handler) -} - -// handler retrieves a parameter handler by package path and function name. -// This method is used during proposal execution to find the appropriate handler. -// -// Parameters: -// - pkgPath: package path of the target contract -// - function: function name to be called -// -// Returns: -// - ParameterHandler: the matching parameter handler -// - error: error if handler not found or casting fails -func (r *ParameterRegistry) handler(pkgPath, function string) (ParameterHandler, error) { - // Generate lookup key - key := makeHandlerKey(pkgPath, function) - - // Retrieve handler from registry - h, exists := r.handlers.Get(key) - if !exists { - return nil, ufmt.Errorf("handler not found for %s", key) - } - - // Cast to correct type - handler, ok := h.(ParameterHandlerOptions) - if !ok { - return nil, ufmt.Errorf("failed to cast handler %s to ParameterHandler", key) - } - - return &handler, nil -} - -// NewParameterRegistry creates a new empty parameter registry. -// -// Returns: -// - *ParameterRegistry: new registry instance -func NewParameterRegistry() *ParameterRegistry { - return &ParameterRegistry{handlers: avl.NewTree()} -} - -// makeHandlerKey creates a unique identifier for a handler based on package path and function. -// -// Parameters: -// - pkgPath: package path of the target contract -// - function: function name to be called -// -// Returns: -// - string: unique key in format "pkgPath:function" -func makeHandlerKey(pkgPath, function string) string { - return ufmt.Sprintf("%s:%s", pkgPath, function) -} - -// createParameterHandlers initializes and configures all supported parameter handlers. -// This function defines all the parameter changes that can be executed through governance proposals. -// It covers configuration changes for various system components including pools, staking, fees, etc. -// -// Returns: -// - *ParameterRegistry: fully configured registry with all supported handlers -func createParameterHandlers() *ParameterRegistry { - registry := NewParameterRegistry() - - // Define all handler configurations for different system components - handlers := []*ParameterHandlerOptions{ - // Access control - { - pkgPath: ACCESS_PATH, - function: "UpdateSwapWhiteList", - paramCount: 1, - handlerFunc: func(params []string) error { - // Update swap whitelist - access.UpdateSwapWhiteList(cross, std.Address(params[0])) - - return nil - }, - }, - { - pkgPath: ACCESS_PATH, - function: "RemoveFromSwapWhiteList", - paramCount: 1, - handlerFunc: func(params []string) error { - // Remove from swap whitelist - access.RemoveFromSwapWhiteList(cross, std.Address(params[0])) - - return nil - }, - }, - // Community pool token transfers - { - pkgPath: COMMUNITY_POOL_PATH, - function: "TransferToken", - paramCount: 3, - handlerFunc: func(params []string) error { - // Transfer tokens from community pool to specified address - cp.TransferToken( - cross, - params[0], // pkgPath - std.Address(params[1]), // to - parseNumber(params[2], kindInt64).(int64), // amount - ) - - return nil - }, - }, - // Emission distribution configuration - { - pkgPath: EMISSION_PATH, - function: "SetDistributionStartTime", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set distribution start time - en.SetDistributionStartTime(cross, parseInt64(params[0])) - - return nil - }, - }, - { - pkgPath: EMISSION_PATH, - function: "ChangeDistributionPct", - paramCount: 8, - handlerFunc: func(params []string) error { - // Parse distribution targets and percentages - target01 := parseNumber(params[0], kindInt).(int) // target01 - pct01 := parseNumber(params[1], kindInt64).(int64) // pct01 - target02 := parseNumber(params[2], kindInt).(int) // target02 - pct02 := parseNumber(params[3], kindInt64).(int64) // pct02 - target03 := parseNumber(params[4], kindInt).(int) // target03 - pct03 := parseNumber(params[5], kindInt64).(int64) // pct03 - target04 := parseNumber(params[6], kindInt).(int) // target04 - pct04 := parseNumber(params[7], kindInt64).(int64) // pct04 - - // Update emission distribution percentages - en.ChangeDistributionPct( - cross, - target01, // target01 - pct01, // pct01 - target02, // target02 - pct02, // pct02 - target03, // target03 - pct03, // pct03 - target04, // target04 - pct04, // pct04 - ) - - return nil - }, - }, - // Governance configuration changes - { - pkgPath: GOV_GOVERNANCE_PATH, - function: "Reconfigure", - paramCount: 7, - handlerFunc: func(params []string) error { - // Parse governance configuration parameters - votingStartDelay := parseInt64(params[0]) - votingPeriod := parseInt64(params[1]) - votingWeightSmoothingDuration := parseInt64(params[2]) - quorum := parseInt64(params[3]) - proposalCreationThreshold := parseInt64(params[4]) - executionDelay := parseInt64(params[5]) - executionWindow := parseInt64(params[6]) - - // Reconfigure governance parameters through governance process - Reconfigure( - cross, - votingStartDelay, - votingPeriod, - votingWeightSmoothingDuration, - quorum, - proposalCreationThreshold, - executionDelay, - executionWindow, - ) - - return nil - }, - }, - - // Pool protocol fee configuration - { - pkgPath: POOL_PATH, - function: "CollectProtocol", - paramCount: 6, - handlerFunc: func(params []string) error { - // Collect protocol fees - pl.CollectProtocol( - cross, - params[0], // token0Path - params[1], // token1Path - uint32(parseUint64(params[2])), // fee - std.Address(params[3]), // recipient - params[4], // amount0Requested - params[5], // amount1Requested - ) - - return nil - }, - }, - { - pkgPath: POOL_PATH, - function: "SetFeeProtocol", - paramCount: 2, - handlerFunc: func(params []string) error { - // Parse and validate fee protocol values - feeProtocol0 := parseInt64(params[0]) - feeProtocol1 := parseInt64(params[1]) - - // Validate fee protocol values are within uint8 range - if feeProtocol0 > 255 { - panic(ufmt.Sprintf("feeProtocol0 out of range: %d", feeProtocol0)) - } - - if feeProtocol1 > 255 { - panic(ufmt.Sprintf("feeProtocol1 out of range: %d", feeProtocol1)) - } - - // Set protocol fee percentages - pl.SetFeeProtocol( - cross, - uint8(feeProtocol0), // feeProtocol0 - uint8(feeProtocol1), // feeProtocol1 - ) - - return nil - }, - }, - // Pool creation fee - { - pkgPath: POOL_PATH, - function: "SetPoolCreationFee", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set fee required to create new pools - pl.SetPoolCreationFee(cross, parseInt64(params[0])) // fee - return nil - }, - }, - // Pool withdrawal fee - { - pkgPath: POOL_PATH, - function: "SetWithdrawalFee", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set fee for withdrawing from pools - pl.SetWithdrawalFee(cross, parseUint64(params[0])) // fee - return nil - }, - }, - - // Protocol fee distribution - { - pkgPath: PROTOCOL_FEE_PATH, - function: "SetDevOpsPct", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set percentage of protocol fees going to development operations - pf.SetDevOpsPct(cross, parseInt64(params[0])) // pct - return nil - }, - }, - - // Router swap fee - { - pkgPath: ROUTER_PATH, - function: "SetSwapFee", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set fee charged for token swaps - rr.SetSwapFee(cross, parseUint64(params[0])) // fee - return nil - }, - }, - - // Staker configuration handlers - { - pkgPath: STAKER_PATH, - function: "SetDepositGnsAmount", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set minimum GNS amount required for staking deposits - sr.SetDepositGnsAmount(cross, parseInt64(params[0])) // amount - return nil - }, - }, - { - pkgPath: STAKER_PATH, - function: "SetMinimumRewardAmount", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set minimum GNS amount required for staking deposits - sr.SetMinimumRewardAmount(cross, parseInt64(params[0])) // amount - return nil - }, - }, - { - pkgPath: STAKER_PATH, - function: "SetTokenMinimumRewardAmount", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set minimum GNS amount required for staking deposits - // params[0] is a string in the format "tokenPath:amount" - sr.SetTokenMinimumRewardAmount(cross, params[0]) // amount - return nil - }, - }, - { - pkgPath: STAKER_PATH, - function: "SetPoolTier", - paramCount: 2, - handlerFunc: func(params []string) error { - // Assign tier level to a specific pool - sr.SetPoolTier( - cross, - params[0], // pool - parseUint64(params[1]), // tier - ) - return nil - }, - }, - { - pkgPath: STAKER_PATH, - function: "ChangePoolTier", - paramCount: 2, - handlerFunc: func(params []string) error { - // Change existing pool's tier level - sr.ChangePoolTier( - cross, - params[0], // pool - parseUint64(params[1]), // tier - ) - return nil - }, - }, - { - pkgPath: STAKER_PATH, - function: "RemovePoolTier", - paramCount: 1, - handlerFunc: func(params []string) error { - // Remove tier assignment from a pool - sr.RemovePoolTier(cross, params[0]) // pool - return nil - }, - }, - { - pkgPath: STAKER_PATH, - function: "SetUnStakingFee", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set fee charged for unstaking operations - fee := parseInt64(params[0]) - sr.SetUnStakingFee(cross, fee) - return nil - }, - }, - { - pkgPath: STAKER_PATH, - function: "SetWarmUp", - paramCount: 2, - handlerFunc: func(params []string) error { - // Set warm-up period configuration for staking - percent := parseInt64(params[0]) - block := parseNumber(params[1], kindInt64).(int64) - sr.SetWarmUp(cross, percent, block) - - return nil - }, - }, - - // System halt controls - { - pkgPath: HALT_PATH, - function: "SetHaltLevel", - paramCount: 1, - handlerFunc: func(params []string) error { - // Set system-wide halt status - halt.SetHaltLevel(cross, halt.HaltLevel(params[0])) // true = halt, false = no halt - - return nil - }, - }, - { - pkgPath: HALT_PATH, - function: "SetOperationStatus", - paramCount: 2, - handlerFunc: func(params []string) error { - // Enable or disable specific operation types - opType := halt.OpType(params[0]) - allowed := parseBool(params[1]) - - halt.SetOperationStatus(cross, opType, allowed) - - return nil - }, - }, - } - - // Register all configured handlers in the registry - registerHandlers(registry, handlers) - - return registry -} - -// registerHandlers batch registers all configured handlers into the registry. -// This helper function processes the handler configuration array and adds each handler to the registry. -// -// Parameters: -// - registry: the parameter registry to add handlers to -// - handlerOptions: slice of handler configurations to register -func registerHandlers(registry *ParameterRegistry, handlerOptions []*ParameterHandlerOptions) { - for _, handlerOption := range handlerOptions { - registry.register(*handlerOption) - } -} diff --git a/contract/r/gnoswap/v1/gov/governance/parameter_registry_handler.gno b/contract/r/gnoswap/v1/gov/governance/parameter_registry_handler.gno deleted file mode 100644 index a3081b1..0000000 --- a/contract/r/gnoswap/v1/gov/governance/parameter_registry_handler.gno +++ /dev/null @@ -1,140 +0,0 @@ -package governance - -import "gno.land/p/nt/ufmt" - -// ParameterRegistryHandler is an interface for handlers that need to manage state -// during parameter changes. This is an optional interface that handlers can implement -// if they need to save and restore state during governance operations. -type ParameterRegistryHandler interface { - // GetState returns the current state that should be preserved - GetState() interface{} - // RestoreState restores the handler to the given state - RestoreState(state interface{}) error -} - -// stateManager handles state preservation for parameter handlers that implement -// the ParameterRegistryHandler interface. This allows for rollback capabilities -// during failed governance executions. -type stateManager struct { - states map[string]*handlerState // Maps handler keys to their saved states -} - -// newStateManager creates a new state manager instance. -// -// Returns: -// - *stateManager: new state manager for parameter handlers -func newStateManager() *stateManager { - return &stateManager{ - states: make(map[string]*handlerState), - } -} - -// SaveState preserves the current state of a parameter handler if it supports state management. -// Only handlers that implement ParameterRegistryHandler will have their state saved. -// -// Parameters: -// - pkgPath: package path of the handler -// - function: function name of the handler -// - handler: the parameter handler instance -// -// Returns: -// - error: always nil (state saving is optional) -func (sm *stateManager) SaveState(pkgPath, function string, handler ParameterHandler) error { - key := makeHandlerKey(pkgPath, function) - - // Check if the handler implements the ParameterRegistryHandler interface - if sh, ok := handler.(ParameterRegistryHandler); ok { - // Save the current state for potential rollback - sm.states[key] = &handlerState{ - pkgPath: pkgPath, - function: function, - state: sh.GetState(), - handler: handler, - } - return nil - } - - // Handlers that do not implement the ParameterRegistryHandler interface do not need to save state - return nil -} - -// RestoreStates restores all saved handler states. -// This is used for rollback operations when governance execution fails. -// -// Returns: -// - error: restoration error if any handler fails to restore -func (sm *stateManager) RestoreStates() error { - for _, state := range sm.states { - // Verify handler still implements the interface - handler, ok := state.handler.(ParameterRegistryHandler) - if !ok { - return ufmt.Errorf("handler %s does not implement ParameterRegistryHandler", state.pkgPath) - } - - // Restore the saved state - err := handler.RestoreState(state.state) - if err != nil { - return ufmt.Errorf("failed to restore state for %s: %v", - makeHandlerKey(state.pkgPath, state.function), err) - } - } - - return nil -} - -// handlerState stores the preserved state information for a parameter handler. -type handlerState struct { - pkgPath string // Package path of the handler - function string // Function name of the handler - state any // Preserved state data - handler ParameterHandler // Reference to the handler instance -} - -// registryHandler wraps existing functions as a ParameterHandler interface. -// This is a simple wrapper for functions that don't need state management. -type registryHandler struct { - fn func(params []string) error // The wrapped function -} - -// NewRegistryHandler creates a new registry handler wrapper around a function. -// This allows simple functions to be used as parameter handlers without implementing -// the full ParameterRegistryHandler interface. -// -// Parameters: -// - fn: function to wrap as a parameter handler -// -// Returns: -// - ParameterRegistryHandler: wrapped function that implements the interface -func NewRegistryHandler(fn func(params []string) error) ParameterRegistryHandler { - return ®istryHandler{fn} -} - -// Execute runs the wrapped function with the provided parameters. -// -// Parameters: -// - params: parameters to pass to the wrapped function -// -// Returns: -// - error: execution error from the wrapped function -func (h *registryHandler) Execute(params []string) error { - return h.fn(params) -} - -// RestoreState is a no-op for simple registry handlers since they don't manage state. -// -// Parameters: -// - state: state data (ignored) -// -// Returns: -// - error: always nil -func (h *registryHandler) RestoreState(state interface{}) error { - return nil -} - -// GetState returns nil for simple registry handlers since they don't manage state. -// -// Returns: -// - interface{}: always nil -func (h *registryHandler) GetState() interface{} { - return nil -} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal.gno b/contract/r/gnoswap/v1/gov/governance/proposal.gno deleted file mode 100644 index 3f765bb..0000000 --- a/contract/r/gnoswap/v1/gov/governance/proposal.gno +++ /dev/null @@ -1,280 +0,0 @@ -package governance - -import ( - "std" -) - -// ProposalType defines the different types of proposals supported by the governance system. -// Each type has different execution behavior and validation requirements. -type ProposalType string - -const ( - Text ProposalType = "TEXT" // Informational proposals for community discussion - CommunityPoolSpend ProposalType = "COMMUNITY_POOL_SPEND" // Proposals to spend community pool funds - ParameterChange ProposalType = "PARAMETER_CHANGE" // Proposals to modify system parameters -) - -// String returns the human-readable string representation of the proposal type. -func (p ProposalType) String() string { - switch p { - case Text: - return "Text" - case CommunityPoolSpend: - return "CommunityPoolSpend" - case ParameterChange: - return "ParameterChange" - default: - return "Unknown" - } -} - -// IsExecutable determines whether this proposal type can be executed. -// Text proposals are informational only and cannot be executed. -func (p ProposalType) IsExecutable() bool { - switch p { - case Text: - return false - case CommunityPoolSpend, ParameterChange: - return true - default: - return false - } -} - -// Proposal represents a governance proposal with all its associated data and state. -// This is the core structure that tracks proposal lifecycle from creation to execution. -type Proposal struct { - id int64 // Unique identifier for the proposal - proposer std.Address // The address of the proposer - configVersion int64 // The version of the governance config used - status *ProposalStatus // Current status and voting information - metadata *ProposalMetadata // Title and description - data *ProposalData // Type-specific proposal data - createdAt int64 // Creation timestamp - createdHeight int64 // Block height at creation -} - -// ID returns the unique identifier of the proposal. -func (p *Proposal) ID() int64 { - return p.id -} - -// Type returns the type of this proposal. -func (p *Proposal) Type() ProposalType { - return p.data.ProposalType() -} - -// IsTextType checks if this is a text proposal. -func (p *Proposal) IsTextType() bool { - return p.Type() == Text -} - -// IsCommunityPoolSpendType checks if this is a community pool spend proposal. -func (p *Proposal) IsCommunityPoolSpendType() bool { - return p.Type() == CommunityPoolSpend -} - -// IsParameterChangeType checks if this is a parameter change proposal. -func (p *Proposal) IsParameterChangeType() bool { - return p.Type() == ParameterChange -} - -// Status returns the current status string of the proposal at the given time. -func (p *Proposal) Status(current int64) string { - return p.status.StatusType(current).String() -} - -// StatusType returns the current status type of the proposal at the given time. -func (p *Proposal) StatusType(current int64) ProposalStatusType { - return p.status.StatusType(current) -} - -// IsActive determines if the proposal is currently active (can be voted on or executed). -// A proposal is considered active if it's not rejected, expired, executed, or canceled. -func (p *Proposal) IsActive(current int64) bool { - // Text proposals become inactive once they pass (no execution needed) - if p.IsTextType() { - if p.status.IsPassed(current) { - return false - } - } - - // If the proposal is rejected, expired, executed, or canceled, it is not active - if p.status.IsRejected(current) || - p.status.IsExpired(current) || - p.status.IsExecuted(current) || - p.status.IsCanceled(current) { - return false - } - - return true -} - -// IsVotingPeriod checks if the proposal is currently in its voting period. -func (p *Proposal) IsVotingPeriod(votedAt int64) bool { - return p.StatusType(votedAt) == StatusActive -} - -// IsExecutable determines if the proposal can be executed at the given time. -// Only executable proposal types that have passed voting can be executed. -func (p *Proposal) IsExecutable(current int64) bool { - // Only certain proposal types can be executed - if !p.Type().IsExecutable() { - return false - } - - return p.status.IsExecutable(current) -} - -// Validate performs comprehensive validation of the proposal data and metadata. -// This ensures all proposal components meet requirements before storage. -func (p *Proposal) Validate() error { - // Validate type-specific proposal data - if err := p.data.Validate(); err != nil { - return err - } - - // Validate proposal metadata (title and description) - if err := p.metadata.Validate(); err != nil { - return err - } - - return nil -} - -// Title returns the proposal title. -func (p *Proposal) Title() string { - return p.metadata.Title() -} - -// Description returns the proposal description. -func (p *Proposal) Description() string { - return p.metadata.Description() -} - -// ConfigVersion returns the governance configuration version used for this proposal. -func (p *Proposal) ConfigVersion() int64 { - return p.configVersion -} - -// IsProposer checks if the given address is the proposer of this proposal. -func (p *Proposal) IsProposer(addr std.Address) bool { - return p.proposer == addr -} - -// Proposer returns the address of the proposal creator. -func (p *Proposal) Proposer() std.Address { - return p.proposer -} - -// CreatedAt returns the creation timestamp of the proposal. -func (p *Proposal) CreatedAt() int64 { - return p.status.schedule.createTime -} - -// VotingYesWeight returns the total weight of "yes" votes. -func (p *Proposal) VotingYesWeight() int64 { - return p.status.voteStatus.yea -} - -// VotingNoWeight returns the total weight of "no" votes. -func (p *Proposal) VotingNoWeight() int64 { - return p.status.voteStatus.nay -} - -// VotingTotalWeight returns total weight of all votes cast. -func (p *Proposal) VotingTotalWeight() int64 { - return p.status.voteStatus.TotalVoteWeight() -} - -// VotingQuorumAmount returns minimum vote weight required for proposal to pass. -func (p *Proposal) VotingQuorumAmount() int64 { - return p.status.voteStatus.quorumAmount -} - -// VotingMaxWeight returns maximum possible voting weight for this proposal. -func (p *Proposal) VotingMaxWeight() int64 { - return p.status.voteStatus.maxVotingWeight -} - -// CommunityPoolSpendTokenPath returns the token path for community pool spend proposals. -// Returns empty string for other proposal types. -func (p *Proposal) CommunityPoolSpendTokenPath() string { - if p.data == nil { - return "" - } - - return p.data.communityPoolSpend.tokenPath -} - -// vote records a vote for this proposal and updates vote tallies. -// This is an internal method called during voting process. -func (p *Proposal) vote(votedYes bool, weight int64) error { - return p.status.vote(votedYes, weight) -} - -// updateVoteStatus updates the voting status with new parameters. -// This is used for dynamic voting requirement adjustments. -func (p *Proposal) updateVoteStatus(maxVotingWeight, quorum int64) error { - return p.status.updateVoteStatus(maxVotingWeight, quorum) -} - -// execute marks the proposal as executed and records execution details. -// This method validates execution conditions before proceeding. -func (p *Proposal) execute(executedAt int64, executedHeight int64, executedBy std.Address) error { - // Verify proposal is in executable state - if !p.IsExecutable(executedAt) { - return errProposalNotExecutable - } - - // Mark proposal as executed - return p.status.execute(executedAt, executedHeight, executedBy) -} - -// cancel marks the proposal as canceled and records cancellation details. -// This method validates cancellation conditions before proceeding. -func (p *Proposal) cancel(canceledAt int64, canceledHeight int64, canceledBy std.Address) error { - if p.status.IsCanceled(canceledAt) { - return errAlreadyCanceledProposal - } - - if !p.status.IsUpcoming(canceledAt) { - return errUnableToCancleVotingProposal - } - - // Mark proposal as canceled - return p.status.cancel(canceledAt, canceledHeight, canceledBy) -} - -// NewProposal creates a new proposal instance with the provided parameters. -// NewProposal is the main constructor for creating governance proposals. -// - metadata: proposal title and description -// - data: type-specific proposal data -// - proposerAddress: address of the proposal creator -// - configVersion: governance configuration version -// - createdAt: creation timestamp -// - createdHeight: creation block height -// -// Returns: -// - *Proposal: newly created proposal instance -func NewProposal( - proposalID int64, - status *ProposalStatus, - metadata *ProposalMetadata, - data *ProposalData, - proposerAddress std.Address, - configVersion int64, - createdAt int64, - createdHeight int64, -) *Proposal { - return &Proposal{ - id: proposalID, - proposer: proposerAddress, - status: status, - metadata: metadata, - data: data, - configVersion: configVersion, - createdAt: createdAt, - createdHeight: createdHeight, - } -} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_action_status.gno b/contract/r/gnoswap/v1/gov/governance/proposal_action_status.gno deleted file mode 100644 index 001d45b..0000000 --- a/contract/r/gnoswap/v1/gov/governance/proposal_action_status.gno +++ /dev/null @@ -1,128 +0,0 @@ -package governance - -import "std" - -// ProposalActionStatus tracks the execution and cancellation status of a proposal. -// This structure manages the action-related state including who performed actions and when. -type ProposalActionStatus struct { - canceled bool // Whether the proposal has been canceled - canceledAt int64 // Timestamp when proposal was canceled - canceledHeight int64 // Block height when proposal was canceled - canceledBy std.Address // Who canceled the proposal - - executed bool // Whether the proposal has been executed - executedAt int64 // Timestamp when proposal was executed - executedHeight int64 // Block height when proposal was executed - executedBy std.Address // Who executed the proposal - - executable bool // Whether this proposal type supports execution -} - -// IsCanceled returns whether the proposal has been canceled. -// -// Returns: -// - bool: true if proposal has been canceled -func (p *ProposalActionStatus) IsCanceled() bool { - return p.canceled -} - -// CanceledBy returns the address that canceled the proposal. -// Only meaningful if IsCanceled() returns true. -// -// Returns: -// - std.Address: address of the canceller -func (p *ProposalActionStatus) CanceledBy() std.Address { - return p.canceledBy -} - -// IsExecuted returns whether the proposal has been executed. -// -// Returns: -// - bool: true if proposal has been executed -func (p *ProposalActionStatus) IsExecuted() bool { - return p.executed -} - -// ExecutedBy returns the address that executed the proposal. -// Only meaningful if IsExecuted() returns true. -// -// Returns: -// - std.Address: address of the executor -func (p *ProposalActionStatus) ExecutedBy() std.Address { - return p.executedBy -} - -// IsExecutable returns whether this proposal type can be executed. -// Text proposals return false, while other types return true. -// -// Returns: -// - bool: true if proposal type supports execution -func (p *ProposalActionStatus) IsExecutable() bool { - return p.executable -} - -// cancel marks the proposal as canceled and records cancellation details. -// This method validates that the proposal is eligible for cancellation. -// -// Parameters: -// - canceledAt: timestamp when cancellation occurred -// - canceledHeight: block height when cancellation occurred -// - canceledBy: address performing the cancellation -// -// Returns: -// - error: cancellation error if proposal cannot be canceled -func (p *ProposalActionStatus) cancel(canceledAt int64, canceledHeight int64, canceledBy std.Address) error { - // Only executable proposals can be canceled (text proposals cannot) - if !p.executable { - return errProposalNotExecutable - } - - // Record cancellation details - p.canceled = true - p.canceledAt = canceledAt - p.canceledHeight = canceledHeight - p.canceledBy = canceledBy - - return nil -} - -// execute marks the proposal as executed and records execution details. -// This method validates that the proposal is eligible for execution. -// -// Parameters: -// - executedAt: timestamp when execution occurred -// - executedHeight: block height when execution occurred -// - executedBy: address performing the execution -// -// Returns: -// - error: execution error if proposal cannot be executed -func (p *ProposalActionStatus) execute(executedAt int64, executedHeight int64, executedBy std.Address) error { - // Only executable proposals can be executed (text proposals cannot) - if !p.executable { - return errProposalNotExecutable - } - - // Record execution details - p.executed = true - p.executedAt = executedAt - p.executedHeight = executedHeight - p.executedBy = executedBy - - return nil -} - -// NewProposalActionStatus creates a new action status for a proposal. -// Initializes the status with default values and the executable flag. -// -// Parameters: -// - executable: whether this proposal type can be executed -// -// Returns: -// - *ProposalActionStatus: new action status instance -func NewProposalActionStatus(executable bool) *ProposalActionStatus { - return &ProposalActionStatus{ - canceled: false, // Proposal starts as not canceled - executed: false, // Proposal starts as not executed - executable: executable, // Set based on proposal type - } -} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_data.gno b/contract/r/gnoswap/v1/gov/governance/proposal_data.gno deleted file mode 100644 index baaba28..0000000 --- a/contract/r/gnoswap/v1/gov/governance/proposal_data.gno +++ /dev/null @@ -1,406 +0,0 @@ -package governance - -import ( - "std" - "strings" - - "gno.land/p/nt/ufmt" -) - -// ProposalMetadata contains descriptive information about a proposal. -// This includes the title and description that are displayed to voters. -type ProposalMetadata struct { - title string // Proposal title (max 255 characters) - description string // Detailed proposal description (max 10,000 characters) -} - -// Title returns the proposal title. -// -// Returns: -// - string: proposal title -func (p *ProposalMetadata) Title() string { - return p.title -} - -// Description returns the proposal description. -// -// Returns: -// - string: proposal description -func (p *ProposalMetadata) Description() string { - return p.description -} - -// Validate performs comprehensive validation of the proposal metadata. -// Checks title and description length and content requirements. -// -// Returns: -// - error: validation error if metadata is invalid -func (p *ProposalMetadata) Validate() error { - // Validate title meets requirements - if err := p.validateTitle(p.title); err != nil { - return err - } - - // Validate description meets requirements - if err := p.validateDescription(p.description); err != nil { - return err - } - - return nil -} - -// validateTitle checks if the proposal title meets length and content requirements. -// -// Parameters: -// - title: title string to validate -// -// Returns: -// - error: validation error if title is invalid -func (p *ProposalMetadata) validateTitle(title string) error { - // Title cannot be empty - if title == "" { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("title is empty"), - ) - } - - // Title cannot exceed maximum length - if len(title) > maxTitleLength { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("title is too long, max length is %d", maxTitleLength), - ) - } - - return nil -} - -// validateDescription checks if the proposal description meets length and content requirements. -// -// Parameters: -// - description: description string to validate -// -// Returns: -// - error: validation error if description is invalid -func (p *ProposalMetadata) validateDescription(description string) error { - // Description cannot be empty - if description == "" { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("description is empty"), - ) - } - - // Description cannot exceed maximum length - if len(description) > maxDescriptionLength { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("description is too long, max length is %d", maxDescriptionLength), - ) - } - - return nil -} - -// NewProposalMetadata creates a new proposal metadata instance with trimmed input. -// -// Parameters: -// - title: proposal title -// - description: proposal description -// -// Returns: -// - *ProposalMetadata: new metadata instance with trimmed whitespace -func NewProposalMetadata(title string, description string) *ProposalMetadata { - return &ProposalMetadata{ - title: strings.TrimSpace(title), - description: strings.TrimSpace(description), - } -} - -// ProposalData contains the type-specific data for a proposal. -// This structure holds different data depending on the proposal type. -type ProposalData struct { - proposalType ProposalType // Type of proposal (Text, CommunityPoolSpend, ParameterChange) - communityPoolSpend CommunityPoolSpendInfo // Data for community pool spending proposals - execution ExecutionInfo // Data for parameter change proposals -} - -// ProposalType returns the type of this proposal. -// -// Returns: -// - ProposalType: the proposal type -func (p *ProposalData) ProposalType() ProposalType { - return p.proposalType -} - -// CommunityPoolSpend returns the community pool spending information. -// -// Returns: -// - CommunityPoolSpendInfo: community pool spending details -func (p *ProposalData) CommunityPoolSpend() CommunityPoolSpendInfo { - return p.communityPoolSpend -} - -// Execution returns the execution information for parameter changes. -// -// Returns: -// - ExecutionInfo: parameter change execution details -func (p *ProposalData) Execution() ExecutionInfo { - return p.execution -} - -// Validate performs type-specific validation of the proposal data. -// Different proposal types have different validation requirements. -// -// Returns: -// - error: validation error if data is invalid -func (p *ProposalData) Validate() error { - // Validate based on proposal type - if p.proposalType == Text { - return p.validateText() - } - - if p.proposalType == CommunityPoolSpend { - return p.validateCommunityPoolSpend() - } - - if p.proposalType == ParameterChange { - return p.validateParameterChange() - } - - return nil -} - -// validateText validates text proposal data. -// Text proposals have no additional validation requirements. -// -// Returns: -// - error: always nil for text proposals -func (p *ProposalData) validateText() error { - return nil -} - -// validateCommunityPoolSpend validates community pool spend proposal data. -// Checks recipient address, token path, and amount validity. -// -// Returns: -// - error: validation error if community pool spend data is invalid -func (p *ProposalData) validateCommunityPoolSpend() error { - // Validate recipient address - if !p.communityPoolSpend.to.IsValid() { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("to is invalid address"), - ) - } - - // Validate token path is provided - if p.communityPoolSpend.tokenPath == "" { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("tokenPath is empty"), - ) - } - - // Validate amount is positive - if p.communityPoolSpend.amount == 0 { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("amount is 0"), - ) - } - - return nil -} - -// validateParameterChange validates parameter change proposal data. -// Checks execution count, message format, and parameter validity. -// -// Returns: -// - error: validation error if parameter change data is invalid -func (p *ProposalData) validateParameterChange() error { - // Validate execution count is positive - if p.execution.num <= 0 { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("numToExecute is less than or equal to 0"), - ) - } - - // Validate execution messages are provided - if len(p.execution.msgs) == 0 { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("executions is empty"), - ) - } - - // Validate execution count matches message count - if len(p.execution.msgs) != int(p.execution.num) { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("executions is not equal to numToExecute"), - ) - } - - // Validate execution count doesn't exceed maximum - if p.execution.num > maxNumberOfExecution { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("numToExecute is greater than %d", maxNumberOfExecution), - ) - } - - // Validate parameter change message format - parameterChangesInfos := p.execution.ParameterChangesInfos() - if len(parameterChangesInfos) != int(p.execution.num) { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("invalid parameter change info"), - ) - } - - return nil -} - -// NewProposalData creates a new proposal data instance with the specified components. -// -// Parameters: -// - proposalType: type of the proposal -// - communityPoolSpend: community pool spending information -// - execution: parameter change execution information -// -// Returns: -// - *ProposalData: new proposal data instance -func NewProposalData(proposalType ProposalType, communityPoolSpend CommunityPoolSpendInfo, execution ExecutionInfo) *ProposalData { - return &ProposalData{ - proposalType: proposalType, - communityPoolSpend: communityPoolSpend, - execution: execution, - } -} - -// NewProposalTextData creates proposal data for a text proposal. -// Text proposals have no additional data requirements. -// -// Returns: -// - *ProposalData: proposal data configured for text proposal -func NewProposalTextData() *ProposalData { - return NewProposalData( - Text, - CommunityPoolSpendInfo{}, - ExecutionInfo{}, - ) -} - -// NewProposalCommunityPoolSpendData creates proposal data for a community pool spend proposal. -// Automatically generates the execution message for the token transfer. -// -// Parameters: -// - tokenPath: path of the token to transfer -// - to: recipient address for the transfer -// - amount: amount of tokens to transfer -// - communityPoolPackagePath: package path of the community pool contract -// -// Returns: -// - *ProposalData: proposal data configured for community pool spending -func NewProposalCommunityPoolSpendData( - tokenPath string, - to std.Address, - amount int64, - communityPoolPackagePath string, -) *ProposalData { - // Create execution message for the token transfer - executionInfoMessage := makeExecuteMessage( - communityPoolPackagePath, - "TransferToken", - []string{tokenPath, to.String(), ufmt.Sprintf("%d", amount)}, - ) - - return NewProposalData( - CommunityPoolSpend, - CommunityPoolSpendInfo{to, tokenPath, amount}, - ExecutionInfo{ - num: 1, - msgs: []string{executionInfoMessage}, - }, - ) -} - -// NewProposalExecutionData creates proposal data for a parameter change proposal. -// Parses the execution string to create the execution structure. -// -// Parameters: -// - numToExecute: number of parameter changes to execute -// - executions: encoded execution string with parameter changes -// -// Returns: -// - *ProposalData: proposal data configured for parameter changes -func NewProposalExecutionData(numToExecute int64, executions string) *ProposalData { - // Split execution string into individual messages - msgs := strings.Split(executions, messageSeparator) - - return NewProposalData( - ParameterChange, - CommunityPoolSpendInfo{}, - ExecutionInfo{numToExecute, msgs}, - ) -} - -// CommunityPoolSpendInfo contains information for community pool spending proposals. -type CommunityPoolSpendInfo struct { - to std.Address // Recipient address for token transfer - tokenPath string // Path of the token to transfer - amount int64 // Amount of tokens to transfer -} - -// ExecutionInfo contains information for parameter change execution. -// Messages are encoded strings that specify function calls and parameters. -type ExecutionInfo struct { - num int64 // Number of parameter changes to execute - msgs []string // Execution messages separated by messageSeparator (*GOV*) -} - -// ParameterChangesInfos parses the execution messages and returns structured parameter change information. -// Each message is expected to be in format: pkgPath*EXE*function*EXE*params -// -// Returns: -// - []ParameterChangeInfo: slice of parsed parameter change information -func (e *ExecutionInfo) ParameterChangesInfos() []ParameterChangeInfo { - // Return empty slice if no executions - if e.num <= 0 { - return []ParameterChangeInfo{} - } - - infos := make([]ParameterChangeInfo, 0) - - // Parse each execution message - for _, msg := range e.msgs { - // Split message into components: pkgPath, function, params - params := strings.Split(msg, parameterSeparator) - if len(params) != 3 { - continue // Skip malformed messages - } - - pkgPath := params[0] - function := params[1] - executionParams := strings.Split(params[2], ",") - - // Create parameter change info structure - infos = append(infos, ParameterChangeInfo{ - pkgPath: pkgPath, - function: function, - params: executionParams, - }) - } - - return infos -} - -// ParameterChangeInfo represents a single parameter change to be executed. -type ParameterChangeInfo struct { - pkgPath string // Package path of the target contract - function string // Function name to call - params []string // Parameters to pass to the function -} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_manager.gno b/contract/r/gnoswap/v1/gov/governance/proposal_manager.gno deleted file mode 100644 index 34e8018..0000000 --- a/contract/r/gnoswap/v1/gov/governance/proposal_manager.gno +++ /dev/null @@ -1,95 +0,0 @@ -package governance - -import "std" - -// ProposalManager manages the association between users and their created proposals. -// This structure provides efficient lookup of proposals by user address and maintains -// the relationship for governance operations and queries. -type ProposalManager struct { - userProposals map[string]map[int64]bool // Maps user address to their proposal IDs -} - -// GetUserProposals retrieves all proposal IDs created by a specific user. -// Returns an empty slice if the user has no proposals. -func (pm *ProposalManager) GetUserProposals(user std.Address) []int64 { - // Check if user has any proposals - _, ok := pm.userProposals[user.String()] - if !ok { - return []int64{} - } - - proposalIDs := make([]int64, 0) - - // Collect all proposal IDs for this user - for proposalID := range pm.userProposals[user.String()] { - proposalIDs = append(proposalIDs, proposalID) - } - - return proposalIDs -} - -// HasProposal checks if a specific user has created a specific proposal. -// Returns true if the user created the specified proposal. -func (pm *ProposalManager) HasProposal(user std.Address, proposalID int64) bool { - // First check if user has any proposals - proposals, ok := pm.userProposals[user.String()] - if !ok { - return false - } - - // Then check if specific proposal exists for this user - _, ok = proposals[proposalID] - if !ok { - return false - } - - return true -} - -// addProposal associates a proposal with its creator. -// This is called when a new proposal is created to establish the relationship. -// -// Parameters: -// - user: address of the proposal creator -// - proposalID: ID of the created proposal -func (pm *ProposalManager) addProposal(user std.Address, proposalID int64) { - // Initialize user's proposal map if it doesn't exist - if _, ok := pm.userProposals[user.String()]; !ok { - pm.userProposals[user.String()] = make(map[int64]bool) - } - - // Add the proposal to the user's list - pm.userProposals[user.String()][proposalID] = true -} - -// removeProposal removes the association between a user and proposal. -// This could be used for cleanup operations (though currently not used in practice). -// -// Parameters: -// - user: address of the proposal creator -// - proposalID: ID of the proposal to remove -func (pm *ProposalManager) removeProposal(user std.Address, proposalID int64) { - // Exit early if user doesn't have the proposal - if !pm.HasProposal(user, proposalID) { - return - } - - // Double-check user exists (defensive programming) - if _, ok := pm.userProposals[user.String()]; !ok { - return - } - - // Remove the proposal from user's list - delete(pm.userProposals[user.String()], proposalID) -} - -// NewProposalManager creates a new proposal manager instance. -// Initializes the internal data structures for managing user-proposal relationships. -// -// Returns: -// - *ProposalManager: new proposal manager instance -func NewProposalManager() *ProposalManager { - return &ProposalManager{ - userProposals: make(map[string]map[int64]bool), - } -} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_schedule_status.gno b/contract/r/gnoswap/v1/gov/governance/proposal_schedule_status.gno deleted file mode 100644 index 89d01db..0000000 --- a/contract/r/gnoswap/v1/gov/governance/proposal_schedule_status.gno +++ /dev/null @@ -1,108 +0,0 @@ -package governance - -// ProposalScheduleStatus represents the pre-calculated time schedule for a proposal. -// This structure defines all the important timestamps in a proposal's lifecycle, -// from creation through voting to execution and expiration. -type ProposalScheduleStatus struct { - createTime int64 // When the proposal was created - activeTime int64 // When voting starts (CreateTime + VotingStartDelay) - votingEndTime int64 // When voting ends (ActiveTime + VotingPeriod) - executableTime int64 // When execution window starts (VotingEndTime + ExecutionDelay) - expiredTime int64 // When execution window ends (ExecutableTime + ExecutionWindow) -} - -// IsPassedCreatedAt checks if the current time has passed the proposal creation time. -// This is always true once a proposal exists. -// -// Parameters: -// - current: timestamp to check against -// -// Returns: -// - bool: true if current time is at or after creation time -func (p *ProposalScheduleStatus) IsPassedCreatedAt(current int64) bool { - return p.createTime <= current -} - -// IsPassedActiveAt checks if the current time has passed the voting start time. -// When true, the proposal enters its active voting period. -// -// Parameters: -// - current: timestamp to check against -// -// Returns: -// - bool: true if voting period has started -func (p *ProposalScheduleStatus) IsPassedActiveAt(current int64) bool { - return p.activeTime <= current -} - -// IsPassedVotingEndedAt checks if the current time has passed the voting end time. -// When true, no more votes can be cast on the proposal. -// -// Parameters: -// - current: timestamp to check against -// -// Returns: -// - bool: true if voting period has ended -func (p *ProposalScheduleStatus) IsPassedVotingEndedAt(current int64) bool { - return p.votingEndTime <= current -} - -// IsPassedExecutableAt checks if the current time has passed the execution start time. -// When true, approved proposals can be executed (after execution delay). -// -// Parameters: -// - current: timestamp to check against -// -// Returns: -// - bool: true if execution window has started -func (p *ProposalScheduleStatus) IsPassedExecutableAt(current int64) bool { - return p.executableTime <= current -} - -// IsPassedExpiredAt checks if the current time has passed the execution expiration time. -// When true, the proposal can no longer be executed and has expired. -// -// Parameters: -// - current: timestamp to check against -// -// Returns: -// - bool: true if execution window has expired -func (p *ProposalScheduleStatus) IsPassedExpiredAt(current int64) bool { - return p.expiredTime <= current -} - -// NewProposalScheduleStatus creates a new schedule status with calculated timestamps. -// This constructor takes the governance timing parameters and calculates all -// important timestamps for the proposal's lifecycle. -// -// Parameters: -// - votingStartDelay: delay before voting starts (seconds) -// - votingPeriod: duration of voting period (seconds) -// - executionDelay: delay before execution can start (seconds) -// - executionWindow: window during which execution is allowed (seconds) -// - createdAt: timestamp when proposal was created -// -// Returns: -// - *ProposalScheduleStatus: new schedule status with calculated times -func NewProposalScheduleStatus( - votingStartDelay, - votingPeriod, - executionDelay, - executionWindow, - createdAt int64, -) *ProposalScheduleStatus { - // Calculate all phase timestamps based on creation time and configuration - createTime := createdAt - activeTime := createTime + votingStartDelay // When voting can start - votingEndTime := activeTime + votingPeriod // When voting ends - executableTime := votingEndTime + executionDelay // When execution can start - expiredTime := executableTime + executionWindow // When execution window closes - - return &ProposalScheduleStatus{ - createTime: createTime, - activeTime: activeTime, - votingEndTime: votingEndTime, - executableTime: executableTime, - expiredTime: expiredTime, - } -} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_status.gno b/contract/r/gnoswap/v1/gov/governance/proposal_status.gno deleted file mode 100644 index 2caeaec..0000000 --- a/contract/r/gnoswap/v1/gov/governance/proposal_status.gno +++ /dev/null @@ -1,314 +0,0 @@ -package governance - -import ( - "std" -) - -// ProposalStatusType represents the current status of a proposal in its lifecycle. -// These statuses determine what actions are available for a proposal. -type ProposalStatusType int - -const ( - _ ProposalStatusType = iota - StatusUpcoming // Proposal created but voting hasn't started yet - StatusActive // Proposal is in voting period - StatusPassed // Proposal has passed but hasn't been executed (or is text proposal) - StatusRejected // Proposal failed to meet voting requirements - StatusExecutable // Proposal can be executed (passed and in execution window) - StatusExecuted // Proposal has been successfully executed - StatusExpired // Proposal execution window has passed - StatusCanceled // Proposal has been canceled -) - -// String returns the string representation of ProposalStatusType for display purposes. -// -// Returns: -// - string: human-readable status name -func (s ProposalStatusType) String() string { - switch s { - case StatusUpcoming: - return "upcoming" - case StatusActive: - return "active" - case StatusPassed: - return "passed" - case StatusRejected: - return "rejected" - case StatusExecutable: - return "executable" - case StatusExecuted: - return "executed" - case StatusExpired: - return "expired" - case StatusCanceled: - return "canceled" - default: - return "unknown" - } -} - -// ProposalStatus manages the complete status of a proposal including scheduling, voting, and actions. -// This is the central status tracking structure that coordinates different aspects of proposal state. -type ProposalStatus struct { - schedule *ProposalScheduleStatus // Time-based scheduling information - actionStatus *ProposalActionStatus // Execution and cancellation status - voteStatus *ProposalVoteStatus // Voting tallies and requirements -} - -// StatusType determines the current status of the proposal based on timing, voting, and actions. -// This is the main status calculation method that considers all factors. -// -// Parameters: -// - current: current timestamp to evaluate status at -// -// Returns: -// - ProposalStatusType: current status of the proposal -func (p *ProposalStatus) StatusType(current int64) ProposalStatusType { - // Check action-based statuses first (these override time-based statuses) - if p.actionStatus.IsExecuted() { - return StatusExecuted - } - - if p.actionStatus.IsCanceled() { - return StatusCanceled - } - - // Check time-based statuses - if !p.schedule.IsPassedActiveAt(current) { - return StatusUpcoming - } - - if !p.schedule.IsPassedVotingEndedAt(current) { - return StatusActive - } - - // Check voting outcome - if p.voteStatus.IsRejected() { - return StatusRejected - } - - // For passed proposals, check execution status - if !p.actionStatus.IsExecutable() || !p.schedule.IsPassedExecutableAt(current) { - return StatusPassed - } - - if !p.schedule.IsPassedExpiredAt(current) { - return StatusExecutable - } - - return StatusExpired -} - -// IsUpcoming checks if the proposal is in upcoming status. -// -// Parameters: -// - current: timestamp to check status at -// -// Returns: -// - bool: true if proposal is upcoming -func (p *ProposalStatus) IsUpcoming(current int64) bool { - return p.StatusType(current) == StatusUpcoming -} - -// IsActive checks if the proposal is in active voting status. -// -// Parameters: -// - current: timestamp to check status at -// -// Returns: -// - bool: true if proposal is active (voting period) -func (p *ProposalStatus) IsActive(current int64) bool { - return p.StatusType(current) == StatusActive -} - -// IsPassed checks if the proposal has passed voting. -// -// Parameters: -// - current: timestamp to check status at -// -// Returns: -// - bool: true if proposal has passed -func (p *ProposalStatus) IsPassed(current int64) bool { - return p.StatusType(current) == StatusPassed -} - -// IsRejected checks if the proposal has been rejected by voting. -// -// Parameters: -// - current: timestamp to check status at -// -// Returns: -// - bool: true if proposal was rejected -func (p *ProposalStatus) IsRejected(current int64) bool { - return p.StatusType(current) == StatusRejected -} - -// IsExecutable checks if the proposal is in executable status. -// -// Parameters: -// - current: timestamp to check status at -// -// Returns: -// - bool: true if proposal can be executed -func (p *ProposalStatus) IsExecutable(current int64) bool { - return p.StatusType(current) == StatusExecutable -} - -// IsExpired checks if the proposal execution window has expired. -// -// Parameters: -// - current: timestamp to check status at -// -// Returns: -// - bool: true if proposal has expired -func (p *ProposalStatus) IsExpired(current int64) bool { - return p.StatusType(current) == StatusExpired -} - -// IsExecuted checks if the proposal has been executed. -// -// Parameters: -// - current: timestamp to check status at -// -// Returns: -// - bool: true if proposal has been executed -func (p *ProposalStatus) IsExecuted(current int64) bool { - return p.StatusType(current) == StatusExecuted -} - -// IsCanceled checks if the proposal has been canceled. -// -// Parameters: -// - current: timestamp to check status at -// -// Returns: -// - bool: true if proposal has been canceled -func (p *ProposalStatus) IsCanceled(current int64) bool { - return p.StatusType(current) == StatusCanceled -} - -// YesWeight returns the total weight of "yes" votes. -// -// Returns: -// - int64: total "yes" vote weight -func (p *ProposalStatus) YesWeight() int64 { - return p.voteStatus.YesWeight() -} - -// NoWeight returns the total weight of "no" votes. -// -// Returns: -// - int64: total "no" vote weight -func (p *ProposalStatus) NoWeight() int64 { - return p.voteStatus.NoWeight() -} - -// TotalVoteWeight returns the total weight of all votes cast. -// -// Returns: -// - int64: total vote weight -func (p *ProposalStatus) TotalVoteWeight() int64 { - return p.voteStatus.TotalVoteWeight() -} - -// DiffVoteWeight returns the absolute difference between yes and no votes. -// -// Returns: -// - int64: absolute difference in vote weights -func (p *ProposalStatus) DiffVoteWeight() int64 { - return p.voteStatus.DiffVoteWeight() -} - -// cancel marks the proposal as canceled with the provided details. -// This delegates to the action status for actual cancellation logic. -// -// Parameters: -// - canceledAt: timestamp when proposal was canceled -// - canceledHeight: block height when proposal was canceled -// - canceledBy: address that canceled the proposal -// -// Returns: -// - error: cancellation error if operation fails -func (p *ProposalStatus) cancel(canceledAt int64, canceledHeight int64, canceledBy std.Address) error { - return p.actionStatus.cancel(canceledAt, canceledHeight, canceledBy) -} - -// execute marks the proposal as executed with the provided details. -// This delegates to the action status for actual execution logic. -// -// Parameters: -// - executedAt: timestamp when proposal was executed -// - executedHeight: block height when proposal was executed -// - executedBy: address that executed the proposal -// -// Returns: -// - error: execution error if operation fails -func (p *ProposalStatus) execute(executedAt int64, executedHeight int64, executedBy std.Address) error { - return p.actionStatus.execute(executedAt, executedHeight, executedBy) -} - -// vote records a vote on the proposal and updates vote tallies. -// This delegates to the vote status for actual vote recording. -// -// Parameters: -// - votedYes: true for "yes" vote, false for "no" vote -// - weight: voting weight to apply -// -// Returns: -// - error: voting error if operation fails -func (p *ProposalStatus) vote(votedYes bool, weight int64) error { - if votedYes { - return p.voteStatus.addYesVoteWeight(weight) - } - - return p.voteStatus.addNoVoteWeight(weight) -} - -// updateVoteStatus updates the voting parameters and recalculates requirements. -// This is used when voting parameters change dynamically. -// -// Parameters: -// - maxVotingWeight: updated maximum voting weight -// - quorum: updated quorum percentage -// -// Returns: -// - error: update error if operation fails -func (p *ProposalStatus) updateVoteStatus(maxVotingWeight, quorum int64) error { - return p.voteStatus.updateVoteStatus(maxVotingWeight, quorum) -} - -// NewProposalStatus creates a new proposal status with the specified configuration. -// This initializes all status components with the governance configuration and timing. -// -// Parameters: -// - config: governance configuration to use -// - maxVotingWeight: maximum voting weight for this proposal -// - executable: whether this proposal type can be executed -// - createdAt: timestamp when proposal was created -// -// Returns: -// - *ProposalStatus: new proposal status instance -func NewProposalStatus( - config Config, - maxVotingWeight int64, - executable bool, - createdAt int64, -) *ProposalStatus { - return &ProposalStatus{ - // Initialize time-based scheduling - schedule: NewProposalScheduleStatus( - config.VotingStartDelay, - config.VotingPeriod, - config.ExecutionDelay, - config.ExecutionWindow, - createdAt, - ), - // Initialize action status (execution/cancellation tracking) - actionStatus: NewProposalActionStatus(executable), - // Initialize vote status with voting requirements - voteStatus: NewProposalVoteStatus( - maxVotingWeight, - config.Quorum, - ), - } -} diff --git a/contract/r/gnoswap/v1/gov/governance/proposal_vote_status.gno b/contract/r/gnoswap/v1/gov/governance/proposal_vote_status.gno deleted file mode 100644 index 244ecc2..0000000 --- a/contract/r/gnoswap/v1/gov/governance/proposal_vote_status.gno +++ /dev/null @@ -1,163 +0,0 @@ -package governance - -// ProposalVoteStatus tracks the voting tallies and requirements for a proposal. -// This structure manages vote counting, quorum calculation, and voting outcome determination. -type ProposalVoteStatus struct { - yea int64 // Total weight of "yes" votes collected - nay int64 // Total weight of "no" votes collected - maxVotingWeight int64 // The max voting weight at the time of proposal creation - quorumAmount int64 // How many total votes must be collected for the proposal to be valid -} - -// TotalVoteWeight returns the total weight of all votes cast (yes + no). -// -// Returns: -// - int64: combined weight of all votes -func (p *ProposalVoteStatus) TotalVoteWeight() int64 { - return p.yea + p.nay -} - -// DiffVoteWeight returns the absolute difference between yes and no votes. -// This can be used to determine the margin of victory or defeat. -// -// Returns: -// - int64: absolute difference between yes and no vote weights -func (p *ProposalVoteStatus) DiffVoteWeight() int64 { - if p.yea > p.nay { - return p.yea - p.nay - } - - return p.nay - p.yea -} - -// YesWeight returns the total weight of "yes" votes. -// -// Returns: -// - int64: total "yes" vote weight -func (p *ProposalVoteStatus) YesWeight() int64 { - return p.yea -} - -// NoWeight returns the total weight of "no" votes. -// -// Returns: -// - int64: total "no" vote weight -func (p *ProposalVoteStatus) NoWeight() int64 { - return p.nay -} - -// IsVotingFinished determines if voting has effectively ended due to mathematical impossibility -// of changing the outcome. This happens when the remaining uncast votes cannot change the result. -// -// Returns: -// - bool: true if voting outcome is mathematically determined -func (p *ProposalVoteStatus) IsVotingFinished() bool { - totalVotes := p.TotalVoteWeight() - - // If we haven't reached quorum yet, voting is not finished - if totalVotes < p.quorumAmount { - return false - } - - // Calculate remaining votes that could still be cast - remainingVotes := p.maxVotingWeight - totalVotes - - // If the difference between yes/no is greater than remaining votes, - // the outcome cannot change, so voting is effectively finished - return remainingVotes-p.DiffVoteWeight() <= 0 -} - -// IsRejected determines if the proposal has been rejected by voting. -// A proposal is rejected if voting is finished and it did not pass. -// -// Returns: -// - bool: true if proposal has been rejected -func (p *ProposalVoteStatus) IsRejected() bool { - // Only consider rejection if voting is finished - if !p.IsVotingFinished() { - return false - } - - // Proposal is rejected if it didn't pass - return !p.IsPassed() -} - -// IsPassed determines if the proposal has passed the voting requirements. -// A proposal passes if it receives at least the quorum amount of "yes" votes. -// -// Returns: -// - bool: true if proposal has passed -func (p *ProposalVoteStatus) IsPassed() bool { - return p.yea >= p.quorumAmount -} - -// addYesVoteWeight adds the specified weight to the "yes" vote tally. -// This is called when a user votes "yes" on the proposal. -// -// Parameters: -// - yea: vote weight to add to "yes" votes -// -// Returns: -// - error: always nil (reserved for future validation) -func (p *ProposalVoteStatus) addYesVoteWeight(yea int64) error { - p.yea += yea - - return nil -} - -// addNoVoteWeight adds the specified weight to the "no" vote tally. -// This is called when a user votes "no" on the proposal. -// -// Parameters: -// - nay: vote weight to add to "no" votes -// -// Returns: -// - error: always nil (reserved for future validation) -func (p *ProposalVoteStatus) addNoVoteWeight(nay int64) error { - p.nay += nay - - return nil -} - -// updateVoteStatus updates the voting parameters and recalculates the quorum requirement. -// This can be used if voting parameters change dynamically. -// -// Parameters: -// - maxVotingWeight: updated maximum voting weight -// - quorum: updated quorum percentage -// -// Returns: -// - error: always nil (reserved for future validation) -func (p *ProposalVoteStatus) updateVoteStatus(maxVotingWeight, quorum int64) error { - // Update maximum voting weight - p.maxVotingWeight = maxVotingWeight - - // Recalculate quorum amount based on new parameters - p.quorumAmount = maxVotingWeight * quorum / 100 - - return nil -} - -// NewProposalVoteStatus creates a new vote status for a proposal. -// Initializes vote tallies to zero and calculates the quorum requirement. -// -// Parameters: -// - maxVotingWeight: maximum possible voting weight for this proposal -// - quorum: quorum percentage required for passage (0-100) -// -// Returns: -// - *ProposalVoteStatus: new vote status instance -func NewProposalVoteStatus( - maxVotingWeight int64, - quorum int64, -) *ProposalVoteStatus { - // Calculate the absolute vote weight needed to meet quorum - quorumAmount := maxVotingWeight * quorum / 100 - - return &ProposalVoteStatus{ - yea: 0, // Start with no "yes" votes - nay: 0, // Start with no "no" votes - maxVotingWeight: maxVotingWeight, // Set maximum possible votes - quorumAmount: quorumAmount, // Set required votes for passage - } -} diff --git a/contract/r/gnoswap/v1/gov/governance/state.gno b/contract/r/gnoswap/v1/gov/governance/state.gno deleted file mode 100644 index b3e8dcb..0000000 --- a/contract/r/gnoswap/v1/gov/governance/state.gno +++ /dev/null @@ -1,239 +0,0 @@ -package governance - -import ( - "std" - - "gno.land/p/nt/avl" -) - -// Global state variables for governance system -var ( - configCounter *Counter // Counter for generating config version numbers - proposalCounter *Counter // Counter for generating unique proposal IDs - - configs *avl.Tree // Tree storing governance configurations by version - proposals *avl.Tree // Tree storing all proposals by ID - proposalManager *ProposalManager // Manager for user-proposal associations - proposalUserVotingInfos *avl.Tree // Tree storing voting info for each proposal by user - - // Function to retrieve user voting snapshots (can be overridden for testing) - getUserVotingInfoSnapshotFn func(snapshotTime int64) (map[string]*VotingInfo, int64, bool) -) - -// init initializes the governance system state when the contract is deployed. -// This function sets up all necessary data structures and default configurations. -func init() { - initConfig() - initProposal() - initStakerDelegationSnapshots() -} - -// initConfig initializes the governance configuration system. -// Sets up the configuration counter and creates the default initial configuration. -func initConfig() { - configCounter = NewCounter() - configs = avl.NewTree() - - // Create the initial governance configuration with default parameters - nextConfigVersion := nextConfigVersion() - config := Config{ - VotingStartDelay: 86400, // 1 day - delay before voting starts - VotingPeriod: 604800, // 7 days - duration for collecting votes - VotingWeightSmoothingDuration: 86400, // 1 day - period for averaging voting weight - Quorum: 50, // 50% of total xGNS supply required - ProposalCreationThreshold: 1_000_000_000, // 1 billion - minimum balance to create proposals - ExecutionDelay: 86400, // 1 day - waiting period before execution - ExecutionWindow: 2592000, // 30 days - window for executing proposals - } - - setConfig(nextConfigVersion, config) -} - -// initProposal initializes the proposal management system. -// Sets up counters, storage trees, and management structures for proposals. -func initProposal() { - proposalCounter = NewCounter() - proposals = avl.NewTree() - proposalManager = NewProposalManager() - proposalUserVotingInfos = avl.NewTree() -} - -// initStakerDelegationSnapshots initializes the voting snapshot function. -// Sets up the default function to retrieve voting weights from staker contract. -func initStakerDelegationSnapshots() { - getUserVotingInfoSnapshotFn = func(snapshotTime int64) (map[string]*VotingInfo, int64, bool) { - return getUserVotingInfotWithDelegationSnapshots(snapshotTime) - } -} - -// getCurrentConfigVersion returns the current governance configuration version. -// -// Returns: -// - int64: current configuration version number -func getCurrentConfigVersion() int64 { - return configCounter.Get() -} - -// nextConfigVersion increments and returns the next configuration version number. -// This is used when creating new governance configurations. -// -// Returns: -// - int64: next configuration version number -func nextConfigVersion() int64 { - return configCounter.next() -} - -// getCurrentProposalID returns the current proposal ID (last assigned). -// -// Returns: -// - int64: current proposal ID -func getCurrentProposalID() int64 { - return proposalCounter.Get() -} - -// nextProposalID increments and returns the next unique proposal ID. -// This is used when creating new proposals. -// -// Returns: -// - int64: next unique proposal ID -func nextProposalID() int64 { - return proposalCounter.next() -} - -// getConfig retrieves a specific governance configuration by version number. -// -// Parameters: -// - version: configuration version to retrieve -// -// Returns: -// - Config: governance configuration for the specified version -// - bool: true if configuration exists, false otherwise -func getConfig(version int64) (cfg Config, ok bool) { - if val, exists := configs.Get(formatInt(version)); !exists { - return - } else { - cfg, ok = val.(Config) - } - - return -} - -// setConfig stores a governance configuration with the specified version number. -// -// Parameters: -// - version: configuration version number -// - config: governance configuration to store -func setConfig(version int64, config Config) { - configs.Set(formatInt(version), config) -} - -// getCurrentConfig retrieves the current active governance configuration. -// -// Returns: -// - Config: current governance configuration -// - bool: true if configuration exists, false otherwise -func getCurrentConfig() (Config, bool) { - return getConfig(getCurrentConfigVersion()) -} - -// getProposal retrieves a specific proposal by its ID. -// -// Parameters: -// - proposalID: unique identifier of the proposal -// -// Returns: -// - *Proposal: pointer to the proposal if found -// - bool: true if proposal exists, false otherwise -func getProposal(proposalID int64) (proposal *Proposal, ok bool) { - if val, exists := proposals.Get(formatInt(proposalID)); !exists { - return - } else { - proposal, ok = val.(*Proposal) - } - return -} - -// addProposal stores a new proposal in the system. -// Also registers the proposal with the proposal manager for user tracking. -// -// Parameters: -// - proposal: proposal to store -// -// Returns: -// - bool: true if proposal was successfully added -func addProposal(proposal *Proposal) bool { - id := proposal.ID() - // Store proposal in main proposals tree - proposals.Set(formatInt(id), proposal) - - // Register proposal with user in proposal manager - proposalManager.addProposal(proposal.Proposer(), id) - - return true -} - -// getUserProposals retrieves all proposals created by a specific user. -// -// Parameters: -// - user: address of the user -// -// Returns: -// - []*Proposal: slice of proposals created by the user -func getUserProposals(user std.Address) (proposals []*Proposal) { - // Get proposal IDs for this user - proposalIDs := proposalManager.GetUserProposals(user) - - // Retrieve each proposal by ID - for _, proposalID := range proposalIDs { - if proposal, ok := getProposal(proposalID); !ok { - continue // Skip if proposal not found (shouldn't happen) - } else { - proposals = append(proposals, proposal) - } - } - return -} - -// getProposalUserVotingInfos retrieves all voting information for a specific proposal. -// Returns a mapping of user addresses to their voting information. -// -// Parameters: -// - proposalID: unique identifier of the proposal -// -// Returns: -// - map[string]*VotingInfo: mapping of user addresses to voting information -// - bool: true if voting information exists for the proposal -func getProposalUserVotingInfos(proposalID int64) (userVotingInfos map[string]*VotingInfo, ok bool) { - id := formatInt(proposalID) - if userVotingInfosI, exists := proposalUserVotingInfos.Get(id); !exists { - return - } else { - userVotingInfos, ok = userVotingInfosI.(map[string]*VotingInfo) - } - return -} - -// getProposalUserVotingInfo retrieves voting information for a specific user on a specific proposal. -// -// Parameters: -// - proposalID: unique identifier of the proposal -// - userAddress: address of the user -// -// Returns: -// - *VotingInfo: voting information for the user -// - bool: true if voting information exists for the user -func getProposalUserVotingInfo(proposalID int64, userAddress std.Address) (*VotingInfo, bool) { - // First get all voting info for the proposal - userVotingInfos, exists := getProposalUserVotingInfos(proposalID) - if !exists { - return nil, false - } - - // Then lookup the specific user's voting info - val, exists := userVotingInfos[userAddress.String()] - if !exists { - return nil, false - } - - return val, true -} diff --git a/contract/r/gnoswap/v1/gov/governance/utils.gno b/contract/r/gnoswap/v1/gov/governance/utils.gno deleted file mode 100644 index 5355928..0000000 --- a/contract/r/gnoswap/v1/gov/governance/utils.gno +++ /dev/null @@ -1,159 +0,0 @@ -package governance - -import ( - "encoding/base64" - "strconv" - "strings" - - "gno.land/p/nt/avl" - - "gno.land/p/onbloc/json" - "gno.land/p/nt/ufmt" -) - -// iterTree iterates over an AVL tree and applies a callback function to each element. -func iterTree(tree *avl.Tree, cb func(key string, value any) bool) { - tree.Iterate("", "", cb) -} - -// strToInt converts a string to an integer. -func strToInt(str string) int { - res, err := strconv.Atoi(str) - if err != nil { - panic(err.Error()) - } - - return res -} - -// marshal marshals a JSON node to a string. -func marshal(data *json.Node) string { - b, err := json.Marshal(data) - if err != nil { - panic(err.Error()) - } - - return string(b) -} - -// b64Encode encodes a string to base64. -func b64Encode(data string) string { - return string(base64.StdEncoding.EncodeToString([]byte(data))) -} - -// b64Decode decodes a base64 string. -func b64Decode(data string) string { - decoded, err := base64.StdEncoding.DecodeString(data) - if err != nil { - panic(err.Error()) - } - return string(decoded) -} - -// formatInt formats an integer to a string. -func formatInt(v any) string { - switch v := v.(type) { - case int8: - return strconv.FormatInt(int64(v), 10) - case int32: - return strconv.FormatInt(int64(v), 10) - case int64: - return strconv.FormatInt(v, 10) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} - -// parseInt64 parses a string to int64. -func parseInt64(s string) int64 { - num, err := strconv.ParseInt(s, 10, 64) - if err != nil { - panic(ufmt.Sprintf("invalid int64 value: %s", s)) - } - - return num -} - -// parseUint64 parses a string to uint64. -func parseUint64(s string) uint64 { - num, err := strconv.ParseUint(s, 10, 64) - if err != nil { - panic(ufmt.Sprintf("invalid uint64 value: %s", s)) - } - - return num -} - -// formatBool formats a boolean to a string. -func formatBool(v bool) string { - return strconv.FormatBool(v) -} - -// numberKind represents the type of number to parse. -type numberKind int - -const ( - kindInt numberKind = iota - kindInt64 - kindUint64 -) - -// parseNumber parses a string to a number (int, int64, or uint64). -func parseNumber(s string, kind numberKind) any { - switch kind { - case kindInt: - num, err := strconv.ParseInt(s, 10, 64) - if err != nil { - panic(ufmt.Sprintf("invalid int value: %s", s)) - } - return int(num) - case kindInt64: - num, err := strconv.ParseInt(s, 10, 64) - if err != nil { - panic(ufmt.Sprintf("invalid int64 value: %s", s)) - } - return num - case kindUint64: - num, err := strconv.ParseUint(s, 10, 64) - if err != nil { - panic(ufmt.Sprintf("invalid uint64 value: %s", s)) - } - return num - default: - panic(ufmt.Sprintf("unsupported number kind: %v", kind)) - } -} - -// parseBool parses a string to a boolean. -func parseBool(s string) bool { - switch s { - case "true": - return true - case "false": - return false - default: - panic(ufmt.Sprintf("invalid bool value: %s", s)) - } -} - -// makeExecuteMessage creates a message to execute a function. -// Message format: *EXE**EXE*. -func makeExecuteMessage(pkgPath, function string, params []string) string { - messageParams := make([]string, 0) - - messageParams = append(messageParams, pkgPath) - messageParams = append(messageParams, function) - messageParams = append(messageParams, strings.Join(params, ",")) - - return strings.Join(messageParams, parameterSeparator) -} - -// parseMessage parses an execution message into its components. -func parseMessage(msg string) (pkgPath string, function string, params []string, err error) { - parts := strings.Split(msg, parameterSeparator) - if len(parts) != 3 { - return "", "", nil, errInvalidMessageFormat - } - - return parts[0], parts[1], strings.Split(parts[2], ","), nil -} diff --git a/contract/r/gnoswap/v1/gov/governance/voting_info.gno b/contract/r/gnoswap/v1/gov/governance/voting_info.gno deleted file mode 100644 index 8a60d9f..0000000 --- a/contract/r/gnoswap/v1/gov/governance/voting_info.gno +++ /dev/null @@ -1,150 +0,0 @@ -package governance - -import ( - "std" -) - -// VotingInfo tracks voting-related information for a specific user on a specific proposal. -// This structure maintains the user's voting eligibility, voting history, and voting power. -type VotingInfo struct { - voterAddress std.Address // Address of the voter - availableVoteWeight int64 // Total voting weight available to this user for this proposal - votedWeight int64 // Actual weight used when voting (0 if not voted) - votedHeight int64 // Block height when vote was cast - votedAt int64 // Timestamp when vote was cast - votedYes bool // True if voted "yes", false if voted "no" - voted bool // True if user has already voted -} - -// VotingType returns a human-readable string representation of the vote choice. -// -// Returns: -// - string: "yes" or "no" based on voting choice -func (v *VotingInfo) VotingType() string { - if v.votedYes { - return "yes" - } - - return "no" -} - -// IsVoted checks if the user has already cast their vote. -// -// Returns: -// - bool: true if user has voted on this proposal -func (v *VotingInfo) IsVoted() bool { - return v.voted -} - -// VotedYes checks if the user voted "yes" on the proposal. -// Only meaningful if IsVoted() returns true. -// -// Returns: -// - bool: true if user voted "yes" -func (v *VotingInfo) VotedYes() bool { - return v.votedYes -} - -// VotedNo checks if the user voted "no" on the proposal. -// Only meaningful if IsVoted() returns true. -// -// Returns: -// - bool: true if user voted "no" -func (v *VotingInfo) VotedNo() bool { - return !v.votedYes -} - -// AvailableVoteWeight returns the total voting weight available to this user. -// This weight is determined at proposal creation time based on delegation snapshots. -// -// Returns: -// - int64: available voting weight -func (v *VotingInfo) AvailableVoteWeight() int64 { - return v.availableVoteWeight -} - -// VotedWeight returns the weight actually used when voting. -// Returns 0 if the user hasn't voted yet. -// -// Returns: -// - int64: weight used for voting, or 0 if not voted -func (v *VotingInfo) VotedWeight() int64 { - if !v.voted { - return 0 - } - - return v.votedWeight -} - -// voteYes records a "yes" vote with the specified weight and timing information. -// This is an internal helper method that delegates to the main vote function. -// -// Parameters: -// - weight: voting weight to use for this vote -// - votedHeight: block height when vote is cast -// - votedAt: timestamp when vote is cast -// -// Returns: -// - error: voting error if vote cannot be recorded -func (v *VotingInfo) voteYes(weight int64, votedHeight int64, votedAt int64) error { - return v.vote(true, weight, votedHeight, votedAt) -} - -// voteNo records a "no" vote with the specified weight and timing information. -// This is an internal helper method that delegates to the main vote function. -// -// Parameters: -// - weight: voting weight to use for this vote -// - votedHeight: block height when vote is cast -// - votedAt: timestamp when vote is cast -// -// Returns: -// - error: voting error if vote cannot be recorded -func (v *VotingInfo) voteNo(weight int64, votedHeight int64, votedAt int64) error { - return v.vote(false, weight, votedHeight, votedAt) -} - -// vote records a vote with the specified choice, weight, and timing information. -// This is the core voting method that prevents double voting and records all vote details. -// -// Parameters: -// - votedYes: true for "yes" vote, false for "no" vote -// - weight: voting weight to use for this vote -// - votedHeight: block height when vote is cast -// - votedAt: timestamp when vote is cast -// -// Returns: -// - error: voting error if user has already voted -func (v *VotingInfo) vote(votedYes bool, weight int64, votedHeight int64, votedAt int64) error { - // Prevent double voting - each user can only vote once per proposal - if v.voted { - return errAlreadyVoted - } - - // Record all voting details - v.votedWeight = weight - v.votedHeight = votedHeight - v.votedAt = votedAt - v.voted = true - v.votedYes = votedYes - - return nil -} - -// NewVotingInfo creates a new voting information structure for a user. -// This constructor initializes the voting eligibility based on delegation snapshots. -// -// Parameters: -// - availableVoteWeight: total voting weight available to this user -// - voterAddress: address of the voter -// -// Returns: -// - *VotingInfo: newly created voting information structure -func NewVotingInfo(availableVoteWeight int64, voterAddress std.Address) *VotingInfo { - return &VotingInfo{ - availableVoteWeight: availableVoteWeight, - voterAddress: voterAddress, - // Other fields are initialized to zero values (false, 0) - // voted starts as false, indicating no vote has been cast - } -} diff --git a/contract/r/gnoswap/v1/gov/staker/api_delegation.gno b/contract/r/gnoswap/v1/gov/staker/api_delegation.gno deleted file mode 100644 index ff3b9be..0000000 --- a/contract/r/gnoswap/v1/gov/staker/api_delegation.gno +++ /dev/null @@ -1,25 +0,0 @@ -package staker - -import ( - "gno.land/r/gnoswap/v1/gov/xgns" -) - -// GetTotalxGnsSupply returns the total amount of xGNS supply. -func GetTotalxGnsSupply() int64 { - return xgns.TotalSupply() -} - -// GetTotalVoteWeight returns the total amount of xGNS used for voting. -func GetTotalVoteWeight() int64 { - return xgns.VotingSupply() -} - -// GetTotalDelegated returns the total amount of xGNS delegated. -func GetTotalDelegated() int64 { - return totalDelegatedAmount -} - -// GetTotalLockedAmount returns the total amount of locked GNS. -func GetTotalLockedAmount() int64 { - return totalLockedAmount -} diff --git a/contract/r/gnoswap/v1/gov/staker/api_staker.gno b/contract/r/gnoswap/v1/gov/staker/api_staker.gno deleted file mode 100644 index 5c11a68..0000000 --- a/contract/r/gnoswap/v1/gov/staker/api_staker.gno +++ /dev/null @@ -1,77 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/p/onbloc/json" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/v1/protocol_fee" -) - -// GetLockedAmount returns total locked GNS. -func GetLockedAmount() int64 { - lockedAmount := int64(0) - - delegations.Iterate("", "", func(key string, value any) bool { - delegation, ok := value.(*Delegation) - if !ok { - panic(ufmt.Sprintf("failed to cast delegations's element to *Delegation: %T", value)) - } - lockedAmount += delegation.DelegatedAmount() - return false - }) - - return lockedAmount -} - -// GetClaimableRewardByAddress returns claimable reward for address. -func GetClaimableRewardByAddress(addr std.Address) string { - return GetClaimableRewardByRewardID(addr.String()) -} - -// GetClaimableRewardByLaunchpad returns claimable reward for launchpad. -func GetClaimableRewardByLaunchpad(addr std.Address) string { - return GetClaimableRewardByRewardID(makeLaunchpadRewardID(addr.String())) -} - -// GetClaimableRewardByRewardID returns claimable reward by ID. -func GetClaimableRewardByRewardID(rewardID string) string { - func(cur realm) { - emission.MintAndDistributeGns(cross) - protocol_fee.DistributeProtocolFee(cross) - }(cross) - - emissionDistributedAmount := emission.GetAccuDistributedToGovStaker() - emissionReward, _ := emissionRewardManager.GetClaimableRewardAmount(emissionDistributedAmount, rewardID, time.Now().Unix()) - - protocolFeeDistributedAmounts := getDistributedProtocolFees() - protocolFeeRewards, _ := protocolFeeRewardManager.GetClaimableRewardAmounts(protocolFeeDistributedAmounts, rewardID, time.Now().Unix()) - - if emissionReward == 0 && len(protocolFeeRewards) == 0 { - return "" - } - - data := json.Builder(). - WriteString("height", formatInt(std.ChainHeight())). - WriteString("now", formatInt(time.Now().Unix())). - WriteString("emissionReward", formatInt(emissionReward)). - Node() - - // Always include protocolFees array, even if empty - pfArr := json.ArrayNode("", nil) - for tokenPath, protocolFeeReward := range protocolFeeRewards { - if protocolFeeReward > 0 { - pfObj := json.Builder(). - WriteString("tokenPath", tokenPath). - WriteString("amount", formatInt(protocolFeeReward)). - Node() - pfArr.AppendArray(pfObj) - } - } - data.AppendObject("protocolFees", pfArr) - - return marshal(data) -} diff --git a/contract/r/gnoswap/v1/gov/staker/assert.gno b/contract/r/gnoswap/v1/gov/staker/assert.gno deleted file mode 100644 index 3e52a7d..0000000 --- a/contract/r/gnoswap/v1/gov/staker/assert.gno +++ /dev/null @@ -1,27 +0,0 @@ -package staker - -import "gno.land/p/nt/ufmt" - -// assertIsValidDelegateAmount validates that the delegation amount meets system requirements. -// This function checks minimum amount and multiple requirements. -// -// Parameters: -// - amount: amount to validate -// -// Returns: -// - error: nil if valid, error describing validation failure -func assertIsValidDelegateAmount(amount int64) { - if amount < minimumAmount { - panic(makeErrorWithDetails( - errLessThanMinimum, - ufmt.Sprintf("minimum amount to delegate is %d (requested:%d)", minimumAmount, amount), - )) - } - - if amount%minimumAmount != 0 { - panic(makeErrorWithDetails( - errInvalidAmount, - ufmt.Sprintf("amount must be multiple of %d", minimumAmount), - )) - } -} diff --git a/contract/r/gnoswap/v1/gov/staker/consts.gno b/contract/r/gnoswap/v1/gov/staker/consts.gno deleted file mode 100644 index fa12dd1..0000000 --- a/contract/r/gnoswap/v1/gov/staker/consts.gno +++ /dev/null @@ -1,12 +0,0 @@ -package staker - -import ( - u256 "gno.land/p/gnoswap/uint256" -) - -const ( - GNOT string = "gnot" - minimumAmount = 1_000_000 // 1 GNS -) - -var q128 = u256.MustFromDecimal("340282366920938463463374607431768211456") diff --git a/contract/r/gnoswap/v1/gov/staker/counter.gno b/contract/r/gnoswap/v1/gov/staker/counter.gno deleted file mode 100644 index 4c2cd21..0000000 --- a/contract/r/gnoswap/v1/gov/staker/counter.gno +++ /dev/null @@ -1,13 +0,0 @@ -package staker - -type Counter struct { - id int64 -} - -func NewCounter() *Counter { return &Counter{id: 0} } -func (c *Counter) Get() int64 { return c.id } - -func (c *Counter) next() int64 { - c.id++ - return c.id -} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation.gno b/contract/r/gnoswap/v1/gov/staker/delegation.gno deleted file mode 100644 index d5a0e30..0000000 --- a/contract/r/gnoswap/v1/gov/staker/delegation.gno +++ /dev/null @@ -1,164 +0,0 @@ -package staker - -import "std" - -// Error message constants -const ( - errCollectAmountExceedsCollectable = "amount to collect is greater than collectable amount" - errOverflowInCollectedAmount = "overflow in delegation collected amount: current(%d) + collect(%d)" -) - -// DelegationType represents the type of delegation operation -type DelegationType string - -const ( - DelegateType DelegationType = "DELEGATE" - UnDelegateType DelegationType = "UNDELEGATE" -) - -func (d DelegationType) String() string { return string(d) } -func (d DelegationType) IsDelegate() bool { return d == DelegateType } -func (d DelegationType) IsUnDelegate() bool { return d == UnDelegateType } - -// Delegation represents a delegation between two addresses -type Delegation struct { - id int64 - delegateAmount int64 - unDelegateAmount int64 - collectedAmount int64 - delegateFrom std.Address - delegateTo std.Address - createdHeight int64 - createdAt int64 - withdraws []*DelegationWithdraw -} - -// NewDelegation creates a new delegation -func NewDelegation( - id int64, - delegateFrom, delegateTo std.Address, - delegateAmount, createdHeight, createdAt int64, -) *Delegation { - return &Delegation{ - id: id, - delegateFrom: delegateFrom, - delegateTo: delegateTo, - delegateAmount: delegateAmount, - createdHeight: createdHeight, - createdAt: createdAt, - unDelegateAmount: 0, - collectedAmount: 0, - withdraws: make([]*DelegationWithdraw, 0), - } -} - -// Basic getters -func (d *Delegation) ID() int64 { return d.id } -func (d *Delegation) DelegateFrom() std.Address { return d.delegateFrom } -func (d *Delegation) DelegateTo() std.Address { return d.delegateTo } -func (d *Delegation) CreatedAt() int64 { return d.createdAt } - -// Amount getters -func (d *Delegation) TotalDelegatedAmount() int64 { return d.delegateAmount } -func (d *Delegation) UnDelegatedAmount() int64 { return d.unDelegateAmount } -func (d *Delegation) CollectedAmount() int64 { return d.collectedAmount } - -// Calculated amounts -func (d *Delegation) DelegatedAmount() int64 { return d.delegateAmount - d.unDelegateAmount } -func (d *Delegation) LockedAmount() int64 { return d.delegateAmount - d.collectedAmount } -func (d *Delegation) IsEmpty() bool { return d.LockedAmount() == 0 } - -// CollectableAmount calculates the total amount that can be collected at the given time -func (d *Delegation) CollectableAmount(currentTime int64) (total int64) { - for _, withdraw := range d.withdraws { - total += withdraw.CollectableAmount(currentTime) - } - return -} - -// unDelegate processes an undelegation with lockup period -func (d *Delegation) unDelegate( - amount, currentHeight, currentTimestamp, unDelegationLockupPeriod int64, -) { - d.unDelegateAmount += amount - d.withdraws = append(d.withdraws, NewDelegationWithdraw( - d.id, - amount, - currentHeight, - currentTimestamp, - unDelegationLockupPeriod, - )) -} - -// UnDelegateWithoutLockup processes an immediate undelegation without lockup -func (d *Delegation) unDelegateWithoutLockup( - amount, currentHeight, currentTime int64, -) { - d.unDelegateAmount += amount - d.collectedAmount += amount - d.withdraws = append(d.withdraws, NewDelegationWithdrawWithoutLockup( - d.id, - amount, - currentHeight, - currentTime, - )) -} - -// Collect processes the collection of available amounts -func (d *Delegation) collect(amount, currentTime int64) error { - if amount > d.CollectableAmount(currentTime) { - return makeErrorWithDetails( - errInvalidAmount, - errCollectAmountExceedsCollectable, - ) - } - - return d.processCollection(amount, currentTime) -} - -// processCollection handles the actual collection logic -func (d *Delegation) processCollection(amount, currentTime int64) error { - remainingToCollect := amount - - for _, withdraw := range d.withdraws { - if remainingToCollect <= 0 { - break - } - - if !withdraw.IsCollectable(currentTime) { - continue - } - - collectableAmount := withdraw.CollectableAmount(currentTime) - if collectableAmount <= 0 { - continue - } - - amountToCollect := min(remainingToCollect, collectableAmount) - - if err := withdraw.collect(amountToCollect, currentTime); err != nil { - return err - } - - updatedAmount, err := addToCollectedAmount(d.collectedAmount, amountToCollect) - if err != nil { - return err - } - d.collectedAmount = updatedAmount - remainingToCollect -= amountToCollect - } - - return nil -} - -func addToCollectedAmount(collectedAmount, amount int64) (int64, error) { - return collectedAmount + amount, nil -} - -// min returns the smaller of two int64 values -func min(a, b int64) int64 { - if a < b { - return a - } - return b -} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_history.gno b/contract/r/gnoswap/v1/gov/staker/delegation_history.gno deleted file mode 100644 index c0ba83f..0000000 --- a/contract/r/gnoswap/v1/gov/staker/delegation_history.gno +++ /dev/null @@ -1,61 +0,0 @@ -package staker - -// DelegationHistory represents a chronological list of delegation records -// used to track delegation changes over time for snapshot calculations -type DelegationHistory []*DelegationRecord - -// getRecordsBy retrieves delegation records that occurred at or after the specified snapshot time. -// This method is used to filter historical records for calculating delegation snapshots at specific points in time. -// -// Parameters: -// - snapshotTime: timestamp to filter records from (inclusive) -// -// Returns: -// - DelegationHistory: filtered records occurring at or after snapshotTime -func (dh DelegationHistory) getRecordsBy(snapshotTime int64) DelegationHistory { - records := make(DelegationHistory, 0) - - historyIndex := -1 - - // Find the first record at or after the snapshot time - for index, record := range dh { - if record.CreatedAt() >= snapshotTime { - historyIndex = index - break - } - } - - // If no records found at or after snapshot time, return empty slice - if historyIndex == -1 { - return records - } - - // Return all records from the found index onwards - records = append(records, dh[historyIndex:]...) - - return records -} - -// addRecord appends a new delegation record to the history. -// This method maintains the chronological order of delegation events. -// -// Parameters: -// - delegationRecord: the delegation record to add to history -// -// Returns: -// - DelegationHistory: updated history with the new record appended -func (dh DelegationHistory) addRecord(delegationRecord *DelegationRecord) DelegationHistory { - return append(dh, delegationRecord) -} - -// removeRecordsBy removes historical records that occurred before the specified time. -// This method is used for cleanup operations to remove old historical data. -// -// Parameters: -// - previousTime: cutoff timestamp for record removal -// -// Returns: -// - DelegationHistory: filtered history containing only records at or after previousTime -func (dh DelegationHistory) removeRecordsBy(previousTime int64) DelegationHistory { - return dh.getRecordsBy(previousTime) -} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_mananger.gno b/contract/r/gnoswap/v1/gov/staker/delegation_mananger.gno deleted file mode 100644 index 32b4d4d..0000000 --- a/contract/r/gnoswap/v1/gov/staker/delegation_mananger.gno +++ /dev/null @@ -1,144 +0,0 @@ -package staker - -import ( - "std" -) - -// DelegationManager manages the mapping between users and their delegation IDs. -// It provides efficient lookup and management of user delegations organized by delegator and delegatee addresses. -type DelegationManager struct { - // userDelegations maps delegator address -> delegatee address -> list of delegation IDs - // This nested mapping allows efficient retrieval of delegations by both delegator and delegatee - userDelegations map[string]map[string][]int64 -} - -// GetUserDelegationIDsWithDelegatee retrieves all delegation IDs for a specific delegator-delegatee pair. -// This method is used to find delegations from a specific user to a specific delegate. -// -// Parameters: -// - delegator: address of the user who delegated tokens -// - delegatee: address of the user who received the delegation -// -// Returns: -// - []int64: list of delegation IDs for the specified pair -func (dm *DelegationManager) GetUserDelegationIDsWithDelegatee(delegator std.Address, delegatee std.Address) []int64 { - delegatorAddress := delegator.String() - delegateeAddress := delegatee.String() - - return dm.userDelegations[delegatorAddress][delegateeAddress] -} - -// GetUserDelegationIDs retrieves all delegation IDs for a specific delegator across all delegatees. -// This method is used to find all delegations made by a specific user. -// -// Parameters: -// - delegator: address of the user whose delegations to retrieve -// -// Returns: -// - []int64: list of all delegation IDs for the delegator -func (dm *DelegationManager) GetUserDelegationIDs(delegator std.Address) []int64 { - delegatorAddress := delegator.String() - delegationIDs := make([]int64, 0) - - // Return empty slice if no delegations exist for this user - if dm.userDelegations[delegatorAddress] == nil { - return delegationIDs - } - - // Collect delegation IDs from all delegatees - for _, toDelegations := range dm.userDelegations[delegatorAddress] { - delegationIDs = append(delegationIDs, toDelegations...) - } - - return delegationIDs -} - -// addDelegation adds a delegation ID to the manager's tracking system. -// This method creates the necessary nested map structure if it doesn't exist -// and ensures no duplicate delegation IDs are stored. -// -// Parameters: -// - delegator: address of the user who made the delegation -// - delegatee: address of the user who received the delegation -// - delegationID: unique identifier for the delegation -func (dm *DelegationManager) addDelegation(delegator, delegatee std.Address, delegationID int64) { - delegatorAddress := delegator.String() - delegateeAddress := delegatee.String() - - // Initialize delegator map if it doesn't exist - if _, ok := dm.userDelegations[delegatorAddress]; !ok { - dm.userDelegations[delegatorAddress] = make(map[string][]int64) - } - - // Initialize delegatee slice if it doesn't exist - if _, ok := dm.userDelegations[delegatorAddress][delegateeAddress]; !ok { - dm.userDelegations[delegatorAddress][delegateeAddress] = make([]int64, 0) - } - - // Check for duplicate delegation IDs before adding - delegationIDs := dm.userDelegations[delegatorAddress][delegateeAddress] - for _, id := range delegationIDs { - if id == delegationID { - return - } - } - - // Add the new delegation ID - dm.userDelegations[delegatorAddress][delegateeAddress] = append( - delegationIDs, - delegationID, - ) -} - -// removeDelegation removes a delegation ID from the manager's tracking system. -// This method finds and removes the specified delegation ID from the appropriate slice. -// -// Parameters: -// - delegator: address of the user who made the delegation -// - delegatee: address of the user who received the delegation -// - delegationID: unique identifier for the delegation to remove -func (dm *DelegationManager) removeDelegation(delegator, delegatee std.Address, delegationID int64) { - delegatorAddress := delegator.String() - delegateeAddress := delegatee.String() - - // Check if delegator exists in the map - userDelegations, ok := dm.userDelegations[delegatorAddress] - if !ok { - return - } - - // Check if delegatee exists for this delegator - delegationIDs, ok := userDelegations[delegateeAddress] - if !ok { - return - } - - index := -1 - - // Find the index of the delegation ID to remove - for i, id := range delegationIDs { - if id == delegationID { - index = i - break - } - } - - // Remove the delegation ID if found - if index != -1 { - dm.userDelegations[delegatorAddress][delegateeAddress] = append( - delegationIDs[:index], - delegationIDs[index+1:]..., - ) - } -} - -// NewDelegationManager creates a new instance of DelegationManager. -// This factory function initializes the nested map structure for tracking user delegations. -// -// Returns: -// - *DelegationManager: initialized delegation manager instance -func NewDelegationManager() *DelegationManager { - return &DelegationManager{ - userDelegations: make(map[string]map[string][]int64), - } -} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_record.gno b/contract/r/gnoswap/v1/gov/staker/delegation_record.gno deleted file mode 100644 index 681c198..0000000 --- a/contract/r/gnoswap/v1/gov/staker/delegation_record.gno +++ /dev/null @@ -1,156 +0,0 @@ -package staker - -import ( - "std" -) - -// DelegationRecord represents a single delegation event in the system. -// This struct tracks delegation or undelegation actions with their associated metadata -// and is used for historical tracking and snapshot calculations. -type DelegationRecord struct { - // delegationType indicates whether this is a delegation or undelegation action - delegationType DelegationType - // delegateFrom is the address of the user who initiated the delegation - delegateFrom std.Address - // delegateTo is the address of the user who received the delegation - delegateTo std.Address - // delegateAmount is the amount delegated (set only for delegation actions) - delegateAmount int64 - // unDelegateAmount is the amount undelegated (set only for undelegation actions) - unDelegateAmount int64 - // createdAt is the timestamp when this record was created - createdAt int64 -} - -// DelegationType returns the type of delegation action (DELEGATE or UNDELEGATE). -// -// Returns: -// - DelegationType: the type of this delegation record -func (d *DelegationRecord) DelegationType() DelegationType { - return d.delegationType -} - -// DelegateAmount returns the amount that was delegated. -// This value is non-zero only for delegation actions. -// -// Returns: -// - int64: amount delegated (0 for undelegation records) -func (d *DelegationRecord) DelegateAmount() int64 { - return d.delegateAmount -} - -// UnDelegateAmount returns the amount that was undelegated. -// This value is non-zero only for undelegation actions. -// -// Returns: -// - int64: amount undelegated (0 for delegation records) -func (d *DelegationRecord) UnDelegateAmount() int64 { - return d.unDelegateAmount -} - -// DelegateFrom returns the address of the user who initiated the delegation. -// -// Returns: -// - std.Address: delegator's address -func (d *DelegationRecord) DelegateFrom() std.Address { - return d.delegateFrom -} - -// DelegateTo returns the address of the user who received the delegation. -// -// Returns: -// - std.Address: delegatee's address -func (d *DelegationRecord) DelegateTo() std.Address { - return d.delegateTo -} - -// CreatedAt returns the timestamp when this delegation record was created. -// -// Returns: -// - int64: creation timestamp -func (d *DelegationRecord) CreatedAt() int64 { - return d.createdAt -} - -// NewDelegationRecord creates a new delegation record with the specified parameters. -// This factory function properly sets either delegateAmount or unDelegateAmount based on the delegation type. -// -// Parameters: -// - delegationType: type of delegation action (DELEGATE or UNDELEGATE) -// - delegationAmount: amount being delegated or undelegated -// - delegateFrom: address of the delegator -// - delegateTo: address of the delegatee -// - createdAt: timestamp of the action -// -// Returns: -// - *DelegationRecord: newly created delegation record -func NewDelegationRecord( - delegationType DelegationType, - delegationAmount int64, - delegateFrom std.Address, - delegateTo std.Address, - createdAt int64, -) *DelegationRecord { - delegateAmount := int64(0) - unDelegateAmount := int64(0) - - // Set the appropriate amount field based on delegation type - if delegationType.IsDelegate() { - delegateAmount = delegationAmount - } else { - unDelegateAmount = delegationAmount - } - - return &DelegationRecord{ - delegationType: delegationType, - delegateAmount: delegateAmount, - unDelegateAmount: unDelegateAmount, - delegateFrom: delegateFrom, - delegateTo: delegateTo, - createdAt: createdAt, - } -} - -// NewDelegationDelegateRecordBy creates a delegation record from an existing Delegation instance. -// This factory function is used to create historical records for delegation actions. -// -// Parameters: -// - delegation: the delegation instance to create a record from -// -// Returns: -// - *DelegationRecord: delegation record representing the delegation action -func NewDelegationDelegateRecordBy( - delegation *Delegation, -) *DelegationRecord { - return NewDelegationRecord( - DelegateType, - delegation.DelegatedAmount(), - delegation.DelegateFrom(), - delegation.DelegateTo(), - delegation.CreatedAt(), - ) -} - -// NewDelegationWithdrawRecordBy creates an undelegation record for a withdrawal action. -// This factory function is used to create historical records for undelegation actions. -// -// Parameters: -// - delegation: the delegation instance being withdrawn from -// - withdrawAmount: amount being withdrawn -// - currentTime: timestamp of the withdrawal action -// -// Returns: -// - *DelegationRecord: delegation record representing the undelegation action -func NewDelegationWithdrawRecordBy( - delegation *Delegation, - withdrawAmount int64, - currentTime int64, -) *DelegationRecord { - return NewDelegationRecord( - UnDelegateType, - withdrawAmount, - delegation.DelegateFrom(), - delegation.DelegateTo(), - currentTime, - ) -} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_snapshot.gno b/contract/r/gnoswap/v1/gov/staker/delegation_snapshot.gno deleted file mode 100644 index 08a66c5..0000000 --- a/contract/r/gnoswap/v1/gov/staker/delegation_snapshot.gno +++ /dev/null @@ -1,159 +0,0 @@ -package staker - -import "std" - -// DelegationSnapshot represents a point-in-time view of delegation states. -// It maps delegatee addresses to their corresponding delegation snapshot items, -// providing efficient lookup and manipulation of delegation states at specific timestamps. -type DelegationSnapshot map[string]*DelegationSnapshotItem - -// clone creates a deep copy of the delegation snapshot. -// This method is used to create independent copies for historical calculations -// without modifying the original snapshot state. -// -// Returns: -// - DelegationSnapshot: deep copy of the current snapshot -func (d *DelegationSnapshot) clone() DelegationSnapshot { - clone := make(DelegationSnapshot) - - for k, v := range *d { - clone[k] = v.clone() - } - - return clone -} - -// addRecord applies a delegation record to the snapshot, updating delegation amounts. -// This method creates new snapshot items if they don't exist and removes empty items. -// -// Parameters: -// - delegationRecord: the delegation record to apply to the snapshot -// -// Returns: -// - DelegationSnapshot: updated snapshot with the record applied -func (d DelegationSnapshot) addRecord(delegationRecord *DelegationRecord) DelegationSnapshot { - delegateTo := delegationRecord.DelegateTo() - delegateToStr := delegateTo.String() - - // Create new snapshot item if it doesn't exist - _, ok := d[delegateToStr] - if !ok { - d[delegateToStr] = NewDelegationSnapshotItem(delegateTo) - } - - // Apply the delegation record to the snapshot item - d[delegateToStr].addRecord(delegationRecord) - - // Remove empty snapshot items to keep the map clean - if d[delegateToStr].IsEmpty() { - delete(d, delegateToStr) - } - - return d -} - -// subRecord subtracts a delegation record from the snapshot. -// This method is used for calculating historical snapshots by removing -// the effects of delegation records that occurred after a specific time. -// -// Parameters: -// - delegationRecord: the delegation record to subtract from the snapshot -// -// Returns: -// - DelegationSnapshot: updated snapshot with the record subtracted -func (d DelegationSnapshot) subRecord(delegationRecord *DelegationRecord) DelegationSnapshot { - delegateTo := delegationRecord.DelegateTo() - delegateToStr := delegateTo.String() - - // Create new snapshot item if it doesn't exist - _, ok := d[delegateToStr] - if !ok { - d[delegateToStr] = NewDelegationSnapshotItem(delegateTo) - } - - // Subtract the delegation record from the snapshot item - d[delegateToStr].subRecord(delegationRecord) - - return d -} - -// DelegationSnapshotItem represents delegation information for a specific delegatee. -// It tracks the total delegation amount and the delegator's address. -type DelegationSnapshotItem struct { - // delegationAmount is the total amount delegated to this delegatee - delegationAmount int64 - // delegatorAddress is the address of the delegatee receiving delegations - delegatorAddress std.Address -} - -// DelegatorAddress returns the address of the delegatee. -// -// Returns: -// - std.Address: delegatee's address -func (d *DelegationSnapshotItem) DelegatorAddress() std.Address { - return d.delegatorAddress -} - -// DelegationAmount returns the total delegation amount for this delegatee. -// -// Returns: -// - int64: total delegated amount -func (d *DelegationSnapshotItem) DelegationAmount() int64 { - return d.delegationAmount -} - -// IsEmpty checks if the delegation amount is zero. -// Empty snapshot items are typically removed from the snapshot map. -// -// Returns: -// - bool: true if delegation amount is zero, false otherwise -func (d *DelegationSnapshotItem) IsEmpty() bool { - return d.delegationAmount == 0 -} - -// clone creates a deep copy of the delegation snapshot item. -// -// Returns: -// - *DelegationSnapshotItem: independent copy of the snapshot item -func (d *DelegationSnapshotItem) clone() *DelegationSnapshotItem { - return &DelegationSnapshotItem{ - delegatorAddress: d.delegatorAddress, - delegationAmount: d.delegationAmount, - } -} - -// addRecord applies a delegation record to this snapshot item. -// It increases the delegation amount for delegate actions and decreases for undelegate actions. -// -// Parameters: -// - delegationRecord: the delegation record to apply -func (d *DelegationSnapshotItem) addRecord(delegationRecord *DelegationRecord) { - d.delegationAmount += delegationRecord.DelegateAmount() - d.delegationAmount -= delegationRecord.UnDelegateAmount() -} - -// subRecord subtracts the delegation amount from the snapshot by the delegation record. -// It is used to get previous delegation snapshots by reversing the effects of current delegation records. -// This method performs the inverse operation of addRecord. -// -// Parameters: -// - delegationRecord: the delegation record to subtract from the snapshot -func (d *DelegationSnapshotItem) subRecord(delegationRecord *DelegationRecord) { - d.delegationAmount -= delegationRecord.DelegateAmount() - d.delegationAmount += delegationRecord.UnDelegateAmount() -} - -// NewDelegationSnapshotItem creates a new delegation snapshot item for a delegatee. -// The initial delegation amount is set to zero. -// -// Parameters: -// - delegatorAddress: address of the delegatee -// -// Returns: -// - *DelegationSnapshotItem: new snapshot item with zero delegation amount -func NewDelegationSnapshotItem(delegatorAddress std.Address) *DelegationSnapshotItem { - return &DelegationSnapshotItem{ - delegatorAddress: delegatorAddress, - delegationAmount: 0, - } -} diff --git a/contract/r/gnoswap/v1/gov/staker/delegation_withdraw.gno b/contract/r/gnoswap/v1/gov/staker/delegation_withdraw.gno deleted file mode 100644 index 4a26819..0000000 --- a/contract/r/gnoswap/v1/gov/staker/delegation_withdraw.gno +++ /dev/null @@ -1,177 +0,0 @@ -package staker - -// DelegationWithdraw represents a pending withdrawal from a delegation. -// This struct tracks undelegated amounts that are subject to lockup periods -// and manages the collection process once the lockup period expires. -type DelegationWithdraw struct { - // delegationID is the unique identifier of the associated delegation - delegationID int64 - // unDelegateAmount is the total amount that was undelegated - unDelegateAmount int64 - // unDelegatedHeight is the height when the undelegation occurred - unDelegatedHeight int64 - // unDelegatedAt is the timestamp when the undelegation occurred - unDelegatedAt int64 - // collectedAmount is the amount that has already been collected - collectedAmount int64 - // collectableTime is the timestamp when collection becomes available - collectableTime int64 - // collectedAt is the timestamp when collection occurred - collectedAt int64 - // collected indicates whether the withdrawal has been fully collected - collected bool -} - -// DelegationID returns the unique identifier of the associated delegation. -// -// Returns: -// - int64: delegation ID -func (d *DelegationWithdraw) DelegationID() int64 { - return d.delegationID -} - -// UnDelegateAmount returns the total amount that was undelegated. -// -// Returns: -// - int64: undelegated amount -func (d *DelegationWithdraw) UnDelegateAmount() int64 { - return d.unDelegateAmount -} - -// UnDelegatedAt returns the timestamp when the undelegation occurred. -// -// Returns: -// - int64: undelegation timestamp -func (d *DelegationWithdraw) UnDelegatedAt() int64 { - return d.unDelegatedAt -} - -// CollectableAmount calculates the amount available for collection at the given time. -// Returns zero if the withdrawal is not yet collectable or has been fully collected. -// -// Parameters: -// - currentTime: current timestamp to check collectability against -// -// Returns: -// - int64: amount available for collection -func (d *DelegationWithdraw) CollectableAmount(currentTime int64) int64 { - if d.IsCollectable(currentTime) { - return d.unDelegateAmount - d.collectedAmount - } - - return 0 -} - -// IsCollectable determines whether the withdrawal can be collected at the given time. -// A withdrawal is collectable if: -// - The undelegated amount is positive -// - There is remaining uncollected amount -// - The current time is at or after the collectable time -// -// Parameters: -// - currentTime: current timestamp to check against -// -// Returns: -// - bool: true if the withdrawal can be collected, false otherwise -func (d *DelegationWithdraw) IsCollectable(currentTime int64) bool { - if d.unDelegateAmount <= 0 { - return false - } - - if d.unDelegateAmount-d.collectedAmount <= 0 { - return false - } - - if currentTime < d.collectableTime { - return false - } - - return true -} - -// IsCollected returns whether the withdrawal has been fully collected. -// -// Returns: -// - bool: true if fully collected, false otherwise -func (d *DelegationWithdraw) IsCollected() bool { - return d.collected -} - -// collect processes the collection of the specified amount from this withdrawal. -// This method validates collectability and updates the collection state. -// -// Parameters: -// - amount: amount to collect -// - currentTime: current timestamp -// -// Returns: -// - error: nil on success, error if collection is not allowed -func (d *DelegationWithdraw) collect(amount int64, currentTime int64) error { - if !d.IsCollectable(currentTime) { - return errInvalidAmount - } - - d.collected = true - d.collectedAt = currentTime - d.collectedAmount += amount - - return nil -} - -// NewDelegationWithdraw creates a new delegation withdrawal with lockup period. -// The withdrawal will be collectable after the lockup period expires. -// -// Parameters: -// - delegationID: unique identifier of the associated delegation -// - unDelegateAmount: amount being withdrawn -// - createdAt: timestamp when the withdrawal was created -// - unDelegationLockupPeriod: duration of the lockup period in seconds -// -// Returns: -// - *DelegationWithdraw: new withdrawal instance with lockup -func NewDelegationWithdraw( - delegationID, - unDelegateAmount, - createdHeight, - createdAt, - unDelegationLockupPeriod int64, -) *DelegationWithdraw { - return &DelegationWithdraw{ - delegationID: delegationID, - unDelegateAmount: unDelegateAmount, - unDelegatedHeight: createdHeight, - unDelegatedAt: createdAt, - collectableTime: createdAt + unDelegationLockupPeriod, - collectedAmount: 0, - collectedAt: 0, - collected: false, - } -} - -// NewDelegationWithdrawWithoutLockup creates a new delegation withdrawal that is immediately collectable. -// This is used for special cases like redelegation where no lockup period is required. -// -// Parameters: -// - delegationID: unique identifier of the associated delegation -// - unDelegateAmount: amount being withdrawn -// - createdAt: timestamp when the withdrawal was created -// -// Returns: -// - *DelegationWithdraw: new withdrawal instance that is immediately collected -func NewDelegationWithdrawWithoutLockup( - delegationID, - unDelegateAmount, - createdHeight, - createdAt int64, -) *DelegationWithdraw { - return &DelegationWithdraw{ - delegationID: delegationID, - unDelegateAmount: unDelegateAmount, - unDelegatedHeight: createdHeight, - unDelegatedAt: createdAt, - collectableTime: createdAt, - collectedAmount: unDelegateAmount, - collectedAt: createdAt, - collected: true, - } -} diff --git a/contract/r/gnoswap/v1/gov/staker/doc.gno b/contract/r/gnoswap/v1/gov/staker/doc.gno deleted file mode 100644 index cdd5c05..0000000 --- a/contract/r/gnoswap/v1/gov/staker/doc.gno +++ /dev/null @@ -1,4 +0,0 @@ -// Package staker manages GNS token staking and delegation functionality. -// It handles delegation of voting power, distribution of protocol rewards, -// and implements a 7-day lockup period for unstaking operations. -package staker diff --git a/contract/r/gnoswap/v1/gov/staker/emission_reward_manager.gno b/contract/r/gnoswap/v1/gov/staker/emission_reward_manager.gno deleted file mode 100644 index aa946ba..0000000 --- a/contract/r/gnoswap/v1/gov/staker/emission_reward_manager.gno +++ /dev/null @@ -1,235 +0,0 @@ -package staker - -import ( - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - u256 "gno.land/p/gnoswap/uint256" -) - -var errFailedToCastRewardState = "failed to cast rewardStates's element to *EmissionRewardState: %T" - -// EmissionRewardManager manages the distribution of emission rewards to stakers. -type EmissionRewardManager struct { - // rewardStates maps address to EmissionRewardState for tracking individual staker rewards - rewardStates *avl.Tree // address -> EmissionRewardState - - // accumulatedRewardX128PerStake tracks the cumulative reward per unit of stake with 128-bit precision - accumulatedRewardX128PerStake *u256.Uint - // distributedAmount tracks the total amount of rewards distributed - distributedAmount int64 - // accumulatedTimestamp tracks the last timestamp when rewards were accumulated - accumulatedTimestamp int64 - // totalStakedAmount tracks the total amount of tokens staked in the system - totalStakedAmount int64 -} - -// GetAccumulatedRewardX128PerStake returns the accumulated reward per stake with 128-bit precision. -func (e *EmissionRewardManager) GetAccumulatedRewardX128PerStake() *u256.Uint { - return e.accumulatedRewardX128PerStake -} - -// GetAccumulatedTimestamp returns the last timestamp when rewards were accumulated. -func (e *EmissionRewardManager) GetAccumulatedTimestamp() int64 { - return e.accumulatedTimestamp -} - -// GetTotalStakedAmount returns the total amount of tokens staked in the system. -func (e *EmissionRewardManager) GetTotalStakedAmount() int64 { - return e.totalStakedAmount -} - -// GetDistributedAmount returns the total amount of rewards distributed. -func (e *EmissionRewardManager) GetDistributedAmount() int64 { - return e.distributedAmount -} - -// GetClaimableRewardAmount calculates the claimable reward amount for a specific address. -func (e *EmissionRewardManager) GetClaimableRewardAmount( - currentDistributedAmount int64, - address string, - currentTimestamp int64, -) (int64, error) { - rewardStateI, ok := e.rewardStates.Get(address) - if !ok { - return 0, nil - } - - rewardState, ok := rewardStateI.(*EmissionRewardState) - if !ok { - return 0, ufmt.Errorf( - "failed to cast rewardStates's element to *EmissionRewardState: %T", - rewardStateI, - ) - } - - accumulatedRewardX128PerStake, err := e.calculateAccumulatedRewardX128PerStake( - currentDistributedAmount, - currentTimestamp, - ) - if err != nil { - return 0, err - } - - return rewardState.GetClaimableRewardAmount(accumulatedRewardX128PerStake, currentTimestamp) -} - -// calculateAccumulatedRewardX128PerStake calculates the updated accumulated reward per stake. -func (e *EmissionRewardManager) calculateAccumulatedRewardX128PerStake( - currentDistributedAmount int64, - currentTimestamp int64, -) (*u256.Uint, error) { - // If we're looking at a past timestamp, return current state - if currentTimestamp < e.accumulatedTimestamp { - return e.accumulatedRewardX128PerStake, nil - } - - // If no tokens are staked, no rewards to distribute - if e.totalStakedAmount == 0 { - return e.accumulatedRewardX128PerStake, nil - } - - // Newly distributed rewards since last update - distributedAmountDelta := currentDistributedAmount - e.distributedAmount - if distributedAmountDelta <= 0 { - // Non-positive delta. nothing to do more. - return e.accumulatedRewardX128PerStake, nil - } - - // Reward per stake for the new distribution - distributedAmountDeltaX128PerStake := u256.Zero().Div( - u256.Zero().Lsh(u256.NewUintFromInt64(distributedAmountDelta), 128), - u256.NewUintFromInt64(e.totalStakedAmount), - ) - - // Add to accumulated reward per stake - accumulatedReward := u256.Zero().Add(e.accumulatedRewardX128PerStake, distributedAmountDeltaX128PerStake) - return accumulatedReward, nil -} - -// updateAccumulatedRewardX128PerStake updates the internal accumulated reward state. -// This method should be called before any stake changes to ensure accurate reward calculations. -// Updates accumulated reward per stake with current distribution data. -func (e *EmissionRewardManager) updateAccumulatedRewardX128PerStake( - currentDistributedAmount int64, - currentTimestamp int64, -) error { - // DO NOT apply out-of-order timestamps - if currentTimestamp < e.accumulatedTimestamp { - return nil - } - - // to avoid accumulating a large delta later. - if e.totalStakedAmount == 0 { - return nil - } - - // Update accumulated reward state - accumulatedRewardX128PerStake, err := e.calculateAccumulatedRewardX128PerStake( - currentDistributedAmount, - currentTimestamp, - ) - if err != nil { - return err - } - - e.accumulatedRewardX128PerStake = accumulatedRewardX128PerStake.Clone() - e.distributedAmount = currentDistributedAmount - e.accumulatedTimestamp = currentTimestamp - - return nil -} - -// addStake adds a stake for an address and updates their reward state. -// This method ensures rewards are properly calculated before the stake change. -// Adds stake for specified address and updates reward calculations. -func (e *EmissionRewardManager) addStake(address string, amount int64, currentTimestamp int64) error { - rewardState, ok, err := e.getRewardState(address) - if err != nil { - return err - } - if !ok { - // if the address is unseen, initialize a snapshot to avoid nil deref - rewardState = NewEmissionRewardState(e.accumulatedRewardX128PerStake.Clone()) - } - - err = rewardState.addStakeWithUpdateRewardDebtX128(amount, e.accumulatedRewardX128PerStake, currentTimestamp) - if err != nil { - return err - } - - e.rewardStates.Set(address, rewardState) - e.totalStakedAmount = e.totalStakedAmount + amount - return nil -} - -// removeStake removes a stake for an address and updates their reward state. -// This method ensures rewards are properly calculated before the stake change. -// Removes stake for specified address and updates reward calculations. -func (e *EmissionRewardManager) removeStake(address string, amount int64, currentTimestamp int64) error { - rewardState, ok, err := e.getRewardState(address) - if err != nil { - return err - } - if !ok { - // if the address is unseen, initialize a snapshot to avoid nil deref - rewardState = NewEmissionRewardState(e.accumulatedRewardX128PerStake.Clone()) - } - - err = rewardState.removeStakeWithUpdateRewardDebtX128(amount, e.accumulatedRewardX128PerStake, currentTimestamp) - if err != nil { - return err - } - - // persist updated state - e.rewardStates.Set(address, rewardState) - e.totalStakedAmount -= amount - if e.totalStakedAmount < 0 { - e.totalStakedAmount = 0 // defensive clamp - } - - return nil -} - -// claimRewards processes reward claiming for an address. -// This method calculates and returns the amount of rewards claimed. -// Claims available rewards for specified address. -func (e *EmissionRewardManager) claimRewards(address string, currentTimestamp int64) (claimedRewardAmount int64, err error) { - rewardState, ok, err := e.getRewardState(address) - if err != nil || !ok { - return 0, err - } - - claimedRewardAmount, cErr := rewardState.claimRewardsWithUpdateRewardDebtX128(e.accumulatedRewardX128PerStake, currentTimestamp) - if cErr != nil { - return 0, cErr - } - - e.rewardStates.Set(address, rewardState) - return claimedRewardAmount, nil -} - -// NewEmissionRewardManager creates a new instance of EmissionRewardManager. -// This factory function initializes all tracking structures for emission reward management. -// NewEmissionRewardManager creates new emission reward manager instance. -func NewEmissionRewardManager() *EmissionRewardManager { - return &EmissionRewardManager{ - accumulatedRewardX128PerStake: u256.NewUint(0), - accumulatedTimestamp: 0, - totalStakedAmount: 0, - distributedAmount: 0, - rewardStates: avl.NewTree(), - } -} - -func (e *EmissionRewardManager) getRewardState(addr string) (*EmissionRewardState, bool, error) { - ri, ok := e.rewardStates.Get(addr) - if !ok { - return nil, false, nil - } - rs, castOk := ri.(*EmissionRewardState) - if !castOk { - return nil, false, ufmt.Errorf(errFailedToCastRewardState, ri) - } - return rs, true, nil -} diff --git a/contract/r/gnoswap/v1/gov/staker/emission_reward_state.gno b/contract/r/gnoswap/v1/gov/staker/emission_reward_state.gno deleted file mode 100644 index b609d92..0000000 --- a/contract/r/gnoswap/v1/gov/staker/emission_reward_state.gno +++ /dev/null @@ -1,250 +0,0 @@ -package staker - -import ( - "errors" - - u256 "gno.land/p/gnoswap/uint256" -) - -var errNotClaimable = errors.New("not claimable") - -// EmissionRewardState tracks emission reward information for an individual staker. -// This struct maintains reward debt, accumulated rewards, and claiming history -// to ensure accurate reward calculations and prevent double-claiming. -type EmissionRewardState struct { - // rewardDebtX128 represents the reward debt with 128-bit precision scaling - // Used to calculate rewards earned since the last update - rewardDebtX128 *u256.Uint - // accumulatedRewardAmount is the total rewards accumulated but not yet claimed - accumulatedRewardAmount int64 - // accumulatedTimestamp is the last timestamp when rewards were accumulated - accumulatedTimestamp int64 - // claimedRewardAmount is the total amount of rewards that have been claimed - claimedRewardAmount int64 - // claimedTimestamp is the last timestamp when rewards were claimed - claimedTimestamp int64 - // stakedAmount is the current amount of tokens staked by this address - stakedAmount int64 -} - -// IsClaimable checks if rewards can be claimed at the given timestamp. -// Rewards are claimable if the current timestamp is greater than the last claimed timestamp. -// -// Parameters: -// - currentTimestamp: current timestamp to check against -// -// Returns: -// - bool: true if rewards can be claimed, false otherwise -func (e *EmissionRewardState) IsClaimable(currentTimestamp int64) bool { - return e.claimedTimestamp < currentTimestamp -} - -// GetClaimableRewardAmount calculates the total amount of rewards that can be claimed. -// This includes both accumulated rewards and newly earned rewards based on current state. -// -// Parameters: -// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake -// - currentTimestamp: current timestamp -// -// Returns: -// - int64: total claimable reward amount -func (e *EmissionRewardState) GetClaimableRewardAmount( - accumulatedRewardX128PerStake *u256.Uint, - currentTimestamp int64, -) (int64, error) { - rewardAmount, err := e.calculateClaimableRewards(accumulatedRewardX128PerStake, currentTimestamp) - if err != nil { - return 0, err - } - return e.accumulatedRewardAmount + rewardAmount, nil -} - -// calculateClaimableRewards calculates newly earned rewards since the last update. -// Uses the difference between current and stored reward debt to calculate earnings. -// -// Parameters: -// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake -// - currentTimestamp: current timestamp -// -// Returns: -// - int64: newly earned reward amount since last update -// - error: nil on success, error if calculation fails -func (e *EmissionRewardState) calculateClaimableRewards( - accumulatedRewardX128PerStake *u256.Uint, - currentTimestamp int64, -) (int64, error) { - // Don't calculate rewards for past timestamps or when nothing is staked - if currentTimestamp < e.accumulatedTimestamp || e.stakedAmount == 0 { - return 0, nil - } - - // Calculate the difference in accumulated rewards per stake since last update - // Using modular arithmetic for accumulator values - underflow is allowed and handled correctly - rewardDebtDeltaX128 := u256.Zero().Sub( - accumulatedRewardX128PerStake, - e.rewardDebtX128, - ) - - // Calculate reward amount by multiplying reward debt delta by staked amount and dividing by Q128 - // rewardAmount = (rewardDebtDeltaX128 * stakedAmount) / Q128 - rewardAmount := u256.MulDiv( - rewardDebtDeltaX128, - u256.NewUintFromInt64(e.stakedAmount), - q128, - ) - return safeConvertToInt64(rewardAmount), nil -} - -// addStake increases the staked amount for this address. -// This method should be called when a user increases their stake. -// -// Parameters: -// - amount: amount of stake to add -func (e *EmissionRewardState) addStake(amount int64) { - e.adjustStake(amount) -} - -// removeStake decreases the staked amount for this address. -// This method should be called when a user decreases their stake. -// -// Parameters: -// - amount: amount of stake to remove -func (e *EmissionRewardState) removeStake(amount int64) { - e.adjustStake(-amount) -} - -// adjustStake is a small internal helper to centralize bound checks and math. -func (e *EmissionRewardState) adjustStake(delta int64) { - if delta == 0 { - return - } - // clamp at zero on underflow - newAmt := e.stakedAmount + delta - if newAmt < 0 { - newAmt = 0 - } - e.stakedAmount = newAmt -} - -// claimRewards processes reward claiming and updates the claim state. -// This method validates claimability and transfers accumulated rewards to claimed status. -// -// Parameters: -// - currentTimestamp: current timestamp -// -// Returns: -// - int64: amount of rewards claimed -// - error: nil on success, error if claiming is not allowed -func (e *EmissionRewardState) claimRewards(currentTimestamp int64) (int64, error) { - if !e.IsClaimable(currentTimestamp) { - return 0, errNotClaimable - } - claimedRewardAmount := e.accumulatedRewardAmount - e.claimedRewardAmount - e.claimedRewardAmount = e.accumulatedRewardAmount - e.claimedTimestamp = currentTimestamp - return claimedRewardAmount, nil -} - -// updateRewardDebtX128 updates the reward debt and accumulates new rewards. -// This method should be called before any stake changes to ensure accurate reward tracking. -// -// Parameters: -// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake -// - currentTimestamp: current timestamp -func (e *EmissionRewardState) updateRewardDebtX128( - accumulatedRewardX128PerStake *u256.Uint, - currentTimestamp int64, -) error { - rewardAmount, err := e.calculateClaimableRewards(accumulatedRewardX128PerStake, currentTimestamp) - if err != nil { - return err - } - - // Accumulate newly earned rewards - if rewardAmount != 0 { - e.accumulatedRewardAmount += rewardAmount - } - - // Deep copy to avoid aliasing with external state - e.rewardDebtX128 = accumulatedRewardX128PerStake.Clone() - e.accumulatedTimestamp = currentTimestamp - return nil -} - -// addStakeWithUpdateRewardDebtX128 adds stake and updates reward debt in one operation. -// This ensures rewards are properly calculated before the stake change takes effect. -// -// Parameters: -// - amount: amount of stake to add -// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake -// - currentTimestamp: current timestamp -func (e *EmissionRewardState) addStakeWithUpdateRewardDebtX128( - amount int64, - accumulatedRewardX128PerStake *u256.Uint, - currentTimestamp int64, -) error { - if err := e.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { - return err - } - e.addStake(amount) - return nil -} - -// removeStakeWithUpdateRewardDebtX128 removes stake and updates reward debt in one operation. -// This ensures rewards are properly calculated before the stake change takes effect. -// -// Parameters: -// - amount: amount of stake to remove -// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake -// - currentTimestamp: current timestamp -func (e *EmissionRewardState) removeStakeWithUpdateRewardDebtX128( - amount int64, - accumulatedRewardX128PerStake *u256.Uint, - currentTimestamp int64, -) error { - if err := e.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { - return err - } - e.removeStake(amount) - return nil -} - -// claimRewardsWithUpdateRewardDebtX128 claims rewards and updates reward debt in one operation. -// This ensures all rewards are properly calculated before claiming. -// -// Parameters: -// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake -// - currentTimestamp: current timestamp -// -// Returns: -// - int64: amount of rewards claimed -// - error: nil on success, error if claiming fails -func (e *EmissionRewardState) claimRewardsWithUpdateRewardDebtX128( - accumulatedRewardX128PerStake *u256.Uint, - currentTimestamp int64, -) (int64, error) { - if err := e.updateRewardDebtX128(accumulatedRewardX128PerStake, currentTimestamp); err != nil { - return 0, err - } - return e.claimRewards(currentTimestamp) -} - -// NewEmissionRewardState creates a new emission reward state for a staker. -// This factory function initializes the state with the current system reward debt. -// -// Parameters: -// - accumulatedRewardX128PerStake: current system-wide accumulated reward per stake -// -// Returns: -// - *EmissionRewardState: new emission reward state instance -func NewEmissionRewardState(accumulatedRewardX128PerStake *u256.Uint) *EmissionRewardState { - return &EmissionRewardState{ - // Deep copy the input to snapshot the current accumulator value. - rewardDebtX128: accumulatedRewardX128PerStake.Clone(), - accumulatedRewardAmount: 0, - accumulatedTimestamp: 0, - claimedRewardAmount: 0, - claimedTimestamp: 0, - stakedAmount: 0, - } -} diff --git a/contract/r/gnoswap/v1/gov/staker/errors.gno b/contract/r/gnoswap/v1/gov/staker/errors.gno deleted file mode 100644 index 781fecc..0000000 --- a/contract/r/gnoswap/v1/gov/staker/errors.gno +++ /dev/null @@ -1,32 +0,0 @@ -package staker - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errNoPermission = errors.New("[GNOSWAP-GOV_STAKER-001] caller has no permission") - errDataNotFound = errors.New("[GNOSWAP-GOV_STAKER-002] requested data not found") - errTransferFailed = errors.New("[GNOSWAP-GOV_STAKER-003] transfer failed") - errInvalidAmount = errors.New("[GNOSWAP-GOV_STAKER-004] invalid amount") - errNoDelegatedAmount = errors.New("[GNOSWAP-GOV_STAKER-005] zero delegated amount") - errNoDelegatedTarget = errors.New("[GNOSWAP-GOV_STAKER-006] did not delegated to that address") - errNotEnoughDelegated = errors.New("[GNOSWAP-GOV_STAKER-007] not enough delegated") - errInvalidAddress = errors.New("[GNOSWAP-GOV_STAKER-008] invalid address") - errFutureTime = errors.New("[GNOSWAP-GOV_STAKER-009] can not use future time") - errNotEnoughBalance = errors.New("[GNOSWAP-GOV_STAKER-010] not enough balance") - errLessThanMinimum = errors.New("[GNOSWAP-GOV_STAKER-011] can not delegate less than minimum amount") -) - -func makeErrorWithDetails(err error, detail string) error { - return ufmt.Errorf("%s || %s", err.Error(), detail) -} - -// checkTransferError checks transfer error. -func checkTransferError(err error) { - if err != nil { - panic(makeErrorWithDetails(errTransferFailed, err.Error())) - } -} diff --git a/contract/r/gnoswap/v1/gov/staker/getter_delegation_snapshot.gno b/contract/r/gnoswap/v1/gov/staker/getter_delegation_snapshot.gno deleted file mode 100644 index e80b520..0000000 --- a/contract/r/gnoswap/v1/gov/staker/getter_delegation_snapshot.gno +++ /dev/null @@ -1,34 +0,0 @@ -package staker - -// GetDelegationSnapshots retrieves the delegation snapshot at a specific point in time. -// This function reconstructs historical delegation states by taking the current snapshot -// and reversing the effects of delegation records that occurred after the specified time. -// -// The algorithm works by: -// 1. Cloning the current delegation snapshot -// 2. Getting all delegation records that occurred at or after the snapshot time -// 3. Subtracting each record in reverse chronological order to restore the historical state -// -// Parameters: -// - snapshotTime: timestamp to retrieve the snapshot for -// -// Returns: -// - DelegationSnapshot: delegation state at the specified time -// - bool: true if snapshot was successfully calculated, false otherwise -func GetDelegationSnapshots(snapshotTime int64) (DelegationSnapshot, bool) { - // Get current delegation snapshots and create a working copy - delegationSnapshots := getDelegationSnapshots() - currentDelegationSnapshot := delegationSnapshots.clone() - - // Get delegation history and filter for records after snapshot time - delegationHistory := getDelegationHistory() - historyRecords := delegationHistory.getRecordsBy(snapshotTime) - - // Apply records in reverse order to reconstruct historical state - // This effectively "undoes" all delegation changes that happened after snapshotTime - for i := len(historyRecords) - 1; i >= 0; i-- { - currentDelegationSnapshot = currentDelegationSnapshot.subRecord(historyRecords[i]) - } - - return currentDelegationSnapshot, true -} diff --git a/contract/r/gnoswap/v1/gov/staker/gnomod.toml b/contract/r/gnoswap/v1/gov/staker/gnomod.toml deleted file mode 100644 index b1c9071..0000000 --- a/contract/r/gnoswap/v1/gov/staker/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/gov/staker" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_manager.gno b/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_manager.gno deleted file mode 100644 index af4c17b..0000000 --- a/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_manager.gno +++ /dev/null @@ -1,298 +0,0 @@ -package staker - -import ( - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - u256 "gno.land/p/gnoswap/uint256" -) - -// ProtocolFeeRewardManager manages the distribution of protocol fee rewards to stakers. -// Unlike emission rewards, protocol fees can come from multiple tokens, requiring -// separate tracking and distribution mechanisms for each token type. -type ProtocolFeeRewardManager struct { - // rewardStates maps address to ProtocolFeeRewardState for tracking individual staker rewards - rewardStates *avl.Tree // address -> ProtocolFeeRewardState - - // accumulatedProtocolFeeX128PerStake maps token path to accumulated fee per stake with 128-bit precision - accumulatedProtocolFeeX128PerStake map[string]*u256.Uint - // protocolFeeAmounts maps token path to total distributed protocol fee amounts - protocolFeeAmounts map[string]int64 - // accumulatedTimestamp tracks the last timestamp when fees were accumulated - accumulatedTimestamp int64 - // totalStakedAmount tracks the total amount of tokens staked in the system - totalStakedAmount int64 -} - -// GetAccumulatedProtocolFeeX128PerStake returns the accumulated protocol fee per stake for a specific token. -// -// Parameters: -// - token: token path to get accumulated fee for -// -// Returns: -// - *u256.Uint: accumulated protocol fee per stake for the token (scaled by 2^128) -func (p *ProtocolFeeRewardManager) GetAccumulatedProtocolFeeX128PerStake(token string) *u256.Uint { - return p.accumulatedProtocolFeeX128PerStake[token] -} - -// GetAccumulatedTimestamp returns the last timestamp when protocol fees were accumulated. -// -// Returns: -// - int64: last accumulated timestamp -func (p *ProtocolFeeRewardManager) GetAccumulatedTimestamp() int64 { - return p.accumulatedTimestamp -} - -// GetClaimableRewardAmounts calculates the claimable reward amounts for all tokens for a specific address. -// This method computes rewards based on current protocol fee distribution state and staking history. -// -// Parameters: -// - protocolFeeAmounts: current protocol fee amounts for all tokens -// - address: staker's address to calculate rewards for -// - currentTimestamp: current timestamp -// -// Returns: -// - map[string]int64: map of token path to claimable reward amount -func (p *ProtocolFeeRewardManager) GetClaimableRewardAmounts( - protocolFeeAmounts map[string]int64, - address string, - currentTimestamp int64, -) (map[string]int64, error) { - rewardStateI, ok := p.rewardStates.Get(address) - if !ok { - return make(map[string]int64), nil - } - - rewardState, ok := rewardStateI.(*ProtocolFeeRewardState) - if !ok { - return nil, ufmt.Errorf( - "failed to cast rewardStates's element to *ProtocolFeeRewardState: %T", - rewardStateI, - ) - } - - accumulatedRewardX128PerStake, _, err := p.calculateAccumulatedRewardX128PerStake( - protocolFeeAmounts, - currentTimestamp, - ) - if err != nil { - return nil, err - } - - return rewardState.GetClaimableRewardAmounts(accumulatedRewardX128PerStake, currentTimestamp) -} - -// calculateAccumulatedRewardX128PerStake calculates the updated accumulated reward per stake for all tokens. -// This method computes new accumulated reward rates based on newly distributed protocol fees. -// -// Parameters: -// - protocolFeeAmounts: current protocol fee amounts for all tokens -// - currentTimestamp: current timestamp -// -// Returns: -// - map[string]*u256.Uint: updated accumulated reward per stake for each token -// - map[string]int64: updated protocol fee amounts for each token -func (p *ProtocolFeeRewardManager) calculateAccumulatedRewardX128PerStake( - protocolFeeAmounts map[string]int64, - currentTimestamp int64, -) (map[string]*u256.Uint, map[string]int64, error) { - // If we're looking at a past timestamp, return current state - if p.accumulatedTimestamp > currentTimestamp { - return p.accumulatedProtocolFeeX128PerStake, p.protocolFeeAmounts, nil - } - - accumulatedProtocolFeesX128PerStake := make(map[string]*u256.Uint) - changedProtocolFeeAmounts := make(map[string]int64) - - // Process each token's protocol fees - for token, protocolFeeAmount := range protocolFeeAmounts { - previousProtocolFeeAmount, ok := p.protocolFeeAmounts[token] - if !ok { - previousProtocolFeeAmount = 0 - } - - protocolFeeDelta := protocolFeeAmount - previousProtocolFeeAmount - - // If no new fees for this token, keep existing rate - if protocolFeeDelta <= 0 { - accumulatedProtocolFeesX128PerStake[token] = p.accumulatedProtocolFeeX128PerStake[token] - if accumulatedProtocolFeesX128PerStake[token] == nil { - accumulatedProtocolFeesX128PerStake[token] = u256.NewUint(0) - } - } - - // Scale the fee delta by 2^128 for precision - protocolFeeDeltaX128 := u256.NewUintFromInt64(protocolFeeDelta) - protocolFeeDeltaX128 = u256.Zero().Lsh(protocolFeeDeltaX128, 128) - - protocolFeeDeltaX128PerStake := u256.Zero() - - // Calculate fee per stake if there are staked tokens - if p.totalStakedAmount > 0 { - feePerStake := u256.Zero().Div(protocolFeeDeltaX128, u256.NewUintFromInt64(p.totalStakedAmount)) - protocolFeeDeltaX128PerStake = feePerStake - } - - // Get current accumulated fee per stake for this token - accumulatedProtocolFeeX128PerStake := u256.Zero() - if p.accumulatedProtocolFeeX128PerStake[token] != nil { - accumulatedProtocolFeeX128PerStake = p.accumulatedProtocolFeeX128PerStake[token] - } - - // Add the new fee per stake to the accumulated amount - accumulatedProtocolFeeX128PerStake = u256.Zero().Add(accumulatedProtocolFeeX128PerStake, protocolFeeDeltaX128PerStake) - accumulatedProtocolFeesX128PerStake[token] = accumulatedProtocolFeeX128PerStake.Clone() - - changedProtocolFeeAmounts[token] = protocolFeeAmount - } - - return accumulatedProtocolFeesX128PerStake, changedProtocolFeeAmounts, nil -} - -// updateAccumulatedProtocolFeeX128PerStake updates the internal accumulated protocol fee state. -// This method should be called before any stake changes to ensure accurate reward calculations. -// -// Parameters: -// - protocolFeeAmounts: current protocol fee amounts for all tokens -// - currentTimestamp: current timestamp -func (p *ProtocolFeeRewardManager) updateAccumulatedProtocolFeeX128PerStake( - protocolFeeAmounts map[string]int64, - currentTimestamp int64, -) error { - // Don't update if we're looking at a past timestamp - if p.accumulatedTimestamp > currentTimestamp { - return nil - } - - accumulatedProtocolFeeX128PerStake, changedProtocolFeeAmounts, err := p.calculateAccumulatedRewardX128PerStake( - protocolFeeAmounts, - currentTimestamp, - ) - if err != nil { - return err - } - - p.accumulatedProtocolFeeX128PerStake = accumulatedProtocolFeeX128PerStake - p.protocolFeeAmounts = changedProtocolFeeAmounts - p.accumulatedTimestamp = currentTimestamp - - return nil -} - -// addStake adds a stake for an address and updates their protocol fee reward state. -// This method ensures rewards are properly calculated before the stake change. -// -// Parameters: -// - address: staker's address -// - amount: amount of stake to add -// - currentTimestamp: current timestamp -func (p *ProtocolFeeRewardManager) addStake(address string, amount int64, currentTimestamp int64) error { - rewardStateI, ok := p.rewardStates.Get(address) - if !ok { - rewardStateI = NewProtocolFeeRewardState(p.accumulatedProtocolFeeX128PerStake) - } - - rewardState, ok := rewardStateI.(*ProtocolFeeRewardState) - if !ok { - return ufmt.Errorf( - "failed to cast rewardStates's element to *ProtocolFeeRewardState: %T", - rewardStateI, - ) - } - - err := rewardState.addStakeWithUpdateRewardDebtX128(amount, p.accumulatedProtocolFeeX128PerStake, currentTimestamp) - if err != nil { - return err - } - - p.rewardStates.Set(address, rewardState) - - p.totalStakedAmount = p.totalStakedAmount + amount - - return nil -} - -// removeStake removes a stake for an address and updates their protocol fee reward state. -// This method ensures rewards are properly calculated before the stake change. -// -// Parameters: -// - address: staker's address -// - amount: amount of stake to remove -// - currentTimestamp: current timestamp -func (p *ProtocolFeeRewardManager) removeStake(address string, amount int64, currentTimestamp int64) error { - rewardStateI, ok := p.rewardStates.Get(address) - if !ok { - rewardStateI = NewProtocolFeeRewardState(p.accumulatedProtocolFeeX128PerStake) - } - - rewardState, ok := rewardStateI.(*ProtocolFeeRewardState) - if !ok { - return ufmt.Errorf( - "failed to cast rewardStates's element to *ProtocolFeeRewardState: %T", - rewardStateI, - ) - } - - err := rewardState.removeStakeWithUpdateRewardDebtX128(amount, p.accumulatedProtocolFeeX128PerStake, currentTimestamp) - if err != nil { - return err - } - - p.rewardStates.Set(address, rewardState) - - p.totalStakedAmount = p.totalStakedAmount - amount - - return nil -} - -// claimRewards processes protocol fee reward claiming for an address. -// This method calculates and returns the amounts of rewards claimed for each token. -// -// Parameters: -// - address: staker's address claiming rewards -// - currentTimestamp: current timestamp -// -// Returns: -// - map[string]int64: map of token path to claimed reward amount -// - error: nil on success, error if claiming fails -func (p *ProtocolFeeRewardManager) claimRewards(address string, currentTimestamp int64) (map[string]int64, error) { - rewardStateI, ok := p.rewardStates.Get(address) - if !ok { - return make(map[string]int64), nil - } - - rewardState, ok := rewardStateI.(*ProtocolFeeRewardState) - if !ok { - return nil, ufmt.Errorf( - "failed to cast rewardStates's element to *ProtocolFeeRewardState: %T", - rewardStateI, - ) - } - - claimedRewards, err := rewardState.claimRewardsWithUpdateRewardDebtX128( - p.accumulatedProtocolFeeX128PerStake, - currentTimestamp, - ) - if err != nil { - return nil, err - } - - p.rewardStates.Set(address, rewardState) - - return claimedRewards, nil -} - -// NewProtocolFeeRewardManager creates a new instance of ProtocolFeeRewardManager. -// This factory function initializes all tracking structures for multi-token protocol fee reward management. -// -// Returns: -// - *ProtocolFeeRewardManager: new protocol fee reward manager instance -func NewProtocolFeeRewardManager() *ProtocolFeeRewardManager { - return &ProtocolFeeRewardManager{ - rewardStates: avl.NewTree(), - protocolFeeAmounts: make(map[string]int64), - accumulatedProtocolFeeX128PerStake: make(map[string]*u256.Uint), - accumulatedTimestamp: 0, - totalStakedAmount: 0, - } -} diff --git a/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_state.gno b/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_state.gno deleted file mode 100644 index ae6f3f5..0000000 --- a/contract/r/gnoswap/v1/gov/staker/protocol_fee_reward_state.gno +++ /dev/null @@ -1,298 +0,0 @@ -package staker - -import ( - "errors" - - u256 "gno.land/p/gnoswap/uint256" -) - -// ProtocolFeeRewardState tracks protocol fee reward information for an individual staker across multiple tokens. -// Unlike emission rewards which are single-token, protocol fees can come from various trading pairs, -// requiring separate tracking and calculation for each token type. -type ProtocolFeeRewardState struct { - // rewardDebtX128 maps token path to reward debt with 128-bit precision scaling - // Used to calculate rewards earned since the last update for each token - rewardDebtX128 map[string]*u256.Uint - // accumulatedRewards maps token path to total rewards accumulated but not yet claimed - accumulatedRewards map[string]int64 - // claimedRewards maps token path to total amount of rewards that have been claimed - claimedRewards map[string]int64 - // accumulatedTimestamp is the last timestamp when rewards were accumulated - accumulatedTimestamp int64 - // claimedTimestamp is the last timestamp when rewards were claimed - claimedTimestamp int64 - // stakedAmount is the current amount of tokens staked by this address - stakedAmount int64 -} - -// IsClaimable checks if rewards can be claimed at the given timestamp. -// Rewards are claimable if the current timestamp is greater than the last claimed timestamp. -// -// Parameters: -// - currentTimestamp: current timestamp to check against -// -// Returns: -// - bool: true if rewards can be claimed, false otherwise -func (p *ProtocolFeeRewardState) IsClaimable(currentTimestamp int64) bool { - return p.claimedTimestamp < currentTimestamp -} - -// GetClaimableRewardAmounts calculates the claimable reward amounts for all tokens. -// This includes both accumulated rewards and newly earned rewards based on current state. -// -// Parameters: -// - accumulatedRewardsX128PerStake: current system-wide accumulated rewards per stake for all tokens -// - currentTimestamp: current timestamp -// -// Returns: -// - map[string]int64: map of token path to claimable reward amount -// - error: nil on success, error if claiming is not allowed -func (p *ProtocolFeeRewardState) GetClaimableRewardAmounts( - accumulatedRewardsX128PerStake map[string]*u256.Uint, - currentTimestamp int64, -) (map[string]int64, error) { - rewardAmounts, err := p.calculateClaimableRewards(accumulatedRewardsX128PerStake, currentTimestamp) - if err != nil { - return nil, err - } - - return rewardAmounts, nil -} - -// calculateClaimableRewards calculates newly earned rewards for all tokens since the last update. -// This method uses the difference between current and stored reward debt to calculate earnings. -// -// Parameters: -// - accumulatedRewardsX128PerStake: current system-wide accumulated rewards per stake for all tokens -// - currentTimestamp: current timestamp -// -// Returns: -// - map[string]int64: map of token path to newly earned reward amount -func (p *ProtocolFeeRewardState) calculateClaimableRewards( - accumulatedRewardsX128PerStake map[string]*u256.Uint, - currentTimestamp int64, -) (map[string]int64, error) { - // Don't calculate rewards for past timestamps - if p.accumulatedTimestamp >= currentTimestamp { - return p.accumulatedRewards, nil - } - - rewardAmounts := make(map[string]int64) - - // Calculate rewards for each token type - for token, accumulatedRewardX128PerStake := range accumulatedRewardsX128PerStake { - // Initialize reward debt if it doesn't exist for this token - if p.rewardDebtX128[token] == nil { - p.rewardDebtX128[token] = u256.Zero() - } - - // Calculate the difference in accumulated rewards per stake since last update - // Using modular arithmetic for accumulator values - underflow is allowed and handled correctly - rewardDebtDeltaX128 := u256.Zero().Sub( - accumulatedRewardX128PerStake, - p.rewardDebtX128[token], - ) - - // Multiply by staked amount to get total reward for this staker and token - rewardAmount := u256.MulDiv( - rewardDebtDeltaX128, - u256.NewUintFromInt64(p.stakedAmount), - q128, - ) - - rewardAmounts[token] = safeConvertToInt64(rewardAmount) - } - - return rewardAmounts, nil -} - -// addStake increases the staked amount for this address. -// This method should be called when a user increases their stake. -// -// Parameters: -// - amount: amount of stake to add -func (p *ProtocolFeeRewardState) addStake(amount int64) { - p.stakedAmount = p.stakedAmount + amount -} - -// removeStake decreases the staked amount for this address. -// This method should be called when a user decreases their stake. -// -// Parameters: -// - amount: amount of stake to remove -func (p *ProtocolFeeRewardState) removeStake(amount int64) { - p.stakedAmount = p.stakedAmount - amount -} - -// claimRewards processes reward claiming for all tokens and updates the claim state. -// This method validates claimability and transfers accumulated rewards to claimed status. -// -// Parameters: -// - currentTimestamp: current timestamp -// -// Returns: -// - map[string]int64: map of token path to claimed reward amount -// - error: nil on success, error if claiming is not allowed -func (p *ProtocolFeeRewardState) claimRewards(currentTimestamp int64) (map[string]int64, error) { - if !p.IsClaimable(currentTimestamp) { - return nil, errors.New("not claimable") - } - - if p.accumulatedTimestamp < currentTimestamp { - return nil, errors.New("must update reward debt before claiming rewards") - } - - currentClaimedRewards := make(map[string]int64) - - // Calculate and update claimed amounts for each token - for token, rewardAmount := range p.accumulatedRewards { - currentClaimedRewards[token] = rewardAmount - p.claimedRewards[token] - p.claimedRewards[token] = rewardAmount - } - - p.claimedTimestamp = currentTimestamp - - return currentClaimedRewards, nil -} - -// updateRewardDebtX128 updates the reward debt and accumulates new rewards for all tokens. -// This method should be called before any stake changes to ensure accurate reward tracking. -// -// Parameters: -// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake for all tokens -// - currentTimestamp: current timestamp -func (p *ProtocolFeeRewardState) updateRewardDebtX128( - accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, - currentTimestamp int64, -) error { - // Don't update if we're looking at a past timestamp - if p.accumulatedTimestamp >= currentTimestamp { - return nil - } - - // Calculate and accumulate new rewards for all tokens - rewardAmounts, err := p.calculateClaimableRewards(accumulatedProtocolFeeX128PerStake, currentTimestamp) - if err != nil { - return err - } - - p.rewardDebtX128 = cloneAccumulatedProtocolFeeX128PerStake(accumulatedProtocolFeeX128PerStake) - - // Add newly calculated rewards to accumulated amounts - for token, rewardAmount := range rewardAmounts { - p.accumulatedRewards[token] = p.accumulatedRewards[token] + rewardAmount - } - - p.accumulatedTimestamp = currentTimestamp - - return nil -} - -// addStakeWithUpdateRewardDebtX128 adds stake and updates reward debt in one operation. -// This ensures rewards are properly calculated before the stake change takes effect. -// -// Parameters: -// - amount: amount of stake to add -// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake -// - currentTimestamp: current timestamp -func (p *ProtocolFeeRewardState) addStakeWithUpdateRewardDebtX128( - amount int64, - accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, - currentTimestamp int64, -) error { - err := p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp) - if err != nil { - return err - } - - p.addStake(amount) - - return nil -} - -// removeStakeWithUpdateRewardDebtX128 removes stake and updates reward debt in one operation. -// This ensures rewards are properly calculated before the stake change takes effect. -// -// Parameters: -// - amount: amount of stake to remove -// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake -// - currentTimestamp: current timestamp -func (p *ProtocolFeeRewardState) removeStakeWithUpdateRewardDebtX128( - amount int64, - accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, - currentTimestamp int64, -) error { - err := p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp) - if err != nil { - return err - } - - p.removeStake(amount) - - return nil -} - -// claimRewardsWithUpdateRewardDebtX128 claims rewards and updates reward debt in one operation. -// This ensures all rewards are properly calculated before claiming. -// -// Parameters: -// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake -// - currentTimestamp: current timestamp -// -// Returns: -// - map[string]int64: map of token path to claimed reward amount -// - error: nil on success, error if claiming fails -func (p *ProtocolFeeRewardState) claimRewardsWithUpdateRewardDebtX128( - accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, - currentTimestamp int64, -) (map[string]int64, error) { - p.updateRewardDebtX128(accumulatedProtocolFeeX128PerStake, currentTimestamp) - - return p.claimRewards(currentTimestamp) -} - -// NewProtocolFeeRewardState creates a new protocol fee reward state for a staker. -// This factory function initializes the state with the current system reward debt for all tokens. -// -// Parameters: -// - accumulatedProtocolFeeX128PerStake: current system-wide accumulated protocol fees per stake for all tokens -// -// Returns: -// - *ProtocolFeeRewardState: new protocol fee reward state instance -func NewProtocolFeeRewardState( - accumulatedProtocolFeeX128PerStake map[string]*u256.Uint, -) *ProtocolFeeRewardState { - rewardDebtX128 := make(map[string]*u256.Uint) - - // Clone reward debt for each token to avoid reference issues - for token, accumulatedProtocolFeeX128PerStake := range accumulatedProtocolFeeX128PerStake { - rewardDebtX128[token] = accumulatedProtocolFeeX128PerStake.Clone() - } - - return &ProtocolFeeRewardState{ - rewardDebtX128: rewardDebtX128, - claimedRewards: map[string]int64{}, - accumulatedRewards: map[string]int64{}, - stakedAmount: 0, - accumulatedTimestamp: 0, - claimedTimestamp: 0, - } -} - -// cloneAccumulatedProtocolFeeX128PerStake creates a deep copy of the accumulated protocol fee map. -// This utility function prevents reference sharing between different reward states. -// -// Parameters: -// - accumulatedProtocolFeeX128PerStake: map to clone -// -// Returns: -// - map[string]*u256.Uint: deep copy of the input map -func cloneAccumulatedProtocolFeeX128PerStake(accumulatedProtocolFeeX128PerStake map[string]*u256.Uint) map[string]*u256.Uint { - clone := make(map[string]*u256.Uint) - - for token, item := range accumulatedProtocolFeeX128PerStake { - clone[token] = item.Clone() - } - - return clone -} diff --git a/contract/r/gnoswap/v1/gov/staker/staker_delegate.gno b/contract/r/gnoswap/v1/gov/staker/staker_delegate.gno deleted file mode 100644 index 5600b6d..0000000 --- a/contract/r/gnoswap/v1/gov/staker/staker_delegate.gno +++ /dev/null @@ -1,469 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/gns" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/referral" - "gno.land/r/gnoswap/v1/gov/xgns" -) - -// Delegate delegates GNS tokens to an address. -// -// Converts GNS to xGNS and assigns voting power. -// Primary mechanism for participating in governance. -// Can delegate to self or any other address. -// -// Parameters: -// - to: Address to receive voting power (can be self) -// - amount: Amount of GNS to stake and delegate -// - referrer: Optional referral address for tracking -// -// Process: -// 1. Transfers GNS from caller -// 2. Mints equivalent xGNS (1:1 ratio) -// 3. Assigns voting power to target address -// 4. Creates delegation snapshot for voting -// -// Requirements: -// - Minimum 1 GNS delegation -// - Valid target address -// - Sufficient GNS balance -// - Approval for GNS transfer -// -// Returns delegated amount. -func Delegate( - cur realm, - to std.Address, - amount int64, - referrer string, -) int64 { - halt.AssertIsNotHaltedGovStaker() - - prevRealm := std.PreviousRealm() - access.AssertIsUser(prevRealm) - access.AssertIsValidAddress(to) - - assertIsValidDelegateAmount(amount) - - caller := prevRealm.Address() - from := caller - currentRealm := std.CurrentRealm() - currentHeight := std.ChainHeight() - currentTimestamp := time.Now().Unix() - - emission.MintAndDistributeGns(cross) - - delegation, err := delegate( - from, - to, - amount, - currentHeight, - currentTimestamp, - ) - if err != nil { - panic(err) - } - - gns.TransferFrom(cross, from, currentRealm.Address(), amount) - xgns.Mint(cross, from, amount) - - registeredReferrer := registerReferrer(caller, referrer) - - std.Emit( - "Delegate", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "from", delegation.DelegateFrom().String(), - "to", delegation.DelegateTo().String(), - "amount", formatInt(delegation.DelegatedAmount()), - "referrer", registeredReferrer, - ) - - return amount -} - -// Undelegate undelegates xGNS from the existing delegate. -// -// Initiates withdrawal of staked GNS with lockup period. -// Voting power removed immediately, tokens locked for 7 days. -// Prevents governance attacks through time delay. -// -// Parameters: -// - from: Address currently delegated to -// - amount: Amount of xGNS to undelegate -// -// Process: -// 1. Removes voting power immediately -// 2. Burns xGNS tokens -// 3. Creates withdrawal request with timestamp -// 4. Locks GNS for 7-day cooldown period -// -// Requirements: -// - Must have delegated to target address -// - Sufficient delegated amount -// - Cannot undelegate during active votes -// -// After 7 days, use Collect() to claim GNS. -// Returns undelegated amount. -func Undelegate( - cur realm, - from std.Address, - amount int64, -) int64 { - halt.AssertIsNotHaltedGovStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsValidAddress(from) - - assertIsValidDelegateAmount(amount) - - currentHeight := std.ChainHeight() - currentTimestamp := time.Now().Unix() - - emission.MintAndDistributeGns(cross) - - unDelegationAmount, err := unDelegate( - caller, - from, - amount, - currentHeight, - currentTimestamp, - ) - if err != nil { - panic(err) - } - - prevRealm := std.PreviousRealm() - std.Emit( - "Undelegate", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "from", caller.String(), - "to", from.String(), - "amount", formatInt(unDelegationAmount), - ) - - return unDelegationAmount -} - -// Redelegate redelegates xGNS from existing delegate to another. -// -// Atomic operation to change delegation target. -// Maintains voting power continuity without unstaking. -// Useful for vote delegation services and dao coordination. -// -// Parameters: -// - delegatee: Current address delegated to -// - newDelegatee: New address to delegate to -// - amount: Amount of xGNS to redelegate -// -// Process: -// 1. Validates current delegation exists -// 2. Removes voting power from old delegatee -// 3. Assigns voting power to new delegatee -// 4. Updates delegation snapshots -// -// Requirements: -// - Must have active delegation to current delegatee -// - Both addresses must be valid -// - Amount must not exceed current delegation -// - Cannot redelegate to same address -// -// No lockup period - instant redelegation. -// Returns redelegated amount. -func Redelegate( - cur realm, - delegatee, - newDelegatee std.Address, - amount int64, -) int64 { - halt.AssertIsNotHaltedGovStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsValidAddress(delegatee) - access.AssertIsValidAddress(newDelegatee) - - assertIsValidDelegateAmount(amount) - - currentHeight := std.ChainHeight() - currentTimestamp := time.Now().Unix() - delegator := caller - - emission.MintAndDistributeGns(cross) - - unDelegationAmount, err := unDelegateWithoutLockup( - delegator, - delegatee, - amount, - currentHeight, - currentTimestamp, - ) - if err != nil { - panic(err) - } - - delegation, err := delegate( - delegator, - newDelegatee, - unDelegationAmount, - currentHeight, - currentTimestamp, - ) - - prevRealm := std.PreviousRealm() - std.Emit( - "Undelegate", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "from", delegation.DelegateFrom().String(), - "to", delegation.DelegateTo().String(), - "amount", formatInt(delegation.DelegatedAmount()), - ) - - std.Emit( - "Redelegate", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "from", delegation.DelegateFrom().String(), - "to", delegation.DelegateTo().String(), - "amount", formatInt(delegation.DelegatedAmount()), - ) - - return amount -} - -// CollectUndelegatedGns collects undelegated GNS tokens. -// Allows users to collect GNS tokens that completed undelegation lockup period. -// Burns xGNS and returns GNS tokens. -func CollectUndelegatedGns(cur realm) int64 { - halt.AssertIsNotHaltedGovStaker() - halt.AssertIsNotHaltedWithdraw() - - prevRealm := std.PreviousRealm() - caller := prevRealm.Address() - currentTime := time.Now().Unix() - - emission.MintAndDistributeGns(cross) - - collectedAmount, err := collectDelegations(caller, currentTime) - if err != nil { - panic(err) - } - - if collectedAmount == 0 { - return 0 - } - - xgns.Burn(cross, caller, collectedAmount) - gns.Transfer(cross, caller, collectedAmount) - - totalLockedAmount -= collectedAmount - if totalLockedAmount < 0 { - totalLockedAmount = 0 - } - - std.Emit( - "CollectUndelegatedGns", - "prevAddr", prevRealm.Address().String(), - "prevRealm", prevRealm.PkgPath(), - "from", prevRealm.Address().String(), - "to", caller.String(), - "collectedAmount", formatInt(collectedAmount), - ) - - return collectedAmount -} - -// delegate processes delegation operations. -// Validates delegation amount, creates delegation records, and updates reward tracking. -func delegate( - from std.Address, - to std.Address, - amount, - currentHeight, - currentTimestamp int64, -) (*Delegation, error) { - delegationID := nextDelegationID() - delegation := NewDelegation( - delegationID, - from, - to, - amount, - currentHeight, - currentTimestamp, - ) - delegationRecord := NewDelegationDelegateRecordBy(delegation) - - addDelegation(delegationID, delegation) - addDelegationRecord(delegationRecord) - addStakeEmissionReward(from.String(), amount, currentTimestamp) - addStakeProtocolFeeReward(from.String(), amount, time.Now().Unix()) - - totalDelegatedAmount += amount - totalLockedAmount += amount - - return delegation, nil -} - -// unDelegate processes undelegation operations with lockup. -// Validates undelegation amount, processes withdrawals, and updates reward tracking. -func unDelegate( - delegator, - delegatee std.Address, - amount, - currentHeight, - currentTimestamp int64, -) (int64, error) { - delegations := getUserDelegationsWithDelegatee(delegator, delegatee) - if len(delegations) == 0 { - return 0, nil - } - - unDelegationAmount := amount - - // Process undelegation across multiple delegation records if necessary - for _, delegation := range delegations { - if delegation.IsEmpty() { - removeDelegation(delegation.ID()) - continue - } - - currentUnDelegationAmount := unDelegationAmount - - if currentUnDelegationAmount > delegation.DelegatedAmount() { - currentUnDelegationAmount = delegation.DelegatedAmount() - } - - delegation.unDelegate( - currentUnDelegationAmount, - currentHeight, - currentTimestamp, - unDelegationLockupPeriod, - ) - - delegationRecord := NewDelegationWithdrawRecordBy(delegation, currentUnDelegationAmount, currentTimestamp) - - setDelegation(delegation.ID(), delegation) - addDelegationRecord(delegationRecord) - removeStakeEmissionReward(delegator.String(), currentUnDelegationAmount, currentTimestamp) - removeStakeProtocolFeeReward(delegator.String(), currentUnDelegationAmount, currentTimestamp) - - unDelegationAmount -= currentUnDelegationAmount - if unDelegationAmount <= 0 { - break - } - } - - totalDelegatedAmount -= amount - - if totalDelegatedAmount < 0 { - totalDelegatedAmount = 0 - } - - return amount, nil -} - -// unDelegateWithoutLockup processes undelegation without lockup. -// Used for redelegation where tokens are immediately available. -func unDelegateWithoutLockup( - delegator, - delegatee std.Address, - amount, - currentHeight, - currentTime int64, -) (int64, error) { - delegations := getUserDelegationsWithDelegatee(delegator, delegatee) - if len(delegations) == 0 { - return 0, nil - } - - unDelegationAmount := amount - - // Process undelegation across multiple delegation records if necessary - for _, delegation := range delegations { - if delegation.IsEmpty() { - removeDelegation(delegation.ID()) - continue - } - - currentUnDelegationAmount := unDelegationAmount - - if currentUnDelegationAmount > delegation.DelegatedAmount() { - currentUnDelegationAmount = delegation.DelegatedAmount() - } - - delegation.unDelegateWithoutLockup( - currentUnDelegationAmount, - currentHeight, - currentTime, - ) - - unDelegationAmount -= currentUnDelegationAmount - if unDelegationAmount <= 0 { - break - } - } - - totalDelegatedAmount -= amount - - if totalDelegatedAmount < 0 { - totalDelegatedAmount = 0 - } - - return amount, nil -} - -// collectDelegations processes collection of undelegated tokens. -// Iterates through user delegations and collects available amounts. -func collectDelegations(user std.Address, currentTime int64) (int64, error) { - collectedAmount := int64(0) - - delegations := getUserDelegations(user) - if len(delegations) == 0 { - return collectedAmount, nil - } - - // Collect from all available delegations - for _, delegation := range delegations { - collectableAmount := delegation.CollectableAmount(currentTime) - - if collectableAmount == 0 { - continue - } - - err := delegation.collect(collectableAmount, currentTime) - if err != nil { - return collectedAmount, err - } - - collectedAmount, err = addToCollectedAmount(collectedAmount, collectableAmount) - if err != nil { - return collectedAmount, err - } - - // Remove empty delegations to keep storage clean - if delegation.IsEmpty() { - removeDelegation(delegation.ID()) - } - } - - return collectedAmount, nil -} - -// registerReferrer registers or validates referrer for delegation. -// Handles referral system integration for delegation operations. -func registerReferrer(caller std.Address, referrer string) string { - success := referral.TryRegister(cross, caller, referrer) - actualReferrer := referrer - - if !success { - actualReferrer = referral.GetReferral(referrer) - } - - return actualReferrer -} diff --git a/contract/r/gnoswap/v1/gov/staker/staker_delegation_snapshot.gno b/contract/r/gnoswap/v1/gov/staker/staker_delegation_snapshot.gno deleted file mode 100644 index 96ad779..0000000 --- a/contract/r/gnoswap/v1/gov/staker/staker_delegation_snapshot.gno +++ /dev/null @@ -1,82 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" -) - -// CleanStakerDelegationSnapshotByAdmin cleans old delegation history records. -// This administrative function removes delegation history records older than the specified threshold -// to prevent unlimited growth of historical data and optimize storage usage. -// -// The cleanup process: -// 1. Calculates cutoff time by subtracting threshold from current time -// 2. Filters delegation history to keep only records after cutoff time -// 3. Updates the delegation history with filtered records -// -// Parameters: -// - cur: realm context for cross-realm calls -// - threshold: time threshold in seconds (records older than this will be removed) -// -// Panics: -// - if caller is not admin -// -// Note: This operation is irreversible and will permanently remove historical data -func CleanStakerDelegationSnapshotByAdmin(cur realm, threshold int64) { - halt.AssertIsNotHaltedGovStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - // Calculate cutoff time by subtracting threshold from current time - cutoffTimestamp := time.Now().Unix() - threshold - - // Filter records after cutoff time - delegationHistory := getDelegationHistory() - recentDelegationHistory := delegationHistory.getRecordsBy(cutoffTimestamp) - - // Update delegation history with filtered records - setDelegationHistory(recentDelegationHistory) -} - -// SetUnDelegationLockupPeriodByAdmin sets the undelegation lockup period. -// This administrative function configures the time period that undelegated tokens -// must wait before they can be collected by users. -// -// The lockup period serves as a security mechanism to: -// - Prevent rapid delegation/undelegation cycles -// - Provide time for governance decisions to take effect -// - Maintain system stability during volatile periods -// -// Parameters: -// - cur: realm context for cross-realm calls -// - period: lockup period in seconds (must be non-negative) -// -// Panics: -// - if caller is not admin -// - if period is negative -// -// Note: This change affects all future undelegation operations -func SetUnDelegationLockupPeriodByAdmin(cur realm, period int64) { - halt.AssertIsNotHaltedGovStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - if period < 0 { - panic("period must be greater than 0") - } - - setUnDelegationLockupPeriod(period) - - previousRealm := std.PreviousRealm() - std.Emit( - "SetUnDelegationLockupPeriod", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "period", formatInt(period), - ) -} diff --git a/contract/r/gnoswap/v1/gov/staker/staker_reward.gno b/contract/r/gnoswap/v1/gov/staker/staker_reward.gno deleted file mode 100644 index 879ec76..0000000 --- a/contract/r/gnoswap/v1/gov/staker/staker_reward.gno +++ /dev/null @@ -1,280 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/p/nt/ufmt" - prbac "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoland/wugnot" - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/gns" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" - "gno.land/r/gnoswap/v1/gov/xgns" -) - -const WUGNOT_PATH string = "gno.land/r/gnoland/wugnot" - -// CollectReward collects accumulated rewards based on xGNS holdings. -// -// Claims all pending rewards from governance staking. -// Distributes protocol fees and emission rewards proportionally. -// Multi-token rewards system based on xGNS share. -// -// Reward Types: -// 1. Emission rewards: GNS from protocol emission -// 2. Protocol fees: Various tokens from swap/pool fees -// 3. Withdrawal fees: 1% of liquidity provider rewards -// 4. Pool creation fees: 100 GNS per pool -// -// Distribution Formula: -// -// userReward = (userXGNS / totalXGNS) * accumulatedRewards -// -// Process: -// 1. Calculates share based on xGNS balance -// 2. Claims GNS emission rewards -// 3. Claims protocol fee rewards (all tokens) -// 4. Transfers all rewards to caller -// 5. Resets user's reward tracking -// -// No parameters required - automatically determines caller's rewards. -// Transfers rewards directly to caller. -func CollectReward(cur realm) { - halt.AssertIsNotHaltedGovStaker() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - from := std.CurrentRealm().Address() - currentTimestamp := time.Now().Unix() - - emissionReward, protocolFeeRewards, err := claimRewards(caller.String(), currentTimestamp) - if err != nil { - panic(err) - } - - // Transfer emission rewards (GNS tokens) if any - if emissionReward > 0 { - gns.Transfer(cross, caller, emissionReward) - - previousRealm := std.PreviousRealm() - std.Emit( - "CollectEmissionReward", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "from", from.String(), - "to", caller.String(), - "emissionRewardAmount", formatInt(emissionReward), - ) - } - - // Transfer protocol fee rewards for each token type - for tokenPath, amount := range protocolFeeRewards { - if amount > 0 { - err := transferToken(tokenPath, from, caller, amount) - if err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "CollectProtocolFeeReward", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "tokenPath", tokenPath, - "from", from.String(), - "to", caller.String(), - "collectedAmount", formatInt(amount), - ) - } - } -} - -// CollectRewardFromLaunchPad collects rewards for launchpad project wallets. -// -// Parameters: -// - to: recipient address for rewards -// -// Only callable by launchpad contract. -func CollectRewardFromLaunchPad(cur realm, to std.Address) { - halt.AssertIsNotHaltedGovStaker() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - access.AssertIsLaunchpad(caller) - - from := std.CurrentRealm().Address() - currentTimestamp := time.Now().Unix() - - launchpadRewardID := makeLaunchpadRewardID(to.String()) - _, exists := getLaunchpadProjectDeposit(launchpadRewardID) - if !exists { - panic(makeErrorWithDetails( - errNoDelegatedAmount, - ufmt.Sprintf("%s is not project wallet from launchpad", to.String()), - )) - } - - emissionReward, protocolFeeRewards, err := claimRewardsFromLaunchpad(to.String(), currentTimestamp) - if err != nil { - panic(err) - } - - // Transfer emission rewards (GNS tokens) to project wallet if any - if emissionReward > 0 { - gns.Transfer(cross, to, emissionReward) - - previousRealm := std.PreviousRealm() - std.Emit( - "CollectEmissionFromLaunchPad", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "from", from.String(), - "to", to.String(), - "emissionRewardAmount", formatInt(emissionReward), - ) - } - - // Transfer protocol fee rewards to project wallet for each token type - for tokenPath, amount := range protocolFeeRewards { - if amount > 0 { - err := transferToken(tokenPath, from, to, amount) - if err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "CollectProtocolFeeFromLaunchPad", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "tokenPath", tokenPath, - "from", from.String(), - "to", to.String(), - "collectedAmount", formatInt(amount), - ) - } - } -} - -// SetAmountByProjectWallet sets the amount of reward for the project wallet. -// This function is exclusively callable by the launchpad contract to manage -// xGNS balances for project wallets that participate in launchpad offerings. -// -// The function handles both adding and removing stakes: -// - When adding: mints xGNS to launchpad address and starts reward accumulation -// - When removing: burns xGNS from launchpad address and stops reward accumulation -// Adjusts stake amount for project wallet address. -// Panics: -// - if caller is not the launchpad contract -// - if system is halted for withdrawals -// - if access control operations fail -func SetAmountByProjectWallet(cur realm, addr std.Address, amount int64, add bool) { - halt.AssertIsNotHaltedGovStaker() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - currentTimestamp := time.Now().Unix() - - access.AssertIsLaunchpad(caller) - - launchpadAddr, exists := access.GetAddress(prbac.ROLE_LAUNCHPAD.String()) - if !exists { - panic(ufmt.Sprintf("launchpad address not found")) - } - - if add { - // Add stake for the project wallet and mint xGNS to launchpad - err := addStakeFromLaunchpad(addr.String(), amount, currentTimestamp) - if err != nil { - panic(err) - } - - xgns.Mint(cross, launchpadAddr, amount) - } else { - // Remove stake for the project wallet and burn xGNS from launchpad - err := removeStakeFromLaunchpad(addr.String(), amount, currentTimestamp) - if err != nil { - panic(err) - } - - xgns.Burn(cross, launchpadAddr, amount) - } -} - -// claimRewards claims both emission and protocol fee rewards. -// Coordinates claiming process for both reward types. -func claimRewards(rewardID string, currentTimestamp int64) (int64, map[string]int64, error) { - emissionReward, err := claimRewardsEmissionReward(rewardID, currentTimestamp) - if err != nil { - return 0, nil, err - } - - protocolFeeRewards, err := claimRewardsProtocolFeeReward(rewardID, currentTimestamp) - if err != nil { - return 0, nil, err - } - - return emissionReward, protocolFeeRewards, nil -} - -// claimRewardsFromLaunchpad claims rewards for launchpad project wallets. -// Uses special reward ID format for launchpad integration. -func claimRewardsFromLaunchpad(address string, currentTimestamp int64) (int64, map[string]int64, error) { - launchpadRewardID := makeLaunchpadRewardID(address) - - return claimRewards(launchpadRewardID, currentTimestamp) -} - -// transferToken transfers tokens from the staker contract to a recipient address. -// transferToken handles token transfers for reward distribution. -// Supports both native GNOT (through wUGNOT unwrapping) and GRC20 tokens. -func transferToken( - tokenPath string, - from, to std.Address, - amount int64, -) error { - common.MustRegistered(tokenPath) - - // Validate recipient address - if !to.IsValid() { - return makeErrorWithDetails( - errInvalidAddress, - ufmt.Sprintf("invalid address %s to transfer protocol fee", to.String()), - ) - } - - // Validate transfer amount - if amount < 0 { - return makeErrorWithDetails( - errInvalidAmount, - ufmt.Sprintf("invalid amount %d to transfer protocol fee", amount), - ) - } - - // Check sufficient balance - balance := common.BalanceOf(tokenPath, from) - if balance < amount { - return makeErrorWithDetails( - errNotEnoughBalance, - ufmt.Sprintf("not enough %s balance(%d) to collect(%d)", tokenPath, balance, amount), - ) - } - - // Handle native GNOT transfer through wUGNOT unwrapping - isGnoNativeCoin := tokenPath == WUGNOT_PATH - if isGnoNativeCoin { - wugnot.Withdraw(cross, amount) - - sendCoin := std.Coin{Denom: "ugnot", Amount: amount} - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(from, to, std.Coins{sendCoin}) - - return nil - } - - // Handle GRC20 token transfer - return common.Transfer(cross, tokenPath, to, amount) -} diff --git a/contract/r/gnoswap/v1/gov/staker/state.gno b/contract/r/gnoswap/v1/gov/staker/state.gno deleted file mode 100644 index dff6663..0000000 --- a/contract/r/gnoswap/v1/gov/staker/state.gno +++ /dev/null @@ -1,523 +0,0 @@ -package staker - -import ( - "std" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/emission" - pf "gno.land/r/gnoswap/v1/protocol_fee" -) - -// Global state variables for staker contract -var ( - // unDelegationLockupPeriod defines the time period (in seconds) that undelegated tokens must wait before collection - unDelegationLockupPeriod int64 - - // delegationCounter provides unique delegation IDs for new delegations - delegationCounter *Counter - // delegations stores all delegation records indexed by delegation ID - delegations *avl.Tree - // delegationManager tracks user delegation relationships and provides efficient lookup - delegationManager *DelegationManager - - // delegationHistory maintains chronological record of all delegation events for snapshot calculations - delegationHistory DelegationHistory - // delegationSnapshots stores current delegation state for each delegatee - delegationSnapshots DelegationSnapshot - - // emissionRewardManager handles distribution and tracking of GNS emission rewards to stakers - emissionRewardManager *EmissionRewardManager - // protocolFeeRewardManager handles distribution and tracking of multi-token protocol fee rewards - protocolFeeRewardManager *ProtocolFeeRewardManager - - // launchpadProjectDeposits tracks xGNS deposits for launchpad project wallets - launchpadProjectDeposits *avl.Tree // project owner address -> deposit amount - - // emissionRewardBalance tracks the current balance of emission rewards (unused) - emissionRewardBalance int64 - // protocolFeeBalances tracks current balances of protocol fees by token (unused) - protocolFeeBalances map[string]int64 - - // totalDelegated tracks the total amount of xGNS delegated - totalDelegatedAmount int64 - // totalLockedAmount tracks the total amount of locked GNS - totalLockedAmount int64 -) - -// init initializes the global state variables with default values and empty structures -func init() { - // Default lockup period is 7 days - unDelegationLockupPeriod = 60 * 60 * 24 * 7 // 7 days - - // Initialize totalDelegated and totalLockedAmount - totalDelegatedAmount = 0 - totalLockedAmount = 0 - - // Initialize delegation tracking structures - delegationCounter = NewCounter() - delegations = avl.NewTree() - delegationManager = NewDelegationManager() - - // Initialize delegation history and snapshot tracking - delegationHistory = make(DelegationHistory, 0) - delegationSnapshots = make(DelegationSnapshot) - - // Initialize reward management systems - emissionRewardManager = NewEmissionRewardManager() - protocolFeeRewardManager = NewProtocolFeeRewardManager() - - // Initialize launchpad integration - launchpadProjectDeposits = avl.NewTree() -} - -// getUnDelegationLockupPeriod returns the current undelegation lockup period in seconds. -// -// Returns: -// - int64: lockup period in seconds -func getUnDelegationLockupPeriod() int64 { - return unDelegationLockupPeriod -} - -// setUnDelegationLockupPeriod updates the undelegation lockup period. -// This affects all future undelegation operations. -// -// Parameters: -// - period: new lockup period in seconds -func setUnDelegationLockupPeriod(period int64) { - unDelegationLockupPeriod = period -} - -// getCurrentDelegationID returns the current delegation counter value. -// -// Returns: -// - int64: current delegation ID counter -func getCurrentDelegationID() int64 { - return delegationCounter.Get() -} - -// nextDelegationID generates and returns the next unique delegation ID. -// -// Returns: -// - int64: next available delegation ID -func nextDelegationID() int64 { - return delegationCounter.next() -} - -// getDelegations returns the delegation storage tree. -// -// Returns: -// - *avl.Tree: delegation storage tree -func getDelegations() *avl.Tree { - return delegations -} - -// getDelegation retrieves a delegation by its ID. -// -// Parameters: -// - delegationID: unique identifier of the delegation -// -// Returns: -// - *Delegation: delegation instance or nil if not found -func getDelegation(delegationID int64) *Delegation { - id := formatInt(delegationID) - delegation, ok := delegations.Get(id) - if !ok { - return nil - } - - if delegation, ok := delegation.(*Delegation); !ok { - panic(ufmt.Sprintf("failed to cast delegations's element to *Delegation: %T", delegation)) - } else { - return delegation - } -} - -// setDelegation stores or updates a delegation in the storage tree. -// -// Parameters: -// - delegationID: unique identifier of the delegation -// - delegation: delegation instance to store -// -// Returns: -// - bool: true if successfully stored -func setDelegation(delegationID int64, delegation *Delegation) bool { - id := formatInt(delegationID) - - delegations.Set(id, delegation) - - return true -} - -// addDelegation adds a new delegation to storage and updates the delegation manager. -// -// Parameters: -// - delegationID: unique identifier of the delegation -// - delegation: delegation instance to add -// -// Returns: -// - bool: true if successfully added -func addDelegation(delegationID int64, delegation *Delegation) bool { - if ok := setDelegation(delegationID, delegation); !ok { - return false - } - - delegationManager.addDelegation( - delegation.DelegateFrom(), - delegation.DelegateTo(), - delegationID, - ) - - return true -} - -// removeDelegation removes a delegation from storage and updates the delegation manager. -// -// Parameters: -// - delegationID: unique identifier of the delegation to remove -// -// Returns: -// - bool: true if successfully removed -func removeDelegation(delegationID int64) bool { - delegation := getDelegation(delegationID) - if delegation == nil { - return false - } - - id := formatInt(delegation.ID()) - _, ok := delegations.Remove(id) - - delegationManager.removeDelegation( - delegation.DelegateFrom(), - delegation.DelegateTo(), - delegationID, - ) - - return ok -} - -// getUserDelegations retrieves all delegations for a specific user. -// -// Parameters: -// - user: user's address -// -// Returns: -// - []*Delegation: list of user's delegations -func getUserDelegations(user std.Address) (delegations []*Delegation) { - for _, delegationID := range delegationManager.GetUserDelegationIDs(user) { - delegations = append(delegations, getDelegation(delegationID)) - } - return -} - -// getUserDelegationsWithDelegatee retrieves all delegations from a user to a specific delegatee. -// Note: Current implementation returns all user delegations regardless of delegatee (potential bug). -// -// Parameters: -// - user: user's address -// - delegatee: delegatee's address (currently unused) -// -// Returns: -// - []*Delegation: list of user's delegations to the delegatee -func getUserDelegationsWithDelegatee(user std.Address, delegatee std.Address) (delegations []*Delegation) { - for _, delegationID := range delegationManager.GetUserDelegationIDs(user) { - delegations = append(delegations, getDelegation(delegationID)) - } - return -} - -// getDelegationHistory returns the current delegation history. -// -// Returns: -// - DelegationHistory: chronological list of delegation records -func getDelegationHistory() DelegationHistory { - return delegationHistory -} - -// addDelegationRecord adds a new delegation record to history and updates snapshots. -// -// Parameters: -// - delegationRecord: delegation record to add -func addDelegationRecord(delegationRecord *DelegationRecord) { - delegationHistory = delegationHistory.addRecord(delegationRecord) - delegationSnapshots = delegationSnapshots.addRecord(delegationRecord) -} - -// setDelegationHistory replaces the current delegation history. -// -// Parameters: -// - history: new delegation history to set -func setDelegationHistory(history DelegationHistory) { - delegationHistory = history -} - -// getDelegationSnapshots returns the current delegation snapshots. -// -// Returns: -// - DelegationSnapshot: current delegation state for all delegatees -func getDelegationSnapshots() DelegationSnapshot { - return delegationSnapshots -} - -// setDelegationSnapshots replaces the current delegation snapshots. -// -// Parameters: -// - snapshot: new delegation snapshot to set -func setDelegationSnapshots(snapshot DelegationSnapshot) { - delegationSnapshots = snapshot -} - -// addStakeEmissionReward adds stake to emission reward tracking for an address. -// This method updates the emission reward distribution state and adds stake for the specified address. -// -// Parameters: -// - address: staker's address -// - amount: amount of stake to add -// - currentTimestamp: current timestamp -func addStakeEmissionReward(address string, amount int64, currentTimestamp int64) error { - distributedAmount := emission.GetAccuDistributedToGovStaker() - - err := emissionRewardManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp) - if err != nil { - return err - } - - return emissionRewardManager.addStake(address, amount, currentTimestamp) -} - -// removeStakeEmissionReward removes stake from emission reward tracking for an address. -// This method updates the emission reward distribution state and removes stake for the specified address. -// -// Parameters: -// - address: staker's address -// - amount: amount of stake to remove -// - currentTimestamp: current timestamp -func removeStakeEmissionReward(address string, amount int64, currentTimestamp int64) error { - distributedAmount := emission.GetAccuDistributedToGovStaker() - - err := emissionRewardManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp) - if err != nil { - return err - } - - return emissionRewardManager.removeStake(address, amount, currentTimestamp) -} - -// claimRewardsEmissionReward claims emission rewards for an address. -// This method updates the emission reward distribution state and processes reward claiming. -// -// Parameters: -// - address: staker's address claiming rewards -// - currentTimestamp: current timestamp -// -// Returns: -// - int64: amount of emission rewards claimed -// - error: nil on success, error if claiming fails -func claimRewardsEmissionReward(address string, currentTimestamp int64) (int64, error) { - distributedAmount := emission.GetAccuDistributedToGovStaker() - - err := emissionRewardManager.updateAccumulatedRewardX128PerStake(distributedAmount, currentTimestamp) - if err != nil { - return 0, err - } - - return emissionRewardManager.claimRewards(address, currentTimestamp) -} - -// addStakeProtocolFeeReward adds stake to protocol fee reward tracking for an address. -// This method distributes protocol fees and updates the protocol fee reward state. -// -// Parameters: -// - address: staker's address -// - amount: amount of stake to add -// - currentTimestamp: current timestamp -func addStakeProtocolFeeReward(address string, amount int64, currentTimestamp int64) error { - pf.DistributeProtocolFee(cross) - - distributedAmounts := getDistributedProtocolFees() - - err := protocolFeeRewardManager.updateAccumulatedProtocolFeeX128PerStake(distributedAmounts, currentTimestamp) - if err != nil { - return err - } - - return protocolFeeRewardManager.addStake(address, amount, currentTimestamp) -} - -// removeStakeProtocolFeeReward removes stake from protocol fee reward tracking for an address. -// This method distributes protocol fees and updates the protocol fee reward state. -// -// Parameters: -// - address: staker's address -// - amount: amount of stake to remove -// - currentTimestamp: current timestamp -func removeStakeProtocolFeeReward(address string, amount int64, currentTimestamp int64) error { - pf.DistributeProtocolFee(cross) - - distributedAmounts := getDistributedProtocolFees() - - err := protocolFeeRewardManager.updateAccumulatedProtocolFeeX128PerStake(distributedAmounts, currentTimestamp) - if err != nil { - return err - } - - return protocolFeeRewardManager.removeStake(address, amount, currentTimestamp) -} - -// claimRewardsProtocolFeeReward claims protocol fee rewards for an address. -// This method distributes protocol fees and processes reward claiming for all token types. -// -// Parameters: -// - address: staker's address claiming rewards -// - currentTimestamp: current timestamp -// -// Returns: -// - map[string]int64: protocol fee rewards claimed by token -// - error: nil on success, error if claiming fails -func claimRewardsProtocolFeeReward(address string, currentTimestamp int64) (map[string]int64, error) { - pf.DistributeProtocolFee(cross) - - distributedAmounts := getDistributedProtocolFees() - - err := protocolFeeRewardManager.updateAccumulatedProtocolFeeX128PerStake(distributedAmounts, currentTimestamp) - if err != nil { - return nil, err - } - - return protocolFeeRewardManager.claimRewards(address, currentTimestamp) -} - -// getDistributedProtocolFees retrieves the current distributed protocol fee amounts for all tokens. -// This method queries the protocol fee contract for accumulated distributions. -// -// Returns: -// - map[string]int64: distributed amounts by token path -func getDistributedProtocolFees() map[string]int64 { - return pf.GetAccuTransfersToGovStaker() -} - -// getLaunchpadProjectDeposit retrieves the deposit amount for a launchpad project. -// -// Parameters: -// - ownerAddress: project owner's address identifier -// -// Returns: -// - int64: deposit amount -// - bool: true if project exists, false otherwise -func getLaunchpadProjectDeposit(ownerAddress string) (int64, bool) { - deposit, ok := launchpadProjectDeposits.Get(ownerAddress) - if !ok { - return 0, false - } - - amount, ok := deposit.(int64) - if !ok { - panic(ufmt.Sprintf("failed to cast deposit to int64: %T", deposit)) - } - - return amount, true -} - -// setLaunchpadProjectDeposit sets the deposit amount for a launchpad project. -// -// Parameters: -// - ownerAddress: project owner's address identifier -// - deposit: deposit amount to set -// -// Returns: -// - bool: true if successfully set -func setLaunchpadProjectDeposit(ownerAddress string, deposit int64) bool { - launchpadProjectDeposits.Set(ownerAddress, deposit) - - return true -} - -// removeLaunchpadProjectDeposit removes a launchpad project deposit record. -// -// Parameters: -// - ownerAddress: project owner's address identifier -// -// Returns: -// - bool: true if successfully removed -func removeLaunchpadProjectDeposit(ownerAddress string) bool { - _, ok := launchpadProjectDeposits.Remove(ownerAddress) - - return ok -} - -// addStakeFromLaunchpad adds stake for a launchpad project and updates reward tracking. -// This method creates a special reward ID for launchpad projects and manages their deposit tracking. -// -// Parameters: -// - address: project wallet address -// - amount: amount of stake to add -// - currentTimestamp: current timestamp -func addStakeFromLaunchpad(address string, amount int64, currentTimestamp int64) error { - launchpadRewardID := makeLaunchpadRewardID(address) - err := addStakeEmissionReward(launchpadRewardID, amount, currentTimestamp) - if err != nil { - return err - } - - err = addStakeProtocolFeeReward(launchpadRewardID, amount, currentTimestamp) - if err != nil { - return err - } - - deposit, exists := getLaunchpadProjectDeposit(launchpadRewardID) - if !exists { - deposit = 0 - } - - deposit += amount - setLaunchpadProjectDeposit(launchpadRewardID, deposit) - - return nil -} - -// removeStakeFromLaunchpad removes stake for a launchpad project and updates reward tracking. -// This method manages launchpad project deposit tracking and ensures non-negative deposits. -// -// Parameters: -// - address: project wallet address -// - amount: amount of stake to remove -// - currentTimestamp: current timestamp -func removeStakeFromLaunchpad(address string, amount int64, currentTimestamp int64) error { - launchpadRewardID := makeLaunchpadRewardID(address) - err := removeStakeEmissionReward(launchpadRewardID, amount, currentTimestamp) - if err != nil { - return err - } - - err = removeStakeProtocolFeeReward(launchpadRewardID, amount, currentTimestamp) - if err != nil { - return err - } - - deposit, exists := getLaunchpadProjectDeposit(launchpadRewardID) - if !exists { - deposit = 0 - } - - deposit -= amount - if deposit < 0 { - deposit = 0 - } - - setLaunchpadProjectDeposit(launchpadRewardID, deposit) - - return nil -} - -// makeLaunchpadRewardID creates a special reward identifier for launchpad projects. -// This ensures launchpad project rewards are tracked separately from regular user stakes. -// -// Parameters: -// - address: project wallet address -// -// Returns: -// - string: formatted launchpad reward ID -func makeLaunchpadRewardID(address string) string { - return ufmt.Sprintf("launchpad:%s", address) -} diff --git a/contract/r/gnoswap/v1/gov/staker/util.gno b/contract/r/gnoswap/v1/gov/staker/util.gno deleted file mode 100644 index 7620b34..0000000 --- a/contract/r/gnoswap/v1/gov/staker/util.gno +++ /dev/null @@ -1,124 +0,0 @@ -package staker - -import ( - b64 "encoding/base64" - "strconv" - - "gno.land/p/nt/avl" - "gno.land/p/onbloc/json" - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" -) - -// marshal data to json string -func marshal(data *json.Node) string { - b, err := json.Marshal(data) - if err != nil { - panic(err.Error()) - } - - return string(b) -} - -// b64Encode encodes data to base64 string -func b64Encode(data string) string { - return string(b64.StdEncoding.EncodeToString([]byte(data))) -} - -// formatUint formats a uint64 to a string -func formatUint(v uint64) string { - return strconv.FormatUint(v, 10) -} - -// formatInt formats an int64 to a string -func formatInt(v int64) string { - return strconv.FormatInt(v, 10) -} - -// getUint64FromTree returns the uint64 value from the tree -func getUint64FromTree(tree *avl.Tree, key string) uint64 { - value, exists := tree.Get(key) - if !exists { - return 0 - } - - v, ok := value.(uint64) - if !ok { - panic(ufmt.Sprintf("failed to cast value to uint64: %T", value)) - } - - return v -} - -// updateUint64InTree updates the uint64 value in the tree -func updateUint64InTree(tree *avl.Tree, key string, delta uint64, add bool) uint64 { - current := getUint64FromTree(tree, key) - var newValue uint64 - if add { - newValue = current + delta - } else { - if current < delta { - panic(makeErrorWithDetails( - errNotEnoughBalance, - ufmt.Sprintf("not enough balance: current(%d) < requested(%d)", current, delta), - )) - } - newValue = current - delta - } - - tree.Set(key, newValue) - - return newValue -} - -// getOrCreateInnerTree returns the inner tree for the given key -func getOrCreateInnerTree(tree *avl.Tree, key string) *avl.Tree { - value, exists := tree.Get(key) - if !exists { - innerTree := avl.NewTree() - tree.Set(key, innerTree) - return innerTree - } - - v, ok := value.(*avl.Tree) - if !ok { - panic(ufmt.Sprintf("failed to cast value to *avl.Tree: %T", value)) - } - - return v -} - -// milliToSec converts milliseconds to seconds -func milliToSec(ms int64) int64 { - var msPerSec int64 = 1000 - return ms / msPerSec -} - -// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. -// -// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds -// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. -// -// Parameters: -// - value (*u256.Uint): The unsigned 256-bit integer to be converted. -// -// Returns: -// - int64: The converted value if it falls within the int64 range. -// -// Panics: -// - If the `value` exceeds the range of int64, the function will panic with an error indicating -// the overflow and the original value. -func safeConvertToInt64(value *u256.Uint) int64 { - const INT64_MAX = 9223372036854775807 - const MAX_INT64 = "9223372036854775807" - - res, overflow := value.Uint64WithOverflow() - if overflow || res > uint64(INT64_MAX) { - panic(ufmt.Sprintf( - "amount(%s) overflows int64 range (max %s)", - value.ToString(), - MAX_INT64, - )) - } - return int64(res) -} diff --git a/contract/r/gnoswap/v1/gov/xgns/doc.gno b/contract/r/gnoswap/v1/gov/xgns/doc.gno deleted file mode 100644 index aae22e1..0000000 --- a/contract/r/gnoswap/v1/gov/xgns/doc.gno +++ /dev/null @@ -1,4 +0,0 @@ -// Package xgns implements the GRC20-compliant xGNS token that represents -// staked GNS tokens. It manages minting/burning operations and tracks -// voting power for governance. -package xgns diff --git a/contract/r/gnoswap/v1/gov/xgns/errors.gno b/contract/r/gnoswap/v1/gov/xgns/errors.gno deleted file mode 100644 index 8f9fb02..0000000 --- a/contract/r/gnoswap/v1/gov/xgns/errors.gno +++ /dev/null @@ -1,14 +0,0 @@ -package xgns - -import ( - "errors" -) - -var errNoPermission = errors.New("[GNOSWAP-XGNS-001] caller has no permission") - -// checkErr panics if an error occurs. -func checkErr(err error) { - if err != nil { - panic(err.Error()) - } -} diff --git a/contract/r/gnoswap/v1/gov/xgns/gnomod.toml b/contract/r/gnoswap/v1/gov/xgns/gnomod.toml deleted file mode 100644 index 875d4b1..0000000 --- a/contract/r/gnoswap/v1/gov/xgns/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/gov/xgns" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/gov/xgns/xgns.gno b/contract/r/gnoswap/v1/gov/xgns/xgns.gno deleted file mode 100644 index ae021ad..0000000 --- a/contract/r/gnoswap/v1/gov/xgns/xgns.gno +++ /dev/null @@ -1,126 +0,0 @@ -package xgns - -import ( - "std" - "strings" - - "gno.land/p/demo/tokens/grc20" - "gno.land/p/nt/ownable" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - - prbac "gno.land/p/gnoswap/rbac" -) - -var ( - admin = ownable.NewWithAddress(std.DerivePkgAddr(prbac.ROLE_GOV_STAKER.String())) - token, ledger = grc20.NewToken("XGNS", "xGNS", 6) -) - -// TotalSupply returns the total supply of xGNS tokens. -func TotalSupply() int64 { - return token.TotalSupply() -} - -// VotingSupply returns total supply eligible for voting. -func VotingSupply() int64 { - total := token.TotalSupply() - launchpad, ok := access.GetAddress(prbac.ROLE_LAUNCHPAD.String()) - if !ok { - panic(ufmt.Sprintf("launchpad address not found")) - } - - return total - token.BalanceOf(launchpad) -} - -// BalanceOf returns token balance for address. -// -// Parameters: -// - owner: address to check balance for -// -// Returns balance amount. -func BalanceOf(owner std.Address) int64 { - return token.BalanceOf(owner) -} - -// Render returns a formatted representation of the token state. -func Render(path string) string { - parts := strings.Split(path, "/") - c := len(parts) - - switch { - case path == "": - return token.RenderHome() - case c == 2 && parts[0] == "balance": - balance := token.BalanceOf(std.Address(parts[1])) - return ufmt.Sprintf("%d\n", balance) - default: - return "404\n" - } -} - -// Mint mints tokens to address. -// -// Parameters: -// - to: recipient address -// - amount: amount to mint -// -// Only callable by governance staker contract. -func Mint(cur realm, to std.Address, amount int64) { - halt.AssertIsNotHaltedXGns() - - caller := std.PreviousRealm().Address() - access.AssertIsGovStaker(caller) - - checkErr(ledger.Mint(to, amount)) -} - -// MintByLaunchPad mints tokens to address. -// -// Parameters: -// - to: recipient address -// - amount: amount to mint -// -// Only callable by launchpad contract. -func MintByLaunchPad(cur realm, to std.Address, amount int64) { - halt.AssertIsNotHaltedXGns() - - caller := std.PreviousRealm().Address() - access.AssertIsLaunchpad(caller) - - checkErr(ledger.Mint(to, amount)) -} - -// Burn burns tokens from address. -// -// Parameters: -// - from: address to burn from -// - amount: amount to burn -// -// Only callable by governance staker contract. -func Burn(cur realm, from std.Address, amount int64) { - halt.AssertIsNotHaltedXGns() - - caller := std.PreviousRealm().Address() - access.AssertIsGovStaker(caller) - - checkErr(ledger.Burn(from, amount)) -} - -// BurnByLaunchPad burns tokens from address. -// -// Parameters: -// - from: address to burn from -// - amount: amount to burn -// -// Only callable by launchpad contract. -func BurnByLaunchPad(cur realm, from std.Address, amount int64) { - halt.AssertIsNotHaltedXGns() - - caller := std.PreviousRealm().Address() - access.AssertIsLaunchpad(caller) - - checkErr(ledger.Burn(from, amount)) -} diff --git a/contract/r/gnoswap/v1/launchpad/README.md b/contract/r/gnoswap/v1/launchpad/README.md deleted file mode 100644 index ce17447..0000000 --- a/contract/r/gnoswap/v1/launchpad/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Launchpad - -Token distribution platform for early-stage projects. - -## Overview - -Launchpad enables new projects to distribute tokens to GNS stakers with tiered lock periods and pro-rata reward distribution. For more details about the concept, check out [docs](https://docs.gnoswap.io/core-concepts/launchpad). - -## Configuration - -- **Pool Tiers**: 30, 90, 180 days -- **Minimum Start Delay**: 7 days -- **Auto-delegation**: Staked GNS converts to xGNS -- **Tier Allocation**: Customizable per project - -## Core Features - -- GNS staking for project token rewards -- Multiple tier durations with different rewards -- Automatic xGNS delegation for governance -- Pro-rata distribution based on stake size -- Conditional participation requirements - -## Key Functions - -### `CreateProject` -Creates new token distribution project. - -### `DepositGns` -Stakes GNS to earn project tokens. - -### `CollectRewardByDepositId` -Claims earned project tokens. - -### `CollectDepositGns` -Withdraws GNS after lock period. - -### `TransferLeftFromProjectByAdmin` -Refunds unclaimed rewards to project. - -## Usage - -```go -// Create project -projectId := CreateProject( - name, tokenPath, recipient, amount, - conditionTokens, conditionAmounts, - tier30Ratio, tier90Ratio, tier180Ratio, - startTime -) - -// Stake GNS -depositId := DepositGns(projectTierId, amount, referrer) - -// Collect rewards -CollectRewardByDepositId(depositId) - -// Withdraw after lock period -CollectDepositGns(depositId) -``` - -## Security - -- GNS locked until tier period ends -- Automatic governance delegation -- Conditional requirements prevent abuse \ No newline at end of file diff --git a/contract/r/gnoswap/v1/launchpad/api_deposit.gno b/contract/r/gnoswap/v1/launchpad/api_deposit.gno deleted file mode 100644 index 819f25c..0000000 --- a/contract/r/gnoswap/v1/launchpad/api_deposit.gno +++ /dev/null @@ -1,18 +0,0 @@ -package launchpad - -// ApiGetDepositByDepositId retrieves deposit information by deposit ID. -func ApiGetDepositByDepositId(depositId string) string { - deposit, exist := deposits.Get(depositId) - if !exist { - return "" - } - - builder := metaBuilder() - d, ok := deposit.(*Deposit) - if !ok { - panic("failed to cast deposit to *Deposit") - } - depositBuilder(builder, d) - - return marshal(builder.Node()) -} diff --git a/contract/r/gnoswap/v1/launchpad/api_project.gno b/contract/r/gnoswap/v1/launchpad/api_project.gno deleted file mode 100644 index 1b8b60a..0000000 --- a/contract/r/gnoswap/v1/launchpad/api_project.gno +++ /dev/null @@ -1,92 +0,0 @@ -package launchpad - -import ( - "std" - "strconv" - - "gno.land/p/nt/ufmt" -) - -// ApiGetProjectAndTierStatisticsByProjectId retrieves project and tier statistics by project ID. -func ApiGetProjectAndTierStatisticsByProjectId(projectId string) string { - project, err := getProject(projectId) - if err != nil { - return "" - } - - builder := metaBuilder().WriteString("projectId", projectId) - projectBuilder(builder, project) - - for _, duration := range []int64{30, 90, 180} { - if tier, err := project.getTier(duration); err == nil { - tierBuilder(builder, ufmt.Sprintf("tier%d", duration), tier) - } - } - - return marshal(builder.Node()) -} - -// ApiGetProjectStatisticsByProjectId retrieves project statistics by project ID. -func ApiGetProjectStatisticsByProjectId(projectId string) string { - project, err := getProject(projectId) - if err != nil { - return "" - } - - builder := metaBuilder().WriteString("projectId", projectId) - projectBuilder(builder, project) - - return marshal(builder.Node()) -} - -// ApiGetTierStatisticsByProjectId retrieves tier statistics by project ID. -func ApiGetTierStatisticsByProjectId(projectId string) string { - project, err := getProject(projectId) - if err != nil { - return "" - } - - builder := metaBuilder().WriteString("projectId", projectId) - - for _, duration := range []int64{30, 90, 180} { - if tier, err := project.getTier(duration); err == nil { - tierBuilder(builder, ufmt.Sprintf("tier%d", duration), tier) - } - } - - return marshal(builder.Node()) -} - -// ApiGetProjectStatisticsByProjectTierId retrieves project statistics by project tier ID. -func ApiGetProjectStatisticsByProjectTierId(tierId string) string { - projectId, duration := parseProjectTierID(tierId) - project, err := getProject(projectId) - if err != nil { - return "" - } - - tier, err := project.getTier(duration) - if err != nil { - return "" - } - - builder := metaBuilder().WriteString("projectId", projectId) - tierBuilder(builder, "tier", tier) - - return marshal(builder.Node()) -} - -// ApiGetProjectActiveOf retrieves project active status by project ID. -func ApiGetProjectActiveOf(projectId string) string { - project, err := getProject(projectId) - if err != nil { - return "" - } - projectActiveResult := project.IsActive(std.ChainHeight()) - builder := (metaBuilder(). - WriteString("projectId", project.id). - WriteString("isActive", strconv.FormatBool(projectActiveResult)). - WriteString("currentHeight", strconv.FormatInt(std.ChainHeight(), 10))). - WriteString("startTime", strconv.FormatInt(project.standardTier().startTime, 10)) - return marshal(builder.Node()) -} diff --git a/contract/r/gnoswap/v1/launchpad/api_reward.gno b/contract/r/gnoswap/v1/launchpad/api_reward.gno deleted file mode 100644 index e2d0b9f..0000000 --- a/contract/r/gnoswap/v1/launchpad/api_reward.gno +++ /dev/null @@ -1,26 +0,0 @@ -package launchpad - -import ( - "std" - - gs "gno.land/r/gnoswap/v1/gov/staker" -) - -// ApiGetProjectRecipientRewardByProjectId retrieves the claimable reward for a project recipient by project ID. -func ApiGetProjectRecipientRewardByProjectId(projectId string) string { - project, exist := projects.Get(projectId) - if !exist { - return "0" - } - - return gs.GetClaimableRewardByAddress(project.(*Project).recipient) -} - -// ApiGetProjectRecipientRewardByAddress retrieves the claimable reward for a recipient by address. -func ApiGetProjectRecipientRewardByAddress(address std.Address) string { - if !address.IsValid() { - return "0" - } - - return gs.GetClaimableRewardByAddress(address) -} diff --git a/contract/r/gnoswap/v1/launchpad/assert.gno b/contract/r/gnoswap/v1/launchpad/assert.gno deleted file mode 100644 index 783e9a3..0000000 --- a/contract/r/gnoswap/v1/launchpad/assert.gno +++ /dev/null @@ -1,62 +0,0 @@ -package launchpad - -import ( - "std" - - "gno.land/p/nt/ufmt" -) - -// assertIsDepositOwner asserts that the caller is the owner of the deposit. -// Panics if the caller is not the owner of the deposit. -func assertIsDepositOwner(depositID string, caller std.Address) { - deposit, err := getDeposit(depositID) - if err != nil { - panic(err.Error()) - } - - if !deposit.IsOwner(caller) { - panic(makeErrorWithDetails(errInvalidOwner, ufmt.Sprintf("(%s)", caller.String())).Error()) - } -} - -// assertIsValidAmount panics if the amount is zero. -func assertIsValidAmount(amount int64) { - if amount < minimumDepositAmount { - panic(makeErrorWithDetails( - errInvalidAmount, - ufmt.Sprintf("amount(%d) should greater than minimum deposit amount(%d)", amount, minimumDepositAmount), - )) - } - - if (amount % minimumDepositAmount) != 0 { - panic(makeErrorWithDetails( - errInvalidAmount, - ufmt.Sprintf("amount(%d) must be a multiple of 1_000_000", amount), - )) - } -} - -// assertHasProject asserts that the caller is the owner of at least one project. -// Panics if the caller is not the owner of any project. -func assertHasProject(caller std.Address) { - hasProject := false - - projects.Iterate("", "", func(key string, value interface{}) bool { - project, ok := value.(*Project) - if !ok { - panic(ufmt.Sprintf("failed to cast projects's element to *Project: %T", value)) - } - - hasProject = project.IsOwner(caller) - - // if true, break the loop - return hasProject - }) - - if !hasProject { - panic(makeErrorWithDetails( - errInvalidOwner, - ufmt.Sprintf("caller %s is not the owner of any project", caller.String()), - )) - } -} diff --git a/contract/r/gnoswap/v1/launchpad/consts.gno b/contract/r/gnoswap/v1/launchpad/consts.gno deleted file mode 100644 index 5b23243..0000000 --- a/contract/r/gnoswap/v1/launchpad/consts.gno +++ /dev/null @@ -1,44 +0,0 @@ -package launchpad - -import ( - u256 "gno.land/p/gnoswap/uint256" -) - -const ( - projectTier30 = int64(30) - projectTier90 = int64(90) - projectTier180 = int64(180) - - dayTime = int64(24 * 60 * 60) // 86400 - - minimumDepositAmount = int64(1_000_000) - - stringSplitterPad = "*PAD*" - - projectMinimumStartDelayTime = int64(60 * 60) // 1 hour -) - -// contract paths -const ( - GOV_XGNS_PATH string = "gno.land/r/gnoswap/v1/gov/xgns" -) - -var projectTierDurations = []int64{ - projectTier30, - projectTier90, - projectTier180, -} - -var projectTierDurationTimes = map[int64]int64{ - projectTier30: dayTime * projectTier30, // 30 days - projectTier90: dayTime * projectTier90, // 90 days - projectTier180: dayTime * projectTier180, // 180 days -} - -var projectTierRewardCollectableDuration = map[int64]int64{ - projectTier30: dayTime * 3, // 3 days - projectTier90: dayTime * 7, // 7 days - projectTier180: dayTime * 14, // 14 days -} - -var q128 = u256.MustFromDecimal("340282366920938463463374607431768211456") diff --git a/contract/r/gnoswap/v1/launchpad/counter.gno b/contract/r/gnoswap/v1/launchpad/counter.gno deleted file mode 100644 index 7fede4a..0000000 --- a/contract/r/gnoswap/v1/launchpad/counter.gno +++ /dev/null @@ -1,25 +0,0 @@ -package launchpad - -// Counter manages unique incrementing IDs. -type Counter struct { - id int64 -} - -// NewCounter creates a new Counter starting at 0. -func NewCounter() *Counter { - return &Counter{ - id: 0, - } -} - -// next increments the counter and returns the next ID. -func (c *Counter) next() int64 { - c.id++ - - return c.id -} - -// Get returns the current ID without incrementing. -func (c *Counter) Get() int64 { - return c.id -} diff --git a/contract/r/gnoswap/v1/launchpad/deposit.gno b/contract/r/gnoswap/v1/launchpad/deposit.gno deleted file mode 100644 index cf23148..0000000 --- a/contract/r/gnoswap/v1/launchpad/deposit.gno +++ /dev/null @@ -1,120 +0,0 @@ -package launchpad - -import ( - "std" -) - -// Deposit represents a deposit made by a user in a launchpad project. -// -// This struct contains the necessary data and methods to manage and distribute -// rewards for a specific deposit. -// -// Fields: -// - depositor (std.Address): The address of the depositor. -// - id (string): The unique identifier for the deposit. -// - projectID (string): The ID of the project associated with the deposit. -// - tier (int64): The tier of the deposit. -// - depositAmount (int64): The amount of the deposit. -// - withdrawnHeight (int64): The height at which the deposit was withdrawn. -// - withdrawnTime (int64): The time when the deposit was withdrawn. -// - createdTime (int64): The time when the deposit was created. -// - endTime (int64): The time when the deposit ends. -type Deposit struct { - depositor std.Address - - id string - projectID string - tier int64 // 30, 60, 180 // instead of tierId - depositAmount int64 - withdrawnHeight int64 - withdrawnTime int64 - createdHeight int64 - createdAt int64 - endTime int64 -} - -func (d *Deposit) ID() string { - return d.id -} - -func (d *Deposit) ProjectID() string { - return d.projectID -} - -func (d *Deposit) ProjectTierID() string { - return makeProjectTierID(d.projectID, d.tier) -} - -func (d *Deposit) Tier() int64 { - return d.tier -} - -func (d *Deposit) Depositor() std.Address { - return d.depositor -} - -func (d *Deposit) DepositAmount() int64 { - return d.depositAmount -} - -func (d *Deposit) CreatedHeight() int64 { - return d.createdHeight -} - -func (d *Deposit) DepositTime() int64 { - return d.createdAt -} - -func (d *Deposit) WithdrawnTime() int64 { - return d.withdrawnTime -} - -func (d *Deposit) IsOwner(address std.Address) bool { - return d.depositor.String() == address.String() -} - -func (d *Deposit) EndTime() int64 { - return d.endTime -} - -func (d *Deposit) IsEnded(currentTime int64) bool { - return d.endTime < currentTime -} - -func (d *Deposit) IsWithdrawn() bool { - return d.withdrawnTime > 0 && d.withdrawnHeight > 0 -} - -func (d *Deposit) withdraw(currentHeight, currentTime int64) int64 { - d.withdrawnTime = currentTime - d.withdrawnHeight = currentHeight - - previousDepositAmount := d.depositAmount - d.depositAmount = 0 - - return previousDepositAmount -} - -// NewDeposit returns a pointer to a new Deposit with the given values. -func NewDeposit( - depositID string, - projectID string, - tier int64, - depositor std.Address, - depositAmount int64, - createdHeight int64, - createdTime int64, - endTime int64, -) *Deposit { - return &Deposit{ - id: depositID, - projectID: projectID, - tier: tier, - depositor: depositor, - depositAmount: depositAmount, - withdrawnHeight: 0, - createdHeight: createdHeight, - createdAt: createdTime, - endTime: endTime, - } -} diff --git a/contract/r/gnoswap/v1/launchpad/errors.gno b/contract/r/gnoswap/v1/launchpad/errors.gno deleted file mode 100644 index eac0992..0000000 --- a/contract/r/gnoswap/v1/launchpad/errors.gno +++ /dev/null @@ -1,46 +0,0 @@ -package launchpad - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errNoLeftReward = errors.New("[GNOSWAP-LAUNCHPAD-001] no left reward") - errInvalidAddress = errors.New("[GNOSWAP-LAUNCHPAD-002] invalid address") - errDataNotFound = errors.New("[GNOSWAP-LAUNCHPAD-003] requested data not found") - errActiveProject = errors.New("[GNOSWAP-LAUNCHPAD-004] project is active") - errInactiveProject = errors.New("[GNOSWAP-LAUNCHPAD-005] project is inactive") - errInactiveTier = errors.New("[GNOSWAP-LAUNCHPAD-006] pool is inactive") - errInvalidInput = errors.New("[GNOSWAP-LAUNCHPAD-007] invalid input data") - errDuplicateProject = errors.New("[GNOSWAP-LAUNCHPAD-008] can not create same project in same block") - errInvalidTier = errors.New("[GNOSWAP-LAUNCHPAD-009] invalid pool") - errInsufficientBalance = errors.New("[GNOSWAP-LAUNCHPAD-010] insufficient balance") - errInvalidLength = errors.New("[GNOSWAP-LAUNCHPAD-011] invalid length") - errNotEnoughBalance = errors.New("[GNOSWAP-LAUNCHPAD-012] not enough balance") - errInvalidCondition = errors.New("[GNOSWAP-LAUNCHPAD-013] invalid transfer condition") - errConvertFail = errors.New("[GNOSWAP-LAUNCHPAD-014] convert fail") - errNotUserCaller = errors.New("[GNOSWAP-LAUNCHPAD-015] only user caller") - errInvalidData = errors.New("[GNOSWAP-LAUNCHPAD-016] invalid data") - errInvalidAmount = errors.New("[GNOSWAP-LAUNCHPAD-017] invalid amount") - errDuplicateDeposit = errors.New("[GNOSWAP-LAUNCHPAD-018] duplicate deposit") - errInvalidRewardState = errors.New("[GNOSWAP-LAUNCHPAD-019] invalid reward state") - errNotExistDeposit = errors.New("[GNOSWAP-LAUNCHPAD-020] not exist deposit") - errAlreadyExistDeposit = errors.New("[GNOSWAP-LAUNCHPAD-021] already exist deposit") - errInvalidProjectId = errors.New("[GNOSWAP-LAUNCHPAD-022] invalid project id") - errAlreadyCollected = errors.New("[GNOSWAP-LAUNCHPAD-023] already collected") - errNotYetClaimReward = errors.New("[GNOSWAP-LAUNCHPAD-024] not yet claim reward") - errInvalidCaller = errors.New("[GNOSWAP-LAUNCHPAD-025] invalid caller") - errInvalidOwner = errors.New("[GNOSWAP-LAUNCHPAD-026] invalid owner") - errInvalidAvgBlockTime = errors.New("[GNOSWAP-LAUNCHPAD-027] invalid average block time") - errInvalidTime = errors.New("[GNOSWAP-LAUNCHPAD-028] invalid time") - errTierHasParticipants = errors.New("[GNOSWAP-LAUNCHPAD-029] tier has participants") - errNotYetEndedProject = errors.New("[GNOSWAP-LAUNCHPAD-030] project lock period is not over yet") - errTransferFailed = errors.New("[GNOSWAP-LAUNCHPAD-031] transfer failed") -) - -// makeErrorWithDetails creates an error with additional context. -func makeErrorWithDetails(err error, details string) error { - return ufmt.Errorf("%s || %s", err.Error(), details) -} diff --git a/contract/r/gnoswap/v1/launchpad/gnomod.toml b/contract/r/gnoswap/v1/launchpad/gnomod.toml deleted file mode 100644 index a5a449f..0000000 --- a/contract/r/gnoswap/v1/launchpad/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/launchpad" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/launchpad/json_builder.gno b/contract/r/gnoswap/v1/launchpad/json_builder.gno deleted file mode 100644 index 415d2ed..0000000 --- a/contract/r/gnoswap/v1/launchpad/json_builder.gno +++ /dev/null @@ -1,102 +0,0 @@ -package launchpad - -import ( - "std" - "strings" - "time" - - "gno.land/p/onbloc/json" - "gno.land/p/nt/ufmt" -) - -// projectStatsBuilder adds ProjectStats fields to JSON -func projectStatsBuilder(b *json.NodeBuilder, project *Project) *json.NodeBuilder { - return b. - WriteString("totalDeposit", ufmt.Sprintf("%d", project.TotalDepositAmount())). - WriteString("actualDeposit", ufmt.Sprintf("%d", project.CurrentDepositAmount())). - WriteString("totalParticipant", ufmt.Sprintf("%d", project.TotalDepositCount())). - WriteString("actualParticipant", ufmt.Sprintf("%d", project.CurrentDepositCount())). - WriteString("totalCollected", ufmt.Sprintf("%d", project.TotalCollectedAmount())) -} - -// refundInfoBuilder adds RefundInfo fields to JSON -func refundInfoBuilder(b *json.NodeBuilder, project *Project) *json.NodeBuilder { - return b. - WriteString("refundedAmount", ufmt.Sprintf("%d", project.RemainingAmount())) -} - -// TierBuilder adds Tier fields to JSON -func tierBuilder(b *json.NodeBuilder, prefix string, tier *ProjectTier) *json.NodeBuilder { - // Add tiers info - b.WriteString(prefix+"Id", tier.id) - b.WriteString(prefix+"TierAmount", ufmt.Sprintf("%d", tier.TotalDistributeAmount())) - b.WriteString(prefix+"TierAmountPerSecondX128", tier.DistributeAmountPerSecondX128().ToString()) - b.WriteString(prefix+"Started", ufmt.Sprintf("%d", tier.StartTime())) - b.WriteString(prefix+"Ended", ufmt.Sprintf("%d", tier.EndTime())) - b.WriteString(prefix+"TotalDepositAmount", ufmt.Sprintf("%d", tier.TotalDepositAmount())) - b.WriteString(prefix+"ActualDepositAmount", ufmt.Sprintf("%d", tier.CurrentDepositAmount())) - b.WriteString(prefix+"TotalParticipant", ufmt.Sprintf("%d", tier.TotalDepositCount())) - b.WriteString(prefix+"ActualParticipant", ufmt.Sprintf("%d", tier.CurrentDepositCount())) - b.WriteString(prefix+"UserCollectedAmount", ufmt.Sprintf("%d", tier.TotalCollectedAmount())) - return b -} - -// ProjectBuilder adds Project fields to JSON -func projectBuilder(b *json.NodeBuilder, project *Project) *json.NodeBuilder { - b.WriteString("name", project.name) - b.WriteString("tokenPath", project.tokenPath) - b.WriteString("depositAmount", ufmt.Sprintf("%d", project.depositAmount)) - b.WriteString("recipient", project.recipient.String()) - - tokenPaths := []string{} - amounts := []string{} - - for _, condition := range project.getConditions() { - tokenPaths = append(tokenPaths, condition.TokenPath()) - amounts = append(amounts, ufmt.Sprintf("%d", condition.MinimumAmount())) - } - b.WriteString("conditionsToken", strings.Join(tokenPaths, ",")) - b.WriteString("conditionsAmount", strings.Join(amounts, ",")) - - // Add time info - b.WriteString("createdTime", ufmt.Sprintf("%d", project.CreatedAt())) - b.WriteString("startedTime", ufmt.Sprintf("%d", project.standardTier().StartTime())) - b.WriteString("endedTime", ufmt.Sprintf("%d", project.standardTier().EndTime())) - - // Add refund info - refundInfoBuilder(b, project) - - return b -} - -// DepositBuilder adds Deposit fields to JSON -func depositBuilder(b *json.NodeBuilder, deposit *Deposit) *json.NodeBuilder { - return b. - WriteString("depositId", deposit.id). - WriteString("projectId", deposit.ProjectID()). - WriteString("tier", ufmt.Sprintf("%d", deposit.Tier())). - WriteString("depositor", deposit.Depositor().String()). - WriteString("amount", ufmt.Sprintf("%d", deposit.DepositAmount())). - WriteString("depositHeight", ufmt.Sprintf("%d", deposit.CreatedHeight())). - WriteString("depositTime", ufmt.Sprintf("%d", deposit.DepositTime())) -} - -// MetaBuilder adds metadata fields to JSON -func metaBuilder() *json.NodeBuilder { - height := std.ChainHeight() - now := time.Now().Unix() - - return json.Builder(). - WriteString("height", ufmt.Sprintf("%d", height)). - WriteString("now", ufmt.Sprintf("%d", now)) -} - -// Marshals a JSON node to a string, panics if marshalling fails -func marshal(data *json.Node) string { - b, err := json.Marshal(data) - if err != nil { - panic(err.Error()) - } - - return string(b) -} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_deposit.gno b/contract/r/gnoswap/v1/launchpad/launchpad_deposit.gno deleted file mode 100644 index e84b377..0000000 --- a/contract/r/gnoswap/v1/launchpad/launchpad_deposit.gno +++ /dev/null @@ -1,188 +0,0 @@ -package launchpad - -import ( - "std" - "time" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/gns" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/referral" - "gno.land/r/gnoswap/v1/common" - gov_staker "gno.land/r/gnoswap/v1/gov/staker" - "gno.land/r/gnoswap/v1/gov/xgns" -) - -// DepositGns deposits GNS tokens to a launchpad project tier. -// -// Parameters: -// - targetProjectTierID: format "{projectId}:{tierType}" -// - depositAmount: amount of GNS to deposit -// - referrer: referral address (optional) -// -// Returns deposit ID. -func DepositGns(cur realm, targetProjectTierID string, depositAmount int64, referrer string) string { - halt.AssertIsNotHaltedLaunchpad() - - previousRealm := std.PreviousRealm() - access.AssertIsUser(previousRealm) - - assertIsValidAmount(depositAmount) - - projectID, tierDuration := parseProjectTierID(targetProjectTierID) - caller := previousRealm.Address() - - deposit, rewardState, isFirstDeposit, distributeAmountPerSecondX128, err := depositGns( - projectID, - tierDuration, - depositAmount, - caller, - ) - if err != nil { - panic(err.Error()) - } - - actualReferrer, success := registerReferral(referrer, caller) - if !success { - actualReferrer = referral.GetReferral(std.PreviousRealm().Address().String()) - } - - if isFirstDeposit { - std.Emit( - "FirstDepositForProjectTier", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "targetProjectTierId", targetProjectTierID, - "amount", formatInt(depositAmount), - "depositId", deposit.ID(), - "claimableTime", formatInt(rewardState.ClaimableTime()), - "tierAmountPerSecondX128", distributeAmountPerSecondX128, - ) - } - - std.Emit( - "DepositGns", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "targetProjectTierId", targetProjectTierID, - "amount", formatInt(depositAmount), - "depositId", deposit.ID(), - "claimableTime", formatInt(rewardState.ClaimableTime()), - "referrer", actualReferrer, - ) - - launchpadAddress := std.CurrentRealm().Address() - - // stake governance token to the project - err = stakeGovernance(projectID, depositAmount, launchpadAddress, caller) - if err != nil { - panic(err.Error()) - } - - return deposit.ID() -} - -// depositGns deposits GNS to a project tier. -func depositGns( - projectID string, - tierDuration int64, - depositAmount int64, - callerAddress std.Address, -) (*Deposit, *RewardState, bool, string, error) { - project, err := getProject(projectID) - if err != nil { - return nil, nil, false, "", err - } - - balanceOfFn := func(tokenPath string, caller std.Address) int64 { - if tokenPath == GOV_XGNS_PATH { - return xgns.BalanceOf(caller) - } - - return common.BalanceOf(tokenPath, caller) - } - - err = project.CheckConditions(callerAddress, balanceOfFn) - if err != nil { - return nil, nil, false, "", err - } - - projectTier, err := project.getTier(tierDuration) - if err != nil { - return nil, nil, false, "", err - } - - currentTime := time.Now().Unix() - currentHeight := std.ChainHeight() - - if !projectTier.isActivated(currentTime) { - return nil, nil, false, "", makeErrorWithDetails(errInactiveProject, projectID) - } - - depositID := nextDepositID() - deposit := NewDeposit( - depositID, - projectID, - tierDuration, - callerAddress, - depositAmount, - currentHeight, - currentTime, - projectTier.endTime, - ) - deposits.Set(depositID, deposit) - - projectTier.deposit(deposit) - - rewardManager, err := getProjectTierRewardManager(projectTier.ID()) - if err != nil { - return nil, nil, false, "", err - } - - isFirstDeposit := !rewardManager.IsInitialized() - - rewardState := rewardManager.addRewardStateByDeposit(deposit) - - err = rewardManager.updateRewardPerDepositX128(projectTier.CurrentDepositAmount(), currentHeight, currentTime) - if err != nil { - return nil, nil, false, "", err - } - - return deposit, - rewardState, - isFirstDeposit, - rewardManager.DistributeAmountPerSecondX128().ToString(), - nil -} - -// registerReferral registers a referral for a caller. -func registerReferral(referrer string, callerAddress std.Address) (string, bool) { - success := referral.TryRegister(cross, callerAddress, referrer) - actualReferrer := referrer - if !success { - actualReferrer = referral.GetReferral(callerAddress.String()) - } - - return actualReferrer, success -} - -// stakeGovernance stakes governance token to the project. -func stakeGovernance(projectID string, depositAmount int64, launchpadAddress std.Address, callerAddress std.Address) error { - project, err := getProject(projectID) - if err != nil { - return err - } - - gov_staker.SetAmountByProjectWallet(cross, project.Recipient(), depositAmount, true) - - gns.TransferFrom( - cross, - callerAddress, - launchpadAddress, - depositAmount, - ) - - xgns.MintByLaunchPad(cross, launchpadAddress, depositAmount) - - return nil -} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_project.gno b/contract/r/gnoswap/v1/launchpad/launchpad_project.gno deleted file mode 100644 index 9631b8a..0000000 --- a/contract/r/gnoswap/v1/launchpad/launchpad_project.gno +++ /dev/null @@ -1,470 +0,0 @@ -package launchpad - -import ( - "errors" - "std" - "strconv" - "strings" - "time" - - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" -) - -// CreateProject creates a new launchpad project with tiered allocations. -// -// Parameters: -// - name: project name -// - tokenPath: reward token contract path -// - recipient: project recipient address -// - depositAmount: amount of tokens to deposit -// - conditionTokens: comma-separated token paths for conditions -// - conditionAmounts: comma-separated minimum amounts for conditions -// - tier30Ratio: allocation ratio for 30-day tier -// - tier90Ratio: allocation ratio for 90-day tier -// - tier180Ratio: allocation ratio for 180-day tier -// - startTime: unix timestamp for project start -// -// Returns project ID. -// Only callable by admin or governance. -func CreateProject( - cur realm, - name string, - tokenPath string, - recipient std.Address, - depositAmount int64, - conditionTokens string, - conditionAmounts string, - tier30Ratio int64, - tier90Ratio int64, - tier180Ratio int64, - startTime int64, -) string { - halt.AssertIsNotHaltedLaunchpad() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - launchpadAddr := std.CurrentRealm().Address() - currentHeight := std.ChainHeight() - currentTime := time.Now().Unix() - - params := &createProjectParams{ - name: name, - tokenPath: tokenPath, - recipient: recipient, - depositAmount: depositAmount, - conditionTokens: conditionTokens, - conditionAmounts: conditionAmounts, - tier30Ratio: tier30Ratio, - tier90Ratio: tier90Ratio, - tier180Ratio: tier180Ratio, - startTime: startTime, - currentTime: currentTime, - currentHeight: currentHeight, - minimumStartDelayTime: projectMinimumStartDelayTime, - } - - project, err := createProject(params) - if err != nil { - panic(err) - } - - tokenBalance := common.BalanceOf(tokenPath, caller) - if tokenBalance < depositAmount { - panic( - makeErrorWithDetails( - errInsufficientBalance, - ufmt.Sprintf("caller(%s) balance(%d) < depositAmount(%d)", launchpadAddr.String(), tokenBalance, depositAmount), - ), - ) - } - - err = common.TransferFrom( - cross, - tokenPath, - caller, - launchpadAddr, - depositAmount, - ) - if err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "CreateProject", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "name", name, - "tokenPath", tokenPath, - "recipient", recipient.String(), - "depositAmount", formatInt(depositAmount), - "conditionsToken", params.conditionTokens, - "conditionsAmount", params.conditionAmounts, - "tier30Ratio", formatInt(params.tier30Ratio), - "tier90Ratio", formatInt(params.tier90Ratio), - "tier180Ratio", formatInt(params.tier180Ratio), - "startTime", formatInt(params.startTime), - "projectId", project.ID(), - "tier30Amount", formatInt(project.tiers[projectTier30].TotalDistributeAmount()), - "tier30EndTime", formatInt(project.tiers[projectTier30].EndTime()), - "tier90Amount", formatInt(project.tiers[projectTier90].TotalDistributeAmount()), - "tier90EndTime", formatInt(project.tiers[projectTier90].EndTime()), - "tier180Amount", formatInt(project.tiers[projectTier180].TotalDistributeAmount()), - "tier180EndTime", formatInt(project.tiers[projectTier180].EndTime()), - ) - - return project.ID() -} - -// createProject creates a new project with the given parameters. -// This function validates the input parameters, creates the project structure, -// and sets up the project tiers and reward managers. -// Returns the created project and any error. -func createProject(params *createProjectParams) (*Project, error) { - if err := params.validate(); err != nil { - return nil, err - } - - // create project - project := NewProject( - params.name, - params.tokenPath, - params.depositAmount, - params.recipient, - params.currentHeight, - params.currentTime, - ) - - // check duplicate project - if projects.Has(project.ID()) { - return nil, makeErrorWithDetails( - errDuplicateProject, - ufmt.Sprintf("project(%s) already exists", project.ID()), - ) - } - - projectConditions, err := NewProjectConditionsWithError(params.conditionTokens, params.conditionAmounts) - if err != nil { - return nil, err - } - - for _, condition := range projectConditions { - project.addProjectCondition(condition.tokenPath, condition) - } - - projectTierRatios := map[int64]int64{ - projectTier30: params.tier30Ratio, - projectTier90: params.tier90Ratio, - projectTier180: params.tier180Ratio, - } - - accumulatedTierDistributeAmount := int64(0) - - for _, duration := range projectTierDurations { - rewardCollectableDuration := projectTierRewardCollectableDuration[duration] - tierDurationTime := projectTierDurationTimes[duration] - tierDistributeAmount := params.depositAmount * projectTierRatios[duration] / 100 - accumulatedTierDistributeAmount += tierDistributeAmount - - // if the last tier, distribute the remaining amount - if duration == projectTier180 { - remainTierDistributeAmount := params.depositAmount - accumulatedTierDistributeAmount - tierDistributeAmount += remainTierDistributeAmount - } - - projectTier := NewProjectTier( - project.ID(), - rewardCollectableDuration, - tierDistributeAmount, - params.startTime, - params.startTime+tierDurationTime, - ) - project.addProjectTier(duration, projectTier) - - projectTierRewardManagers.Set(projectTier.ID(), NewRewardManager( - projectTier.TotalDistributeAmount(), - projectTier.StartTime(), - projectTier.EndTime(), - params.startTime, - params.startTime+tierDurationTime, - )) - } - - projects.Set(project.ID(), project) - - return project, nil -} - -// TransferLeftFromProjectByAdmin transfers the remaining rewards of a project to a specified recipient. -// Only admin or governance can call this function. Returns the amount of rewards transferred. -func TransferLeftFromProjectByAdmin(cur realm, projectID string, recipient std.Address) int64 { - halt.AssertIsNotHaltedLaunchpad() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - currentHeight := std.ChainHeight() - currentTime := time.Now().Unix() - - projectLeftReward, err := transferLeftFromProject(projectID, recipient, currentTime) - if err != nil { - panic(err) - } - - project, err := getProject(projectID) - if err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "TransferLeftFromProjectByAdmin", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "projectId", projectID, - "recipient", recipient.String(), - "tokenPath", project.tokenPath, - "leftReward", formatInt(projectLeftReward), - "tier30Full", formatInt(project.tiers[projectTier30].TotalDepositAmount()), - "tier30Left", formatInt(project.tiers[projectTier30].calculateLeftReward()), - "tier90Full", formatInt(project.tiers[projectTier90].TotalDepositAmount()), - "tier90Left", formatInt(project.tiers[projectTier90].calculateLeftReward()), - "tier180Full", formatInt(project.tiers[projectTier180].TotalDepositAmount()), - "tier180Left", formatInt(project.tiers[projectTier180].calculateLeftReward()), - "currentHeight", formatInt(currentHeight), - "currentTime", formatInt(currentTime), - ) - - return projectLeftReward -} - -// transferLeftFromProject transfers the remaining rewards of a project to a specified recipient. -// This function is called by an admin to transfer any unclaimed rewards from a project to a recipient address. -// It validates the project ID, checks the recipient conditions, calculates the remaining rewards, and performs the transfer. -// Returns the amount of rewards transferred to the recipient and any error. -func transferLeftFromProject(projectID string, recipient std.Address, currentTime int64) (int64, error) { - project, err := getProject(projectID) - if err != nil { - return 0, err - } - - if err := validateRefundProject(project, recipient, currentTime); err != nil { - return 0, err - } - - emission.MintAndDistributeGns(cross) - - accumTotalDistributeAmount := int64(0) - accumLeftReward := int64(0) - accumCollectedReward := int64(0) - - tierMap := project.getTiers() - for _, tier := range tierMap { - if !tier.isEnded(currentTime) { - return 0, errors.New(ufmt.Sprintf("tier(%d) is not ended", tier.ID())) - } - - if tier.CurrentDepositCount() > 0 { - return 0, errors.New(ufmt.Sprintf("tier(%d) has (%d) participants", tier.ID(), tier.CurrentDepositCount())) - } - - leftReward := tier.calculateLeftReward() - accumLeftReward += leftReward - accumCollectedReward += tier.TotalCollectedAmount() - accumTotalDistributeAmount += tier.TotalDistributeAmount() - } - - if accumTotalDistributeAmount != accumCollectedReward+accumLeftReward { - return 0, errors.New(ufmt.Sprintf("accumTotalDistributeAmount(%d) != accumCollectedReward(%d)+accumLeftReward(%d)", accumTotalDistributeAmount, accumCollectedReward, accumLeftReward)) - } - - projectLeftReward := project.RemainingAmount() - - if projectLeftReward > 0 { - if err := common.Transfer(cross, project.tokenPath, recipient, int64(projectLeftReward)); err != nil { - return 0, makeErrorWithDetails(errTransferFailed, ufmt.Sprintf("token(%s), amount(%d)", project.tokenPath, projectLeftReward)) - } - } - - return projectLeftReward, nil -} - -// validateTransferLeft validates the transfer of remaining tokens -func validateRefundProject(project *Project, recipient std.Address, currentTime int64) error { - if !recipient.IsValid() { - return errors.New(ufmt.Sprintf("invalid recipient address(%s)", recipient.String())) - } - - return project.validateRefundRemainingAmount(currentTime) -} - -type createProjectParams struct { - name string - tokenPath string - recipient std.Address - depositAmount int64 - conditionTokens string - conditionAmounts string - tier30Ratio int64 - tier90Ratio int64 - tier180Ratio int64 - startTime int64 - currentTime int64 - currentHeight int64 - minimumStartDelayTime int64 -} - -func (p *createProjectParams) validate() error { - if err := p.validateName(); err != nil { - return err - } - - if err := p.validateTokenPath(); err != nil { - return err - } - - if err := p.validateRecipient(); err != nil { - return err - } - - if err := p.validateDepositAmount(); err != nil { - return err - } - - if err := p.validateRatio(); err != nil { - return err - } - - if err := p.validateStartTime(p.currentTime, p.minimumStartDelayTime); err != nil { - return err - } - - if err := p.validateConditions(); err != nil { - return err - } - - return nil -} - -// validateName checks if the project name is valid. -func (p *createProjectParams) validateName() error { - if p.name == "" { - return makeErrorWithDetails(errInvalidInput, "project name cannot be empty") - } - - if len(p.name) > 100 { - return makeErrorWithDetails(errInvalidInput, "project name is too long") - } - - return nil -} - -// validateTokenPath validates the token path is not empty and is registered. -func (p *createProjectParams) validateTokenPath() error { - if p.tokenPath == "" { - return makeErrorWithDetails(errInvalidInput, "tokenPath cannot be empty") - } - - if err := common.IsRegistered(p.tokenPath); err != nil && !isGovernanceToken(p.tokenPath) { - return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) not registered", p.tokenPath)) - } - - return nil -} - -// validateRecipient checks if the recipient address is valid. -func (p *createProjectParams) validateRecipient() error { - if !p.recipient.IsValid() { - return makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("recipient address(%s)", p.recipient.String())) - } - - return nil -} - -// validateDepositAmount ensures that the deposit amount is greater than zero. -func (p *createProjectParams) validateDepositAmount() error { - if p.depositAmount == 0 { - return makeErrorWithDetails(errInvalidInput, "deposit amount cannot be 0") - } - - if p.depositAmount < 0 { - return makeErrorWithDetails(errInvalidInput, "deposit amount cannot be negative") - } - - return nil -} - -// validateRatio checks if the sum of the tier ratios equals 100. -func (p *createProjectParams) validateRatio() error { - sum := p.tier30Ratio + p.tier90Ratio + p.tier180Ratio - if sum != 100 { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("invalid ratio, sum of all tiers(30:%d, 90:%d, 180:%d) should be 100", p.tier30Ratio, p.tier90Ratio, p.tier180Ratio), - ) - } - - return nil -} - -// validateStartTime checks if the start time is available with minimum delay requirement. -func (p *createProjectParams) validateStartTime(now int64, minimumStartDelayTime int64) error { - availableStartTime := now + minimumStartDelayTime - - if p.startTime < availableStartTime { - return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("start time(%d) must be greater than now(%d)", p.startTime, availableStartTime)) - } - - return nil -} - -func (p *createProjectParams) validateConditions() error { - if p.conditionTokens == "" && p.conditionAmounts == "" { - return nil - } - - tokenPaths := strings.Split(p.conditionTokens, stringSplitterPad) - minimumAmounts := strings.Split(p.conditionAmounts, stringSplitterPad) - - if len(tokenPaths) != len(minimumAmounts) { - return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("conditionTokens and conditionAmounts are not matched")) - } - - tokenPathMap := make(map[string]bool) - - for _, tokenPath := range tokenPaths { - err := common.IsRegistered(tokenPath) - if err != nil && !isGovernanceToken(tokenPath) { - return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) not registered", tokenPath)) - } - - if tokenPathMap[tokenPath] { - return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) is duplicated", tokenPath)) - } - - tokenPathMap[tokenPath] = true - } - - for _, amountStr := range minimumAmounts { - minimumAmount, err := strconv.ParseInt(amountStr, 10, 64) - if err != nil { - return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("invalid condition amount(%s)", amountStr)) - } - - if minimumAmount <= 0 { - return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("condition amount(%s) is not available", amountStr)) - } - } - - return nil -} - -func isGovernanceToken(tokenPath string) bool { - return tokenPath == GOV_XGNS_PATH -} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_protocol_fee.gno b/contract/r/gnoswap/v1/launchpad/launchpad_protocol_fee.gno deleted file mode 100644 index c394173..0000000 --- a/contract/r/gnoswap/v1/launchpad/launchpad_protocol_fee.gno +++ /dev/null @@ -1,24 +0,0 @@ -package launchpad - -import ( - "std" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - gov_staker "gno.land/r/gnoswap/v1/gov/staker" -) - -// CollectProtocolFee collects protocol fee from gov/staker for project recipient wallets. -// Only users can call this function. -func CollectProtocolFee(cur realm) { - halt.AssertIsNotHaltedLaunchpad() - halt.AssertIsNotHaltedWithdraw() - - previousRealm := std.PreviousRealm() - access.AssertIsUser(previousRealm) - - caller := previousRealm.Address() - assertHasProject(caller) - - gov_staker.CollectRewardFromLaunchPad(cross, caller) -} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_reward.gno b/contract/r/gnoswap/v1/launchpad/launchpad_reward.gno deleted file mode 100644 index b342e1a..0000000 --- a/contract/r/gnoswap/v1/launchpad/launchpad_reward.gno +++ /dev/null @@ -1,77 +0,0 @@ -package launchpad - -import ( - "std" - "time" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" -) - -// CollectRewardByDepositId collects reward from a specific deposit. -// -// Parameters: -// - depositID: ID of the deposit to collect from -// -// Returns amount of reward collected. -// Only callable by deposit owner. -func CollectRewardByDepositId(cur realm, depositID string) int64 { - halt.AssertIsNotHaltedLaunchpad() - halt.AssertIsNotHaltedWithdraw() - - previousRealm := std.PreviousRealm() - access.AssertIsUser(previousRealm) - - caller := previousRealm.Address() - assertIsDepositOwner(depositID, caller) - - deposit := mustGetDeposit(depositID) - currentHeight := std.ChainHeight() - currentTime := time.Now().Unix() - rewardAmount, err := collectDepositReward(deposit, currentHeight, currentTime) - if err != nil { - panic(err) - } - - std.Emit( - "CollectRewardByDepositId", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "depositId", depositID, - "amount", formatInt(rewardAmount), - ) - - return rewardAmount -} - -// collectDepositReward calculates and collects the reward for a deposit. -func collectDepositReward(deposit *Deposit, currentHeight, currentTime int64) (int64, error) { - if currentTime <= 0 { - return 0, makeErrorWithDetails(errInvalidTime, "currentTime must be positive") - } - - // Get project tier and reward manager - projectTier, err := getProjectTier(deposit.ProjectID(), deposit.Tier()) - if err != nil { - return 0, err - } - - rewardManager, err := getProjectTierRewardManager(projectTier.ID()) - if err != nil { - return 0, err - } - - // Update reward state before collection - err = rewardManager.updateRewardPerDepositX128(projectTier.CurrentDepositAmount(), currentHeight, currentTime) - if err != nil { - return 0, err - } - - // Collect reward - rewardAmount, err := rewardManager.collectReward(deposit.ID(), currentTime) - if err != nil { - return 0, err - } - - return rewardAmount, nil -} diff --git a/contract/r/gnoswap/v1/launchpad/launchpad_withdraw.gno b/contract/r/gnoswap/v1/launchpad/launchpad_withdraw.gno deleted file mode 100644 index 9c9bccc..0000000 --- a/contract/r/gnoswap/v1/launchpad/launchpad_withdraw.gno +++ /dev/null @@ -1,116 +0,0 @@ -package launchpad - -import ( - "std" - "time" - - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" - gov_staker "gno.land/r/gnoswap/v1/gov/staker" -) - -// CollectDepositGns collects rewards from a deposit. -// -// Parameters: -// - depositID: ID of the deposit to collect from -// -// Returns amount collected and any error. -func CollectDepositGns(cur realm, depositID string) (int64, error) { - halt.AssertIsNotHaltedLaunchpad() - halt.AssertIsNotHaltedWithdraw() - - previousRealm := std.PreviousRealm() - access.AssertIsUser(previousRealm) - - caller := previousRealm.Address() - assertIsDepositOwner(depositID, caller) - - emission.MintAndDistributeGns(cross) - - deposit := mustGetDeposit(depositID) - - currentTime := time.Now().Unix() - recipient, withdrawalAmount, err := withdrawDeposit(deposit, std.ChainHeight(), currentTime) - if err != nil { - panic(err.Error()) - } - - unStakeGovernance(recipient, withdrawalAmount) - - std.Emit( - "CollectDepositGns", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "depositId", depositID, - "amount", formatInt(withdrawalAmount), - ) - - return withdrawalAmount, nil -} - -// withdrawDeposit withdraws a deposit and updates the reward manager. -func withdrawDeposit(deposit *Deposit, currentHeight, currentTime int64) (std.Address, int64, error) { - // Input validation - if deposit == nil { - return "", 0, makeErrorWithDetails(errNotExistDeposit, "deposit is nil") - } - - if currentTime <= 0 { - return "", 0, makeErrorWithDetails(errInvalidTime, "currentTime must be positive") - } - - // State validation - if deposit.IsWithdrawn() { - return "", 0, makeErrorWithDetails(errAlreadyCollected, ufmt.Sprintf("(%s)", deposit.ID())) - } - - if !deposit.IsEnded(currentTime) { - return "", 0, makeErrorWithDetails(errNotYetEndedProject, ufmt.Sprintf("(%s)", deposit.ID())) - } - - // Get project and tier information - project, err := getProject(deposit.ProjectID()) - if err != nil { - return "", 0, err - } - - projectTier, err := project.getTier(deposit.Tier()) - if err != nil { - return "", 0, err - } - - // Get reward manager and update rewards before withdrawal - rewardManager, err := getProjectTierRewardManager(projectTier.ID()) - if err != nil { - return "", 0, err - } - - // Update rewards with current deposit amount - err = rewardManager.updateRewardPerDepositX128(projectTier.CurrentDepositAmount(), currentHeight, currentTime) - if err != nil { - return "", 0, err - } - - // Process withdrawal from project tier - projectTier.withdraw(deposit) - - // Update rewards with new deposit amount after withdrawal - err = rewardManager.updateRewardPerDepositX128(projectTier.CurrentDepositAmount(), currentHeight, currentTime) - if err != nil { - return "", 0, err - } - - // Finalize withdrawal - withdrawalAmount := deposit.withdraw(currentHeight, currentTime) - deposits.Set(deposit.ID(), deposit) - - return project.Recipient(), withdrawalAmount, nil -} - -// unStakeGovernance removes the staked amount from governance system -func unStakeGovernance(recipient std.Address, withdrawalAmount int64) { - gov_staker.SetAmountByProjectWallet(cross, recipient, withdrawalAmount, false) -} diff --git a/contract/r/gnoswap/v1/launchpad/project.gno b/contract/r/gnoswap/v1/launchpad/project.gno deleted file mode 100644 index 892b947..0000000 --- a/contract/r/gnoswap/v1/launchpad/project.gno +++ /dev/null @@ -1,256 +0,0 @@ -package launchpad - -import ( - "errors" - "std" - - "gno.land/p/nt/ufmt" -) - -// Project represents a launchpad project. -// -// This struct contains the necessary data and methods to manage and distribute -// rewards for a specific project. -// -// Fields: -// - id (string): The unique identifier for the project, formatted as "{tokenPath}:{createdHeight}". -// - name (string): The name of the project. -// - tokenPath (string): The path of the token associated with the project. -// - depositAmount (int64): The total amount of tokens deposited for the project. -// - recipient (std.Address): The address to receive the project's rewards. -// - conditions (map[string]*ProjectCondition): A map of token paths to their associated conditions. -// - tiers (map[int64]*ProjectTier): A map of tier durations to their associated tiers. -// - tiersRatios (map[int64]int64): A map of tier durations to their associated ratios. -// - createdBlockTimeInfo (BlockTimeInfo): The block time and height information for the creation of the project. -type Project struct { - id string // 'tokenPath:createdHeight' - name string - tokenPath string - depositAmount int64 - recipient std.Address // string - conditions map[string]*ProjectCondition // tokenPath -> Condition - tiers map[int64]*ProjectTier - tiersRatios map[int64]int64 - createdHeight int64 - createdAt int64 -} - -func (p *Project) ID() string { - return p.id -} - -func (p *Project) Name() string { - return p.name -} - -func (p *Project) TokenPath() string { - return p.tokenPath -} - -func (p *Project) DepositAmount() int64 { - return p.depositAmount -} - -func (p *Project) Recipient() std.Address { - return p.recipient -} - -func (p *Project) TiersRatios() map[int64]int64 { - return p.tiersRatios -} - -func (p *Project) CreatedAt() int64 { - return p.createdAt -} - -func (p *Project) CreatedHeight() int64 { - return p.createdHeight -} - -func (p *Project) StartTime() int64 { - return p.standardTier().StartTime() -} - -func (p *Project) EndTime() int64 { - return p.standardTier().EndTime() -} - -func (p *Project) IsActive(currentTime int64) bool { - return p.standardTier().isActivated(currentTime) -} - -func (p *Project) IsEnded(currentTime int64) bool { - return p.standardTier().isEnded(currentTime) -} - -func (p *Project) IsOwner(caller std.Address) bool { - return p.recipient == caller -} - -func (p *Project) RemainingAmount() int64 { - remainingAmount := int64(0) - - for _, tier := range p.getTiers() { - remainingAmount += tier.calculateLeftReward() - } - - return remainingAmount -} - -func (p *Project) CheckConditions(caller std.Address, balanceOfFunc func(tokenPath string, caller std.Address) int64) error { - conditions := p.getConditions() - if conditions == nil { - return makeErrorWithDetails(errInvalidData, "conditions is nil") - } - - for _, condition := range conditions { - // xGNS(or GNS) may have a zero condition - if !condition.IsAvailable() { - continue - } - - tokenPath := condition.TokenPath() - balance := balanceOfFunc(tokenPath, caller) - - if err := condition.CheckBalanceCondition(tokenPath, balance); err != nil { - return err - } - } - - return nil -} - -func (p *Project) TotalDepositCount() int64 { - totalRecipient := int64(0) - - for _, tier := range p.getTiers() { - totalRecipient += tier.totalDepositCount - } - - return totalRecipient -} - -func (p *Project) TotalDepositAmount() int64 { - totalDepositAmount := int64(0) - - for _, tier := range p.getTiers() { - totalDepositAmount += tier.TotalDepositAmount() - } - - return totalDepositAmount -} - -func (p *Project) CurrentDepositCount() int64 { - totalDepositCount := int64(0) - - for _, tier := range p.getTiers() { - totalDepositCount += tier.CurrentDepositCount() - } - - return totalDepositCount -} - -func (p *Project) CurrentDepositAmount() int64 { - totalDepositAmount := int64(0) - - for _, tier := range p.getTiers() { - totalDepositAmount += tier.CurrentDepositAmount() - } - - return totalDepositAmount -} - -func (p *Project) TotalCollectedAmount() int64 { - totalCollectedAmount := int64(0) - - for _, tier := range p.getTiers() { - totalCollectedAmount += tier.TotalCollectedAmount() - } - - return totalCollectedAmount -} - -func (p *Project) getConditions() map[string]*ProjectCondition { - return p.conditions -} - -func (p *Project) getTiers() map[int64]*ProjectTier { - return p.tiers -} - -func (p *Project) getTier(duration int64) (*ProjectTier, error) { - tier, exists := p.tiers[duration] - if !exists { - return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("tier(%s) not found", duration)) - } - - return tier, nil -} - -func (p *Project) standardTier() *ProjectTier { - projectTier, exists := p.tiers[projectTier180] - if !exists { - return nil - } - - return projectTier -} - -func (p *Project) validateRefundRemainingAmount(currentTime int64) error { - if !p.IsEnded(currentTime) { - return errors.New( - ufmt.Sprintf("project not ended yet(current:%d, endTime: %d)", currentTime, p.EndTime()), - ) - } - - if p.RemainingAmount() == 0 { - return errors.New( - ufmt.Sprintf("project has no remaining amount"), - ) - } - - return nil -} - -func (p *Project) addProjectTier(tierDuration int64, projectTier *ProjectTier) { - p.tiers[tierDuration] = projectTier -} - -func (p *Project) addProjectCondition(tokenPath string, condition *ProjectCondition) { - p.conditions[tokenPath] = condition -} - -func NewProject( - name string, - tokenPath string, - depositAmount int64, - recipient std.Address, - createdHeight int64, - createdAt int64, -) *Project { - return &Project{ - id: makeProjectID(tokenPath, createdHeight), - name: name, - tokenPath: tokenPath, - depositAmount: depositAmount, - recipient: recipient, - conditions: make(map[string]*ProjectCondition), - tiers: make(map[int64]*ProjectTier), - createdHeight: createdHeight, - createdAt: createdAt, - } -} - -// makeProjectID generates a unique project ID based on the given token path and the current block height. -// -// The generated ID combines the `tokenPath` and the current block height in the following format: -// "{tokenPath}:{height}" -// -// Parameters: -// - tokenPath (string): The path of the token associated with the project. -// -// Returns: -// - string: A unique project ID in the format "tokenPath:height". -func makeProjectID(tokenPath string, createdHeight int64) string { - return ufmt.Sprintf("%s:%d", tokenPath, createdHeight) -} diff --git a/contract/r/gnoswap/v1/launchpad/project_condition.gno b/contract/r/gnoswap/v1/launchpad/project_condition.gno deleted file mode 100644 index 5407813..0000000 --- a/contract/r/gnoswap/v1/launchpad/project_condition.gno +++ /dev/null @@ -1,98 +0,0 @@ -package launchpad - -import ( - "strconv" - "strings" - - "gno.land/p/nt/ufmt" -) - -// ProjectCondition represents a condition for a project. -// -// This struct contains the necessary data and methods to manage and distribute -// rewards for a specific project. -// -// Fields: -// - tokenPath (string): The path of the token associated with the project. -// - minimumAmount (int64): The minimum amount of the token required for the project. -type ProjectCondition struct { - tokenPath string - minimumAmount int64 -} - -func (p *ProjectCondition) TokenPath() string { - return p.tokenPath -} - -func (p *ProjectCondition) MinimumAmount() int64 { - return p.minimumAmount -} - -func (p *ProjectCondition) IsAvailable() bool { - return p.tokenPath != "" && p.minimumAmount > 0 -} - -func (p *ProjectCondition) CheckBalanceCondition(inputTokenPath string, inputAmount int64) error { - if p.tokenPath != inputTokenPath { - return makeErrorWithDetails( - errInvalidData, - ufmt.Sprintf("token path(%s) is not matched", inputTokenPath), - ) - } - - if inputAmount < p.minimumAmount { - return makeErrorWithDetails( - errInvalidData, - ufmt.Sprintf("input amount(%d) is less than minimum amount(%d)", inputAmount, p.minimumAmount), - ) - } - - return nil -} - -func NewProjectCondition(tokenPath string, minimumAmount int64) *ProjectCondition { - return &ProjectCondition{ - tokenPath: tokenPath, - minimumAmount: minimumAmount, - } -} - -func NewProjectConditionsWithError(conditionTokens string, conditionAmounts string) ([]*ProjectCondition, error) { - if conditionTokens == "" && conditionAmounts == "" { - return []*ProjectCondition{}, nil - } - - conditions := []*ProjectCondition{} - - tokenPaths := strings.Split(conditionTokens, stringSplitterPad) - minimumAmounts := strings.Split(conditionAmounts, stringSplitterPad) - - for index, tokenPath := range tokenPaths { - if index >= len(minimumAmounts) { - return nil, makeErrorWithDetails( - errInvalidData, - ufmt.Sprintf("condition amount(%s) is not matched with condition token(%s)", conditionAmounts, conditionTokens), - ) - } - - minimumAmount, err := strconv.ParseInt(minimumAmounts[index], 10, 64) - if err != nil { - return nil, makeErrorWithDetails( - errInvalidData, - ufmt.Sprintf("condition amount(%s) is not a valid integer", minimumAmounts[index]), - ) - } - - condition := NewProjectCondition(tokenPath, minimumAmount) - if !condition.IsAvailable() { - return nil, makeErrorWithDetails( - errInvalidData, - ufmt.Sprintf("condition(%s) is not available", condition.TokenPath()), - ) - } - - conditions = append(conditions, condition) - } - - return conditions, nil -} diff --git a/contract/r/gnoswap/v1/launchpad/project_tier.gno b/contract/r/gnoswap/v1/launchpad/project_tier.gno deleted file mode 100644 index 7ee0a72..0000000 --- a/contract/r/gnoswap/v1/launchpad/project_tier.gno +++ /dev/null @@ -1,174 +0,0 @@ -package launchpad - -import ( - "gno.land/p/nt/ufmt" - - u256 "gno.land/p/gnoswap/uint256" -) - -// ProjectTier represents a tier within a project. -// -// This struct contains the necessary data and methods to manage and distribute -// rewards for a specific tier of a project. -// -// Fields: -// - distributeAmountPerSecondX128 (u256.Uint): The amount of tokens to be distributed per second, represented as a Q128 fixed-point number. -// - startTime (int64): The time for the start of the tier. -// - endTime (int64): The time for the end of the tier. -// - id (string): The unique identifier for the tier, formatted as "{projectID}:duration". -// - totalDistributeAmount (int64): The total amount of tokens to be distributed for the tier. -// - totalDepositAmount (int64): The total amount of tokens deposited for the tier. -// - totalWithdrawAmount (int64): The total amount of tokens withdrawn from the tier. -// - totalDepositCount (int64): The total number of deposits made to the tier. -// - totalWithdrawCount (int64): The total number of withdrawals from the tier. -// - totalCollectedAmount (int64): The total amount of tokens collected as rewards for the tier. -type ProjectTier struct { - distributeAmountPerSecondX128 *u256.Uint // distribute amount per second, Q128 - startTime int64 - endTime int64 - - id string // '{projectId}:duration' // duartion == 30, 90, 180 - totalDistributeAmount int64 - totalDepositAmount int64 // accumulated deposit amount - totalWithdrawAmount int64 // accumulated withdraw amount - totalDepositCount int64 // accumulated deposit count - totalWithdrawCount int64 // accumulated withdraw count - totalCollectedAmount int64 // total collected amount by user (reward) -} - -func (t *ProjectTier) ID() string { - return t.id -} - -func (t *ProjectTier) TotalDistributeAmount() int64 { - return t.totalDistributeAmount -} - -func (t *ProjectTier) TotalCollectedAmount() int64 { - return t.totalCollectedAmount -} - -func (t *ProjectTier) TotalDepositAmount() int64 { - return t.totalDepositAmount -} - -func (t *ProjectTier) TotalWithdrawAmount() int64 { - return t.totalWithdrawAmount -} - -func (t *ProjectTier) TotalDepositCount() int64 { - return t.totalDepositCount -} - -func (t *ProjectTier) TotalWithdrawCount() int64 { - return t.totalWithdrawCount -} - -func (t *ProjectTier) CurrentDepositCount() int64 { - return t.totalDepositCount - t.totalWithdrawCount -} - -func (t *ProjectTier) CurrentDepositAmount() int64 { - return t.totalDepositAmount - t.totalWithdrawAmount -} - -func (t *ProjectTier) DistributeAmountPerSecondX128() *u256.Uint { - return t.distributeAmountPerSecondX128 -} - -func (t *ProjectTier) isActivated(currentTime int64) bool { - return t.startTime <= currentTime && currentTime < t.endTime -} - -func (t *ProjectTier) isEnded(currentTime int64) bool { - return t.endTime < currentTime -} - -func (t *ProjectTier) isFirstDeposit() bool { - return t.totalDepositCount == 0 -} - -func (t *ProjectTier) StartTime() int64 { - return t.startTime -} - -func (t *ProjectTier) EndTime() int64 { - return t.endTime -} - -func (t *ProjectTier) deposit(deposit *Deposit) { - t.totalDepositAmount += deposit.DepositAmount() - t.totalDepositCount++ -} - -func (t *ProjectTier) withdraw(deposit *Deposit) { - t.totalWithdrawAmount += deposit.DepositAmount() - t.totalWithdrawCount++ -} - -func (t *ProjectTier) setStartTime(time int64) { - t.startTime = time -} - -func (t *ProjectTier) setEndTime(time int64) { - t.endTime = time -} - -func (t *ProjectTier) calculateLeftReward() int64 { - return t.totalDistributeAmount - t.totalCollectedAmount -} - -func (t *ProjectTier) updateDistributeAmountPerSecond() { - // Use time duration instead of block count - distributeTimeDuration := t.endTime - t.startTime - if distributeTimeDuration <= 0 { - return - } - - totalDistributeAmountX128 := u256.Zero().Mul(u256.NewUintFromInt64(t.totalDistributeAmount), q128.Clone()) - // Divide by time duration in seconds - distributeAmountPerSecondX128 := u256.Zero().Div(totalDistributeAmountX128, u256.NewUintFromInt64(distributeTimeDuration)) - - t.distributeAmountPerSecondX128 = distributeAmountPerSecondX128 -} - -// NewProjectTier returns a pointer to a new ProjectTier with the given values. -func NewProjectTier( - projectID string, - tierDuration int64, - totalDistributeAmount int64, - startTime int64, - endTime int64, -) *ProjectTier { - tier := &ProjectTier{ - id: makeProjectTierID(projectID, tierDuration), - totalDistributeAmount: totalDistributeAmount, - distributeAmountPerSecondX128: u256.Zero(), - startTime: startTime, - endTime: endTime, - totalDepositAmount: 0, - totalWithdrawAmount: 0, - totalDepositCount: 0, - totalWithdrawCount: 0, - totalCollectedAmount: 0, - } - - tier.updateDistributeAmountPerSecond() - - return tier -} - -// makeProjectTierID generates a unique tier ID based on the given project ID and the tier duration. -// -// The generated ID combines the `projectId` and the `duration` in the following format: -// "{projectId}:{duration}" -// -// Parameters: -// - projectId (string): The unique ID of the project associated with the tier. -// - duration (uint64): The duration of the tier (e.g., 30, 90, 180 days). -// -// Returns: -// - string: A unique tier ID in the format "projectId:duration". -func makeProjectTierID(projectID string, duration int64) string { - return ufmt.Sprintf("%s:%d", projectID, duration) -} diff --git a/contract/r/gnoswap/v1/launchpad/reward_manager.gno b/contract/r/gnoswap/v1/launchpad/reward_manager.gno deleted file mode 100644 index 96bfd4b..0000000 --- a/contract/r/gnoswap/v1/launchpad/reward_manager.gno +++ /dev/null @@ -1,311 +0,0 @@ -package launchpad - -import ( - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - u256 "gno.land/p/gnoswap/uint256" -) - -// RewardManager manages the distribution of rewards for a project tier. -// -// This struct contains the necessary data and methods to calculate and track -// rewards for deposits associated with a project tier. -// -// Fields: -// - rewards (avl.Tree): A map of deposit IDs to their associated reward states. -// - distributeAmountPerSecondX128 (u256.Uint): The amount of tokens to be distributed per second, represented as a Q128 fixed-point number. -// - accumulatedRewardPerDepositX128 (u256.Uint): The accumulated reward per GNS stake, represented as a Q128 fixed-point number. -// - totalDistributeAmount (int64): The total amount of tokens to be distributed. -// - totalClaimedAmount (int64): The total amount of tokens claimed. -// - distributeStartTime (int64): The start time of the reward calculation. -// - distributeEndTime (int64): The end time of the reward calculation. -// - accumulatedDistributeAmount (int64): The accumulated amount of tokens distributed. -// - accumulatedHeight (int64): The last height when reward was calculated. -// - rewardClaimableDuration (int64): The duration of reward claimable. -type RewardManager struct { - rewards *avl.Tree // depositId -> RewardState - - distributeAmountPerSecondX128 *u256.Uint // distribute amount per second, Q128 - accumulatedRewardPerDepositX128 *u256.Uint // accumulated reward per GNS stake, Q128 - - totalDistributeAmount int64 // total distributed amount - totalClaimedAmount int64 // total claimed amount - distributeStartTime int64 // start time of reward calculation - distributeEndTime int64 // end time of reward calculation - accumulatedDistributeAmount int64 // accumulated distribute amount - accumulatedHeight int64 // last height when reward was calculated - accumulatedTime int64 // last time when reward was calculated - rewardClaimableDuration int64 // duration of reward claimable -} - -func (r *RewardManager) IsInitialized() bool { - return r.rewards.Size() > 0 -} - -func (r *RewardManager) DistributeAmountPerSecondX128() *u256.Uint { - return r.distributeAmountPerSecondX128 -} - -func (r *RewardManager) AccumulatedHeight() int64 { - return r.accumulatedHeight -} - -func (r *RewardManager) AccumulatedTime() int64 { - return r.accumulatedTime -} - -func (r *RewardManager) DistributeEndTime() int64 { - return r.distributeEndTime -} - -func (r *RewardManager) AccumulatedRewardPerDepositX128() *u256.Uint { - return r.accumulatedRewardPerDepositX128 -} - -func (r *RewardManager) AccumulatedReward() int64 { - res := u256.Zero().Rsh(r.accumulatedRewardPerDepositX128, 128) - return safeConvertToInt64(res) -} - -func (r *RewardManager) getDepositRewardState(depositId string) (*RewardState, error) { - rewardStateI, exists := r.rewards.Get(depositId) - if !exists { - return nil, makeErrorWithDetails(errNotExistDeposit, ufmt.Sprintf("(%s)", depositId)) - } - - rewardState, ok := rewardStateI.(*RewardState) - if !ok { - return nil, ufmt.Errorf("failed to cast rewardState to *RewardState: %T", rewardStateI) - } - - return rewardState, nil -} - -func (r *RewardManager) calculateRewardPerDepositX128(rewardPerSecondX128 *u256.Uint, totalStaked int64, currentTime int64) (*u256.Uint, error) { - accumulatedTime := r.accumulatedTime - if r.distributeStartTime > accumulatedTime { - accumulatedTime = r.distributeStartTime - } - - // not started yet - if currentTime < accumulatedTime { - return u256.Zero(), nil - } - - // past distribute end time - if accumulatedTime > r.distributeEndTime { - return u256.Zero(), nil - } - - // past distribute end time, set to distribute end time - if currentTime > r.distributeEndTime { - currentTime = r.distributeEndTime - } - - if rewardPerSecondX128.IsZero() { - return nil, makeErrorWithDetails( - errNoLeftReward, - ufmt.Sprintf("rewardPerSecond(%d)", rewardPerSecondX128), - ) - } - - // no left reward - if totalStaked == 0 { - return u256.Zero(), nil - } - - // timeDuration * rewardPerSecond / totalStaked - timeDuration := currentTime - accumulatedTime - rewardPerDepositX128 := u256.MulDiv( - u256.NewUintFromInt64(timeDuration), - rewardPerSecondX128, - u256.NewUintFromInt64(totalStaked), - ) - - return rewardPerDepositX128, nil -} - -func (r *RewardManager) addRewardStateByDeposit(deposit *Deposit) *RewardState { - claimableTime := deposit.DepositTime() + r.rewardClaimableDuration - - rewardState := NewRewardState( - r.AccumulatedRewardPerDepositX128(), - deposit.DepositAmount(), - deposit.DepositTime(), - r.distributeEndTime, - claimableTime, - ) - - // if the first deposit, set the distribute start height - if !r.IsInitialized() { - rewardState.setDistributeStartTime(r.distributeStartTime) - rewardState.setDistributeEndTime(r.distributeEndTime) - rewardState.setAccumulatedTime(r.distributeStartTime) - rewardState.setPriceDebtX128(u256.Zero()) - } - - return r.addRewardState(deposit, rewardState) -} - -func (r *RewardManager) addRewardState(deposit *Deposit, rewardState *RewardState) *RewardState { - r.rewards.Set(deposit.ID(), rewardState) - - return rewardState -} - -func (r *RewardManager) addRewardPerDepositX128(rewardPerDepositX128 *u256.Uint, currentHeight, currentTime int64) error { - if rewardPerDepositX128.IsZero() { - return nil - } - - if r.accumulatedTime > currentTime || r.distributeStartTime > currentTime { - return nil - } - - if currentTime > r.distributeEndTime { - currentTime = r.distributeEndTime - } - - accumulated := u256.Zero().Add(r.accumulatedRewardPerDepositX128, rewardPerDepositX128) - r.accumulatedRewardPerDepositX128 = accumulated - r.accumulatedHeight = currentHeight - r.accumulatedTime = currentTime - - return nil -} - -// updateRewardPerDepositX128 updates the reward per deposit state. -// This function calculates and updates the accumulated reward per deposit -// based on the current total deposit amount and height. -// -// Parameters: -// - totalDepositAmount (int64): Current total deposit amount -// - height (int64): Current blockchain height -// - time (int64): Current timestamp -// -// Returns: -// - error: If the update fails -func (r *RewardManager) updateRewardPerDepositX128(totalDepositAmount int64, currentHeight, currentTime int64) error { - if currentTime <= 0 { - return makeErrorWithDetails(errInvalidTime, "time must be positive") - } - - // Calculate and update rewards - rewardPerDepositX128, err := r.calculateRewardPerDepositX128( - r.distributeAmountPerSecondX128, - totalDepositAmount, - currentTime, - ) - if err != nil { - return err - } - - err = r.addRewardPerDepositX128(rewardPerDepositX128, currentHeight, currentTime) - if err != nil { - return err - } - - return nil -} - -func (r *RewardManager) updateDistributeAmountPerSecondX128(totalDistributeAmount int64, distributeStartTime int64, distributeEndTime int64) { - // Use time duration for per-second calculation - timeDuration := distributeEndTime - distributeStartTime - if timeDuration <= 0 { - return - } - - totalDistributeAmountX128 := u256.Zero().Lsh( - u256.NewUintFromInt64(totalDistributeAmount), - 128, - ) - - // Divide by time duration in seconds - amountPerSecondX128 := u256.Zero().Div( - totalDistributeAmountX128, - u256.NewUintFromInt64(timeDuration), - ) - - r.distributeAmountPerSecondX128 = amountPerSecondX128 - r.distributeStartTime = distributeStartTime - r.distributeEndTime = distributeEndTime -} - -// collectReward processes the reward collection for a specific deposit. -// This function ensures that the reward collection is valid and updates -// the claimed amount accordingly. -// -// Parameters: -// - depositId (string): The ID of the deposit -// - currentHeight (int64): Current blockchain height -// -// Returns: -// - int64: The amount of reward collected -// - error: If the collection fails -func (r *RewardManager) collectReward(depositId string, currentTime int64) (int64, error) { - if currentTime < r.accumulatedTime { - return 0, makeErrorWithDetails( - errInvalidRewardState, - ufmt.Sprintf("currentTime %d is less than AccumulatedTime %d", currentTime, r.accumulatedTime), - ) - } - - rewardState, err := r.getDepositRewardState(depositId) - if err != nil { - return 0, err - } - - if !rewardState.IsClaimable(currentTime) { - return 0, makeErrorWithDetails( - errInvalidRewardState, - ufmt.Sprintf("currentTime %d is less than claimableTime %d", currentTime, rewardState.ClaimableTime()), - ) - } - - if currentTime < rewardState.DistributeStartTime() { - return 0, makeErrorWithDetails( - errInvalidRewardState, - ufmt.Sprintf("currentTime %d is less than DistributeStartTime %d", currentTime, rewardState.DistributeStartTime()), - ) - } - - claimableReward := rewardState.calculateClaimableReward(r.accumulatedRewardPerDepositX128) - if claimableReward == 0 { - return 0, nil - } - - rewardState.setClaimedAmount(rewardState.ClaimedAmount() + claimableReward) - r.rewards.Set(depositId, rewardState) - r.totalClaimedAmount += claimableReward - - return claimableReward, nil -} - -// NewRewardManager returns a pointer to a new RewardManager with the given values. -func NewRewardManager( - totalDistributeAmount int64, - distributeStartTime int64, - distributeEndTime int64, - currentHeight int64, - currentTime int64, -) *RewardManager { - manager := &RewardManager{ - totalDistributeAmount: totalDistributeAmount, - distributeStartTime: distributeStartTime, - distributeEndTime: distributeEndTime, - totalClaimedAmount: 0, - accumulatedDistributeAmount: 0, - accumulatedHeight: 0, - accumulatedTime: 0, - accumulatedRewardPerDepositX128: u256.Zero(), - distributeAmountPerSecondX128: u256.Zero(), - rewardClaimableDuration: 0, - rewards: avl.NewTree(), - } - - manager.updateDistributeAmountPerSecondX128(totalDistributeAmount, distributeStartTime, distributeEndTime) - manager.updateRewardPerDepositX128(0, currentHeight, currentTime) - - return manager -} diff --git a/contract/r/gnoswap/v1/launchpad/reward_state.gno b/contract/r/gnoswap/v1/launchpad/reward_state.gno deleted file mode 100644 index 8f48605..0000000 --- a/contract/r/gnoswap/v1/launchpad/reward_state.gno +++ /dev/null @@ -1,158 +0,0 @@ -package launchpad - -import ( - u256 "gno.land/p/gnoswap/uint256" -) - -// RewardState represents the state of a reward for a deposit. -// It contains the necessary data to manage and distribute rewards for a specific deposit. -type RewardState struct { - priceDebtX128 *u256.Uint // price debt per GNS stake, Q128 - claimableTime int64 // time when reward can be claimed - - depositAmount int64 // amount of GNS staked - distributeStartTime int64 // time when launchpad started staking - distributeEndTime int64 // end time of reward calculation - accumulatedRewardAmount int64 // calculated, not collected - accumulatedHeight int64 // last height when reward was calculated - accumulatedTime int64 // last time when reward was calculated - claimedAmount int64 // amount of reward claimed so far -} - -func (r *RewardState) PriceDebtX128() *u256.Uint { - return r.priceDebtX128 -} - -func (r *RewardState) setPriceDebtX128(v *u256.Uint) { - r.priceDebtX128 = v -} - -func (r *RewardState) DepositAmount() int64 { - return r.depositAmount -} - -func (r *RewardState) setDepositAmount(v int64) { - r.depositAmount = v -} - -func (r *RewardState) AccumulatedRewardAmount() int64 { - return r.accumulatedRewardAmount -} - -func (r *RewardState) setAccumulatedRewardAmount(v int64) { - r.accumulatedRewardAmount = v -} - -func (r *RewardState) ClaimedAmount() int64 { - return r.claimedAmount -} - -func (r *RewardState) setClaimedAmount(v int64) { - r.claimedAmount = v -} - -func (r *RewardState) DistributeStartTime() int64 { - return r.distributeStartTime -} - -func (r *RewardState) setDistributeStartTime(v int64) { - r.distributeStartTime = v -} - -func (r *RewardState) DistributeEndTime() int64 { - return r.distributeEndTime -} - -func (r *RewardState) setDistributeEndTime(v int64) { - r.distributeEndTime = v -} - -func (r *RewardState) AccumulatedHeight() int64 { - return r.accumulatedHeight -} - -func (r *RewardState) setAccumulatedHeight(v int64) { - r.accumulatedHeight = v -} - -func (r *RewardState) AccumulatedTime() int64 { - return r.accumulatedTime -} - -func (r *RewardState) setAccumulatedTime(v int64) { - r.accumulatedTime = v -} - -func (r *RewardState) IsClaimable(currentTime int64) bool { - return currentTime >= r.claimableTime -} - -func (r *RewardState) ClaimableTime() int64 { - return r.claimableTime -} - -func (r *RewardState) setClaimableTime(v int64) { - r.claimableTime = v -} - -// calculateReward calculates the total reward amount based on -// the accumulated reward per deposit. -// Returns the total reward amount. -func (r *RewardState) calculateReward(accumRewardPerDepositX128 *u256.Uint) int64 { - if accumRewardPerDepositX128 == nil || r.PriceDebtX128() == nil { - return 0 - } - - actualRewardPerDepositX128 := u256.Zero().Sub(accumRewardPerDepositX128, r.PriceDebtX128()) - if actualRewardPerDepositX128.IsZero() { - return 0 - } - - reward := u256.Zero().Mul(actualRewardPerDepositX128, u256.NewUintFromInt64(r.DepositAmount())) - reward = u256.Zero().Rsh(reward, 128) - - return safeConvertToInt64(reward) -} - -// calculateClaimableReward calculates the amount of reward that can be claimed -// based on the current accumulated reward per deposit. -// Returns the amount of reward that can be claimed. -func (r *RewardState) calculateClaimableReward(accumRewardPerDepositX128 *u256.Uint) int64 { - if accumRewardPerDepositX128 == nil { - return 0 - } - - // Return 0 if accumulated reward is less than price debt - if accumRewardPerDepositX128.Lt(r.priceDebtX128) { - return 0 - } - - reward := r.calculateReward(accumRewardPerDepositX128) - claimedAmount := r.ClaimedAmount() - - if reward <= claimedAmount { - return 0 - } - - return reward - claimedAmount -} - -// NewRewardState returns a pointer to a new RewardState with the given values. -func NewRewardState( - accumulatedRewardPerDepositX128 *u256.Uint, - depositAmount, - distributeStartTime, - distributeEndTime int64, - claimableTime int64, -) *RewardState { - return &RewardState{ - priceDebtX128: accumulatedRewardPerDepositX128, - depositAmount: depositAmount, - distributeStartTime: distributeStartTime, - distributeEndTime: distributeEndTime, - claimableTime: claimableTime, - accumulatedRewardAmount: 0, - claimedAmount: 0, - accumulatedHeight: 0, - } -} diff --git a/contract/r/gnoswap/v1/launchpad/state.gno b/contract/r/gnoswap/v1/launchpad/state.gno deleted file mode 100644 index e1f7974..0000000 --- a/contract/r/gnoswap/v1/launchpad/state.gno +++ /dev/null @@ -1,104 +0,0 @@ -package launchpad - -import ( - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" -) - -var ( - // projectId -> Project - projects *avl.Tree - - // projectTierId -> RewardManager - projectTierRewardManagers *avl.Tree - - // Counter for generating unique deposit IDs - depositCounter *Counter - - // depositId -> Deposit, Tree storing all deposits by ID - deposits *avl.Tree -) - -func init() { - projects = avl.NewTree() - projectTierRewardManagers = avl.NewTree() - - depositCounter = NewCounter() - deposits = avl.NewTree() -} - -func getProject(projectID string) (*Project, error) { - project, ok := projects.Get(projectID) - if !ok { - return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("project(%s) not found", projectID)) - } - - p, ok := project.(*Project) - if !ok { - return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("project(%s) not found", projectID)) - } - - return p, nil -} - -func getProjectTier(projectID string, tierDuration int64) (*ProjectTier, error) { - project, err := getProject(projectID) - if err != nil { - return nil, err - } - - tier, ok := project.tiers[tierDuration] - if !ok { - return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("tier(%d) not found", tierDuration)) - } - - return tier, nil -} - -func getProjectTierRewardManager(projectTierID string) (*RewardManager, error) { - rewardManager, ok := projectTierRewardManagers.Get(projectTierID) - if !ok { - return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("reward manager(%s) not found", projectTierID)) - } - - manager, ok := rewardManager.(*RewardManager) - if !ok { - return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("reward manager(%s) not found", projectTierID)) - } - - return manager, nil -} - -func mustGetDeposit(depositID string) *Deposit { - deposit, err := getDeposit(depositID) - if err != nil { - panic(err) - } - - return deposit -} - -func getDeposit(depositID string) (*Deposit, error) { - depositI, ok := deposits.Get(depositID) - if !ok { - return nil, makeErrorWithDetails(errNotExistDeposit, ufmt.Sprintf("(%s)", depositID)) - } - - deposit, ok := depositI.(*Deposit) - if !ok { - return nil, makeErrorWithDetails(errDataNotFound, ufmt.Sprintf("deposit(%s) not found", depositID)) - } - - return deposit, nil -} - -// getCurrentDepositID returns the current deposit ID (last assigned). -func getCurrentDepositID() string { - return formatInt(depositCounter.Get()) -} - -// nextDepositID increments and returns the next unique deposit ID. -// This is used when creating new deposits. -func nextDepositID() string { - return formatInt(depositCounter.next()) -} diff --git a/contract/r/gnoswap/v1/launchpad/utils.gno b/contract/r/gnoswap/v1/launchpad/utils.gno deleted file mode 100644 index ec4d59d..0000000 --- a/contract/r/gnoswap/v1/launchpad/utils.gno +++ /dev/null @@ -1,76 +0,0 @@ -package launchpad - -import ( - "strconv" - "strings" - - "gno.land/p/nt/ufmt" - - u256 "gno.land/p/gnoswap/uint256" -) - -// formatInt returns the string representation of the int64 value. -func formatInt(value int64) string { - return strconv.FormatInt(value, 10) -} - -// parseProjectTierID parses a project tier ID into its project ID and duration. -// Returns the project ID {tokenPath}:{createdHeight} and the duration of the project tier (30, 90, 180). -func parseProjectTierID(projectTierID string) (string, int64) { - parts := strings.Split(projectTierID, ":") - if len(parts) != 3 { - panic(makeErrorWithDetails( - errInvalidData, - ufmt.Sprintf("(%s)", projectTierID), - )) - } - - projectID := ufmt.Sprintf("%s:%s", parts[0], parts[1]) - - tierDuration, err := strconv.ParseInt(parts[2], 10, 64) - if err != nil { - panic(makeErrorWithDetails( - errInvalidData, - ufmt.Sprintf("(%s)", projectTierID), - )) - } - - // Validate tier duration - if tierDuration != projectTier30 && tierDuration != projectTier90 && tierDuration != projectTier180 { - panic(makeErrorWithDetails( - errInvalidTier, - ufmt.Sprintf("pool type(%d) is not available", tierDuration), - )) - } - - return projectID, tierDuration -} - -// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. -// -// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds -// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. -// -// Parameters: -// - value (*u256.Uint): The unsigned 256-bit integer to be converted. -// -// Returns: -// - int64: The converted value if it falls within the int64 range. -// -// Panics: -// - If the `value` exceeds the range of int64, the function will panic with an error indicating -// the overflow and the original value. -func safeConvertToInt64(value *u256.Uint) int64 { - const INT64_MAX = 9223372036854775807 - const MAX_INT64 = "9223372036854775807" - - res, overflow := value.Uint64WithOverflow() - if overflow || res > uint64(INT64_MAX) { - panic(ufmt.Sprintf( - "amount(%s) overflows int64 range (max %s)", - value.ToString(), - MAX_INT64, - )) - } - return int64(res) -} diff --git a/contract/r/gnoswap/v1/pool/README.md b/contract/r/gnoswap/v1/pool/README.md deleted file mode 100644 index 70b4b55..0000000 --- a/contract/r/gnoswap/v1/pool/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# Pool - -Concentrated liquidity AMM pools with tick-based pricing. - -## Overview - -Pool contracts implement Uniswap V3-style concentrated liquidity, allowing LPs to provide liquidity within custom price ranges for maximum capital efficiency. - -## Configuration - -- **Pool Creation Fee**: 100 GNS (default) -- **Protocol Fee**: 0-10% of swap fees per token -- **Withdrawal Fee**: 1% on collected fees -- **Fee Tiers**: 0.01%, 0.05%, 0.3%, 1% -- **Tick Spacing**: Auto-set by fee tier -- **Max Liquidity Per Tick**: 2^128 - 1 - -## Core Concepts - -### Concentrated Liquidity -Liquidity providers concentrate capital within custom price ranges instead of 0-∞. This allows LPs to allocate capital where it's most likely to generate fees - near the current price for volatile pairs, or within tight ranges for stable pairs. Capital efficiency can improve by orders of magnitude depending on range selection and pair volatility. For more details, check out [GnoSwap Docs](https://docs.gnoswap.io/core-concepts/amm/concentrated-liquidity). - -### Tick System -- Price space divided into discrete ticks (0.01% apart) -- Each tick represents ~0.01% price change -- Positions defined by upper/lower tick boundaries -- Liquidity activated only when price in range - -## Key Functions - -### `CreatePool` -Deploys new trading pair. -- Requires 100 GNS creation fee -- Valid fee tier required -- Initial price via sqrtPriceX96 -- Unique token pair per fee tier - -### `Mint` -Adds liquidity to position (called by Position contract). -- Calculates token amounts from liquidity -- Updates tick bitmap -- Transfers tokens from owner -- Returns actual amounts used - -### `Burn` -Removes liquidity without collecting tokens. -- Two-step: burn then collect -- Calculates owed amounts -- Updates position state - -### `Collect` -Claims tokens from burned position + fees. -- Transfers principal and fees -- Updates tokensOwed -- Applies withdrawal fee - -### `Swap` -Core swap execution (called by Router). -- Iterates through ticks -- Updates price and liquidity -- Calculates fees -- Maintains TWAP oracle - -## Technical Details - -### Price Math - -**Q96 Format**: Prices stored as `sqrtPriceX96 = sqrt(price) * 2^96` - -``` -Price 1:1 → sqrtPriceX96 = 79228162514264337593543950336 -Price 1:4 → sqrtPriceX96 = 39614081257132168796771975168 -Price 100:1 → sqrtPriceX96 = 792281625142643375935439503360 -``` - -**Tick to Price**: `price = 1.0001^tick` -``` -tick 0 = price 1 -tick 6932 = price ~2 -tick -6932 = price ~0.5 -``` - -### Liquidity Math - -**Range Liquidity Formula**: -``` -L = amount / (sqrt(upper) - sqrt(lower)) // current < lower -L = amount * sqrt(current) / (upper - current) // lower < current < upper -L = amount / (sqrt(current) - sqrt(lower)) // current > upper -``` - -**Impermanent Loss**: -- Narrow range: Higher fees, higher IL -- Wide range: Lower fees, lower IL -- Stable pairs: ±0.1% ranges optimal -- Volatile pairs: ±10%+ ranges recommended - -### Fee Mechanics - -**Swap Fees**: -- Charged on input amount -- Accumulates as feeGrowthGlobal -- Distributed pro-rata to in-range liquidity - -**Fee Calculation**: -``` -fees = feeGrowthInside * liquidity -feeGrowthInside = feeGrowthGlobal - feeGrowthOutside -``` - -**Protocol fees**: -- Optional 0-10% of swap fees -- Configurable per pool -- Sent to protocol fee contract - -## Security - -### Reentrancy Protection -- Pools lock during swaps (`slot0.unlocked`) -- External calls after state updates -- Checks-effects-interactions pattern - -### Price Manipulation -- TWAP oracle resists manipulation -- Large swaps limited by liquidity -- Slippage protection required - -### Rounding -- Division rounds down (favors protocol) -- Minimum liquidity enforced -- Full precision for amounts \ No newline at end of file diff --git a/contract/r/gnoswap/v1/pool/api.gno b/contract/r/gnoswap/v1/pool/api.gno deleted file mode 100644 index 53c302f..0000000 --- a/contract/r/gnoswap/v1/pool/api.gno +++ /dev/null @@ -1,53 +0,0 @@ -package pool - -import ( - b64 "encoding/base64" - "strconv" - "strings" - - "gno.land/p/onbloc/json" - - "gno.land/p/nt/ufmt" -) - -func ApiGetPool(poolPath string) string { - if !pools.Has(poolPath) { - return "" - } - - node := json.ObjectNode("", map[string]*json.Node{ - "stat": newStatNode().JSON(), - "response": newRpcPool(poolPath).JSON(), - }) - - return marshal(node) -} - -func posKeyDivide(posKey string) (string, int32, int32) { - kDec, _ := b64.StdEncoding.DecodeString(posKey) - posKey = string(kDec) - - res := strings.Split(posKey, "__") - if len(res) != 3 { - panic(newErrorWithDetail( - errInvalidPositionKey, - ufmt.Sprintf("invalid posKey(%s)", posKey), - )) - } - - owner, _tickLower, _tickUpper := res[0], res[1], res[2] - - tickLower, _ := strconv.Atoi(_tickLower) - tickUpper, _ := strconv.Atoi(_tickUpper) - - return owner, int32(tickLower), int32(tickUpper) -} - -func marshal(node *json.Node) string { - b, err := json.Marshal(node) - if err != nil { - panic(err.Error()) - } - - return string(b) -} diff --git a/contract/r/gnoswap/v1/pool/assert.gno b/contract/r/gnoswap/v1/pool/assert.gno deleted file mode 100644 index e9090b5..0000000 --- a/contract/r/gnoswap/v1/pool/assert.gno +++ /dev/null @@ -1,65 +0,0 @@ -package pool - -import ( - "std" - - "gno.land/p/nt/ufmt" -) - -// assertIsNotEqualsTokens asserts that the token0Path and token1Path are not equal. -func assertIsNotEqualsTokens(token0Path, token1Path string) { - if token0Path == token1Path { - panic(newErrorWithDetail( - errDuplicateTokenInPool, - ufmt.Sprintf("expected token0Path(%s) != token1Path(%s)", token0Path, token1Path), - )) - } -} - -// assertIsSupportedFeeTier asserts that the fee is a supported fee tier. -func assertIsSupportedFeeTier(fee uint32) { - if !isValidFeeTier(fee) { - panic(newErrorWithDetail( - errUnsupportedFeeTier, - ufmt.Sprintf("expected fee(%d) to be one of %d, %d, %d, %d", fee, FeeTier100, FeeTier500, FeeTier3000, FeeTier10000), - )) - } -} - -// assertIsNotExistsPoolPath asserts that the pool path does not exist. -func assertIsNotExistsPoolPath(token0Path, token1Path string, fee uint32) { - poolPath := GetPoolPath(token0Path, token1Path, fee) - - if pools.Has(poolPath) { - panic(newErrorWithDetail( - errPoolAlreadyExists, - ufmt.Sprintf("expected poolPath(%s:%s:%d) not to exist", token0Path, token1Path, fee), - )) - } -} - -// assertIsValidTicks validates the tick range for a liquidity position. -func assertIsValidTicks(tickLower, tickUpper int32) { - if err := validateTicks(tickLower, tickUpper); err != nil { - panic(err) - } -} - -// assertAmountSpecifiedIsNotZero asserts that the amountSpecified is not zero. -func assertAmountSpecifiedIsNotZero(amountSpecified string) { - if amountSpecified == "0" { - panic(newErrorWithDetail( - errInvalidSwapAmount, - ufmt.Sprintf("amountSpecified == 0"), - )) - } -} - -func assertPayerIsPreviousRealmOrOriginCaller(payer std.Address) { - if payer != std.PreviousRealm().Address() && payer != std.OriginCaller() { - panic(newErrorWithDetail( - errInvalidPayer, - ufmt.Sprintf("expected payer(%s) to be the previous realm or the caller", payer), - )) - } -} diff --git a/contract/r/gnoswap/v1/pool/doc.gno b/contract/r/gnoswap/v1/pool/doc.gno deleted file mode 100644 index bca2f92..0000000 --- a/contract/r/gnoswap/v1/pool/doc.gno +++ /dev/null @@ -1,11 +0,0 @@ -// Package pool implements GnoSwap's concentrated liquidity pools based on Uniswap V3. -// It manages liquidity positions, executes swaps, and maintains pool state including -// price, liquidity, and fee calculations. -// -// The pool contract is the core of the GnoSwap AMM, supporting: -// - Concentrated liquidity within custom price ranges -// - Multiple fee tiers (0.01%, 0.05%, 0.3%, 1%) -// - Single-tick and cross-tick swaps -// - Protocol fee collection -// - Tick bitmap optimization for gas efficiency -package pool diff --git a/contract/r/gnoswap/v1/pool/errors.gno b/contract/r/gnoswap/v1/pool/errors.gno deleted file mode 100644 index 4f04149..0000000 --- a/contract/r/gnoswap/v1/pool/errors.gno +++ /dev/null @@ -1,50 +0,0 @@ -package pool - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -// Error definitions for pool operations -var ( - errNoPermission = errors.New("[GNOSWAP-POOL-001] caller has no permission") - errUnsupportedFeeTier = errors.New("[GNOSWAP-POOL-002] unsupported fee tier") - errPoolAlreadyExists = errors.New("[GNOSWAP-POOL-003] pool already created") - errInvalidTickMinMaxRange = errors.New("[GNOSWAP-POOL-004] tickLower and tickUpper are not within the valid range") - errOutOfRange = errors.New("[GNOSWAP-POOL-005] out of range for numeric value") - errInvalidInput = errors.New("[GNOSWAP-POOL-006] invalid input data") - errInvalidPositionKey = errors.New("[GNOSWAP-POOL-007] invalid position key") - errDataNotFound = errors.New("[GNOSWAP-POOL-008] requested data not found") - errLiquidityCalculation = errors.New("[GNOSWAP-POOL-009] invalid liquidity calculated") - errZeroLiquidity = errors.New("[GNOSWAP-POOL-010] zero liquidity") - errDuplicateTokenInPool = errors.New("[GNOSWAP-POOL-011] same token used in single pool") - errTokenSortOrder = errors.New("[GNOSWAP-POOL-012] tokens must be in lexicographical order") - errTickLowerInvalid = errors.New("[GNOSWAP-POOL-013] tickLower is invalid") - errTickUpperInvalid = errors.New("[GNOSWAP-POOL-014] tickUpper is invalid") - errInvalidSwapAmount = errors.New("[GNOSWAP-POOL-015] invalid swap amount") - errInvalidProtocolFeePct = errors.New("[GNOSWAP-POOL-016] invalid protocol fee percentage") - errInvalidWithdrawalFeePct = errors.New("[GNOSWAP-POOL-017] invalid withdrawal fee percentage") - errLockedPool = errors.New("[GNOSWAP-POOL-018] can't swap while pool is locked") - errPriceOutOfRange = errors.New("[GNOSWAP-POOL-019] swap price out of range") - errMustBeNegative = errors.New("[GNOSWAP-POOL-020] negative value expected") - errTransferFailed = errors.New("[GNOSWAP-POOL-021] token transfer failed") - errInvalidTickAndTickSpacing = errors.New("[GNOSWAP-POOL-022] invalid tick and tick spacing requested") - errInvalidAddress = errors.New("[GNOSWAP-POOL-023] invalid address") - errInvalidTickRange = errors.New("[GNOSWAP-POOL-024] tickLower is greater than or equal to tickUpper") - errUnderflow = errors.New("[GNOSWAP-POOL-025] underflow") - errOverFlow = errors.New("[GNOSWAP-POOL-026] overflow") - errBalanceUpdateFailed = errors.New("[GNOSWAP-POOL-027] balance update failed") - errInvalidPayer = errors.New("[GNOSWAP-POOL-028] invalid payer") -) - -// newErrorWithDetail adds detail to an error message. -func newErrorWithDetail(err error, detail string) string { - finalErr := ufmt.Errorf("%s || %s", err.Error(), detail) - return finalErr.Error() -} - -// makeErrorWithDetails creates an error with additional context. -func makeErrorWithDetails(err error, details string) error { - return ufmt.Errorf("%s || %s", err.Error(), details) -} diff --git a/contract/r/gnoswap/v1/pool/factory_param.gno b/contract/r/gnoswap/v1/pool/factory_param.gno deleted file mode 100644 index ced5113..0000000 --- a/contract/r/gnoswap/v1/pool/factory_param.gno +++ /dev/null @@ -1,143 +0,0 @@ -package pool - -import ( - "strings" - - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" -) - -var Q192 = u256.Zero().Lsh(u256.One(), 192) - -var ( - minSqrtRatio = u256.MustFromDecimal(MIN_SQRT_RATIO) - maxSqrtRatio = u256.MustFromDecimal(MAX_SQRT_RATIO) -) - -const ( - FeeTier100 uint32 = 100 - FeeTier500 uint32 = 500 - FeeTier3000 uint32 = 3000 - FeeTier10000 uint32 = 10000 -) - -const ( - GNOT string = "gnot" - WRAPPED_WUGNOT string = "gno.land/r/gnoland/wugnot" -) - -const ( - MIN_SQRT_RATIO string = "4295128739" - MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" -) - -// poolCreateConfig holds the essential parameters for creating a new pool. -type poolCreateConfig struct { - token0Path string - token1Path string - fee uint32 - sqrtPriceX96 *u256.Uint - tickSpacing int32 -} - -// newPoolParams defines the essential parameters for creating a new pool. -func newPoolParams( - token0Path string, - token1Path string, - fee uint32, - sqrtPriceX96 string, - tickSpacing int32, -) *poolCreateConfig { - price := u256.MustFromDecimal(sqrtPriceX96) - return &poolCreateConfig{ - token0Path: token0Path, - token1Path: token1Path, - fee: fee, - sqrtPriceX96: price, - tickSpacing: tickSpacing, - } -} - -func (p *poolCreateConfig) SqrtPriceX96() *u256.Uint { return p.sqrtPriceX96 } -func (p *poolCreateConfig) TickSpacing() int32 { return p.tickSpacing } -func (p *poolCreateConfig) Token0Path() string { return p.token0Path } -func (p *poolCreateConfig) Token1Path() string { return p.token1Path } -func (p *poolCreateConfig) Fee() uint32 { return p.fee } - -func (p *poolCreateConfig) updateWithWrapping() (*poolCreateConfig, error) { - token0Path, token1Path := p.wrap() - - // Always validate that the price is within valid range - if err := validateSqrtPriceX96(p.sqrtPriceX96); err != nil { - return nil, err - } - - if !p.isInOrder() { - token0Path, token1Path = token1Path, token0Path - - // newPrice = 2^192 / oldPrice - newPrice := u256.Zero().Div(Q192, p.sqrtPriceX96) - - // Check if calculated price is within valid range - if err := validateSqrtPriceX96(newPrice); err != nil { - return nil, err - } - - p.sqrtPriceX96 = newPrice - } - return newPoolParams(token0Path, token1Path, p.fee, p.sqrtPriceX96.ToString(), GetFeeAmountTickSpacing(p.fee)), nil -} - -func (p *poolCreateConfig) isSameTokenPath() bool { - return p.token0Path == p.token1Path -} - -// isInOrder checks if token paths are in lexicographical (or, alphabetical) order -func (p *poolCreateConfig) isInOrder() bool { - if strings.Compare(p.token0Path, p.token1Path) < 0 { - return true - } - return false -} - -func (p *poolCreateConfig) wrap() (string, string) { - if p.token0Path == GNOT { - p.token0Path = WRAPPED_WUGNOT - } - if p.token1Path == GNOT { - p.token1Path = WRAPPED_WUGNOT - } - return p.token0Path, p.token1Path -} - -func (p *poolCreateConfig) poolPath() string { - return GetPoolPath(p.token0Path, p.token1Path, p.fee) -} - -func (p *poolCreateConfig) isSupportedFee(feeTier uint32) bool { - switch feeTier { - case FeeTier100, FeeTier500, FeeTier3000, FeeTier10000: - return true - } - return false -} - -// validateSqrtPriceX96 validates that the given sqrtPriceX96 is within valid range -func validateSqrtPriceX96(sqrtPriceX96 *u256.Uint) error { - if sqrtPriceX96.Lt(minSqrtRatio) || sqrtPriceX96.Gt(maxSqrtRatio) { - return makeErrorWithDetails( - errOutOfRange, - ufmt.Sprintf("sqrtPriceX96(%s) is out of range", sqrtPriceX96.ToString()), - ) - } - return nil -} - -func isValidFeeTier(feeTier uint32) bool { - switch feeTier { - case FeeTier100, FeeTier500, FeeTier3000, FeeTier10000: - return true - } - - return false -} diff --git a/contract/r/gnoswap/v1/pool/getter.gno b/contract/r/gnoswap/v1/pool/getter.gno deleted file mode 100644 index cd909b6..0000000 --- a/contract/r/gnoswap/v1/pool/getter.gno +++ /dev/null @@ -1,183 +0,0 @@ -package pool - -import ( - "strings" - - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" -) - -// GetPool retrieves a pool instance based on the provided token paths and fee tier. -func GetPool(token0Path, token1Path string, fee uint32) *Pool { - poolPath := GetPoolPath(token0Path, token1Path, fee) - return mustGetPool(poolPath).Clone() -} - -// GetPoolPath generates a unique pool path string based on the token paths and fee tier. -func GetPoolPath(token0Path, token1Path string, fee uint32) string { - // All the token paths in the pool are sorted in alphabetical order. - if strings.Compare(token1Path, token0Path) < 0 { - token0Path, token1Path = token1Path, token0Path - } - - return ufmt.Sprintf("%s:%s:%d", token0Path, token1Path, fee) -} - -// GetFeeAmountTickSpacing retrieves the tick spacing associated with a given fee amount. -func GetFeeAmountTickSpacing(fee uint32) (spacing int32) { - feeStr := formatUint(fee) - iTickSpacing, exist := feeAmountTickSpacing.Get(feeStr) - if !exist { - panic(newErrorWithDetail( - errUnsupportedFeeTier, - ufmt.Sprintf("expected fee(%d) to be one of %d, %d, %d, %d", fee, FeeTier100, FeeTier500, FeeTier3000, FeeTier10000), - )) - } - - spacing, ok := iTickSpacing.(int32) - if !ok { - panic("failed to cast tick spacing to int32") - } - - return spacing -} - -func GetToken0Path(poolPath string) string { - return mustGetPool(poolPath).Token0Path() -} - -func GetToken1Path(poolPath string) string { - return mustGetPool(poolPath).Token1Path() -} - -func GetFee(poolPath string) uint32 { - return mustGetPool(poolPath).Fee() -} - -func GetBalanceToken0(poolPath string) string { - return mustGetPool(poolPath).BalanceToken0().ToString() -} - -func GetBalanceToken1(poolPath string) string { - return mustGetPool(poolPath).BalanceToken1().ToString() -} - -func GetTickSpacing(poolPath string) int32 { - return mustGetPool(poolPath).TickSpacing() -} - -func GetMaxLiquidityPerTick(poolPath string) string { - return mustGetPool(poolPath).MaxLiquidityPerTick().ToString() -} - -func GetSlot0FeeProtocol(poolPath string) uint8 { - return mustGetPool(poolPath).Slot0FeeProtocol() -} - -func GetSlot0Unlocked(poolPath string) bool { - return mustGetPool(poolPath).Slot0Unlocked() -} - -func GetFeeGrowthGlobal0X128(poolPath string) string { - return mustGetPool(poolPath).FeeGrowthGlobal0X128().ToString() -} - -func GetFeeGrowthGlobal1X128(poolPath string) string { - return mustGetPool(poolPath).FeeGrowthGlobal1X128().ToString() -} - -func GetProtocolFeesToken0(poolPath string) string { - return mustGetPool(poolPath).ProtocolFeesToken0().ToString() -} - -func GetProtocolFeesToken1(poolPath string) string { - return mustGetPool(poolPath).ProtocolFeesToken1().ToString() -} - -func GetLiquidity(poolPath string) string { - return mustGetPool(poolPath).Liquidity().ToString() -} - -func GetPositionFeeGrowthInside0LastX128(poolPath, key string) string { - return mustGetPool(poolPath).PositionFeeGrowthInside0LastX128(key).ToString() -} - -func GetPositionFeeGrowthInside1LastX128(poolPath, key string) string { - return mustGetPool(poolPath).PositionFeeGrowthInside1LastX128(key).ToString() -} - -func GetPositionTokensOwed0(poolPath, key string) string { - return mustGetPool(poolPath).PositionTokensOwed0(key).ToString() -} - -func GetPositionTokensOwed1(poolPath, key string) string { - return mustGetPool(poolPath).PositionTokensOwed1(key).ToString() -} - -func GetTickLiquidityGross(poolPath string, tick int32) string { - return mustGetPool(poolPath).GetTickLiquidityGross(tick).ToString() -} - -func GetTickLiquidityNet(poolPath string, tick int32) string { - return mustGetPool(poolPath).GetTickLiquidityNet(tick).ToString() -} - -func GetTickFeeGrowthOutside0X128(poolPath string, tick int32) string { - return mustGetPool(poolPath).GetTickFeeGrowthOutside0X128(tick).ToString() -} - -func GetTickFeeGrowthOutside1X128(poolPath string, tick int32) string { - return mustGetPool(poolPath).GetTickFeeGrowthOutside1X128(tick).ToString() -} - -func GetTickCumulativeOutside(poolPath string, tick int32) int64 { - return mustGetPool(poolPath).GetTickCumulativeOutside(tick) -} - -func GetTickSecondsPerLiquidityOutsideX128(poolPath string, tick int32) string { - return mustGetPool(poolPath).GetTickSecondsPerLiquidityOutsideX128(tick).ToString() -} - -func GetTickSecondsOutside(poolPath string, tick int32) uint32 { - return mustGetPool(poolPath).GetTickSecondsOutside(tick) -} - -func GetTickInitialized(poolPath string, tick int32) bool { - return mustGetPool(poolPath).GetTickInitialized(tick) -} - -func GetSlot0Tick(poolPath string) int32 { - return mustGetPool(poolPath).Slot0Tick() -} - -func GetSlot0SqrtPriceX96(poolPath string) *u256.Uint { - return u256.Zero().Set(mustGetPool(poolPath).Slot0SqrtPriceX96()) -} - -func GetFeeGrowthGlobalX128(poolPath string) (*u256.Uint, *u256.Uint) { - pool := mustGetPool(poolPath) - return u256.Zero().Set(pool.FeeGrowthGlobal0X128()), u256.Zero().Set(pool.FeeGrowthGlobal1X128()) -} - -func GetTickFeeGrowthOutsideX128(poolPath string, tick int32) (*u256.Uint, *u256.Uint) { - pool := mustGetPool(poolPath) - return u256.Zero().Set(pool.GetTickFeeGrowthOutside0X128(tick)), u256.Zero().Set(pool.GetTickFeeGrowthOutside1X128(tick)) -} - -func GetPositionFeeGrowthInsideLastX128(poolPath, key string) (*u256.Uint, *u256.Uint) { - pool := mustGetPool(poolPath) - return u256.Zero().Set(pool.PositionFeeGrowthInside0LastX128(key)), u256.Zero().Set(pool.PositionFeeGrowthInside1LastX128(key)) -} - -func GetPositionLiquidity(poolPath, key string) *u256.Uint { - return u256.Zero().Set(mustGetPool(poolPath).PositionLiquidity(key)) -} - -func ExistsPoolPath(poolPath string) bool { - return pools.Has(poolPath) -} - -func GetTickCumulative(poolPath string, secondsAgo int64) int64 { - pool := mustGetPool(poolPath) - return pool.Observation().TickCumulative() -} diff --git a/contract/r/gnoswap/v1/pool/gnomod.toml b/contract/r/gnoswap/v1/pool/gnomod.toml deleted file mode 100644 index 6c73620..0000000 --- a/contract/r/gnoswap/v1/pool/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/pool" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/pool/json.gno b/contract/r/gnoswap/v1/pool/json.gno deleted file mode 100644 index 6f128ac..0000000 --- a/contract/r/gnoswap/v1/pool/json.gno +++ /dev/null @@ -1,275 +0,0 @@ -package pool - -import ( - "std" - "strconv" - "time" - - "gno.land/p/onbloc/json" - - u256 "gno.land/p/gnoswap/uint256" -) - -// JsonResponse is an interface that all JSON response types must implement. -type JsonResponse interface { - JSON() *json.Node -} - -type RpcPool struct { - PoolPath string `json:"poolPath"` - - Token0Path string `json:"token0Path"` - Token1Path string `json:"token1Path"` - - Token0Balance string `json:"token0Balance"` - Token1Balance string `json:"token1Balance"` - - Fee uint32 `json:"fee"` - - TickSpacing int32 `json:"tickSpacing"` - - MaxLiquidityPerTick string `json:"maxLiquidityPerTick"` - - Slot0SqrtPriceX96 string `json:"sqrtPriceX96"` - Slot0Tick int32 `json:"tick"` - Slot0FeeProtocol uint8 `json:"feeProtocol"` - Slot0Unlocked bool `json:"unlocked"` - - FeeGrowthGlobal0X128 string `json:"feeGrowthGlobal0X128"` - FeeGrowthGlobal1X128 string `json:"feeGrowthGlobal1X128"` - - Token0ProtocolFee string `json:"token0ProtocolFee"` - Token1ProtocolFee string `json:"token1ProtocolFee"` - - Liquidity string `json:"liquidity"` - - Ticks RpcTicks `json:"ticks"` - - TickBitmaps RpcTickBitmaps `json:"tickBitmaps"` - - Positions RpcPositions `json:"positions"` -} - -func newRpcPool(poolPath string) RpcPool { - rpcPool := RpcPool{} - pool := mustGetPool(poolPath).Clone() - - rpcPool.PoolPath = poolPath - - rpcPool.Token0Path = pool.token0Path - rpcPool.Token1Path = pool.token1Path - - rpcPool.Token0Balance = pool.balances.token0.ToString() - rpcPool.Token1Balance = pool.balances.token1.ToString() - - rpcPool.Fee = pool.fee - - rpcPool.TickSpacing = pool.tickSpacing - - rpcPool.MaxLiquidityPerTick = pool.maxLiquidityPerTick.ToString() - - rpcPool.Slot0SqrtPriceX96 = pool.slot0.sqrtPriceX96.ToString() - rpcPool.Slot0Tick = pool.slot0.tick - rpcPool.Slot0FeeProtocol = pool.slot0.feeProtocol - rpcPool.Slot0Unlocked = pool.slot0.unlocked - - rpcPool.FeeGrowthGlobal0X128 = pool.feeGrowthGlobal0X128.ToString() - rpcPool.FeeGrowthGlobal1X128 = pool.feeGrowthGlobal1X128.ToString() - - rpcPool.Token0ProtocolFee = pool.protocolFees.token0.ToString() - rpcPool.Token1ProtocolFee = pool.protocolFees.token1.ToString() - - rpcPool.Liquidity = pool.liquidity.ToString() - - rpcPool.Ticks = RpcTicks{} - pool.ticks.Iterate("", "", func(tickStr string, iTickInfo any) bool { - tick, err := strconv.ParseInt(tickStr, 10, 32) - if err != nil { - panic(err) - } - tickInfo, ok := iTickInfo.(TickInfo) - if !ok { - panic("failed to cast tick info to TickInfo") - } - - rpcPool.Ticks[int32(tick)] = RpcTickInfo{ - LiquidityGross: tickInfo.liquidityGross.ToString(), - LiquidityNet: tickInfo.liquidityNet.ToString(), - FeeGrowthOutside0X128: tickInfo.feeGrowthOutside0X128.ToString(), - FeeGrowthOutside1X128: tickInfo.feeGrowthOutside1X128.ToString(), - TickCumulativeOutside: tickInfo.tickCumulativeOutside, - SecondsPerLiquidityOutsideX: tickInfo.secondsPerLiquidityOutsideX128.ToString(), - SecondsOutside: tickInfo.secondsOutside, - Initialized: tickInfo.initialized, - } - - return false - }) - - rpcPool.TickBitmaps = RpcTickBitmaps{} - pool.tickBitmaps.Iterate("", "", func(tickStr string, iTickBitmap any) bool { - tick, err := strconv.ParseInt(tickStr, 10, 16) - if err != nil { - panic(err) - } - bm, ok := iTickBitmap.(*u256.Uint) - if !ok { - panic("failed to cast tick bitmap to *u256.Uint") - } - pool.setTickBitmap(int16(tick), bm) - return false - }) - - rpcPositions := []RpcPosition{} - pool.positions.Iterate("", "", func(posKey string, iPositionInfo any) bool { - owner, tickLower, tickUpper := posKeyDivide(posKey) - posInfo, ok := iPositionInfo.(PositionInfo) - if !ok { - panic("failed to cast position info to PositionInfo") - } - - rpcPositions = append(rpcPositions, RpcPosition{ - Owner: owner, - TickLower: tickLower, - TickUpper: tickUpper, - Liquidity: posInfo.liquidity.ToString(), - Token0Owed: posInfo.tokensOwed0.ToString(), - Token1Owed: posInfo.tokensOwed1.ToString(), - }) - - return false - }) - - rpcPool.Positions = rpcPositions - - return rpcPool -} - -func (r RpcPool) JSON() *json.Node { - return makePoolNode(r) -} - -func makePoolNode(pool RpcPool) *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "poolPath": json.StringNode("poolPath", pool.PoolPath), - "token0Path": json.StringNode("token0Path", pool.Token0Path), - "token1Path": json.StringNode("token1Path", pool.Token1Path), - "token0Balance": json.StringNode("token0Balance", pool.Token0Balance), - "token1Balance": json.StringNode("token1Balance", pool.Token1Balance), - "fee": json.NumberNode("fee", float64(pool.Fee)), - "tickSpacing": json.NumberNode("tickSpacing", float64(pool.TickSpacing)), - "maxLiquidityPerTick": json.StringNode("maxLiquidityPerTick", pool.MaxLiquidityPerTick), - "sqrtPriceX96": json.StringNode("sqrtPriceX96", pool.Slot0SqrtPriceX96), - "tick": json.NumberNode("tick", float64(pool.Slot0Tick)), - "feeProtocol": json.NumberNode("feeProtocol", float64(pool.Slot0FeeProtocol)), - "unlocked": json.BoolNode("unlocked", pool.Slot0Unlocked), - "feeGrowthGlobal0X128": json.StringNode("feeGrowthGlobal0X128", pool.FeeGrowthGlobal0X128), - "feeGrowthGlobal1X128": json.StringNode("feeGrowthGlobal1X128", pool.FeeGrowthGlobal1X128), - "token0ProtocolFee": json.StringNode("token0ProtocolFee", pool.Token0ProtocolFee), - "token1ProtocolFee": json.StringNode("token1ProtocolFee", pool.Token1ProtocolFee), - "liquidity": json.StringNode("liquidity", pool.Liquidity), - "ticks": pool.Ticks.JSON(), - "tickBitmaps": pool.TickBitmaps.JSON(), - "positions": pool.Positions.JSON(), - }) -} - -type RpcTickInfo struct { - LiquidityGross string `json:"liquidityGross"` - LiquidityNet string `json:"liquidityNet"` - - FeeGrowthOutside0X128 string `json:"feeGrowthOutside0X128"` - FeeGrowthOutside1X128 string `json:"feeGrowthOutside1X128"` - - TickCumulativeOutside int64 `json:"tickCumulativeOutside"` - - SecondsPerLiquidityOutsideX string `json:"secondsPerLiquidityOutsideX"` - SecondsOutside uint32 `json:"secondsOutside"` - - Initialized bool `json:"initialized"` -} - -func (r RpcTickInfo) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "liquidityGross": json.StringNode("liquidityGross", r.LiquidityGross), - "liquidityNet": json.StringNode("liquidityNet", r.LiquidityNet), - "feeGrowthOutside0X128": json.StringNode("feeGrowthOutside0X128", r.FeeGrowthOutside0X128), - "feeGrowthOutside1X128": json.StringNode("feeGrowthOutside1X128", r.FeeGrowthOutside1X128), - "tickCumulativeOutside": json.NumberNode("tickCumulativeOutside", float64(r.TickCumulativeOutside)), - "secondsPerLiquidityOutsideX": json.StringNode("secondsPerLiquidityOutsideX", r.SecondsPerLiquidityOutsideX), - "secondsOutside": json.NumberNode("secondsOutside", float64(r.SecondsOutside)), - "initialized": json.BoolNode("initialized", r.Initialized), - }) -} - -type RpcTickBitmaps map[int16]string // tick(wordPos) => bitmap(tickWord ^ mask) - -func (r RpcTickBitmaps) JSON() *json.Node { - tickBitmapsJson := map[string]*json.Node{} - for tick, tickBitmap := range r { - tickBitmapsJson[strconv.Itoa(int(tick))] = json.StringNode("", tickBitmap) - } - return json.ObjectNode("", tickBitmapsJson) -} - -type RpcTicks map[int32]RpcTickInfo // tick => RpcTickInfo - -func (r RpcTicks) JSON() *json.Node { - ticksJson := map[string]*json.Node{} - for tick, tickInfo := range r { - ticksJson[strconv.Itoa(int(tick))] = tickInfo.JSON() - } - return json.ObjectNode("", ticksJson) -} - -type RpcPositions []RpcPosition - -func (r RpcPositions) JSON() *json.Node { - positionsJson := make([]*json.Node, len(r)) - for i, pos := range r { - positionsJson[i] = pos.JSON() - } - return json.ArrayNode("", positionsJson) -} - -type RpcPosition struct { - Owner string `json:"owner"` - - TickLower int32 `json:"tickLower"` - TickUpper int32 `json:"tickUpper"` - - Liquidity string `json:"liquidity"` - - Token0Owed string `json:"token0Owed"` - Token1Owed string `json:"token1Owed"` -} - -func (r RpcPosition) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "owner": json.StringNode("owner", r.Owner), - "tickLower": json.NumberNode("tickLower", float64(r.TickLower)), - "tickUpper": json.NumberNode("tickUpper", float64(r.TickUpper)), - "liquidity": json.StringNode("liquidity", r.Liquidity), - "token0Owed": json.StringNode("token0Owed", r.Token0Owed), - "token1Owed": json.StringNode("token1Owed", r.Token1Owed), - }) -} - -type statNode struct { - height int64 - timestamp int64 -} - -func newStatNode() statNode { - return statNode{ - height: std.ChainHeight(), - timestamp: time.Now().Unix(), - } -} - -func (s statNode) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "height": json.NumberNode("height", float64(s.height)), - "timestamp": json.NumberNode("timestamp", float64(s.timestamp)), - }) -} diff --git a/contract/r/gnoswap/v1/pool/liquidity_math.gno b/contract/r/gnoswap/v1/pool/liquidity_math.gno deleted file mode 100644 index eee500f..0000000 --- a/contract/r/gnoswap/v1/pool/liquidity_math.gno +++ /dev/null @@ -1,43 +0,0 @@ -package pool - -import ( - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/p/nt/ufmt" -) - -// liquidityMathAddDelta calculates the new liquidity by applying the delta liquidity to the current liquidity. -// If delta liquidity is negative, it subtracts the absolute value of delta liquidity from the current liquidity. -// If delta liquidity is positive, it adds the absolute value of delta liquidity to the current liquidity. -// Returns the new liquidity as a uint256 value. -func liquidityMathAddDelta(x *u256.Uint, y *i256.Int) *u256.Uint { - if x == nil || y == nil { - panic(newErrorWithDetail( - errInvalidInput, - "x or y is nil", - )) - } - - absDelta := y.Abs() - - // Subtract or add based on the sign of y - if y.IsNeg() { - z := u256.Zero().Sub(x, absDelta) - if z.Gte(x) { - panic(newErrorWithDetail( - errLiquidityCalculation, - ufmt.Sprintf("Condition failed: (z must be < x) (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString()), - )) - } - return z - } - z := u256.Zero().Add(x, absDelta) - if z.Lt(x) { - panic(newErrorWithDetail( - errLiquidityCalculation, - ufmt.Sprintf("Condition failed: (z must be >= x) (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString()), - )) - } - return z -} diff --git a/contract/r/gnoswap/v1/pool/manager.gno b/contract/r/gnoswap/v1/pool/manager.gno deleted file mode 100644 index 7301e84..0000000 --- a/contract/r/gnoswap/v1/pool/manager.gno +++ /dev/null @@ -1,275 +0,0 @@ -package pool - -import ( - "std" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" - - en "gno.land/r/gnoswap/emission" - pf "gno.land/r/gnoswap/v1/protocol_fee" - - "gno.land/r/gnoswap/gns" -) - -const GNS_PATH string = "gno.land/r/gnoswap/gns" - -var ( - feeAmountTickSpacing = avl.NewTree() // feeBps(uint32) -> tickSpacing(int32) - pools = avl.NewTree() // poolPath -> *Pool - - // slot0FeeProtocol represents the protocol fee percentage (0-10). - // This parameter can be modified through governance. - slot0FeeProtocol uint8 = 0 -) - -func init() { - setFeeAmountTickSpacing(100, 1) // 0.01% - setFeeAmountTickSpacing(500, 10) // 0.05% - setFeeAmountTickSpacing(3000, 60) // 0.3% - setFeeAmountTickSpacing(10000, 200) // 1% -} - -// CreatePool creates a new concentrated liquidity pool. -// -// Deploys new AMM pool for token pair with specified fee tier. -// Charges 100 GNS creation fee to prevent spam. -// Sets initial price and tick spacing based on fee tier. -// -// Parameters: -// - token0Path, token1Path: Token contract paths (ordered by address) -// - fee: Fee tier (100=0.01%, 500=0.05%, 3000=0.3%, 10000=1%) -// - sqrtPriceX96: Initial sqrt price in Q64.96 format -// -// Tick spacing by fee tier: -// - 0.01%: 1 tick -// - 0.05%: 10 ticks -// - 0.30%: 60 ticks -// - 1.00%: 200 ticks -// -// Requirements: -// - Tokens must be different -// - Fee tier must be supported -// - Pool must not already exist -// - Caller must have 100 GNS for creation fee -func CreatePool( - cur realm, - token0Path string, - token1Path string, - fee uint32, - sqrtPriceX96 string, -) { - halt.AssertIsNotHaltedPool() - - assertIsNotEqualsTokens(token0Path, token1Path) - assertIsSupportedFeeTier(fee) - assertIsNotExistsPoolPath(token0Path, token1Path, fee) - - en.MintAndDistributeGns(cross) - - poolInfo := newPoolParams( - token0Path, - token1Path, - fee, - sqrtPriceX96, - GetFeeAmountTickSpacing(fee), - ) - - poolInfo, err := poolInfo.updateWithWrapping() - if err != nil { - panic(err) - } - - // check if wrapped token paths are registered - common.MustRegistered(poolInfo.token0Path) - common.MustRegistered(poolInfo.token1Path) - - pool := newPool(poolInfo) - pools.Set(poolInfo.poolPath(), pool) - - if poolCreationFee > 0 { - gns.TransferFrom(cross, std.PreviousRealm().Address(), protocolFeeAddr, poolCreationFee) - pf.AddToProtocolFee(cross, GNS_PATH, poolCreationFee) - - previousRealm := std.PreviousRealm() - std.Emit( - "PoolCreationFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "poolPath", poolInfo.poolPath(), - "feeTokenPath", GNS_PATH, - "feeAmount", formatInt(poolCreationFee), - ) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "CreatePool", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "token0Path", token0Path, - "token1Path", token1Path, - "fee", formatUint(fee), - "sqrtPriceX96", sqrtPriceX96, - "poolPath", pool.PoolPath(), - "tick", formatInt(pool.Slot0Tick()), - "tickSpacing", formatInt(poolInfo.TickSpacing()), - ) -} - -// SetFeeProtocol sets the protocol fee percentage for all pools. -// -// Parameters: -// - feeProtocol0, feeProtocol1: fee percentages (0-10) -// -// Only callable by admin or governance. -func SetFeeProtocol(cur realm, feeProtocol0, feeProtocol1 uint8) { - halt.AssertIsNotHaltedPool() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - err := setFeeProtocolInternal(feeProtocol0, feeProtocol1, "SetFeeProtocol") - if err != nil { - panic(err) - } -} - -// setFeeAmountTickSpacing associates a tick spacing value with a fee amount. -func setFeeAmountTickSpacing(fee uint32, tickSpacing int32) { - feeStr := formatUint(fee) - feeAmountTickSpacing.Set(feeStr, tickSpacing) -} - -// mustGetPool retrieves a pool instance by its path and ensures it exists. -func mustGetPool(poolPath string) (pool *Pool) { - iPool, exist := pools.Get(poolPath) - if !exist { - panic(newErrorWithDetail( - errDataNotFound, - ufmt.Sprintf("expected poolPath(%s) to exist", poolPath), - )) - } - - pool, ok := iPool.(*Pool) - if !ok { - panic(ufmt.Sprintf("failed to cast pool to *Pool: %T", iPool)) - } - return pool -} - -func mustGetPoolBy(token0Path, token1Path string, fee uint32) *Pool { - poolPath := GetPoolPath(token0Path, token1Path, fee) - return mustGetPool(poolPath) -} - -// setFeeProtocolInternal updates the protocol fee for all pools and emits an event. -func setFeeProtocolInternal(feeProtocol0, feeProtocol1 uint8, eventName string) error { - oldFee := slot0FeeProtocol - newFee, err := setFeeProtocol(feeProtocol0, feeProtocol1) - if err != nil { - return err - } - - feeProtocol0Old := oldFee % 16 - feeProtocol1Old := oldFee >> 4 - - previousRealm := std.PreviousRealm() - std.Emit( - eventName, - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "prevFeeProtocol0", formatUint(feeProtocol0Old), - "prevFeeProtocol1", formatUint(feeProtocol1Old), - "feeProtocol0", formatUint(feeProtocol0), - "feeProtocol1", formatUint(feeProtocol1), - "newFee", formatUint(newFee), - ) - - return nil -} - -// setFeeProtocol updates the protocol fee configuration for all managed pools. -// -// This function combines the protocol fee values for token0 and token1 into a single `uint8` value, -// where: -// - Lower 4 bits store feeProtocol0 (for token0). -// - Upper 4 bits store feeProtocol1 (for token1). -// -// The updated fee protocol is applied uniformly to all pools managed by the system. -// -// Parameters: -// - feeProtocol0: protocol fee for token0 (must be 0 or between 4 and 10 inclusive). -// - feeProtocol1: protocol fee for token1 (must be 0 or between 4 and 10 inclusive). -// -// Returns: -// - newFee (uint8): the combined fee protocol value. -// -// Example: -// If feeProtocol0 = 4 and feeProtocol1 = 5: -// -// newFee = 4 + (5 << 4) -// // Results in: 0x54 (84 in decimal) -// // Binary: 0101 0100 -// // ^^^^ ^^^^ -// // fee1=5 fee0=4 -// -// Notes: -// - This function ensures that all pools under management are updated to use the same fee protocol. -// - Caller restrictions (e.g., admin or governance) are not enforced in this function. -// - Ensure the system is not halted before updating fees. -func setFeeProtocol(feeProtocol0, feeProtocol1 uint8) (uint8, error) { - if err := validateFeeProtocol(feeProtocol0, feeProtocol1); err != nil { - return 0, err - } - - // combine both protocol fee into a single byte: - // - feePrtocol0 occupies the lower 4 bits - // - feeProtocol1 is shifted the lower 4 positions to occupy the upper 4 bits - newFee := feeProtocol0 + (feeProtocol1 << 4) // ( << 4 ) = ( * 16 ) - - // Update slot0 for each pool - pools.Iterate("", "", func(poolPath string, iPool any) bool { - pool, ok := iPool.(*Pool) - if !ok { - panic("failed to cast pool to *Pool") - } - pool.slot0.feeProtocol = newFee - - return false - }) - - // update slot0 - slot0FeeProtocol = newFee - return newFee, nil -} - -// validateFeeProtocol validates the fee protocol values for token0 and token1. -// -// This function checks whether the provided fee protocol values (`feeProtocol0` and `feeProtocol1`) -// are valid using the `isValidFeeProtocolValue` function. If either value is invalid, it returns -// an error indicating that the protocol fee percentage is invalid. -// -// Parameters: -// - feeProtocol0: uint8, the fee protocol value for token0. -// - feeProtocol1: uint8, the fee protocol value for token1. -// -// Returns: -// - error: Returns `errInvalidProtocolFeePct` if either `feeProtocol0` or `feeProtocol1` is invalid. -// Returns `nil` if both values are valid. -func validateFeeProtocol(feeProtocol0, feeProtocol1 uint8) error { - if !isValidFeeProtocolValue(feeProtocol0) || !isValidFeeProtocolValue(feeProtocol1) { - return errInvalidProtocolFeePct - } - return nil -} - -// isValidFeeProtocolValue checks if a fee protocol value is within acceptable range. -// valid values are either 0 or between 4 and 10 inclusive. -func isValidFeeProtocolValue(value uint8) bool { - return value == 0 || (value >= 4 && value <= 10) -} diff --git a/contract/r/gnoswap/v1/pool/pool.gno b/contract/r/gnoswap/v1/pool/pool.gno deleted file mode 100644 index 7b33342..0000000 --- a/contract/r/gnoswap/v1/pool/pool.gno +++ /dev/null @@ -1,364 +0,0 @@ -package pool - -import ( - "std" - - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - prabc "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoswap/access" -) - -var ( - positionAddr, _ = access.GetAddress(prabc.ROLE_POSITION.String()) - poolAddr, _ = access.GetAddress(prabc.ROLE_POOL.String()) - protocolFeeAddr, _ = access.GetAddress(prabc.ROLE_PROTOCOL_FEE.String()) - routerAddr, _ = access.GetAddress(prabc.ROLE_ROUTER.String()) -) - -// Mint adds liquidity to a pool position. -// -// Increases liquidity for a position within specified tick range. -// Calculates required token amounts based on current pool price. -// Updates tick state and transfers tokens atomically. -// -// Parameters: -// - token0Path, token1Path: Token contract paths -// - fee: Fee tier (100, 500, 3000, 10000 = 0.01%, 0.05%, 0.3%, 1%) -// - recipient: Position owner address -// - tickLower, tickUpper: Price range boundaries (must be tick-aligned) -// - liquidityAmount: Liquidity to add (Q128 format) -// - positionCaller: Original caller for token transfers -// -// Returns: -// - amount0: Token0 amount consumed (decimal string) -// - amount1: Token1 amount consumed (decimal string) -// -// Requirements: -// - Pool must exist for token pair and fee -// - Liquidity amount must be positive -// - Ticks must be valid and aligned to spacing -// -// Only callable by position contract. -func Mint( - cur realm, - token0Path string, - token1Path string, - fee uint32, - recipient std.Address, - tickLower int32, - tickUpper int32, - liquidityAmount string, - positionCaller std.Address, -) (string, string) { - halt.AssertIsNotHaltedPool() - - caller := std.PreviousRealm().Address() - access.AssertIsPosition(caller) - access.AssertIsValidAddress(positionCaller) - - liquidity := u256.MustFromDecimal(liquidityAmount) - if liquidity.IsZero() { - panic(errZeroLiquidity) - } - - pool := mustGetPoolBy(token0Path, token1Path, fee) - liquidityDelta := safeConvertToInt128(liquidity) - positionParam := newModifyPositionParams(positionCaller, tickLower, tickUpper, liquidityDelta) - _, amount0, amount1, err := pool.modifyPosition(positionParam) - if err != nil { - panic(err) - } - - if amount0.Gt(zero) { - pool.safeTransferFrom(positionCaller, poolAddr, pool.token0Path, amount0, true) - } - - if amount1.Gt(zero) { - pool.safeTransferFrom(positionCaller, poolAddr, pool.token1Path, amount1, false) - } - - return amount0.ToString(), amount1.ToString() -} - -// Burn removes liquidity from a position. -// -// Decreases liquidity and calculates tokens owed to position owner. -// Updates tick state but doesn't transfer tokens (use Collect). -// Two-step process prevents reentrancy attacks. -// -// Parameters: -// - token0Path, token1Path: Token contract paths -// - fee: Fee tier matching the pool -// - tickLower, tickUpper: Position's price range -// - liquidityAmount: Liquidity to remove (uint128) -// - positionCaller: Position owner for validation -// -// Returns: -// - amount0: Token0 owed to position (uint256) -// - amount1: Token1 owed to position (uint256) -// -// Note: Tokens remain in pool until Collect is called. -// Only callable by position contract. -func Burn( - cur realm, - token0Path string, - token1Path string, - fee uint32, - tickLower int32, - tickUpper int32, - liquidityAmount string, // uint128 - positionCaller std.Address, -) (string, string) { // uint256 x2 - halt.AssertIsNotHaltedPool() - - caller := std.PreviousRealm().Address() - access.AssertIsPosition(caller) - access.AssertIsValidAddress(positionCaller) - - liqAmount := u256.MustFromDecimal(liquidityAmount) - liqAmountInt256 := safeConvertToInt128(liqAmount) - liqDelta := i256.Zero().Neg(liqAmountInt256) - - posParams := newModifyPositionParams(positionCaller, tickLower, tickUpper, liqDelta) - pool := mustGetPoolBy(token0Path, token1Path, fee) - position, amount0, amount1, err := pool.modifyPosition(posParams) - if err != nil { - panic(err) - } - - if amount0.Gt(zero) || amount1.Gt(zero) { - amount0 = toUint128(amount0) - amount1 = toUint128(amount1) - - tokensOwed0, overflow := u256.Zero().AddOverflow(position.tokensOwed0, amount0) - if overflow { - panic(errOverFlow) - } - position.tokensOwed0 = tokensOwed0 - - tokensOwed1, overflow := u256.Zero().AddOverflow(position.tokensOwed1, amount1) - if overflow { - panic(errOverFlow) - } - position.tokensOwed1 = tokensOwed1 - } - - positionKey, err := getPositionKey(tickLower, tickUpper) - if err != nil { - panic(err) - } - - pool.setPosition(positionKey, position) - - // mustGetPosition() is called to ensure the position exists - pool.mustGetPosition(positionKey) - - // actual token transfer happens in Collect() - return amount0.ToString(), amount1.ToString() -} - -// Collect transfers owed tokens from a position to recipient. -// -// Claims tokens from burned liquidity and accumulated fees. -// Applies protocol withdrawal fee (1% default) before transfer. -// Supports partial collection via amount limits. -// -// Parameters: -// - token0Path, token1Path: Token contract paths -// - fee: Fee tier of the pool -// - recipient: Address to receive tokens -// - tickLower, tickUpper: Position's price range -// - amount0Requested, amount1Requested: Max amounts to collect (use MAX_UINT128 for all) -// -// Returns: -// - amount0: Token0 actually collected (after fees) -// - amount1: Token1 actually collected (after fees) -// -// Protocol fees: 1% on collected amounts. -// Only callable by position contract. -func Collect( - cur realm, - token0Path string, - token1Path string, - fee uint32, - recipient std.Address, - tickLower int32, - tickUpper int32, - amount0Requested string, - amount1Requested string, -) (string, string) { - halt.AssertIsNotHaltedPool() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - access.AssertIsPosition(caller) - access.AssertIsValidAddress(recipient) - - pool := mustGetPoolBy(token0Path, token1Path, fee) - // Use recipient address instead of getPrevAddr() for position key generation - // Because positions are created with the recipient's address in Mint function, - // and we need to access the same position that was originally created. - // GetPrevAddr() would return the position contract address, but the actual - // position is stored under the recipient's address key. - positionKey, err := getPositionKey(tickLower, tickUpper) - if err != nil { - panic(err) - } - - position := pool.mustGetPosition(positionKey) - - var amount0, amount1 *u256.Uint - - // Smallest of three: amount0Requested, position.tokensOwed0, pool.balances.token0 - amount0Req := u256.MustFromDecimal(amount0Requested) - amount0 = collectToken(amount0Req, position.tokensOwed0, pool.BalanceToken0()) - - amount1Req := u256.MustFromDecimal(amount1Requested) - amount1 = collectToken(amount1Req, position.tokensOwed1, pool.BalanceToken1()) - - if amount0.Gt(u256.Zero()) { - tokenOwed0, overflow := u256.Zero().SubOverflow(position.tokensOwed0, amount0) - if overflow { - panic(errOverFlow) - } - - token0Balance, overflow := u256.Zero().SubOverflow(pool.balances.token0, amount0) - if overflow { - panic(errOverFlow) - } - - position.tokensOwed0 = tokenOwed0 - pool.balances.token0 = token0Balance - if err := common.Approve(cross, pool.token0Path, positionAddr, safeConvertToInt64(amount0)); err != nil { - panic(err) - } - } - if amount1.Gt(u256.Zero()) { - position.tokensOwed1 = u256.Zero().Sub(position.tokensOwed1, amount1) - pool.balances.token1 = u256.Zero().Sub(pool.balances.token1, amount1) - - if err := common.Approve(cross, pool.token1Path, positionAddr, safeConvertToInt64(amount1)); err != nil { - panic(err) - } - } - - pool.setPosition(positionKey, position) - - return amount0.ToString(), amount1.ToString() -} - -// collectToken calculates the actual amount of tokens that can be collected. -// It returns the minimum of: requested amount, tokens owed, and pool balance. -// This ensures collection never exceeds available funds. -func collectToken( - amountReq, tokensOwed, poolBalance *u256.Uint, -) (amount *u256.Uint) { - // find smallest of three amounts - amount = u256Min(amountReq, tokensOwed) - amount = u256Min(amount, poolBalance) - return amount.Clone() -} - -// CollectProtocol collects accumulated protocol fees from swap operations. -// Only callable by admin or governance. -// Returns amount0, amount1 representing protocol fees collected. -func CollectProtocol( - cur realm, - token0Path string, - token1Path string, - fee uint32, - recipient std.Address, - amount0Requested string, // uint128 - amount1Requested string, // uint128 -) (string, string) { - halt.AssertIsNotHaltedPool() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - common.MustRegistered(token0Path) - common.MustRegistered(token1Path) - - amount0, amount1 := collectProtocol( - token0Path, - token1Path, - fee, - recipient, - amount0Requested, - amount1Requested, - ) - - previousRealm := std.PreviousRealm() - std.Emit( - "CollectProtocol", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "token0Path", token0Path, - "token1Path", token1Path, - "fee", formatUint(fee), - "recipient", recipient.String(), - "internal_amount0", amount0, - "internal_amount1", amount1, - ) - - return amount0, amount1 -} - -// collectProtocol performs the actual protocol fee collection. -// It ensures requested amounts don't exceed available protocol fees. -// Returns amount0, amount1 as strings representing collected fees. -func collectProtocol( - token0Path string, - token1Path string, - fee uint32, - recipient std.Address, - amount0Requested string, - amount1Requested string, -) (string, string) { - pool := mustGetPoolBy(token0Path, token1Path, fee) - - amount0Req := u256.MustFromDecimal(amount0Requested) - amount1Req := u256.MustFromDecimal(amount1Requested) - if amount0Req.IsZero() && amount1Req.IsZero() { - return "0", "0" - } - - amount0 := u256Min(amount0Req, pool.ProtocolFeesToken0()) - amount1 := u256Min(amount1Req, pool.ProtocolFeesToken1()) - - amount0, amount1 = pool.saveProtocolFees(amount0.Clone(), amount1.Clone()) - uAmount0 := safeConvertToInt64(amount0) - uAmount1 := safeConvertToInt64(amount1) - - checkTransferError(common.Transfer(cross, pool.token0Path, recipient, uAmount0)) - newBalanceToken0, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount0, true) - if err != nil { - panic(err) - } - pool.balances.token0 = newBalanceToken0 - - checkTransferError(common.Transfer(cross, pool.token1Path, recipient, uAmount1)) - newBalanceToken1, err := updatePoolBalance(pool.BalanceToken0(), pool.BalanceToken1(), amount1, false) - if err != nil { - panic(err) - } - pool.balances.token1 = newBalanceToken1 - - return amount0.ToString(), amount1.ToString() -} - -// saveProtocolFees updates the protocol fee balances after collection. -// Returns amount0, amount1 representing the fees deducted from protocol reserves. -func (p *Pool) saveProtocolFees(amount0, amount1 *u256.Uint) (*u256.Uint, *u256.Uint) { - p.protocolFees.token0 = u256.Zero().Sub(p.ProtocolFeesToken0(), amount0) - p.protocolFees.token1 = u256.Zero().Sub(p.ProtocolFeesToken1(), amount1) - - return amount0, amount1 -} diff --git a/contract/r/gnoswap/v1/pool/pool_type.gno b/contract/r/gnoswap/v1/pool/pool_type.gno deleted file mode 100644 index cc2a0ab..0000000 --- a/contract/r/gnoswap/v1/pool/pool_type.gno +++ /dev/null @@ -1,272 +0,0 @@ -package pool - -import ( - "strconv" - "strings" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -type Balances struct { - // current balance of the pool in token0/token1 - token0 *u256.Uint - token1 *u256.Uint -} - -func newBalances() Balances { - return Balances{ - token0: u256.Zero(), - token1: u256.Zero(), - } -} - -type Slot0 struct { - sqrtPriceX96 *u256.Uint // current price of the pool as a sqrt(token1/token0) Q96 value - tick int32 // current tick of the pool, i.e according to the last tick transition that was run - feeProtocol uint8 // protocol fee for both tokens of the pool - unlocked bool // whether the pool is currently locked to reentrancy -} - -func (s *Slot0) Tick() int32 { return s.tick } -func (s *Slot0) FeeProtocol() uint8 { return s.feeProtocol } - -func newSlot0( - sqrtPriceX96 *u256.Uint, - tick int32, - feeProtocol uint8, - unlocked bool, -) Slot0 { - return Slot0{ - sqrtPriceX96: sqrtPriceX96, - tick: tick, - feeProtocol: feeProtocol, - unlocked: unlocked, - } -} - -type ProtocolFees struct { - // current protocol fees of the pool in token0/token1 - token0 *u256.Uint - token1 *u256.Uint -} - -func newProtocolFees() ProtocolFees { - return ProtocolFees{ - token0: u256.Zero(), - token1: u256.Zero(), - } -} - -// type Pool describes a single Pool's state -// A pool is identificed with a unique key (token0, token1, fee), where token0 < token1 -type Pool struct { - // token0/token1 path of the pool - token0Path string - token1Path string - fee uint32 // fee tier of the pool - tickSpacing int32 // spacing between ticks - slot0 Slot0 - balances Balances // balances of the pool - protocolFees ProtocolFees - maxLiquidityPerTick *u256.Uint // the maximum amount of liquidity that can be added per tick - feeGrowthGlobal0X128 *u256.Uint // uint256 - feeGrowthGlobal1X128 *u256.Uint // uint256 - liquidity *u256.Uint // total amount of liquidity in the pool - ticks *avl.Tree // tick(int32) -> TickInfo - tickBitmaps *avl.Tree // tick(wordPos)(int16) -> bitMap(tickWord ^ mask)(*u256.Uint) - positions *avl.Tree // maps the key (caller, lower tick, upper tick) to a unique position - - observation *Observation -} - -func (p *Pool) PoolPath() string { return GetPoolPath(p.token0Path, p.token1Path, p.fee) } -func (p *Pool) Token0Path() string { return p.token0Path } -func (p *Pool) Token1Path() string { return p.token1Path } -func (p *Pool) Fee() uint32 { return p.fee } -func (p *Pool) BalanceToken0() *u256.Uint { return p.balances.token0 } -func (p *Pool) BalanceToken1() *u256.Uint { return p.balances.token1 } -func (p *Pool) TickSpacing() int32 { return p.tickSpacing } -func (p *Pool) MaxLiquidityPerTick() *u256.Uint { return p.maxLiquidityPerTick } -func (p *Pool) Slot0() Slot0 { return p.slot0 } -func (p *Pool) Slot0SqrtPriceX96() *u256.Uint { return p.slot0.sqrtPriceX96 } -func (p *Pool) Slot0Tick() int32 { return p.slot0.tick } -func (p *Pool) Slot0FeeProtocol() uint8 { return p.slot0.feeProtocol } -func (p *Pool) Slot0Unlocked() bool { return p.slot0.unlocked } -func (p *Pool) FeeGrowthGlobal0X128() *u256.Uint { return p.feeGrowthGlobal0X128 } -func (p *Pool) FeeGrowthGlobal1X128() *u256.Uint { return p.feeGrowthGlobal1X128 } -func (p *Pool) ProtocolFeesToken0() *u256.Uint { return p.protocolFees.token0 } -func (p *Pool) ProtocolFeesToken1() *u256.Uint { return p.protocolFees.token1 } -func (p *Pool) Liquidity() *u256.Uint { return p.liquidity } -func (p *Pool) Observation() *Observation { return p.observation } - -func (p *Pool) Ticks() string { - if p.ticks == nil { - return "[]" - } - - tickInfoStrings := []string{} - - p.ticks.Iterate("", "", func(tickKey string, tickValue any) bool { - tick, err := strconv.ParseInt(tickKey, 10, 32) - if err != nil { - panic(err) - } - tickInfo, ok := tickValue.(TickInfo) - if !ok { - panic("failed to cast tick info to TickInfo") - } - - tickInfoStrings = append(tickInfoStrings, ufmt.Sprintf( - `{"tick":%d,"feeGrowthOutside0X128":"%s","feeGrowthOutside1X128":"%s"}`, - tick, - tickInfo.feeGrowthOutside0X128.ToString(), - tickInfo.feeGrowthOutside1X128.ToString(), - )) - - return false - }) - - return "[" + strings.Join(tickInfoStrings, ",") + "]" -} - -func (p *Pool) Clone() *Pool { - ticks := avl.NewTree() - tickBitmaps := avl.NewTree() - positions := avl.NewTree() - - // clone ticks - p.ticks.Iterate("", "", func(tickKey string, tickValue any) bool { - tickInfo, ok := tickValue.(TickInfo) - if !ok { - panic(ufmt.Sprintf("failed to cast tickValue to TickInfo: %T", tickValue)) - } - - ticks.Set(tickKey, TickInfo{ - feeGrowthOutside0X128: u256.Zero().Set(tickInfo.feeGrowthOutside0X128), - feeGrowthOutside1X128: u256.Zero().Set(tickInfo.feeGrowthOutside1X128), - liquidityGross: u256.Zero().Set(tickInfo.liquidityGross), - liquidityNet: i256.Zero().Set(tickInfo.liquidityNet), - tickCumulativeOutside: tickInfo.tickCumulativeOutside, - secondsPerLiquidityOutsideX128: u256.Zero().Set(tickInfo.secondsPerLiquidityOutsideX128), - secondsOutside: tickInfo.secondsOutside, - initialized: tickInfo.initialized, - }) - return false - }) - - // clone tickBitmaps - p.tickBitmaps.Iterate("", "", func(tickKey string, tickValue any) bool { - tickBitmap, ok := tickValue.(*u256.Uint) - if !ok { - panic(ufmt.Sprintf("failed to cast tickValue to *u256.Uint: %T", tickValue)) - } - tickBitmaps.Set(tickKey, u256.Zero().Set(tickBitmap)) - return false - }) - - // clone positions - p.positions.Iterate("", "", func(positionKey string, positionValue any) bool { - positionInfo, ok := positionValue.(PositionInfo) - if !ok { - panic(ufmt.Sprintf("failed to cast positionValue to PositionInfo: %T", positionValue)) - } - positions.Set(positionKey, PositionInfo{ - liquidity: u256.Zero().Set(positionInfo.liquidity), - feeGrowthInside0LastX128: u256.Zero().Set(positionInfo.feeGrowthInside0LastX128), - feeGrowthInside1LastX128: u256.Zero().Set(positionInfo.feeGrowthInside1LastX128), - tokensOwed0: u256.Zero().Set(positionInfo.tokensOwed0), - tokensOwed1: u256.Zero().Set(positionInfo.tokensOwed1), - }) - return false - }) - - return &Pool{ - token0Path: p.token0Path, - token1Path: p.token1Path, - fee: p.fee, - tickSpacing: p.tickSpacing, - slot0: Slot0{ - sqrtPriceX96: u256.Zero().Set(p.slot0.sqrtPriceX96), - tick: p.slot0.tick, - feeProtocol: p.slot0.feeProtocol, - unlocked: p.slot0.unlocked, - }, - balances: Balances{ - token0: u256.Zero().Set(p.balances.token0), - token1: u256.Zero().Set(p.balances.token1), - }, - protocolFees: ProtocolFees{ - token0: u256.Zero().Set(p.protocolFees.token0), - token1: u256.Zero().Set(p.protocolFees.token1), - }, - maxLiquidityPerTick: u256.Zero().Set(p.maxLiquidityPerTick), - feeGrowthGlobal0X128: u256.Zero().Set(p.feeGrowthGlobal0X128), - feeGrowthGlobal1X128: u256.Zero().Set(p.feeGrowthGlobal1X128), - liquidity: u256.Zero().Set(p.liquidity), - ticks: ticks, - tickBitmaps: tickBitmaps, - positions: positions, - observation: &Observation{ - lastCumulativeUpdateTime: p.observation.lastCumulativeUpdateTime, - tickCumulative: p.observation.tickCumulative, - liquidityCumulative: u256.Zero().Set(p.observation.liquidityCumulative), - }, - } -} - -func (p *Pool) calculateTickCumulative(currentTime int64) (int64, *u256.Uint, int64) { - // calculate time delta - observation := p.Observation() - timeDelta := currentTime - observation.lastCumulativeUpdateTime - if timeDelta <= 0 { - return observation.tickCumulative, observation.liquidityCumulative, observation.lastCumulativeUpdateTime - } - - // update cumulative values (only price is used) - // sqrtPriceCumulativeX96 = sqrtPriceCumulativeX96 + sqrtPriceX96 * timeDelta - tickDelta := int64(p.slot0.Tick()) * timeDelta - - // liquidityCumulative = liquidityCumulative + liquidity * timeDelta - liquidityDelta := u256.Zero().Mul(p.liquidity, u256.NewUintFromInt64(timeDelta)) - - tickCumulative := observation.tickCumulative + tickDelta - liquidityCumulative := u256.Zero().Add(observation.liquidityCumulative, liquidityDelta) - lastCumulativeUpdateTime := currentTime - - return tickCumulative, liquidityCumulative, lastCumulativeUpdateTime -} - -// updatePriceCumulatives updates cumulative price values when pool price changes -// This should be called whenever the pool's price changes (swap, mint, burn) -func (p *Pool) updatePriceCumulatives(currentTime int64) { - if currentTime < p.observation.lastCumulativeUpdateTime { - return - } - - tickCumulative, liquidityCumulative, lastCumulativeUpdateTime := p.calculateTickCumulative(currentTime) - - p.observation.liquidityCumulative = liquidityCumulative - p.observation.tickCumulative = tickCumulative - p.observation.lastCumulativeUpdateTime = lastCumulativeUpdateTime -} - -type Observation struct { - lastCumulativeUpdateTime int64 // last cumulative update time (in seconds) - tickCumulative int64 // cumulative tick - liquidityCumulative *u256.Uint // cumulative liquidity (time-weighted average calculation) -} - -func (t *Observation) LastCumulativeUpdateTime() int64 { return t.lastCumulativeUpdateTime } -func (t *Observation) TickCumulative() int64 { return t.tickCumulative } -func (t *Observation) LiquidityCumulative() string { return t.liquidityCumulative.ToString() } - -func newObservation(currentTime int64) *Observation { - return &Observation{ - lastCumulativeUpdateTime: currentTime, - tickCumulative: 0, - liquidityCumulative: u256.Zero(), - } -} diff --git a/contract/r/gnoswap/v1/pool/position.gno b/contract/r/gnoswap/v1/pool/position.gno deleted file mode 100644 index 5b1f330..0000000 --- a/contract/r/gnoswap/v1/pool/position.gno +++ /dev/null @@ -1,397 +0,0 @@ -package pool - -import ( - "encoding/base64" - - "gno.land/p/nt/ufmt" - plp "gno.land/p/gnoswap/gnsmath" - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/v1/common" -) - -const positionPackagePath = "gno.land/r/gnoswap/v1/position" - -var convertedQ128 = u256.MustFromDecimal(Q128) - -// getPositionKey generates a unique base64-encoded key for a liquidity position. -// -// Creates deterministic identifier for position tracking. -// Ensures unique positions per owner and price range. -// Used internally for position state management. -// -// Parameters: -// - tickLower: Lower boundary tick of position range -// - tickUpper: Upper boundary tick of position range -// -// Key Format: -// -// base64(position_package_path + tickLower_bytes + tickUpper_bytes) -// -// Requirements: -// - tickLower < tickUpper -// - Both ticks within valid range [-887272, 887272] -// -// Returns base64-encoded position key or error. -// Combines position package path, tickLower, and tickUpper. -func getPositionKey( - tickLower int32, - tickUpper int32, -) (string, error) { - if err := validateTicks(tickLower, tickUpper); err != nil { - return "", err - } - - positionKey := ufmt.Sprintf("%s__%d__%d", positionPackagePath, tickLower, tickUpper) - encodedPositionKey := base64.StdEncoding.EncodeToString([]byte(positionKey)) - return encodedPositionKey, nil -} - -// positionUpdate updates a position's liquidity and calculates fees owed. -// Returns the updated position information and any error. -func positionUpdate( - position PositionInfo, - liquidityDelta *i256.Int, - feeGrowthInside0X128 *u256.Uint, - feeGrowthInside1X128 *u256.Uint, -) (PositionInfo, error) { - position.valueOrZero() - - if position.liquidity.IsZero() && liquidityDelta.IsZero() { - return PositionInfo{}, makeErrorWithDetails( - errZeroLiquidity, - "both liquidityDelta and current position's liquidity are zero", - ) - } - - // check negative liquidity - if liquidityDelta.IsNeg() { - // absolute value of negative liquidity delta must be less than current liquidity - absDelta := i256.Zero().Set(liquidityDelta).Abs() - currentLiquidity := position.liquidity - if absDelta.Gt(currentLiquidity) { - return PositionInfo{}, makeErrorWithDetails( - errZeroLiquidity, - ufmt.Sprintf("liquidity delta(%s) is greater than current liquidity(%s)", - liquidityDelta.ToString(), position.liquidity.ToString()), - ) - } - } - - var liquidityNext *u256.Uint - if liquidityDelta.IsZero() { - liquidityNext = position.liquidity - } else { - liquidityNext = liquidityMathAddDelta(position.liquidity, liquidityDelta) - } - - // validate negative feeGrowth before calculation - diff0 := u256.Zero().Sub(feeGrowthInside0X128, position.feeGrowthInside0LastX128) - diff1 := u256.Zero().Sub(feeGrowthInside1X128, position.feeGrowthInside1LastX128) - - // calculate tokensOwed - tokensOwed0 := u256.Zero() - if !diff0.IsZero() { - tokensOwed0 = u256.MulDiv(diff0, position.liquidity, convertedQ128) - } - - tokensOwed1 := u256.Zero() - if !diff1.IsZero() { - tokensOwed1 = u256.MulDiv(diff1, position.liquidity, convertedQ128) - } - - if !liquidityDelta.IsZero() { - position.liquidity = liquidityNext - } - - position.feeGrowthInside0LastX128 = feeGrowthInside0X128 - position.feeGrowthInside1LastX128 = feeGrowthInside1X128 - - // add tokensOwed only when it's greater than 0 - if tokensOwed0.Gt(zero) || tokensOwed1.Gt(zero) { - owed0, overflow := u256.Zero().AddOverflow(position.tokensOwed0, tokensOwed0) - if overflow { - return PositionInfo{}, errOverFlow - } - owed1, overflow := u256.Zero().AddOverflow(position.tokensOwed1, tokensOwed1) - if overflow { - return PositionInfo{}, errOverFlow - } - - position.tokensOwed0 = owed0 - position.tokensOwed1 = owed1 - } - - return position, nil -} - -// calculateToken0Amount calculates the amount of token0 based on price range and liquidity delta. -func calculateToken0Amount(sqrtPriceLower, sqrtPriceUpper *u256.Uint, liquidityDelta *i256.Int) *i256.Int { - return plp.GetAmount0Delta(sqrtPriceLower, sqrtPriceUpper, liquidityDelta) -} - -// calculateToken1Amount calculates the amount of token1 based on price range and liquidity delta. -func calculateToken1Amount(sqrtPriceLower, sqrtPriceUpper *u256.Uint, liquidityDelta *i256.Int) *i256.Int { - return plp.GetAmount1Delta(sqrtPriceLower, sqrtPriceUpper, liquidityDelta) -} - -// PositionLiquidity returns the liquidity of a position. -func (p *Pool) PositionLiquidity(key string) *u256.Uint { - return p.mustGetPosition(key).liquidity -} - -// PositionFeeGrowthInside0LastX128 returns the fee growth of token0 inside a position. -func (p *Pool) PositionFeeGrowthInside0LastX128(key string) *u256.Uint { - return p.mustGetPosition(key).feeGrowthInside0LastX128 -} - -// PositionFeeGrowthInside1LastX128 returns the fee growth of token1 inside a position. -func (p *Pool) PositionFeeGrowthInside1LastX128(key string) *u256.Uint { - return p.mustGetPosition(key).feeGrowthInside1LastX128 -} - -// PositionTokensOwed0 returns the amount of token0 owed by a position. -func (p *Pool) PositionTokensOwed0(key string) *u256.Uint { - return p.mustGetPosition(key).tokensOwed0 -} - -// PositionTokensOwed1 returns the amount of token1 owed by a position. -func (p *Pool) PositionTokensOwed1(key string) *u256.Uint { - return p.mustGetPosition(key).tokensOwed1 -} - -// GetPosition returns the position info for a given key. -func (p *Pool) GetPosition(key string) (PositionInfo, bool) { - iPositionInfo, exist := p.positions.Get(key) - if !exist { - newPosition := PositionInfo{} - newPosition.valueOrZero() - return newPosition, false - } - - positionInfo, ok := iPositionInfo.(PositionInfo) - if !ok { - panic(ufmt.Sprintf("failed to cast iPositionInfo to PositionInfo: %T", iPositionInfo)) - } - return positionInfo, true -} - -// positionUpdateWithKey updates a position in the pool and returns the updated position. -func (p *Pool) positionUpdateWithKey( - positionKey string, - liquidityDelta *i256.Int, - feeGrowthInside0X128, feeGrowthInside1X128 *u256.Uint, -) (PositionInfo, error) { - // if pointer is nil, set to zero for calculation - liquidityDelta = liquidityDelta.NilToZero() - feeGrowthInside0X128 = feeGrowthInside0X128.NilToZero() - feeGrowthInside1X128 = feeGrowthInside1X128.NilToZero() - - // if position does not exist, create a new position - positionToUpdate, _ := p.GetPosition(positionKey) - positionAfterUpdate, err := positionUpdate(positionToUpdate, liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128) - if err != nil { - return PositionInfo{}, err - } - - p.setPosition(positionKey, positionAfterUpdate) - - return positionAfterUpdate, nil -} - -// setPosition sets the position info for a given key. -func (p *Pool) setPosition(posKey string, positionInfo PositionInfo) { - p.positions.Set(posKey, positionInfo) -} - -// mustGetPosition returns the position info for a given key. -func (p *Pool) mustGetPosition(positionKey string) PositionInfo { - positionInfo, exist := p.GetPosition(positionKey) - if !exist { - panic(newErrorWithDetail( - errDataNotFound, - ufmt.Sprintf("positionKey(%s) does not exist", positionKey), - )) - } - return positionInfo -} - -// modifyPosition updates a position in the pool and calculates the amount of tokens -// needed (for minting) or returned (for burning). The calculation depends on the current -// price (tick) relative to the position's price range. -// -// The function handles three cases: -// 1. Current price below range (tick < tickLower): only token0 is used/returned -// 2. Current price in range (tickLower <= tick < tickUpper): both tokens are used/returned -// 3. Current price above range (tick >= tickUpper): only token1 is used/returned -// -// Parameters: -// - params: ModifyPositionParams containing owner, tickLower, tickUpper, and liquidityDelta -// -// Returns: -// - PositionInfo: updated position information -// - *u256.Uint: amount of token0 needed/returned -// - *u256.Uint: amount of token1 needed/returned -func (p *Pool) modifyPosition(params ModifyPositionParams) (PositionInfo, *u256.Uint, *u256.Uint, error) { - if err := validateTicks(params.tickLower, params.tickUpper); err != nil { - return PositionInfo{}, zero, zero, err - } - - // get current state and price bounds - tick := p.Slot0Tick() - // update position state - position, err := p.updatePosition(params, tick) - if err != nil { - return PositionInfo{}, zero, zero, err - } - - liqDelta := params.liquidityDelta - - amount0, amount1 := i256.Zero(), i256.Zero() - - // covert ticks to sqrt price to use in amount calculations - // price = 1.0001^tick, but we use sqrtPriceX96 - sqrtRatioLower := common.TickMathGetSqrtRatioAtTick(params.tickLower) - sqrtRatioUpper := common.TickMathGetSqrtRatioAtTick(params.tickUpper) - sqrtPriceX96 := p.Slot0SqrtPriceX96() - - // calculate token amounts based on current price position relative to range - switch { - case tick < params.tickLower: - // case 1 - // full range between lower and upper tick is used for token0 - // current tick is below the passed range; liquidity can only become in range by crossing from left to - // right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it - amount0 = calculateToken0Amount(sqrtRatioLower, sqrtRatioUpper, liqDelta) - - case tick < params.tickUpper: - // case 2 - liquidityBefore := p.liquidity - // token0 used from current price to upper tick - amount0 = calculateToken0Amount(sqrtPriceX96, sqrtRatioUpper, liqDelta) - // token1 used from lower tick to current price - amount1 = calculateToken1Amount(sqrtRatioLower, sqrtPriceX96, liqDelta) - // update pool's active liquidity since price is in range - p.liquidity = liquidityMathAddDelta(liquidityBefore, liqDelta) - - default: - // case 3 - // full range between lower and upper tick is used for token1 - // current tick is above the passed range; liquidity can only become in range by crossing from right to - // left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it - amount1 = calculateToken1Amount(sqrtRatioLower, sqrtRatioUpper, liqDelta) - } - - return position, amount0.Abs(), amount1.Abs(), nil -} - -// updatePosition modifies the position's liquidity and updates the corresponding tick states. -// -// This function updates the position data based on the specified liquidity delta and tick range. -// It also manages the fee growth, tick state flipping, and cleanup of unused tick data. -// -// Parameters: -// - positionParams: ModifyPositionParams, the parameters for the position modification, which include: -// - owner: The address of the position owner. -// - tickLower: The lower tick boundary of the position. -// - tickUpper: The upper tick boundary of the position. -// - liquidityDelta: The change in liquidity (positive or negative). -// - tick: int32, the current tick position. -// -// Returns: -// - PositionInfo: The updated position information. -// -// Workflow: -// 1. Clone the global fee growth values (token 0 and token 1). -// 2. If the liquidity delta is non-zero: -// - Update the lower and upper ticks using `tickUpdate`, flipping their states if necessary. -// - If a tick's state was flipped, update the tick bitmap to reflect the new state. -// 3. Calculate the fee growth inside the tick range using `getFeeGrowthInside`. -// 4. Generate a unique position key and update the position data using `positionUpdateWithKey`. -// 5. If liquidity is being removed (negative delta), clean up unused tick data by deleting the tick entries. -// 6. Return the updated position. -// -// Notes: -// - The function flips the tick states and cleans up unused tick data when liquidity is removed. -// - It ensures fee growth and position data remain accurate after the update. -// -// Example Usage: -// -// ```gno -// -// updatedPosition := pool.updatePosition(positionParams, currentTick) -// println("Updated Position Info:", updatedPosition) -// -// ``` -func (p *Pool) updatePosition(positionParams ModifyPositionParams, tick int32) (PositionInfo, error) { - feeGrowthGlobal0X128 := p.FeeGrowthGlobal0X128().Clone() - feeGrowthGlobal1X128 := p.FeeGrowthGlobal1X128().Clone() - - var flippedLower, flippedUpper bool - - if !positionParams.liquidityDelta.IsZero() { - flippedLower = p.tickUpdate( - positionParams.tickLower, - tick, - positionParams.liquidityDelta, - feeGrowthGlobal0X128, - feeGrowthGlobal1X128, - false, - p.maxLiquidityPerTick, - ) - - flippedUpper = p.tickUpdate( - positionParams.tickUpper, - tick, - positionParams.liquidityDelta, - feeGrowthGlobal0X128, - feeGrowthGlobal1X128, - true, - p.maxLiquidityPerTick, - ) - - if flippedLower { - p.tickBitmapFlipTick(positionParams.tickLower, p.tickSpacing) - } - - if flippedUpper { - p.tickBitmapFlipTick(positionParams.tickUpper, p.tickSpacing) - } - } - - feeGrowthInside0X128, feeGrowthInside1X128 := p.getFeeGrowthInside( - positionParams.tickLower, - positionParams.tickUpper, - tick, - feeGrowthGlobal0X128, - feeGrowthGlobal1X128, - ) - - positionKey, err := getPositionKey(positionParams.tickLower, positionParams.tickUpper) - if err != nil { - return PositionInfo{}, err - } - - position, err := p.positionUpdateWithKey( - positionKey, - positionParams.liquidityDelta, - feeGrowthInside0X128.Clone(), - feeGrowthInside1X128.Clone(), - ) - if err != nil { - return PositionInfo{}, err - } - - // clear any tick data that is no longer needed - if positionParams.liquidityDelta.IsNeg() { - if flippedLower { - p.deleteTick(positionParams.tickLower) - } - if flippedUpper { - p.deleteTick(positionParams.tickUpper) - } - } - - return position, nil -} diff --git a/contract/r/gnoswap/v1/pool/protocol_fee.gno b/contract/r/gnoswap/v1/pool/protocol_fee.gno deleted file mode 100644 index ce62657..0000000 --- a/contract/r/gnoswap/v1/pool/protocol_fee.gno +++ /dev/null @@ -1,199 +0,0 @@ -package pool - -import ( - "std" - - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" - pf "gno.land/r/gnoswap/v1/protocol_fee" -) - -var ( - // poolCreationFee is the fee that is charged when a user creates a pool. - // The fee is denominated in GNS tokens. - // This parameter can be modified through governance. - poolCreationFee = int64(100_000_000) // 100_GNS - - // withdrawalFeeBPS is the fee that is charged when a user withdraws their collected fees - // The fee is denominated in BPS (Basis Points) - // Example: 100 BPS = 1% - // This parameter can be modified through governance. - withdrawalFeeBPS = uint64(100) // 1% -) - -const ( - MaxBpsValue = uint64(10000) - ZeroBps = uint64(0) -) - -// HandleWithdrawalFee withdraws the fee from the user and returns the amount after the fee -// Only position contract can call this function -// Input: -// - positionId: the id of the LP token -// - token0Path: the path of the token0 -// - amount0: the amount of token0 -// - token1Path: the path of the token1 -// - amount1: the amount of token1 -// - poolPath: the path of the pool -// - positionCaller: the original caller of the position contract -// Output: -// - the amount of token0 after the fee -// - the amount of token1 after the fee -func HandleWithdrawalFee( - cur realm, - positionId uint64, - token0Path string, - amount0 string, // uint256 - token1Path string, - amount1 string, // uint256 - poolPath string, - positionCaller std.Address, -) (string, string) { // uint256 x2 - halt.AssertIsNotHaltedPool() - halt.AssertIsNotHaltedWithdraw() - - // only position contract can call this function - caller := std.PreviousRealm().Address() - access.AssertIsPosition(caller) - - common.MustRegistered(token0Path) - common.MustRegistered(token1Path) - - fee := GetWithdrawalFee() - if fee == ZeroBps { - return amount0, amount1 - } - - feeAmount0, afterAmount0 := calculateAmountWithFee(u256.MustFromDecimal(amount0), u256.NewUint(fee)) - feeAmount1, afterAmount1 := calculateAmountWithFee(u256.MustFromDecimal(amount1), u256.NewUint(fee)) - - checkTransferError(common.TransferFrom(cross, token0Path, positionCaller, protocolFeeAddr, safeConvertToInt64(feeAmount0))) - pf.AddToProtocolFee(cross, token0Path, safeConvertToInt64(feeAmount0)) - checkTransferError(common.TransferFrom(cross, token1Path, positionCaller, protocolFeeAddr, safeConvertToInt64(feeAmount1))) - pf.AddToProtocolFee(cross, token1Path, safeConvertToInt64(feeAmount1)) - - previousRealm := std.PreviousRealm() - std.Emit( - "WithdrawalFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "lpTokenId", formatUint(positionId), - "poolPath", poolPath, - "feeAmount0", feeAmount0.ToString(), - "feeAmount1", feeAmount1.ToString(), - "amount0WithoutFee", afterAmount0.ToString(), - "amount1WithoutFee", afterAmount1.ToString(), - ) - - return afterAmount0.ToString(), afterAmount1.ToString() -} - -// GetPoolCreationFee returns the poolCreationFee -func GetPoolCreationFee() int64 { - return poolCreationFee -} - -// SetPoolCreationFee sets the poolCreationFee. -// Only admin or governance can call this function. -func SetPoolCreationFee(cur realm, fee int64) { - halt.AssertIsNotHaltedPool() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - prevPoolCreationFee := GetPoolCreationFee() - err := setPoolCreationFee(fee) - if err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "SetPoolCreationFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "prevFee", formatInt(prevPoolCreationFee), - "newFee", formatInt(fee), - ) -} - -// GetWithdrawalFee returns the withdrawal fee -func GetWithdrawalFee() uint64 { - return withdrawalFeeBPS -} - -// SetWithdrawalFee sets the withdrawal fee. -// Only admin or governance can call this function. -func SetWithdrawalFee(cur realm, fee uint64) { - halt.AssertIsNotHaltedPool() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - prevWithdrawalFee := GetWithdrawalFee() - - err := setWithdrawalFee(fee) - if err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "SetWithdrawalFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "prevFee", formatUint(prevWithdrawalFee), - "newFee", formatUint(fee), - ) -} - -// calculateAmountWithFee calculates the fee amount and the amount after the fee -// -// Inputs: -// - amount: the amount before the fee -// - fee: the fee in BPS -// -// Outputs: -// - the fee amount -// - the amount after the fee applied -func calculateAmountWithFee(amount, fee *u256.Uint) (feeAmount, afterAmount *u256.Uint) { - feeAmount = u256.Zero().Mul(amount, fee) - feeAmount = u256.Zero().Div(feeAmount, u256.NewUint(MaxBpsValue)) - afterAmount = u256.Zero().Sub(amount, feeAmount) - return -} - -// setPoolCreationFee this function is internal function called by SetPoolCreationFee -// And SetPoolCreationFee -func setPoolCreationFee(fee int64) error { - if fee < 0 { - return makeErrorWithDetails( - errInvalidInput, - "pool creation fee cannot be negative", - ) - } - - // update pool creation fee - poolCreationFee = fee - - return nil -} - -// setWithdrawalFee this function is internal function called by SetWithdrawalFee -// function and SetWithdrawalFee function -func setWithdrawalFee(fee uint64) error { - if fee > MaxBpsValue { - return makeErrorWithDetails( - errInvalidWithdrawalFeePct, - ufmt.Sprintf("fee(%d) must be in range 0 ~ 10000", fee), - ) - } - - withdrawalFeeBPS = fee - - return nil -} diff --git a/contract/r/gnoswap/v1/pool/swap.gno b/contract/r/gnoswap/v1/pool/swap.gno deleted file mode 100644 index c4c706f..0000000 --- a/contract/r/gnoswap/v1/pool/swap.gno +++ /dev/null @@ -1,647 +0,0 @@ -package pool - -import ( - "std" - "strconv" - "time" - - "gno.land/p/nt/ufmt" - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" - - plp "gno.land/p/gnoswap/gnsmath" - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -const MAX_INT256 string = "57896044618658097711785492504343953926634992332820282019728792003956564819967" - -// Hook functions allow external contracts to be notified of swap events. -var ( - // MUST BE IMMUTABLE. - // DO NOT USE THIS VALUE IN ANY ARITHMETIC OPERATIONS' INITIALIZATION - zero = u256.Zero() - fixedPointQ128 = u256.MustFromDecimal(Q128) - - maxInt256 = u256.MustFromDecimal(MAX_INT256) - maxInt64 = i256.MustFromDecimal(MAX_INT64) - - swapStartHook func(poolPath string, timestamp int64) - tickCrossHook func(poolPath string, tickId int32, zeroForOne bool, timestamp int64) - swapEndHook func(poolPath string) error -) - -// SetTickCrossHook sets the hook function called when a tick is crossed during swaps. -// -// Allows staker to monitor liquidity changes at price levels. -// Used for reward calculation when positions enter/exit range. -// -// Only callable by staker contract. -func SetTickCrossHook(cur realm, hook func(poolPath string, tickId int32, zeroForOne bool, timestamp int64)) { - caller := std.PreviousRealm().Address() - access.AssertIsStaker(caller) - tickCrossHook = hook -} - -// SetSwapStartHook sets the hook function called at the beginning of a swap. -// -// Enables pre-swap state tracking for reward distribution. -// Captures timestamp for time-weighted calculations. -// -// Only callable by staker contract. -func SetSwapStartHook(cur realm, hook func(poolPath string, timestamp int64)) { - caller := std.PreviousRealm().Address() - access.AssertIsStaker(caller) - swapStartHook = hook -} - -// SetSwapEndHook sets the hook function called at the end of a swap. -// -// Finalizes reward calculations after swap completion. -// Allows error propagation to revert invalid swaps. -// -// Only callable by staker contract. -func SetSwapEndHook(cur realm, hook func(poolPath string) error) { - caller := std.PreviousRealm().Address() - access.AssertIsStaker(caller) - swapEndHook = hook -} - -// SwapResult encapsulates all state changes from a swap. -// It ensures atomic state transitions that can be applied at once. -type SwapResult struct { - Amount0 *i256.Int - Amount1 *i256.Int - NewSqrtPrice *u256.Uint - NewTick int32 - NewLiquidity *u256.Uint - NewProtocolFees ProtocolFees - FeeGrowthGlobal0X128 *u256.Uint - FeeGrowthGlobal1X128 *u256.Uint -} - -// SwapComputation encapsulates the pure computation logic for swaps. -type SwapComputation struct { - AmountSpecified *i256.Int - SqrtPriceLimitX96 *u256.Uint - ZeroForOne bool - ExactInput bool - InitialState SwapState - Cache SwapCache -} - -// Swap executes a token swap in the pool. -// -// Parameters: -// - token0Path, token1Path: token contract paths -// - fee: pool fee tier -// - recipient: address receiving output tokens -// - zeroForOne: true for token0→token1, false for token1→token0 -// - amountSpecified: amount to swap (positive=exact in, negative=exact out) -// - sqrtPriceLimitX96: price limit in Q96 format -// - payer: address paying input tokens -// -// Returns amount0, amount1 as decimal strings (negative for tokens out). -// Only callable by whitelisted routers. -// Note: Uses tick-based pricing with Q96 fixed-point math. -func Swap( - cur realm, - token0Path string, - token1Path string, - fee uint32, - recipient std.Address, - zeroForOne bool, - amountSpecified string, - sqrtPriceLimitX96 string, - payer std.Address, // router -) (string, string) { - halt.AssertIsNotHaltedPool() - - caller := std.PreviousRealm().Address() - access.AssertIsSwapWhitelisted(caller) - assertPayerIsPreviousRealmOrOriginCaller(payer) - - if amountSpecified == "0" { - panic(newErrorWithDetail( - errInvalidSwapAmount, - "amountSpecified == 0", - )) - } - - pool := mustGetPoolBy(token0Path, token1Path, fee) - - slot0Start := pool.slot0 - if !slot0Start.unlocked { - panic(errLockedPool) - } - - // no liquidity -> no swap, return zero amounts - if pool.liquidity.IsZero() { - return "0", "0" - } - - pool.slot0.unlocked = false - - // Call swap start hook if set - if swapStartHook != nil { - currentTime := time.Now().Unix() - swapStartHook(pool.PoolPath(), currentTime) - } - - sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96) - validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit) - - amounts := i256.MustFromDecimal(amountSpecified) - feeGrowthGlobalX128 := getFeeGrowthGlobal(pool, zeroForOne) - feeProtocol := getFeeProtocol(slot0Start, zeroForOne) - cache := newSwapCache(feeProtocol, pool.liquidity.Clone()) - state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart.Clone(), slot0Start) - - comp := SwapComputation{ - AmountSpecified: amounts, - SqrtPriceLimitX96: sqrtPriceLimit, - ZeroForOne: zeroForOne, - ExactInput: amounts.Gt(i256.Zero()), - InitialState: state, - Cache: cache, - } - - result, err := computeSwap(pool, comp) - if err != nil { - panic(err) - } - - applySwapResult(pool, result) - - // update TWAP state - currentTime := time.Now().Unix() - pool.updatePriceCumulatives(currentTime) - - // actual swap - pool.swapTransfers(zeroForOne, payer, recipient, result.Amount0, result.Amount1) - - pool.slot0.unlocked = true - // Call swap end hook if set - if swapEndHook != nil { - err := swapEndHook(pool.PoolPath()) - if err != nil { - panic(err) - } - } - - previousRealm := std.PreviousRealm() - std.Emit( - "Swap", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "poolPath", GetPoolPath(token0Path, token1Path, fee), - "zeroForOne", formatBool(zeroForOne), - "requestAmount", amountSpecified, - "sqrtPriceLimitX96", sqrtPriceLimitX96, - "payer", payer.String(), - "recipient", recipient.String(), - "token0Amount", result.Amount0.ToString(), - "token1Amount", result.Amount1.ToString(), - "protocolFee0", pool.protocolFees.token0.ToString(), - "protocolFee1", pool.protocolFees.token1.ToString(), - "sqrtPriceX96", pool.slot0.sqrtPriceX96.ToString(), - "exactIn", strconv.FormatBool(comp.ExactInput), - "currentTick", strconv.FormatInt(int64(pool.Slot0Tick()), 10), - "liquidity", pool.Liquidity().ToString(), - "feeGrowthGlobal0X128", pool.FeeGrowthGlobal0X128().ToString(), - "feeGrowthGlobal1X128", pool.FeeGrowthGlobal1X128().ToString(), - "balanceToken0", pool.BalanceToken0().ToString(), - "balanceToken1", pool.BalanceToken1().ToString(), - "ticks", pool.Ticks(), - "tickCumulative", formatInt(pool.Observation().TickCumulative()), - "liquidityCumulative", pool.Observation().LiquidityCumulative(), - "lastCumulativeUpdateTime", formatInt(pool.Observation().LastCumulativeUpdateTime()), - ) - - return result.Amount0.ToString(), result.Amount1.ToString() -} - -// DrySwap simulates a swap without modifying pool state. -// Returns amount0, amount1 and a success boolean. -// Returns false if pool is locked, has no liquidity, or computation fails. -func DrySwap( - token0Path string, - token1Path string, - fee uint32, - zeroForOne bool, - amountSpecified string, - sqrtPriceLimitX96 string, -) (string, string, bool) { - if amountSpecified == "0" { - return "0", "0", false - } - - pool := mustGetPoolBy(token0Path, token1Path, fee) - - // no liquidity -> simulation fails - if pool.liquidity.IsZero() { - return "0", "0", false - } - - slot0Start := pool.slot0 - sqrtPriceLimit := u256.MustFromDecimal(sqrtPriceLimitX96) - validatePriceLimits(slot0Start, zeroForOne, sqrtPriceLimit) - - amounts := i256.MustFromDecimal(amountSpecified) - feeGrowthGlobalX128 := getFeeGrowthGlobal(pool, zeroForOne) - feeProtocol := getFeeProtocol(slot0Start, zeroForOne) - cache := newSwapCache(feeProtocol, pool.liquidity.Clone()) - state := newSwapState(amounts, feeGrowthGlobalX128, cache.liquidityStart, slot0Start) - - comp := SwapComputation{ - AmountSpecified: amounts, - SqrtPriceLimitX96: sqrtPriceLimit, - ZeroForOne: zeroForOne, - ExactInput: amounts.Gt(i256.Zero()), - InitialState: state, - Cache: cache, - } - - result, err := computeSwap(pool, comp) - if err != nil { - return "0", "0", false - } - - if zeroForOne { - if pool.balances.token1.Lt(result.Amount1.Abs()) { - return "0", "0", false - } - } else { - if pool.balances.token0.Lt(result.Amount0.Abs()) { - return "0", "0", false - } - } - - // Validate non-zero amounts - if result.Amount0.IsZero() || result.Amount1.IsZero() { - return "0", "0", false - } - - return result.Amount0.ToString(), result.Amount1.ToString(), true -} - -// computeSwap performs the core swap computation without modifying pool state. -// The computation continues until either: -// - The entire amount is consumed (amountSpecifiedRemaining = 0) -// - The price limit is reached (sqrtPriceX96 = sqrtPriceLimitX96) -// -// Important: This function is critical for AMM price discovery. It iterates through -// tick ranges, calculating swap amounts and fees for each liquidity segment. -// Returns an error if the computation fails at any step. -func computeSwap(pool *Pool, comp SwapComputation) (*SwapResult, error) { - state := comp.InitialState - var err error - - // Compute swap steps until completion - for shouldContinueSwap(state, comp.SqrtPriceLimitX96) { - state, err = computeSwapStep(state, pool, comp.ZeroForOne, comp.SqrtPriceLimitX96, comp.ExactInput, comp.Cache) - if err != nil { - return nil, err - } - } - - // Calculate final amounts - amount0 := state.amountCalculated - amount1 := i256.Zero().Sub(comp.AmountSpecified, state.amountSpecifiedRemaining) - if comp.ZeroForOne == comp.ExactInput { - amount0, amount1 = amount1, amount0 - } - - // Prepare result - result := &SwapResult{ - Amount0: amount0, - Amount1: amount1, - NewSqrtPrice: state.sqrtPriceX96, - NewTick: state.tick, - NewLiquidity: state.liquidity, - NewProtocolFees: ProtocolFees{ - token0: pool.protocolFees.token0, - token1: pool.protocolFees.token1, - }, - FeeGrowthGlobal0X128: pool.feeGrowthGlobal0X128, - FeeGrowthGlobal1X128: pool.feeGrowthGlobal1X128, - } - - // Update protocol fees if necessary - if comp.ZeroForOne { - if state.protocolFee.Gt(zero) { - result.NewProtocolFees.token0 = u256.Zero().Add(result.NewProtocolFees.token0, state.protocolFee) - } - result.FeeGrowthGlobal0X128 = state.feeGrowthGlobalX128.Clone() - } else { - if state.protocolFee.Gt(zero) { - result.NewProtocolFees.token1 = u256.Zero().Add(result.NewProtocolFees.token1, state.protocolFee) - } - result.FeeGrowthGlobal1X128 = state.feeGrowthGlobalX128.Clone() - } - - return result, nil -} - -// applySwapResult updates pool state with computed results. -// All state changes are applied at once to maintain consistency -func applySwapResult(pool *Pool, result *SwapResult) { - pool.slot0.sqrtPriceX96 = result.NewSqrtPrice - pool.slot0.tick = result.NewTick - pool.liquidity = result.NewLiquidity - pool.protocolFees = result.NewProtocolFees - pool.feeGrowthGlobal0X128 = result.FeeGrowthGlobal0X128 - pool.feeGrowthGlobal1X128 = result.FeeGrowthGlobal1X128 -} - -// validatePriceLimits ensures the provided price limit is valid for the swap direction -// The function enforces that: -// For zeroForOne (selling token0): -// - Price limit must be below current price -// - Price limit must be above MIN_SQRT_RATIO -// -// For !zeroForOne (selling token1): -// - Price limit must be above current price -// - Price limit must be below MAX_SQRT_RATIO -func validatePriceLimits(slot0 Slot0, zeroForOne bool, sqrtPriceLimitX96 *u256.Uint) { - if zeroForOne { - cond1 := sqrtPriceLimitX96.Lt(slot0.sqrtPriceX96) - cond2 := sqrtPriceLimitX96.Gt(minSqrtRatio) - if !(cond1 && cond2) { - panic(newErrorWithDetail( - errPriceOutOfRange, - ufmt.Sprintf("sqrtPriceLimitX96(%s) < slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) > MIN_SQRT_RATIO(%s)", - sqrtPriceLimitX96.ToString(), - slot0.sqrtPriceX96.ToString(), - sqrtPriceLimitX96.ToString(), - MIN_SQRT_RATIO), - )) - } - } else { - cond1 := sqrtPriceLimitX96.Gt(slot0.sqrtPriceX96) - cond2 := sqrtPriceLimitX96.Lt(maxSqrtRatio) - if !(cond1 && cond2) { - panic(newErrorWithDetail( - errPriceOutOfRange, - ufmt.Sprintf("sqrtPriceLimitX96(%s) > slot0Start.sqrtPriceX96(%s) && sqrtPriceLimitX96(%s) < MAX_SQRT_RATIO(%s)", - sqrtPriceLimitX96.ToString(), - slot0.sqrtPriceX96.ToString(), - sqrtPriceLimitX96.ToString(), - MAX_SQRT_RATIO), - )) - } - } -} - -// getFeeProtocol returns the appropriate fee protocol based on zero for one. -// When zeroForOne is true, we want the lower 4 bits (% 16). -// Otherwise, we want the upper 4 bits (/ 16). -func getFeeProtocol(slot0 Slot0, zeroForOne bool) uint8 { - shift := uint8(0) - if !zeroForOne { - shift = 4 - } - return (slot0.feeProtocol >> shift) & uint8(0xF) -} - -// getFeeGrowthGlobal returns the appropriate fee growth global based on zero for one. -func getFeeGrowthGlobal(pool *Pool, zeroForOne bool) *u256.Uint { - if zeroForOne { - return pool.feeGrowthGlobal0X128.Clone() - } - return pool.feeGrowthGlobal1X128.Clone() -} - -// shouldContinueSwap checks if swap should continue based on remaining amount and price limit. -func shouldContinueSwap(state SwapState, sqrtPriceLimitX96 *u256.Uint) bool { - return !state.amountSpecifiedRemaining.IsZero() && !state.sqrtPriceX96.Eq(sqrtPriceLimitX96) -} - -// computeSwapStep executes a single step of swap and returns new state -func computeSwapStep( - state SwapState, - pool *Pool, - zeroForOne bool, - sqrtPriceLimitX96 *u256.Uint, - exactInput bool, - cache SwapCache, -) (SwapState, error) { - step := computeSwapStepInit(state, pool, zeroForOne) - - // determining the price target for this step - sqrtRatioTargetX96 := computeTargetSqrtRatio(step, sqrtPriceLimitX96, zeroForOne).Clone() - - // computing the amounts to be swapped at this step - var ( - newState SwapState - err error - ) - - newState, step = computeAmounts(state, sqrtRatioTargetX96, pool, step) - newState, err = updateAmounts(step, newState, exactInput) - if err != nil { - return state, err - } - - // if the protocol fee is on, calculate how much is owed, - // decrement fee amount, and increment protocol fee - if cache.feeProtocol > 0 { - newState, step, err = updateFeeProtocol(step, cache.feeProtocol, newState) - if err != nil { - return state, err - } - } - - // update global fee tracker - if newState.liquidity.Gt(u256.Zero()) { - update := u256.MulDiv(step.feeAmount, fixedPointQ128, newState.liquidity) - feeGrowthGlobalX128 := u256.Zero().Add(newState.feeGrowthGlobalX128, update) - newState.setFeeGrowthGlobalX128(feeGrowthGlobalX128) - } - - // handling tick transitions - if newState.sqrtPriceX96.Eq(step.sqrtPriceNextX96) { - newState = tickTransition(step, zeroForOne, newState, pool) - } else if newState.sqrtPriceX96.Neq(step.sqrtPriceStartX96) { - newState.setTick(common.TickMathGetTickAtSqrtRatio(newState.sqrtPriceX96)) - } - - return newState, nil -} - -// updateFeeProtocol calculates and updates protocol fees for the current step. -func updateFeeProtocol(step StepComputations, feeProtocol uint8, state SwapState) (SwapState, StepComputations, error) { - delta := u256.Zero().Div(step.feeAmount, u256.NewUint(uint64(feeProtocol))) - - newFeeAmount, overflow := u256.Zero().SubOverflow(step.feeAmount, delta) - if overflow { - return state, step, errUnderflow - } - - step.feeAmount = newFeeAmount - - newProtocolFee, overflow := u256.Zero().AddOverflow(state.protocolFee, delta) - if overflow { - return state, step, errOverFlow - } - state.protocolFee = newProtocolFee - - return state, step, nil -} - -// computeSwapStepInit initializes the computation for a single swap step. -func computeSwapStepInit(state SwapState, pool *Pool, zeroForOne bool) StepComputations { - var step StepComputations - step.sqrtPriceStartX96 = state.sqrtPriceX96 - tickNext, initialized := pool.tickBitmapNextInitializedTickWithInOneWord( - state.tick, - pool.tickSpacing, - zeroForOne, - ) - - step.tickNext = tickNext - step.initialized = initialized - - // prevent overshoot the min/max tick - step.clampTickNext() - // get the price for the next tick - sqrtPrice := common.TickMathGetSqrtRatioAtTick(step.tickNext).ToString() - step.sqrtPriceNextX96 = u256.MustFromDecimal(sqrtPrice) - return step -} - -// computeTargetSqrtRatio determines the target sqrt price for the current swap step. -func computeTargetSqrtRatio(step StepComputations, sqrtPriceLimitX96 *u256.Uint, zeroForOne bool) *u256.Uint { - if shouldUsePriceLimit(step.sqrtPriceNextX96, sqrtPriceLimitX96, zeroForOne) { - return sqrtPriceLimitX96 - } - return step.sqrtPriceNextX96 -} - -// shouldUsePriceLimit returns true if the price limit should be used instead of the next tick price -func shouldUsePriceLimit(sqrtPriceNext, sqrtPriceLimit *u256.Uint, zeroForOne bool) bool { - if zeroForOne { - return sqrtPriceNext.Lt(sqrtPriceLimit) - } - return sqrtPriceNext.Gt(sqrtPriceLimit) -} - -// computeAmounts calculates the input and output amounts for the current swap step. -func computeAmounts(state SwapState, sqrtRatioTargetX96 *u256.Uint, pool *Pool, step StepComputations) (SwapState, StepComputations) { - sqrtPriceX96, amountIn, amountOut, feeAmount := plp.SwapMathComputeSwapStep( - state.sqrtPriceX96, - sqrtRatioTargetX96, - state.liquidity, - state.amountSpecifiedRemaining, - uint64(pool.fee), - ) - - step.amountIn = amountIn - step.amountOut = amountOut - step.feeAmount = feeAmount - - state.setSqrtPriceX96(sqrtPriceX96) - - return state, step -} - -// updateAmounts calculates new remaining and calculated amounts based on the swap step. -// For exact input swaps: -// - Decrements remaining input amount by (amountIn + feeAmount) -// - Decrements calculated amount by amountOut -// -// For exact output swaps: -// - Increments remaining output amount by amountOut -// - Increments calculated amount by (amountIn + feeAmount) -func updateAmounts(step StepComputations, state SwapState, exactInput bool) (SwapState, error) { - amountInWithFeeU256 := u256.Zero().Add(step.amountIn, step.feeAmount) - if amountInWithFeeU256.Gt(maxInt256) { - return state, errOverFlow - } - - amountInWithFee := i256.FromUint256(amountInWithFeeU256) - if step.amountOut.Gt(maxInt256) { - return state, errUnderflow - } - - var ( - amountSpecifiedRemaining *i256.Int - amountCalculated *i256.Int - ) - - if exactInput { - amountSpecifiedRemaining = i256.Zero().Sub(state.amountSpecifiedRemaining, amountInWithFee) - amountCalculated = i256.Zero().Sub(state.amountCalculated, i256.FromUint256(step.amountOut)) - } else { - amountSpecifiedRemaining = i256.Zero().Add(state.amountSpecifiedRemaining, i256.FromUint256(step.amountOut)) - amountCalculated = i256.Zero().Add(state.amountCalculated, amountInWithFee) - } - - // If an overflowed value is stored in state, it may cause problems in the next step - if amountCalculated.Gt(maxInt64) { - return state, errOverFlow - } - - state.amountSpecifiedRemaining = amountSpecifiedRemaining - state.amountCalculated = amountCalculated - - return state, nil -} - -// tickTransition handles the transition between price ticks during a swap -func tickTransition(step StepComputations, zeroForOne bool, state SwapState, pool *Pool) SwapState { - // ensure existing state to keep immutability - newState := state - - if step.initialized { - fee0, fee1 := u256.Zero(), u256.Zero() - - if zeroForOne { - fee0 = state.feeGrowthGlobalX128 - fee1 = pool.feeGrowthGlobal1X128 - } else { - fee0 = pool.feeGrowthGlobal0X128 - fee1 = state.feeGrowthGlobalX128 - } - - liquidityNet := pool.tickCross(step.tickNext, fee0, fee1) - - if zeroForOne { - liquidityNet = i256.Zero().Neg(liquidityNet) - } - - newState.liquidity = liquidityMathAddDelta(state.liquidity, liquidityNet) - - if tickCrossHook != nil { - currentTime := time.Now().Unix() - tickCrossHook(pool.PoolPath(), step.tickNext, zeroForOne, currentTime) - } - } - - decrement := int32(0) - if zeroForOne { - decrement = 1 - } - newState.tick = step.tickNext - decrement - - return newState -} - -// swapTransfers handles token transfers for a swap transaction. -// For zeroForOne swaps: transfers token0 from payer to pool and token1 from pool to recipient. -// For oneForZero swaps: transfers token1 from payer to pool and token0 from pool to recipient. -func (p *Pool) swapTransfers(zeroForOne bool, payer, recipient std.Address, amount0, amount1 *i256.Int) { - if zeroForOne { - // payer > POOL - p.safeTransferFrom(payer, poolAddr, p.token0Path, amount0.Abs(), true) - - // POOL > recipient - p.safeTransfer(recipient, p.token1Path, amount1, false) - } else { - // payer > POOL - p.safeTransferFrom(payer, poolAddr, p.token1Path, amount1.Abs(), false) - // POOL > recipient - p.safeTransfer(recipient, p.token0Path, amount0, true) - } -} diff --git a/contract/r/gnoswap/v1/pool/tick.gno b/contract/r/gnoswap/v1/pool/tick.gno deleted file mode 100644 index 43f6967..0000000 --- a/contract/r/gnoswap/v1/pool/tick.gno +++ /dev/null @@ -1,468 +0,0 @@ -package pool - -import ( - "strconv" - - "gno.land/p/nt/ufmt" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -const ( - MIN_TICK int32 = -887272 - MAX_TICK int32 = 887272 -) - -// GetTickLiquidityGross returns the gross liquidity for the specified tick. -func (p *Pool) GetTickLiquidityGross(tick int32) *u256.Uint { - return p.mustGetTick(tick).liquidityGross -} - -// GetTickLiquidityNet returns the net liquidity for the specified tick. -func (p *Pool) GetTickLiquidityNet(tick int32) *i256.Int { - return p.mustGetTick(tick).liquidityNet -} - -// GetTickFeeGrowthOutside0X128 returns the fee growth outside the tick for token 0. -func (p *Pool) GetTickFeeGrowthOutside0X128(tick int32) *u256.Uint { - return p.mustGetTick(tick).feeGrowthOutside0X128 -} - -// GetTickFeeGrowthOutside1X128 returns the fee growth outside the tick for token 1. -func (p *Pool) GetTickFeeGrowthOutside1X128(tick int32) *u256.Uint { - return p.mustGetTick(tick).feeGrowthOutside1X128 -} - -// GetTickCumulativeOutside returns the cumulative liquidity outside the tick. -func (p *Pool) GetTickCumulativeOutside(tick int32) int64 { - return p.mustGetTick(tick).tickCumulativeOutside -} - -// GetTickSecondsPerLiquidityOutsideX128 returns the seconds per liquidity outside the tick. -func (p *Pool) GetTickSecondsPerLiquidityOutsideX128(tick int32) *u256.Uint { - return p.mustGetTick(tick).secondsPerLiquidityOutsideX128 -} - -// GetTickSecondsOutside returns the seconds outside the tick. -func (p *Pool) GetTickSecondsOutside(tick int32) uint32 { - return p.mustGetTick(tick).secondsOutside -} - -// GetTickInitialized returns whether the tick is initialized. -func (p *Pool) GetTickInitialized(tick int32) bool { - return p.mustGetTick(tick).initialized -} - -// getFeeGrowthInside calculates the fee growth within a specified tick range. -// -// This function computes the accumulated fee growth for token 0 and token 1 inside a given tick range -// (`tickLower` to `tickUpper`) relative to the current tick position (`tickCurrent`). It isolates the fee -// growth within the range by subtracting the fee growth below the lower tick and above the upper tick -// from the global fee growth. -// -// Parameters: -// - tickLower: int32, the lower tick boundary of the range. -// - tickUpper: int32, the upper tick boundary of the range. -// - tickCurrent: int32, the current tick index. -// - feeGrowthGlobal0X128: *u256.Uint, the global fee growth for token 0 in X128 precision. -// - feeGrowthGlobal1X128: *u256.Uint, the global fee growth for token 1 in X128 precision. -// -// Returns: -// - *u256.Uint: Fee growth inside the tick range for token 0. -// - *u256.Uint: Fee growth inside the tick range for token 1. -// -// Workflow: -// 1. Retrieve the tick information (`lower` and `upper`) for the lower and upper tick boundaries -// using `p.getTick`. -// 2. Calculate the fee growth below the lower tick using `getFeeGrowthBelowX128`. -// 3. Calculate the fee growth above the upper tick using `getFeeGrowthAboveX128`. -// 4. Subtract the fee growth below and above the range from the global fee growth values: -// feeGrowthInside = feeGrowthGlobal - feeGrowthBelow - feeGrowthAbove -// 5. Return the computed fee growth values for token 0 and token 1 within the range. -// -// Behavior: -// - The fee growth is isolated within the range `[tickLower, tickUpper]`. -// - The function ensures the calculations accurately consider the tick boundaries and the current tick position. -// -// Example: -// -// ```gno -// -// feeGrowth0, feeGrowth1 := pool.getFeeGrowthInside( -// 100, 200, 150, globalFeeGrowth0, globalFeeGrowth1, -// ) -// println("Fee Growth Inside (Token 0):", feeGrowth0) -// println("Fee Growth Inside (Token 1):", feeGrowth1) -// -// ``` -func (p *Pool) getFeeGrowthInside( - tickLower int32, - tickUpper int32, - tickCurrent int32, - feeGrowthGlobal0X128 *u256.Uint, - feeGrowthGlobal1X128 *u256.Uint, -) (*u256.Uint, *u256.Uint) { - lower := p.getTick(tickLower) - upper := p.getTick(tickUpper) - - feeGrowthBelow0X128, feeGrowthBelow1X128 := getFeeGrowthBelowX128(tickLower, tickCurrent, feeGrowthGlobal0X128, feeGrowthGlobal1X128, lower) - feeGrowthAbove0X128, feeGrowthAbove1X128 := getFeeGrowthAboveX128(tickUpper, tickCurrent, feeGrowthGlobal0X128, feeGrowthGlobal1X128, upper) - - feeGrowthInside0X128 := u256.Zero().Sub(u256.Zero().Sub(feeGrowthGlobal0X128, feeGrowthBelow0X128), feeGrowthAbove0X128) - feeGrowthInside1X128 := u256.Zero().Sub(u256.Zero().Sub(feeGrowthGlobal1X128, feeGrowthBelow1X128), feeGrowthAbove1X128) - - return feeGrowthInside0X128, feeGrowthInside1X128 -} - -// tickUpdate updates the state of a specific tick. -// -// This function applies a given liquidity change (liquidityDelta) to the specified tick, updates -// the fee growth values if necessary, and adjusts the net liquidity based on whether the tick -// is an upper or lower boundary. It also verifies that the total liquidity does not exceed the -// maximum allowed value and ensures the net liquidity stays within the valid int128 range. -// -// Parameters: -// - tick: int32, the index of the tick to update. -// - tickCurrent: int32, the current active tick index. -// - liquidityDelta: *i256.Int, the amount of liquidity to add or remove. -// - feeGrowthGlobal0X128: *u256.Uint, the global fee growth value for token 0. -// - feeGrowthGlobal1X128: *u256.Uint, the global fee growth value for token 1. -// - upper: bool, indicates if this is the upper boundary (true for upper, false for lower). -// - maxLiquidity: *u256.Uint, the maximum allowed liquidity. -// -// Returns: -// - flipped: bool, indicates if the tick's initialization state has changed. -// (e.g., liquidity transitioning from zero to non-zero, or vice versa) -// -// Workflow: -// 1. Nil input values are replaced with zero. -// 2. The function retrieves the tick information for the specified tick index. -// 3. Applies the liquidityDelta to compute the new total liquidity (liquidityGross). -// - If the total liquidity exceeds the maximum allowed value, the function panics. -// 4. Checks whether the tick's initialized state has changed and sets the `flipped` flag. -// 5. If the tick was previously uninitialized and its index is less than or equal to the current tick, -// the fee growth values are initialized to the current global values. -// 6. Updates the tick's net liquidity: -// - For an upper boundary, it subtracts liquidityDelta. -// - For a lower boundary, it adds liquidityDelta. -// - Ensures the net liquidity remains within the int128 range using `checkOverFlowInt128`. -// 7. Updates the tick's state with the new values. -// 8. Returns whether the tick's initialized state has flipped. -// -// Panic Conditions: -// - The total liquidity (liquidityGross) exceeds the maximum allowed liquidity (maxLiquidity). -// - The net liquidity (liquidityNet) exceeds the int128 range. -// -// Example: -// -// ```gno -// -// flipped := pool.tickUpdate(10, 5, liquidityDelta, feeGrowth0, feeGrowth1, true, maxLiquidity) -// println("Tick flipped:", flipped) -// -// ``` -func (p *Pool) tickUpdate( - tick int32, - tickCurrent int32, - liquidityDelta *i256.Int, - feeGrowthGlobal0X128 *u256.Uint, - feeGrowthGlobal1X128 *u256.Uint, - upper bool, - maxLiquidity *u256.Uint, -) (flipped bool) { - liquidityDelta = liquidityDelta.NilToZero() - feeGrowthGlobal0X128 = feeGrowthGlobal0X128.NilToZero() - feeGrowthGlobal1X128 = feeGrowthGlobal1X128.NilToZero() - - tickInfo := p.getTick(tick) - - liquidityGrossBefore := tickInfo.liquidityGross.Clone() - liquidityGrossAfter := liquidityMathAddDelta(liquidityGrossBefore, liquidityDelta) - - if !(liquidityGrossAfter.Lte(maxLiquidity)) { - panic(newErrorWithDetail( - errLiquidityCalculation, - ufmt.Sprintf("liquidityGrossAfter(%s) overflows maxLiquidity(%s)", liquidityGrossAfter.ToString(), maxLiquidity.ToString()), - )) - } - - flipped = (liquidityGrossAfter.IsZero()) != (liquidityGrossBefore.IsZero()) - - if liquidityGrossBefore.IsZero() { - if tick <= tickCurrent { - tickInfo.feeGrowthOutside0X128 = feeGrowthGlobal0X128.Clone() - tickInfo.feeGrowthOutside1X128 = feeGrowthGlobal1X128.Clone() - } - tickInfo.initialized = true - } - - tickInfo.liquidityGross = liquidityGrossAfter.Clone() - - if upper { - tickInfo.liquidityNet = i256.Zero().Sub(tickInfo.liquidityNet, liquidityDelta) - checkOverFlowInt128(tickInfo.liquidityNet) - } else { - tickInfo.liquidityNet = i256.Zero().Add(tickInfo.liquidityNet, liquidityDelta) - checkOverFlowInt128(tickInfo.liquidityNet) - } - - p.setTick(tick, tickInfo) - - return flipped -} - -// tickCross updates a tick's state when it is crossed and returns the liquidity net. -func (p *Pool) tickCross( - tick int32, - feeGrowthGlobal0X128 *u256.Uint, - feeGrowthGlobal1X128 *u256.Uint, -) *i256.Int { - thisTick := p.getTick(tick) - - thisTick.feeGrowthOutside0X128 = u256.Zero().Sub(feeGrowthGlobal0X128, thisTick.feeGrowthOutside0X128) - thisTick.feeGrowthOutside1X128 = u256.Zero().Sub(feeGrowthGlobal1X128, thisTick.feeGrowthOutside1X128) - - p.setTick(tick, thisTick) - - return thisTick.liquidityNet.Clone() -} - -// setTick updates the tick data for the specified tick index in the pool. -func (p *Pool) setTick(tick int32, newTickInfo TickInfo) { - tickStr := strconv.Itoa(int(tick)) - p.ticks.Set(tickStr, newTickInfo) -} - -// deleteTick deletes the tick data for the specified tick index in the pool. -func (p *Pool) deleteTick(tick int32) { - tickStr := strconv.Itoa(int(tick)) - p.ticks.Remove(tickStr) -} - -// getTick retrieves the TickInfo associated with the specified tick index from the pool. -// If the TickInfo contains any nil fields, they are replaced with zero values using valueOrZero. -// -// Parameters: -// - tick: The tick index (int32) for which the TickInfo is to be retrieved. -// -// Behavior: -// - Retrieves the TickInfo for the given tick from the pool's tick map. -// - Ensures that all fields of TickInfo are non-nil by calling valueOrZero, which replaces nil values with zero. -// - Returns the updated TickInfo. -// -// Returns: -// - TickInfo: The tick data with all fields guaranteed to have valid values (nil fields are set to zero). -// -// Use Case: -// This function ensures the retrieved tick data is always valid and safe for further operations, -// such as calculations or updates, by sanitizing nil fields in the TickInfo structure. -func (p *Pool) getTick(tick int32) TickInfo { - tickStr := formatInt(tick) - iTickInfo, exist := p.ticks.Get(tickStr) - if !exist { - tickInfo := TickInfo{} - tickInfo.valueOrZero() - return tickInfo - } - - tickInfo, ok := iTickInfo.(TickInfo) - if !ok { - panic(ufmt.Sprintf("failed to cast tickInfo to TickInfo: %T", iTickInfo)) - } - return tickInfo -} - -// mustGetTick retrieves the TickInfo for a specific tick, panicking if the tick does not exist. -// -// This function ensures that the requested tick data exists in the pool's tick mapping. -// If the tick does not exist, it panics with an appropriate error message. -// -// Parameters: -// - tick: int32, the index of the tick to retrieve. -// -// Returns: -// - TickInfo: The information associated with the specified tick. -// -// Behavior: -// - Checks if the tick exists in the pool's tick mapping (`p.ticks`). -// - If the tick exists, it returns the corresponding `TickInfo`. -// - If the tick does not exist, the function panics with a descriptive error. -// -// Panic Conditions: -// - The specified tick does not exist in the pool's mapping. -// -// Example: -// -// ```gno -// -// tickInfo := pool.mustGetTick(10) -// ufmt.Println("Tick Info:", tickInfo) -// -// ``` -func (p *Pool) mustGetTick(tick int32) TickInfo { - tickStr := formatInt(tick) - iTickInfo, exist := p.ticks.Get(tickStr) - if !exist { - panic(newErrorWithDetail( - errDataNotFound, - ufmt.Sprintf("tick(%d) does not exist", tick), - )) - } - - info, ok := iTickInfo.(TickInfo) - if !ok { - panic("failed to cast tick info to TickInfo") - } - - return info -} - -// calculateMaxLiquidityPerTick calculates the maximum liquidity -// per tick for a given tick spacing. -func calculateMaxLiquidityPerTick(tickSpacing int32) *u256.Uint { - // Floor MIN_TICK and MAX_TICK to the nearest multiple of tickSpacing - // This ensures that the tick range is properly aligned with the tickSpacing - // For example, if tickSpacing is 60 and MIN_TICK is -887272: - // -887272 / 60 = -14787.866... -> -14787 * 60 = -887220 - minTick := (MIN_TICK / tickSpacing) * tickSpacing - maxTick := (MAX_TICK / tickSpacing) * tickSpacing - numTicks := uint64((maxTick-minTick)/tickSpacing) + 1 - - return u256.Zero().Div(u256.MustFromDecimal(MAX_UINT128), u256.NewUint(numTicks)) -} - -// getFeeGrowthBelowX128 calculates the fee growth below a specified tick. -// -// This function computes the fee growth for token 0 and token 1 below a given tick (`tickLower`) -// relative to the current tick (`tickCurrent`). The fee growth values are adjusted based on whether -// the `tickCurrent` is above or below the `tickLower`. -// -// Parameters: -// - tickLower: int32, the lower tick boundary for fee calculation. -// - tickCurrent: int32, the current tick index. -// - feeGrowthGlobal0X128: *u256.Uint, the global fee growth for token 0 in X128 precision. -// - feeGrowthGlobal1X128: *u256.Uint, the global fee growth for token 1 in X128 precision. -// - lowerTick: TickInfo, the fee growth and liquidity details for the lower tick. -// -// Returns: -// - *u256.Uint: Fee growth below `tickLower` for token 0. -// - *u256.Uint: Fee growth below `tickLower` for token 1. -// -// Workflow: -// 1. If `tickCurrent` is greater than or equal to `tickLower`: -// - Return the `feeGrowthOutside0X128` and `feeGrowthOutside1X128` values of the `lowerTick`. -// 2. If `tickCurrent` is below `tickLower`: -// - Compute the fee growth below the lower tick by subtracting `feeGrowthOutside` values -// from the global fee growth values (`feeGrowthGlobal0X128` and `feeGrowthGlobal1X128`). -// 3. Return the calculated fee growth values for both tokens. -// -// Behavior: -// - If `tickCurrent >= tickLower`, the fee growth outside the lower tick is returned as-is. -// - If `tickCurrent < tickLower`, the fee growth is calculated as: -// feeGrowthBelow = feeGrowthGlobal - feeGrowthOutside -// -// Example: -// -// ```gno -// -// feeGrowth0, feeGrowth1 := getFeeGrowthBelowX128( -// 100, 150, globalFeeGrowth0, globalFeeGrowth1, lowerTickInfo, -// ) -// println("Fee Growth Below:", feeGrowth0, feeGrowth1) -func getFeeGrowthBelowX128( - tickLower, tickCurrent int32, - feeGrowthGlobal0X128, feeGrowthGlobal1X128 *u256.Uint, - lowerTick TickInfo, -) (*u256.Uint, *u256.Uint) { - if tickCurrent >= tickLower { - return lowerTick.feeGrowthOutside0X128, lowerTick.feeGrowthOutside1X128 - } - - feeGrowthBelow0X128 := u256.Zero().Sub(feeGrowthGlobal0X128, lowerTick.feeGrowthOutside0X128) - feeGrowthBelow1X128 := u256.Zero().Sub(feeGrowthGlobal1X128, lowerTick.feeGrowthOutside1X128) - - return feeGrowthBelow0X128, feeGrowthBelow1X128 -} - -// getFeeGrowthAboveX128 calculates the fee growth above a specified tick. -// -// This function computes the fee growth for token 0 and token 1 above a given tick (`tickUpper`) -// relative to the current tick (`tickCurrent`). The fee growth values are adjusted based on whether -// the `tickCurrent` is above or below the `tickUpper`. -// -// Parameters: -// - tickUpper: int32, the upper tick boundary for fee calculation. -// - tickCurrent: int32, the current tick index. -// - feeGrowthGlobal0X128: *u256.Uint, the global fee growth for token 0 in X128 precision. -// - feeGrowthGlobal1X128: *u256.Uint, the global fee growth for token 1 in X128 precision. -// - upperTick: TickInfo, the fee growth and liquidity details for the upper tick. -// -// Returns: -// - *u256.Uint: Fee growth above `tickUpper` for token 0. -// - *u256.Uint: Fee growth above `tickUpper` for token 1. -// -// Workflow: -// 1. If `tickCurrent` is less than `tickUpper`: -// - Return the `feeGrowthOutside0X128` and `feeGrowthOutside1X128` values of the `upperTick`. -// 2. If `tickCurrent` is greater than or equal to `tickUpper`: -// - Compute the fee growth above the upper tick by subtracting `feeGrowthOutside` values -// from the global fee growth values (`feeGrowthGlobal0X128` and `feeGrowthGlobal1X128`). -// 3. Return the calculated fee growth values for both tokens. -// -// Behavior: -// - If `tickCurrent < tickUpper`, the fee growth outside the upper tick is returned as-is. -// - If `tickCurrent >= tickUpper`, the fee growth is calculated as: -// feeGrowthAbove = feeGrowthGlobal - feeGrowthOutside -// -// Example: -// -// feeGrowth0, feeGrowth1 := getFeeGrowthAboveX128( -// 200, 150, globalFeeGrowth0, globalFeeGrowth1, upperTickInfo, -// ) -// println("Fee Growth Above:", feeGrowth0, feeGrowth1) -// -// ``` -func getFeeGrowthAboveX128( - tickUpper, tickCurrent int32, - feeGrowthGlobal0X128, feeGrowthGlobal1X128 *u256.Uint, - upperTick TickInfo, -) (*u256.Uint, *u256.Uint) { - if tickCurrent < tickUpper { - return upperTick.feeGrowthOutside0X128, upperTick.feeGrowthOutside1X128 - } - - feeGrowthAbove0X128 := u256.Zero().Sub(feeGrowthGlobal0X128, upperTick.feeGrowthOutside0X128) - feeGrowthAbove1X128 := u256.Zero().Sub(feeGrowthGlobal1X128, upperTick.feeGrowthOutside1X128) - - return feeGrowthAbove0X128, feeGrowthAbove1X128 -} - -// validateTicks validates the tick range for a liquidity position. -// -// This function performs three essential checks to ensure the provided -// tick values are valid before creating or modifying a liquidity position. -func validateTicks(tickLower, tickUpper int32) error { - if tickLower >= tickUpper { - return makeErrorWithDetails( - errInvalidTickRange, - ufmt.Sprintf("tickLower(%d), tickUpper(%d)", tickLower, tickUpper), - ) - } - - if tickLower < MIN_TICK { - return makeErrorWithDetails( - errTickLowerInvalid, - ufmt.Sprintf("tickLower(%d) < MIN_TICK(%d)", tickLower, MIN_TICK), - ) - } - - if tickUpper > MAX_TICK { - return makeErrorWithDetails( - errTickUpperInvalid, - ufmt.Sprintf("tickUpper(%d) > MAX_TICK(%d)", tickUpper, MAX_TICK), - ) - } - - return nil -} diff --git a/contract/r/gnoswap/v1/pool/tick_bitmap.gno b/contract/r/gnoswap/v1/pool/tick_bitmap.gno deleted file mode 100644 index 383e4df..0000000 --- a/contract/r/gnoswap/v1/pool/tick_bitmap.gno +++ /dev/null @@ -1,167 +0,0 @@ -package pool - -import ( - "strconv" - - "gno.land/p/nt/ufmt" - plp "gno.land/p/gnoswap/gnsmath" - u256 "gno.land/p/gnoswap/uint256" -) - -// bitMask8 is used for efficient modulo 256 operations -const bitMask8 = 0xff // 256 - 1 - -// tickBitmapFlipTick flips the state of a tick in the tick bitmap. -// -// This function toggles the "initialized" state of a tick in the tick bitmap. -// It ensures that the tick aligns with the specified tick spacing and then -// flips the corresponding bit in the bitmap representation. -// -// Parameters: -// - tick: int32, the tick index to toggle. -// - tickSpacing: int32, the spacing between valid ticks. -// The tick must align with this spacing. -// -// Workflow: -// 1. Validates that the `tick` aligns with `tickSpacing` using `checkTickSpacing`. -// 2. Computes the position of the bit in the tick bitmap: -// - `wordPos`: Determines which word in the bitmap contains the bit. -// - `bitPos`: Identifies the position of the bit within the word. -// 3. Creates a bitmask using `Lsh` (Left Shift) to target the bit at `bitPos`. -// 4. Toggles (flips) the bit using XOR with the current value of the tick bitmap. -// 5. Updates the tick bitmap with the modified word. -// -// Behavior: -// - If the bit is `0` (uninitialized), it will be flipped to `1` (initialized). -// - If the bit is `1` (initialized), it will be flipped to `0` (uninitialized). -// -// Example: -// -// pool.tickBitmapFlipTick(120, 60) -// // This flips the bit for tick 120 with a tick spacing of 60. -// -// Notes: -// - The `tick` must be divisible by `tickSpacing`. If not, the function will panic. -func (p *Pool) tickBitmapFlipTick( - tick int32, - tickSpacing int32, -) { - checkTickSpacing(tick, tickSpacing) - wordPos, bitPos := tickBitmapPosition(tick / tickSpacing) - - mask := u256.Zero().Lsh(u256.One(), uint(bitPos)) - current := p.getTickBitmap(wordPos) - p.setTickBitmap(wordPos, u256.Zero().Xor(current, mask)) -} - -// tickBitmapNextInitializedTickWithInOneWord finds the next initialized tick within -// one word of the bitmap. -func (p *Pool) tickBitmapNextInitializedTickWithInOneWord( - tick int32, - tickSpacing int32, - lte bool, -) (int32, bool) { - compress := tick / tickSpacing - // Round towards negative infinity for negative ticks - if tick < 0 && tick%tickSpacing != 0 { - compress-- - } - - wordPos, bitPos := getWordAndBitPos(compress, lte) - mask := getMaskBit(uint(bitPos), lte) - masked := u256.Zero().And(p.getTickBitmap(wordPos), mask) - initialized := !masked.IsZero() - - nextTick := getNextTick(lte, initialized, compress, bitPos, tickSpacing, masked) - return nextTick, initialized -} - -// getTickBitmap gets the tick bitmap for the given word position -// if the tick bitmap is not initialized, initialize it to zero -func (p *Pool) getTickBitmap(wordPos int16) *u256.Uint { - wordPosStr := strconv.Itoa(int(wordPos)) - - value, exist := p.tickBitmaps.Get(wordPosStr) - if !exist { - p.initTickBitmap(wordPos) - value, exist = p.tickBitmaps.Get(wordPosStr) - if !exist { - panic(newErrorWithDetail( - errDataNotFound, - ufmt.Sprintf("failed to initialize tickBitmap(%d)", wordPos), - )) - } - } - - bitmap, ok := value.(*u256.Uint) - if !ok { - panic(ufmt.Sprintf("failed to cast tickBitmap to *u256.Uint: %T", value)) - } - return bitmap -} - -// setTickBitmap sets the tick bitmap for the given word position -func (p *Pool) setTickBitmap(wordPos int16, tickBitmap *u256.Uint) { - wordPosStr := strconv.Itoa(int(wordPos)) - p.tickBitmaps.Set(wordPosStr, tickBitmap) -} - -// initTickBitmap initializes the tick bitmap for the given word position -func (p *Pool) initTickBitmap(wordPos int16) { - p.setTickBitmap(wordPos, u256.Zero()) -} - -// tickBitmapPosition calculates the word and bit position for a given tick -func tickBitmapPosition(tick int32) (int16, uint8) { - return int16(tick >> 8), uint8(tick) & bitMask8 -} - -// getWordAndBitPos gets tick's wordPos and bitPos depending on the swap direction -func getWordAndBitPos(tick int32, lte bool) (int16, uint8) { - if !lte { - tick++ - } - return tickBitmapPosition(tick) -} - -// getMaskBit generates a mask based on the provided bit position (bitPos) and a boolean flag (lte). -// The function constructs a bitmask with a shift depending on the bit position and the boolean value. -// It either returns the mask or its negation, based on the value of 'lte' (swap direction). -// -// NOTE: should always use a newly created `u256.One()` object. -func getMaskBit(bitPos uint, lte bool) *u256.Uint { - if lte { - if bitPos == bitMask8 { - return u256.Zero().Not(u256.Zero()) // all ones - } - return u256.Zero().Sub(u256.Zero().Lsh(u256.One(), bitPos+1), u256.One()) - } - if bitPos == 0 { - return u256.Zero().Not(u256.Zero()) // all ones - } - return u256.Zero().Not(u256.Zero().Sub(u256.Zero().Lsh(u256.One(), bitPos), u256.One())) -} - -// getNextTick gets the next tick depending on the initialized state and the swap direction -func getNextTick(lte, initialized bool, compress int32, bitPos uint8, tickSpacing int32, masked *u256.Uint) int32 { - if initialized { - return getTickIfInitialized(compress, tickSpacing, bitPos, masked, lte) - } - return getTickIfNotInitialized(compress, tickSpacing, bitPos, lte) -} - -// getTickIfInitialized gets the next tick if the tick bitmap is initialized -func getTickIfInitialized(compress, tickSpacing int32, bitPos uint8, masked *u256.Uint, lte bool) int32 { - if lte { - return (compress - int32(bitPos-plp.BitMathMostSignificantBit(masked))) * tickSpacing - } - return (compress + 1 + int32(plp.BitMathLeastSignificantBit(masked)-bitPos)) * tickSpacing -} - -// getTickIfNotInitialized gets the next tick if the tick bitmap is not initialized -func getTickIfNotInitialized(compress, tickSpacing int32, bitPos uint8, lte bool) int32 { - if lte { - return (compress - int32(bitPos)) * tickSpacing - } - return (compress + 1 + int32(bitMask8-bitPos)) * tickSpacing -} diff --git a/contract/r/gnoswap/v1/pool/transfer.gno b/contract/r/gnoswap/v1/pool/transfer.gno deleted file mode 100644 index 64a19f6..0000000 --- a/contract/r/gnoswap/v1/pool/transfer.gno +++ /dev/null @@ -1,208 +0,0 @@ -package pool - -import ( - "std" - - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/v1/common" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -// safeTransfer performs a token transfer out of the pool while ensuring -// the pool has sufficient balance and updating internal accounting. -// This function is typically used during swaps and liquidity removals. -// -// Important requirements: -// - The amount must be negative (representing an outflow from the pool) -// - The pool must have sufficient balance for the transfer -// - The transfer amount must fit within uint64 range -// -// Parameters: -// - to: destination address for the transfer -// - tokenPath: path identifier of the token to transfer -// - amount: amount to transfer (must be negative) -// - isToken0: true if transferring token0, false for token1 -// -// The function will: -// 1. Validate the amount is negative -// 2. Check pool has sufficient balance -// 3. Execute the transfer -// 4. Update pool's internal balance -// -// Panics if any validation fails or if the transfer fails -func (p *Pool) safeTransfer( - to std.Address, - tokenPath string, - amount *i256.Int, - isToken0 bool, -) { - if amount.Gt(i256.Zero()) { - panic(ufmt.Sprintf( - "%v. got: %s", errMustBeNegative, amount.ToString(), - )) - } - - absAmount := amount.Abs() - - token0 := p.BalanceToken0() - token1 := p.BalanceToken1() - - if err := validatePoolBalance(token0, token1, absAmount, isToken0); err != nil { - panic(err) - } - amountInt64 := safeConvertToInt64(absAmount) - - checkTransferError(common.Transfer(cross, tokenPath, to, amountInt64)) - - newBalance, err := updatePoolBalance(token0, token1, absAmount, isToken0) - if err != nil { - panic(err) - } - - if isToken0 { - p.balances.token0 = newBalance - } else { - p.balances.token1 = newBalance - } -} - -// safeTransferFrom securely transfers tokens into the pool while ensuring balance consistency. -// -// This function performs the following steps: -// 1. Validates and converts the transfer amount to `uint64` using `safeConvertToUint64`. -// 2. Executes the token transfer using `TransferFrom` via the token teller contract. -// 3. Verifies that the destination balance reflects the correct amount after transfer. -// 4. Updates the pool's internal balances (`token0` or `token1`) and validates the updated state. -// -// Parameters: -// - from (std.Address): Source address for the token transfer. -// - to (std.Address): Destination address, typically the pool address. -// - tokenPath (string): Path identifier for the token being transferred. -// - amount (*u256.Uint): The amount of tokens to transfer (must be a positive value). -// - isToken0 (bool): A flag indicating whether the token being transferred is token0 (`true`) or token1 (`false`). -// -// Panics: -// - If the `amount` exceeds the uint64 range during conversion. -// - If the token transfer (`TransferFrom`) fails. -// - If the destination balance after the transfer does not match the expected amount. -// - If the pool's internal balances (`token0` or `token1`) overflow or become inconsistent. -// -// Notes: -// - The function assumes that the sender (`from`) has approved the pool to spend the specified tokens. -// - The balance consistency check ensures that no tokens are lost or double-counted during the transfer. -// - Pool balance updates are performed atomically to ensure internal consistency. -// -// Example: -// p.safeTransferFrom( -// -// sender, poolAddress, "path/to/token0", u256.MustFromDecimal("1000"), true -// -// ) -func (p *Pool) safeTransferFrom( - from, to std.Address, - tokenPath string, - amount *u256.Uint, - isToken0 bool, -) { - amountInt64 := safeConvertToInt64(amount) - - token := common.GetToken(tokenPath) - beforeBalance := token.BalanceOf(to) - - checkTransferError(common.TransferFrom(cross, tokenPath, from, to, amountInt64)) - - afterBalance := token.BalanceOf(to) - if (beforeBalance + amountInt64) != afterBalance { - panic(ufmt.Sprintf( - "%v. beforeBalance(%d) + amount(%d) != afterBalance(%d)", - errTransferFailed, beforeBalance, amountInt64, afterBalance, - )) - } - - // update pool balances - if isToken0 { - beforeToken0 := p.balances.token0.Clone() - p.balances.token0 = u256.Zero().Add(p.balances.token0, amount) - if p.balances.token0.Lt(beforeToken0) { - panic(ufmt.Sprintf( - "%v. token0(%s) < beforeToken0(%s)", - errBalanceUpdateFailed, p.balances.token0.ToString(), beforeToken0.ToString(), - )) - } - } else { - beforeToken1 := p.balances.token1.Clone() - p.balances.token1 = u256.Zero().Add(p.balances.token1, amount) - if p.balances.token1.Lt(beforeToken1) { - panic(ufmt.Sprintf( - "%v. token1(%s) < beforeToken1(%s)", - errBalanceUpdateFailed, p.balances.token1.ToString(), beforeToken1.ToString(), - )) - } - } -} - -// validatePoolBalance checks if the pool has sufficient balance of either token0 and token1 -// before proceeding with a transfer. This prevents the pool won't go into a negative balance. -func validatePoolBalance(token0, token1, amount *u256.Uint, isToken0 bool) error { - if token0 == nil || token1 == nil || amount == nil { - return ufmt.Errorf( - "%v. token0(%s) or token1(%s) or amount(%s) is nil", - errTransferFailed, token0.ToString(), token1.ToString(), amount.ToString(), - ) - } - - if isToken0 { - if token0.Lt(amount) { - return ufmt.Errorf( - "%v. token0(%s) >= amount(%s)", - errTransferFailed, token0.ToString(), amount.ToString(), - ) - } - return nil - } - if token1.Lt(amount) { - return ufmt.Errorf( - "%v. token1(%s) >= amount(%s)", - errTransferFailed, token1.ToString(), amount.ToString(), - ) - } - return nil -} - -// updatePoolBalance calculates the new balance after a transfer and validate. -// It ensures the resulting balance won't be negative or overflow. -func updatePoolBalance( - token0, token1, amount *u256.Uint, - isToken0 bool, -) (*u256.Uint, error) { - var overflow bool - var newBalance *u256.Uint - - if isToken0 { - newBalance, overflow = u256.Zero().SubOverflow(token0, amount) - if isBalanceOverflowOrNegative(overflow, newBalance) { - return nil, ufmt.Errorf( - "%v. cannot decrease, token0(%s) - amount(%s)", - errBalanceUpdateFailed, token0.ToString(), amount.ToString(), - ) - } - return newBalance, nil - } - - newBalance, overflow = u256.Zero().SubOverflow(token1, amount) - if isBalanceOverflowOrNegative(overflow, newBalance) { - return nil, ufmt.Errorf( - "%v. cannot decrease, token1(%s) - amount(%s)", - errBalanceUpdateFailed, token1.ToString(), amount.ToString(), - ) - } - return newBalance, nil -} - -// isBalanceOverflowOrNegative checks if the balance calculation resulted in an overflow or negative value. -func isBalanceOverflowOrNegative(overflow bool, newBalance *u256.Uint) bool { - return overflow || newBalance.Lt(zero) -} diff --git a/contract/r/gnoswap/v1/pool/type.gno b/contract/r/gnoswap/v1/pool/type.gno deleted file mode 100644 index 38912df..0000000 --- a/contract/r/gnoswap/v1/pool/type.gno +++ /dev/null @@ -1,291 +0,0 @@ -package pool - -import ( - "std" - "time" - - "gno.land/p/nt/avl" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/v1/common" -) - -type PositionInfo struct { - liquidity *u256.Uint // amount of liquidity owned by this position - - // Fee growth per unit of liquidity as of the last update - // Used to calculate uncollected fees for token0 - feeGrowthInside0LastX128 *u256.Uint - - // Fee growth per unit of liquidity as of the last update - // Used to calculate uncollected fees for token1 - feeGrowthInside1LastX128 *u256.Uint - - // accumulated fees in token0 waiting to be collected - tokensOwed0 *u256.Uint - - // accumulated fees in token1 waiting to be collected - tokensOwed1 *u256.Uint -} - -// ModifyPositionParams repersents the parameters for modifying a liquidity position. -// This structure is used internally both `Mint` and `Burn` operation to manage -// the liquidity positions. -type ModifyPositionParams struct { - // owner is the address that owns the position - owner std.Address - - // tickLower and atickUpper define the price range - // The actual price range is calculated as 1.0001^tick - // This allows for precision in price range while using integer math. - - tickLower int32 // lower tick of the position - tickUpper int32 // upper tick of the position - - // liquidityDelta represents the change in liquidity - // Positive for minting, negative for burning - liquidityDelta *i256.Int -} - -// newModifyPositionParams creates a new `ModifyPositionParams` instance. -// This is used to preare parameters for the `modifyPosition` function, -// which handles both minting and burning of liquidity positions. -// -// Parameters: -// - owner: address that will own (or owns) the position -// - tickLower: lower tick bound of the position -// - tickUpper: upper tick bound of the position -// - liquidityDelta: amount of liquidity to add (positive) or remove (negative) -// -// The tick parameters represent prices as powers of 1.0001: -// - actual_price = 1.0001^tick -// - For example, tick = 100 means price = 1.0001^100 -// -// Returns: -// - ModifyPositionParams: a new instance of ModifyPositionParams -func newModifyPositionParams( - owner std.Address, - tickLower int32, - tickUpper int32, - liquidityDelta *i256.Int, -) ModifyPositionParams { - return ModifyPositionParams{ - owner: owner, - tickLower: tickLower, - tickUpper: tickUpper, - liquidityDelta: liquidityDelta, - } -} - -// SwapCache holds data that remains constant throughout a swap. -type SwapCache struct { - feeProtocol uint8 // protocol fee for the input token - liquidityStart *u256.Uint // liquidity at the beginning of the swap -} - -func newSwapCache( - feeProtocol uint8, - liquidityStart *u256.Uint, -) SwapCache { - return SwapCache{ - feeProtocol: feeProtocol, - liquidityStart: liquidityStart, - } -} - -// SwapState tracks the changing values during a swap. -// This type helps manage the state transitions that occur as the swap progresses -// across different price ranges. -type SwapState struct { - amountSpecifiedRemaining *i256.Int // amount remaining to be swapped in/out of the input/output token - amountCalculated *i256.Int // amount already swapped out/in of the output/input token - sqrtPriceX96 *u256.Uint // current sqrt(price) - tick int32 // tick associated with the current sqrt(price) - feeGrowthGlobalX128 *u256.Uint // global fee growth of the input token - protocolFee *u256.Uint // amount of input token paid as protocol fee - liquidity *u256.Uint // current liquidity in range -} - -func newSwapState( - amountSpecifiedRemaining *i256.Int, - feeGrowthGlobalX128 *u256.Uint, - liquidity *u256.Uint, - slot0 Slot0, -) SwapState { - return SwapState{ - amountSpecifiedRemaining: amountSpecifiedRemaining, - amountCalculated: i256.Zero(), - sqrtPriceX96: slot0.sqrtPriceX96, - tick: slot0.tick, - feeGrowthGlobalX128: feeGrowthGlobalX128, - protocolFee: u256.Zero(), - liquidity: liquidity, - } -} - -func (s *SwapState) setSqrtPriceX96(sqrtPriceX96 *u256.Uint) { - s.sqrtPriceX96 = sqrtPriceX96.Clone() -} - -func (s *SwapState) setTick(tick int32) { - s.tick = tick -} - -func (s *SwapState) setFeeGrowthGlobalX128(feeGrowthGlobalX128 *u256.Uint) { - s.feeGrowthGlobalX128 = feeGrowthGlobalX128 -} - -func (s *SwapState) setProtocolFee(fee *u256.Uint) { - s.protocolFee = fee -} - -// StepComputations holds intermediate values used during a single step of a swap. -// Each step represents movement from the current tick to the next initialized tick -// or the target price, whichever comes first. -type StepComputations struct { - sqrtPriceStartX96 *u256.Uint // price at the beginning of the step - tickNext int32 // next tick to swap to from the current tick in the swap direction - initialized bool // whether tickNext is initialized - sqrtPriceNextX96 *u256.Uint // sqrt(price) for the next tick (token1/token0) Q96 - amountIn *u256.Uint // how much being swapped in this step - amountOut *u256.Uint // how much is being swapped out in this step - feeAmount *u256.Uint // how much fee is being paid in this step -} - -// init initializes the computation for a single swap step -func (step *StepComputations) initSwapStep(state SwapState, pool *Pool, zeroForOne bool) { - step.sqrtPriceStartX96 = state.sqrtPriceX96 - step.tickNext, step.initialized = pool.tickBitmapNextInitializedTickWithInOneWord( - state.tick, - pool.tickSpacing, - zeroForOne, - ) - - // prevent overshoot the min/max tick - step.clampTickNext() - - // get the price for the next tick - step.sqrtPriceNextX96 = common.TickMathGetSqrtRatioAtTick(step.tickNext) -} - -// clampTickNext ensures that `tickNext` stays within the min, max tick boundaries -// as the tick bitmap is not aware of these bounds -func (step *StepComputations) clampTickNext() { - if step.tickNext < MIN_TICK { - step.tickNext = MIN_TICK - } else if step.tickNext > MAX_TICK { - step.tickNext = MAX_TICK - } -} - -// valueOrZero initializes nil fields in PositionInfo to zero. -// -// This function ensures that all numeric fields in the PositionInfo struct are not nil. -// If a field is nil, it is replaced with a zero value, maintaining consistency and preventing -// potential null pointer issues during calculations. -// -// Fields affected: -// - liquidity: The liquidity amount associated with the position. -// - feeGrowthInside0LastX128: Fee growth for token 0 inside the tick range, last recorded value. -// - feeGrowthInside1LastX128: Fee growth for token 1 inside the tick range, last recorded value. -// - tokensOwed0: The amount of token 0 owed to the position owner. -// - tokensOwed1: The amount of token 1 owed to the position owner. -// -// Behavior: -// - If a field is nil, it is set to its equivalent zero value. -// - If a field already has a value, it remains unchanged. -// -// Example: -// -// position := &PositionInfo{} -// position.valueOrZero() -// println(position.liquidity) // Output: 0 -// -// Notes: -// - This function is useful for ensuring numeric fields are properly initialized -// before performing operations or calculations. -// - Prevents runtime errors caused by nil values. -func (p *PositionInfo) valueOrZero() { - p.liquidity = p.liquidity.NilToZero() - p.feeGrowthInside0LastX128 = p.feeGrowthInside0LastX128.NilToZero() - p.feeGrowthInside1LastX128 = p.feeGrowthInside1LastX128.NilToZero() - p.tokensOwed0 = p.tokensOwed0.NilToZero() - p.tokensOwed1 = p.tokensOwed1.NilToZero() -} - -// TickInfo stores information about a specific tick in the pool. -// TIcks represent discrete price points that can be used as boundaries for positions. -type TickInfo struct { - liquidityGross *u256.Uint // total position liquidity that references this tick - liquidityNet *i256.Int // amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left) - - // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) - // only has relative meaning, not absolute — the value depends on when the tick is initialized - feeGrowthOutside0X128 *u256.Uint - feeGrowthOutside1X128 *u256.Uint - - tickCumulativeOutside int64 // cumulative tick value on the other side of the tick - - // the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick) - // only has relative meaning, not absolute — the value depends on when the tick is initialized - secondsPerLiquidityOutsideX128 *u256.Uint - - // the seconds spent on the other side of the tick (relative to the current tick) - // only has relative meaning, not absolute — the value depends on when the tick is initialized - secondsOutside uint32 - - initialized bool // whether the tick is initialized -} - -// valueOrZero ensures that all fields of TickInfo are valid by setting nil fields to zero, -// while retaining existing values if they are not nil. -// This function updates the TickInfo struct to replace any nil values in its fields -// with their respective zero values, ensuring data consistency. -// -// Behavior: -// - If a field is nil, it is replaced with its zero value. -// - If a field already has a valid value, the value remains unchanged. -// -// Fields: -// - liquidityGross: Gross liquidity for the tick, set to zero if nil, otherwise retains its value. -// - liquidityNet: Net liquidity for the tick, set to zero if nil, otherwise retains its value. -// - feeGrowthOutside0X128: Accumulated fee growth for token0 outside the tick, set to zero if nil, otherwise retains its value. -// - feeGrowthOutside1X128: Accumulated fee growth for token1 outside the tick, set to zero if nil, otherwise retains its value. -// - secondsPerLiquidityOutsideX128: Time per liquidity outside the tick, set to zero if nil, otherwise retains its value. -// -// Use Case: -// This function ensures all numeric fields in TickInfo are non-nil and have valid values, -// preventing potential runtime errors caused by nil values during operations like arithmetic or comparisons. -func (t *TickInfo) valueOrZero() { - t.liquidityGross = t.liquidityGross.NilToZero() - t.liquidityNet = t.liquidityNet.NilToZero() - t.feeGrowthOutside0X128 = t.feeGrowthOutside0X128.NilToZero() - t.feeGrowthOutside1X128 = t.feeGrowthOutside1X128.NilToZero() - t.secondsPerLiquidityOutsideX128 = t.secondsPerLiquidityOutsideX128.NilToZero() -} - -func newPool(poolInfo *poolCreateConfig) *Pool { - maxLiquidityPerTick := calculateMaxLiquidityPerTick(poolInfo.tickSpacing) - tick := common.TickMathGetTickAtSqrtRatio(poolInfo.SqrtPriceX96()) - slot0 := newSlot0(poolInfo.SqrtPriceX96(), tick, slot0FeeProtocol, true) - - return &Pool{ - token0Path: poolInfo.Token0Path(), - token1Path: poolInfo.Token1Path(), - balances: newBalances(), - fee: poolInfo.Fee(), - tickSpacing: poolInfo.TickSpacing(), - maxLiquidityPerTick: maxLiquidityPerTick, - slot0: slot0, - feeGrowthGlobal0X128: u256.Zero(), - feeGrowthGlobal1X128: u256.Zero(), - protocolFees: newProtocolFees(), - liquidity: u256.Zero(), - ticks: avl.NewTree(), - tickBitmaps: avl.NewTree(), - positions: avl.NewTree(), - observation: newObservation(time.Now().Unix()), - } -} diff --git a/contract/r/gnoswap/v1/pool/utils.gno b/contract/r/gnoswap/v1/pool/utils.gno deleted file mode 100644 index c00ef20..0000000 --- a/contract/r/gnoswap/v1/pool/utils.gno +++ /dev/null @@ -1,193 +0,0 @@ -package pool - -import ( - "strconv" - - "gno.land/p/nt/ufmt" - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -const ( - MAX_UINT64 string = "18446744073709551615" - MAX_INT64 string = "9223372036854775807" - MAX_INT128 string = "170141183460469231731687303715884105727" - MAX_UINT128 string = "340282366920938463463374607431768211455" - - INT64_MAX int64 = 9223372036854775807 - - Q96_RESOLUTION uint = 96 - Q128_RESOLUTION uint = 128 - - Q64 string = "18446744073709551616" // 2 ** 64 - Q96 string = "79228162514264337593543950336" // 2 ** 96 - Q128 string = "340282366920938463463374607431768211456" // 2 ** 128 -) - -// safeConvertToUint64 safely converts a *u256.Uint value to a uint64, ensuring no overflow. -// This function attempts to convert the given *u256.Uint value to a uint64. -// If the value exceeds the maximum allowable range for uint64 (2^64 - 1), it panics. -func safeConvertToUint64(value *u256.Uint) uint64 { - res, overflow := value.Uint64WithOverflow() - if overflow { - panic(ufmt.Sprintf( - "%v: amount(%s) overflows uint64 range (max %s)", - errOutOfRange, - value.ToString(), - MAX_UINT64, - )) - } - return res -} - -// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. -// This function attempts to convert the given *u256.Uint value to an int64. -// If the value exceeds the maximum allowable range for int64 (2^63 - 1), it panics. -func safeConvertToInt64(value *u256.Uint) int64 { - res, overflow := value.Uint64WithOverflow() - if overflow || res > uint64(INT64_MAX) { - panic(ufmt.Sprintf( - "%v: amount(%s) overflows int64 range (max %s)", - errOutOfRange, - value.ToString(), - MAX_INT64, - )) - } - return int64(res) -} - -// safeConvertToInt128 safely converts a *u256.Uint value to an *i256.Int, ensuring it does not exceed the int128 range. -// This function converts an unsigned 256-bit integer to a signed 256-bit integer. -// If the value exceeds the maximum allowable int128 range (2^127 - 1), it panics. -func safeConvertToInt128(value *u256.Uint) *i256.Int { - liquidityDelta := i256.FromUint256(value) - if liquidityDelta.Gt(i256.MustFromDecimal(MAX_INT128)) { - panic(ufmt.Sprintf( - "%v: amount(%s) overflows int128 range", - errOverFlow, value.ToString())) - } - return liquidityDelta -} - -// toUint128 ensures a *u256.Uint value fits within the uint128 range. -// -// This function validates that the given `value` is properly initialized and checks whether -// it exceeds the maximum value of uint128. If the value exceeds the uint128 range, -// it applies a masking operation to truncate the value to fit within the uint128 limit. -// -// Parameters: -// - value: *u256.Uint, the value to be checked and possibly truncated. -// -// Returns: -// - *u256.Uint: A value guaranteed to fit within the uint128 range. -// -// Notes: -// - The function first checks if the value is not nil to avoid potential runtime errors. -// - The mask ensures that only the lower 128 bits of the value are retained. -// - If the input value is already within the uint128 range, it is returned unchanged. -// - If masking is required, a new instance is returned without modifying the input. -// - MAX_UINT128 is a constant representing `2^128 - 1`. -func toUint128(value *u256.Uint) *u256.Uint { - if value == nil { - panic(newErrorWithDetail( - errInvalidInput, - "value is nil", - )) - } - - if value.Gt(u256.MustFromDecimal(MAX_UINT128)) { - mask := u256.Zero().Lsh(u256.One(), Q128_RESOLUTION) - mask = u256.Zero().Sub(mask, u256.One()) - return u256.Zero().And(value, mask) - } - return value -} - -// u256Min returns the smaller of two *u256.Uint values. -// -// This function compares two unsigned 256-bit integers and returns the smaller of the two. -// If `num1` is less than `num2`, it returns `num1`; otherwise, it returns `num2`. -// -// Parameters: -// - num1 (*u256.Uint): The first unsigned 256-bit integer. -// - num2 (*u256.Uint): The second unsigned 256-bit integer. -// -// Returns: -// - *u256.Uint: The smaller of `num1` and `num2`. -// -// Notes: -// - This function uses the `Lt` (less than) method of `*u256.Uint` to perform the comparison. -// - The function assumes both input values are non-nil. If nil inputs are possible in the usage context, -// additional validation may be needed. -// -// Example: -// smaller := u256Min(u256.MustFromDecimal("10"), u256.MustFromDecimal("20")) // Returns 10 -// smaller := u256Min(u256.MustFromDecimal("30"), u256.MustFromDecimal("20")) // Returns 20 -func u256Min(num1, num2 *u256.Uint) *u256.Uint { - if num1.Lt(num2) { - return num1 - } - return num2 -} - -// checkTransferError checks transfer error. -func checkTransferError(err error) { - if err != nil { - panic(newErrorWithDetail( - errTransferFailed, - err.Error(), - )) - } -} - -// checkOverFlowInt128 checks if the value overflows the int128 range. -func checkOverFlowInt128(value *i256.Int) { - if value.Gt(i256.MustFromDecimal(MAX_INT128)) { - panic(ufmt.Sprintf( - "%v: amount(%s) overflows int128 range", - errOverFlow, value.ToString())) - } -} - -// checkTickSpacing checks if the tick is divisible by the tickSpacing. -func checkTickSpacing(tick, tickSpacing int32) { - if tick%tickSpacing != 0 { - panic(newErrorWithDetail( - errInvalidTickAndTickSpacing, - ufmt.Sprintf("tick(%d) MOD tickSpacing(%d) != 0(%d)", tick, tickSpacing, tick%tickSpacing), - )) - } -} - -// formatUint converts various unsigned integer types to string representation. -func formatUint(v any) string { - switch v := v.(type) { - case uint8: - return strconv.FormatUint(uint64(v), 10) - case uint32: - return strconv.FormatUint(uint64(v), 10) - case uint64: - return strconv.FormatUint(v, 10) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} - -// formatInt converts various signed integer types to string representation. -func formatInt(v any) string { - switch v := v.(type) { - case int32: - return strconv.FormatInt(int64(v), 10) - case int64: - return strconv.FormatInt(v, 10) - case int: - return strconv.Itoa(v) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} - -// formatBool converts a boolean value to string representation. -func formatBool(v bool) string { - return strconv.FormatBool(v) -} diff --git a/contract/r/gnoswap/v1/position/README.md b/contract/r/gnoswap/v1/position/README.md deleted file mode 100644 index 0614753..0000000 --- a/contract/r/gnoswap/v1/position/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# Position - -NFT-based liquidity position management for concentrated liquidity. - -## Overview - -Each liquidity position is a unique GRC721 NFT containing pool identifier, price range, liquidity amount, accumulated fees, and token balances. - -## Configuration - -- **Withdrawal Fee**: 1% on collected fees -- **Max Position Size**: No limit -- **Transfer Restrictions**: Non-transferable NFTs - -## Core Functions - -### `Mint` - -Creates new position NFT with initial liquidity. - -- Validates tick range alignment -- Calculates optimal token ratio -- Returns actual amounts used - -### `IncreaseLiquidity` - -Adds liquidity to existing position. - -- Maintains same price range -- Pro-rata token amounts - -### `DecreaseLiquidity` - -Removes liquidity while keeping NFT. - -- Two-step: decrease then collect -- Calculates owed tokens - -### `CollectFee` - -Claims accumulated swap fees. - -- No liquidity removal required -- 1% protocol fees applied - -### `Reposition` - -Atomically moves liquidity to new range. - -- Burns old position -- Creates new position -- Mints new NFT - -## Technical Details - -### Tick Alignment - -Ticks must align with pool's tick spacing: - -``` -0.01% fee: every 1 tick -0.05% fee: every 10 ticks -0.3% fee: every 60 ticks -1% fee: every 200 ticks -``` - -### Optimal Range Width - -**Stable Pairs (USDC/USDT)**: - -- Narrow: ±0.05% (max efficiency) -- Medium: ±0.1% (balanced) -- Wide: ±0.5% (safety) - -**Correlated Pairs (WETH/stETH)**: - -- Narrow: ±0.5% -- Medium: ±1% -- Wide: ±2% - -**Volatile Pairs (WETH/USDC)**: - -- Narrow: ±5% -- Medium: ±10% -- Wide: ±25% - -### Capital Efficiency - -Concentration factor vs infinite range: - -``` -Range ±0.1% → 2000x efficient -Range ±1% → 200x efficient -Range ±10% → 20x efficient -Range ±50% → 4x efficient -``` - -### Token Calculations - -**Below range (token1 only)**: - -``` -amount1 = L * (sqrtUpper - sqrtLower) -amount0 = 0 -``` - -**Above range (token0 only)**: - -``` -amount0 = L * (sqrtUpper - sqrtLower) / (sqrtUpper * sqrtLower) -amount1 = 0 -``` - -**In range (both tokens)**: - -``` -amount0 = L * (sqrtUpper - sqrtCurrent) / (sqrtUpper * sqrtCurrent) -amount1 = L * (sqrtCurrent - sqrtLower) -``` - -## Usage - -```go -// Mint new position -tokenId := Mint( - "WETH/USDC:3000", // pool - -887220, // tickLower - 887220, // tickUpper - "1000000", // amount0Desired - "2000000000", // amount1Desired - "950000", // amount0Min - "1900000000", // amount1Min - deadline, - recipient, -) - -// Add liquidity -IncreaseLiquidity( - tokenId, - "500000", - "1000000000", - "475000", - "950000000", - deadline, -) - -// Collect fees -CollectFee(tokenId) -``` - -## Security - -- Tick range validation prevents invalid positions -- Slippage protection on all operations -- Deadline prevents stale transactions -- Position NFTs are non-transferable -- Only owner can manage their positions diff --git a/contract/r/gnoswap/v1/position/api.gno b/contract/r/gnoswap/v1/position/api.gno deleted file mode 100644 index d31e1f2..0000000 --- a/contract/r/gnoswap/v1/position/api.gno +++ /dev/null @@ -1,152 +0,0 @@ -package position - -import ( - "std" - "time" - - u256 "gno.land/p/gnoswap/uint256" - "gno.land/r/gnoswap/v1/common" - "gno.land/r/gnoswap/v1/gnft" - pl "gno.land/r/gnoswap/v1/pool" -) - -const MAX_UINT256 string = "115792089237316195423570985008687907853269984665640564039457584007913129639935" - -func ApiGetPosition(id uint64) string { - _, exist := GetPosition(id) - if !exist { - return "" - } - - rpcPosition := rpcMakePosition(id) - baseStat := NewResponseQueryBase(std.ChainHeight(), time.Now().Unix()) - return makeJsonResponse(&baseStat, &PositionsResponse{Positions: []RpcPosition{rpcPosition}}) -} - -func ApiGetPositionUnclaimedFeeByLpPositionId(lpPositionId uint64) string { - unclaimedFee0, unclaimedFee1 := unclaimedFee(lpPositionId) - fee := RpcUnclaimedFee{ - LpPositionId: lpPositionId, - Fee0: unclaimedFee0.ToString(), - Fee1: unclaimedFee1.ToString(), - } - - baseStat := NewResponseQueryBase(std.ChainHeight(), time.Now().Unix()) - return makeJsonResponse(&baseStat, &UnclaimedFeesResponse{ - Fees: []RpcUnclaimedFee{fee}, - }) -} - -func rpcMakePosition(positionId uint64) RpcPosition { - position := MustGetPosition(positionId) - - currentSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(position.poolKey) - lowerTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickLower) - upperTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickUpper) - - calculatedToken0Balance, calculatedToken1Balance := common.GetAmountsForLiquidity( - currentSqrtPriceX96, - lowerTickSqrtPriceX96, - upperTickSqrtPriceX96, - position.liquidity, - ) - - token0Balance, token1Balance := position.token0Balance, position.token1Balance - - unclaimedFee0 := u256.Zero() - unclaimedFee1 := u256.Zero() - burned := IsBurned(positionId) - if !burned { - unclaimedFee0, unclaimedFee1 = unclaimedFee(positionId) - } - - owner, err := gnft.OwnerOf(positionIdFrom(positionId)) - if err != nil { - owner = zeroAddress - } - - return RpcPosition{ - LpPositionId: positionId, - Burned: burned, - Owner: owner.String(), - Operator: position.operator.String(), - PoolKey: position.poolKey, - TickLower: position.tickLower, - TickUpper: position.tickUpper, - Liquidity: position.liquidity.ToString(), - FeeGrowthInside0LastX128: position.feeGrowthInside0LastX128.ToString(), - FeeGrowthInside1LastX128: position.feeGrowthInside1LastX128.ToString(), - TokensOwed0: position.tokensOwed0.ToString(), - TokensOwed1: position.tokensOwed1.ToString(), - Token0Balance: token0Balance.ToString(), - Token1Balance: token1Balance.ToString(), - CalculatedToken0Balance: calculatedToken0Balance, - CalculatedToken1Balance: calculatedToken1Balance, - FeeUnclaimed0: unclaimedFee0.ToString(), - FeeUnclaimed1: unclaimedFee1.ToString(), - } -} - -func UnclaimedFee(positionId uint64) (*u256.Uint, *u256.Uint) { - return unclaimedFee(positionId) -} - -func unclaimedFee(positionId uint64) (*u256.Uint, *u256.Uint) { - // ref: https://blog.uniswap.org/uniswap-v3-math-primer-2#calculating-uncollected-fees - - position := MustGetPosition(positionId) - - liquidity := position.liquidity - tickLower := position.tickLower - tickUpper := position.tickUpper - - poolKey := position.poolKey - - currentTick := pl.GetSlot0Tick(poolKey) - - feeGrowthGlobal0X128, feeGrowthGlobal1X128 := pl.GetFeeGrowthGlobalX128(poolKey) - tickUpperFeeGrowthOutside0X128, tickUpperFeeGrowthOutside1X128 := pl.GetTickFeeGrowthOutsideX128(poolKey, tickUpper) - tickLowerFeeGrowthOutside0X128, tickLowerFeeGrowthOutside1X128 := pl.GetTickFeeGrowthOutsideX128(poolKey, tickLower) - - feeGrowthInside0LastX128 := position.feeGrowthInside0LastX128 - feeGrowthInside1LastX128 := position.feeGrowthInside1LastX128 - - var tickLowerFeeGrowthBelow0, tickLowerFeeGrowthBelow1, tickUpperFeeGrowthAbove0, tickUpperFeeGrowthAbove1 *u256.Uint - - if currentTick >= tickUpper { - tickUpperFeeGrowthAbove0 = subUint256(feeGrowthGlobal0X128, tickUpperFeeGrowthOutside0X128) - tickUpperFeeGrowthAbove1 = subUint256(feeGrowthGlobal1X128, tickUpperFeeGrowthOutside1X128) - } else { - tickUpperFeeGrowthAbove0 = tickUpperFeeGrowthOutside0X128 - tickUpperFeeGrowthAbove1 = tickUpperFeeGrowthOutside1X128 - } - - if currentTick >= tickLower { - tickLowerFeeGrowthBelow0 = tickLowerFeeGrowthOutside0X128 - tickLowerFeeGrowthBelow1 = tickLowerFeeGrowthOutside1X128 - } else { - tickLowerFeeGrowthBelow0 = subUint256(feeGrowthGlobal0X128, tickLowerFeeGrowthOutside0X128) - tickLowerFeeGrowthBelow1 = subUint256(feeGrowthGlobal1X128, tickLowerFeeGrowthOutside1X128) - } - - feeGrowthInside0X128 := subUint256(feeGrowthGlobal0X128, tickLowerFeeGrowthBelow0) - feeGrowthInside0X128 = subUint256(feeGrowthInside0X128, tickUpperFeeGrowthAbove0) - - feeGrowthInside1X128 := subUint256(feeGrowthGlobal1X128, tickLowerFeeGrowthBelow1) - feeGrowthInside1X128 = subUint256(feeGrowthInside1X128, tickUpperFeeGrowthAbove1) - - diffGrowthInside0X128 := subUint256(feeGrowthInside0X128, feeGrowthInside0LastX128) - unclaimedFee0X128 := u256.Zero().Mul(liquidity, diffGrowthInside0X128) - unclaimedFee0 := u256.Zero().Div(unclaimedFee0X128, u256.MustFromDecimal(Q128)) - - diffGrowthInside1X128 := subUint256(feeGrowthInside1X128, feeGrowthInside1LastX128) - unclaimedFee1X128 := u256.Zero().Mul(liquidity, diffGrowthInside1X128) - unclaimedFee1 := u256.Zero().Div(unclaimedFee1X128, u256.MustFromDecimal(Q128)) - - return unclaimedFee0, unclaimedFee1 -} - -func IsBurned(positionId uint64) bool { - position := MustGetPosition(positionId) - return position.burned -} diff --git a/contract/r/gnoswap/v1/position/assert.gno b/contract/r/gnoswap/v1/position/assert.gno deleted file mode 100644 index fa3fbf7..0000000 --- a/contract/r/gnoswap/v1/position/assert.gno +++ /dev/null @@ -1,111 +0,0 @@ -package position - -import ( - "std" - "time" - - ufmt "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" - "gno.land/r/gnoswap/access" -) - -// assertIsNotExpired panics if the deadline is expired. -func assertIsNotExpired(deadline int64) { - now := time.Now().Unix() - - if now > deadline { - panic(makeErrorWithDetails( - errExpired, - ufmt.Sprintf("transaction too old, now(%d) > deadline(%d)", now, deadline), - )) - } -} - -// assertValidNumberString panics if the input string does not represent a valid integer. -func assertValidNumberString(input string) { - if len(input) == 0 { - panic(newErrorWithDetail( - errInvalidInput, - ufmt.Sprintf("input is empty"))) - } - - bytes := []byte(input) - for i, b := range bytes { - if i == 0 && b == '-' { - continue // Allow if the first character is a negative sign (-) - } - if b < '0' || b > '9' { - panic(newErrorWithDetail( - errInvalidInput, - ufmt.Sprintf("input string : %s", input))) - } - } -} - -// assertValidLiquidityAmount panics if the liquidity amount is zero. -func assertValidLiquidityAmount(liquidity string) { - if u256.MustFromDecimal(liquidity).IsZero() { - panic(newErrorWithDetail( - errZeroLiquidity, - ufmt.Sprintf("liquidity amount must be greater than 0, got %s", liquidity), - )) - } -} - -// assertExistsPosition panics if the position does not exist. -func assertExistsPosition(positionId uint64) { - if !exists(positionId) { - panic(newErrorWithDetail( - errPositionDoesNotExist, - ufmt.Sprintf("position with position ID(%d) doesn't exist", positionId), - )) - } -} - -// assertIsOwnerForToken panics if caller is not the owner of the position. -func assertIsOwnerForToken(positionId uint64, caller std.Address) { - assertExistsPosition(positionId) - - if !isOwner(positionId, caller) { - panic(newErrorWithDetail( - errNoPermission, - ufmt.Sprintf("caller(%s) is not owner of positionId(%d)", caller, positionId), - )) - } -} - -// assertIsOwnerOrOperatorForToken panics if caller is not the owner or operator of the position. -func assertIsOwnerOrOperatorForToken(positionId uint64, caller std.Address) { - assertExistsPosition(positionId) - - if !isOwnerOrOperator(positionId, caller) { - panic(newErrorWithDetail( - errNoPermission, - ufmt.Sprintf("caller(%s) is not owner or approved operator of positionId(%d)", caller, positionId), - )) - } -} - -// assertEqualsAddress panics if addresses are invalid or not equal. -func assertEqualsAddress(prevAddr, otherAddr std.Address) { - access.AssertIsValidAddress(prevAddr) - access.AssertIsValidAddress(otherAddr) - - if prevAddr != otherAddr { - panic(newErrorWithDetail( - errInvalidAddress, - ufmt.Sprintf("(%s, %s)", prevAddr, otherAddr), - )) - } -} - -// assertSlippageIsNotExceeded panics if slippage tolerance is exceeded. -func assertSlippageIsNotExceeded(amount0, amount1, amount0Min, amount1Min *u256.Uint) { - if !(amount0.Gte(amount0Min) && amount1.Gte(amount1Min)) { - panic(newErrorWithDetail( - errSlippage, - ufmt.Sprintf("amount0(%s) >= amount0Min(%s) && amount1(%s) >= amount1Min(%s)", - amount0.ToString(), amount0Min.ToString(), amount1.ToString(), amount1Min.ToString()), - )) - } -} diff --git a/contract/r/gnoswap/v1/position/burn.gno b/contract/r/gnoswap/v1/position/burn.gno deleted file mode 100644 index 69d743f..0000000 --- a/contract/r/gnoswap/v1/position/burn.gno +++ /dev/null @@ -1,210 +0,0 @@ -package position - -import ( - "gno.land/p/nt/ufmt" - prabc "gno.land/p/gnoswap/rbac" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/v1/common" - pl "gno.land/r/gnoswap/v1/pool" -) - -// decreaseLiquidity reduces position liquidity and collects fees. -// If unwrapResult is true, unwraps WUGNOT to GNOT. -// Returns positionId, liquidity, fee0, fee1, amount0, amount1, poolPath. -func decreaseLiquidity(params DecreaseLiquidityParams) (uint64, string, string, string, string, string, string, error) { - caller := params.caller - - // before decrease liquidity, collect fee first - _, fee0Str, fee1Str, _, _, _ := collectFee(params.positionId, params.unwrapResult, params.caller) - - position := MustGetPosition(params.positionId) - positionLiquidity := position.liquidity - if positionLiquidity.IsZero() { - return params.positionId, - "", - fee0Str, - fee1Str, - "", "", - position.poolKey, - makeErrorWithDetails( - errZeroLiquidity, - ufmt.Sprintf("position(position ID:%d) has 0 liquidity", params.positionId), - ) - } - - liquidityToRemove := u256.MustFromDecimal(params.liquidity) - if liquidityToRemove.Gt(positionLiquidity) { - return params.positionId, - liquidityToRemove.ToString(), - fee0Str, - fee1Str, - "", "", - position.poolKey, - makeErrorWithDetails( - errInvalidLiquidity, - ufmt.Sprintf("Liquidity requested(%s) is greater than liquidity held(%s)", liquidityToRemove.ToString(), positionLiquidity.ToString()), - ) - } - - pToken0, pToken1, pFee := splitOf(position.poolKey) - burn0, burn1 := pl.Burn(cross, pToken0, pToken1, pFee, position.tickLower, position.tickUpper, liquidityToRemove.ToString(), caller) - - burnedAmount0 := u256.MustFromDecimal(burn0) - burnedAmount1 := u256.MustFromDecimal(burn1) - if isSlippageExceeded(burnedAmount0, burnedAmount1, params.amount0Min, params.amount1Min) { - return params.positionId, - liquidityToRemove.ToString(), - fee0Str, - fee1Str, - burn0, - burn1, - position.poolKey, - makeErrorWithDetails( - errSlippage, - ufmt.Sprintf("burnedAmount0(%s) >= amount0Min(%s) || burnedAmount1(%s) >= amount1Min(%s)", - burnedAmount0.ToString(), - params.amount0Min.ToString(), - burnedAmount1.ToString(), - params.amount1Min.ToString(), - ), - ) - } - - positionKey := computePositionKey(position.tickLower, position.tickUpper) - feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(position.poolKey, positionKey) - - currentSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(position.poolKey) - lowerTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickLower) - upperTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickUpper) - calculatedToken0BalanceStr, calculatedToken1BalanceStr := common.GetAmountsForLiquidity( - currentSqrtPriceX96, - lowerTickSqrtPriceX96, - upperTickSqrtPriceX96, - position.liquidity, - ) - calculatedToken0Balance := u256.MustFromDecimal(calculatedToken0BalanceStr) - calculatedToken1Balance := u256.MustFromDecimal(calculatedToken1BalanceStr) - - position.tokensOwed0 = updateTokensOwed( - feeGrowthInside0LastX128, - position.feeGrowthInside0LastX128, - position.liquidity, - burnedAmount0, - position.tokensOwed0, - ) - - position.tokensOwed1 = updateTokensOwed( - feeGrowthInside1LastX128, - position.feeGrowthInside1LastX128, - position.liquidity, - burnedAmount1, - position.tokensOwed1, - ) - - position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128 - position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128 - position.liquidity = u256.Zero().Sub(positionLiquidity, liquidityToRemove) - position.token0Balance = u256.Zero().Sub(calculatedToken0Balance, burnedAmount0) - position.token1Balance = u256.Zero().Sub(calculatedToken1Balance, burnedAmount1) - mustUpdatePosition(params.positionId, position) - - collect0, collect1 := pl.Collect( - cross, - pToken0, - pToken1, - pFee, - caller, - position.tickLower, - position.tickUpper, - burn0, - burn1, - ) - - collectAmount0 := u256.MustFromDecimal(collect0) - collectAmount1 := u256.MustFromDecimal(collect1) - - poolAddr, _ := access.GetAddress(prabc.ROLE_POOL.String()) - - if isWrappedToken(pToken0) && params.unwrapResult { - unwrapWithTransferFrom(poolAddr, caller, safeConvertToInt64(collectAmount0)) - } else { - common.TransferFrom(cross, pToken0, poolAddr, caller, safeConvertToInt64(collectAmount0)) - } - - if isWrappedToken(pToken1) && params.unwrapResult { - unwrapWithTransferFrom(poolAddr, caller, safeConvertToInt64(collectAmount1)) - } else { - common.TransferFrom(cross, pToken1, poolAddr, caller, safeConvertToInt64(collectAmount1)) - } - - // Check for underflow when subtracting collected amounts from tokens owed - newOwed0, underflow0 := u256.Zero().SubOverflow(position.tokensOwed0, collectAmount0) - if underflow0 { - panic(ufmt.Sprintf("[POSITION] burn.gno | collect() | tokensOwed0(%s) < collectAmount0(%s)", position.tokensOwed0.ToString(), collectAmount0.ToString())) - } - position.tokensOwed0 = newOwed0 - - newOwed1, underflow1 := u256.Zero().SubOverflow(position.tokensOwed1, collectAmount1) - if underflow1 { - panic(ufmt.Sprintf("[POSITION] burn.gno | collect() | tokensOwed1(%s) < collectAmount1(%s)", position.tokensOwed1.ToString(), collectAmount1.ToString())) - } - position.tokensOwed1 = newOwed1 - - if position.isClear() { - position.burned = true // just update flag (we don't want to burn actual position) - } - - mustUpdatePosition(params.positionId, position) - - return params.positionId, liquidityToRemove.ToString(), fee0Str, fee1Str, collect0, collect1, position.poolKey, nil -} - -func updateTokensOwed( - feeGrowthInsideLastX128 *u256.Uint, - positionFeeGrowthInsideLastX128 *u256.Uint, - positionLiquidity *u256.Uint, - burnedAmount *u256.Uint, - tokensOwed *u256.Uint, -) *u256.Uint { - additionalTokensOwed := calculateTokensOwed(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128, positionLiquidity) - add := u256.Zero().Add(burnedAmount, additionalTokensOwed) - return u256.Zero().Add(tokensOwed, add) -} - -// calculateFees calculates the fees for the current position. -func calculateFees(position Position, currentFeeGrowth FeeGrowthInside) (*u256.Uint, *u256.Uint) { - fee0 := calculateTokensOwed( - currentFeeGrowth.feeGrowthInside0LastX128, - position.feeGrowthInside0LastX128, - position.liquidity, - ) - - fee1 := calculateTokensOwed( - currentFeeGrowth.feeGrowthInside1LastX128, - position.feeGrowthInside1LastX128, - position.liquidity, - ) - - tokensOwed0, overflow0 := u256.Zero().AddOverflow(u256.Zero().Set(position.tokensOwed0), fee0) - if overflow0 { - panic(newErrorWithDetail(errOverflow, "tokensOwed0 + fee0 overflow")) - } - tokensOwed1, overflow1 := u256.Zero().AddOverflow(u256.Zero().Set(position.tokensOwed1), fee1) - if overflow1 { - panic(newErrorWithDetail(errOverflow, "tokensOwed1 + fee1 overflow")) - } - - return tokensOwed0, tokensOwed1 -} - -func calculateTokensOwed( - feeGrowthInsideLastX128 *u256.Uint, - positionFeeGrowthInsideLastX128 *u256.Uint, - positionLiquidity *u256.Uint, -) *u256.Uint { - diff := u256.Zero().Sub(feeGrowthInsideLastX128, positionFeeGrowthInsideLastX128) - // TODO: make Q128 a global variable - return u256.MulDiv(diff, positionLiquidity, u256.MustFromDecimal(Q128)) -} diff --git a/contract/r/gnoswap/v1/position/doc.gno b/contract/r/gnoswap/v1/position/doc.gno deleted file mode 100644 index 62eec08..0000000 --- a/contract/r/gnoswap/v1/position/doc.gno +++ /dev/null @@ -1,5 +0,0 @@ -// Package position manages liquidity positions as NFTs in GnoSwap pools. -// It provides functionality for minting, burning, and managing concentrated liquidity positions, -// handling position ownership through GNFT tokens, and managing wrapped/unwrapped native tokens. -// Each position represents liquidity provided within a specific price range in a pool. -package position diff --git a/contract/r/gnoswap/v1/position/errors.gno b/contract/r/gnoswap/v1/position/errors.gno deleted file mode 100644 index bca698b..0000000 --- a/contract/r/gnoswap/v1/position/errors.gno +++ /dev/null @@ -1,40 +0,0 @@ -package position - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errNoPermission = errors.New("[GNOSWAP-POSITION-001] caller has no permission") - errSlippage = errors.New("[GNOSWAP-POSITION-002] slippage failed") - errWrapUnwrap = errors.New("[GNOSWAP-POSITION-003] wrap, unwrap failed") - errZeroWrappedAmount = errors.New("[GNOSWAP-POSITION-004] zero wrapped amount") - errInvalidInput = errors.New("[GNOSWAP-POSITION-005] invalid input data") - errDataNotFound = errors.New("[GNOSWAP-POSITION-006] requested data not found") - errExpired = errors.New("[GNOSWAP-POSITION-007] transaction expired") - errWugnotMinimum = errors.New("[GNOSWAP-POSITION-008] can not wrap less than minimum amount") - errNotClear = errors.New("[GNOSWAP-POSITION-009] position is not clear") - errZeroLiquidity = errors.New("[GNOSWAP-POSITION-010] zero liquidity") - errPositionExist = errors.New("[GNOSWAP-POSITION-011] position with same positionId already exists") - errInvalidAddress = errors.New("[GNOSWAP-POSITION-012] invalid address") - errPositionDoesNotExist = errors.New("[GNOSWAP-POSITION-013] position does not exist") - errZeroUGNOT = errors.New("[GNOSWAP-POSITION-014] No UGNOTs were sent") - errInsufficientUGNOT = errors.New("[GNOSWAP-POSITION-015] Insufficient UGNOT provided") - errInvalidTokenPath = errors.New("[GNOSWAP-POSITION-016] invalid token address") - errInvalidLiquidityRatio = errors.New("[GNOSWAP-POSITION-017] invalid liquidity ratio") - errUnderflow = errors.New("[GNOSWAP-POSITION-018] underflow") - errOverflow = errors.New("[GNOSWAP-POSITION-019] overflow") - errInvalidLiquidity = errors.New("[GNOSWAP-POSITION-019] invalid liquidity") -) - -// newErrorWithDetail appends additional context or details to an existing error message. -func newErrorWithDetail(err error, detail string) string { - return ufmt.Errorf("%s || %s", err.Error(), detail).Error() -} - -// makeErrorWithDetails creates an error with additional context. -func makeErrorWithDetails(err error, details string) error { - return ufmt.Errorf("%s || %s", err.Error(), details) -} diff --git a/contract/r/gnoswap/v1/position/getter.gno b/contract/r/gnoswap/v1/position/getter.gno deleted file mode 100644 index 99a61aa..0000000 --- a/contract/r/gnoswap/v1/position/getter.gno +++ /dev/null @@ -1,104 +0,0 @@ -package position - -import ( - "std" - - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/v1/gnft" - pl "gno.land/r/gnoswap/v1/pool" -) - -func PositionGetPosition(positionId uint64) Position { - position, _ := GetPosition(positionId) - return position -} - -func PositionGetPositionNonce(positionId uint64) *u256.Uint { - return MustGetPosition(positionId).nonce -} - -func PositionGetPositionOperator(positionId uint64) std.Address { - return MustGetPosition(positionId).operator -} - -func PositionGetPositionPoolKey(positionId uint64) string { - return MustGetPosition(positionId).poolKey -} - -func PositionGetPositionTickLower(positionId uint64) int32 { - return MustGetPosition(positionId).tickLower -} - -func PositionGetPositionTickUpper(positionId uint64) int32 { - return MustGetPosition(positionId).tickUpper -} - -func PositionGetPositionLiquidity(positionId uint64) *u256.Uint { - return MustGetPosition(positionId).liquidity -} - -func PositionGetPositionFeeGrowthInside0LastX128(positionId uint64) *u256.Uint { - return MustGetPosition(positionId).feeGrowthInside0LastX128 -} - -func PositionGetPositionFeeGrowthInside1LastX128(positionId uint64) *u256.Uint { - return MustGetPosition(positionId).feeGrowthInside1LastX128 -} - -func PositionGetPositionTokensOwed0(positionId uint64) *u256.Uint { - return MustGetPosition(positionId).tokensOwed0 -} - -func PositionGetPositionTokensOwed1(positionId uint64) *u256.Uint { - return MustGetPosition(positionId).tokensOwed1 -} - -func PositionGetPositionIsBurned(positionId uint64) bool { - return MustGetPosition(positionId).burned -} - -func PositionIsInRange(positionId uint64) bool { - position := MustGetPosition(positionId) - poolPath := position.poolKey - poolCurrentTick := pl.GetSlot0Tick(poolPath) - - return position.tickLower <= poolCurrentTick && poolCurrentTick < position.tickUpper -} - -func PositionGetPositionOwner(positionId uint64) std.Address { - owner, err := gnft.OwnerOf(positionIdFrom(positionId)) - if err != nil { - panic(newErrorWithDetail( - errDataNotFound, err.Error())) - } - return owner -} - -func PositionGetPositionNonceStr(positionId uint64) string { - return PositionGetPositionNonce(positionId).ToString() -} - -func PositionGetPositionOperatorStr(positionId uint64) string { - return PositionGetPositionOperator(positionId).String() -} - -func PositionGetPositionLiquidityStr(positionId uint64) string { - return PositionGetPositionLiquidity(positionId).ToString() -} - -func PositionGetPositionFeeGrowthInside0LastX128Str(positionId uint64) string { - return PositionGetPositionFeeGrowthInside0LastX128(positionId).ToString() -} - -func PositionGetPositionFeeGrowthInside1LastX128Str(positionId uint64) string { - return PositionGetPositionFeeGrowthInside1LastX128(positionId).ToString() -} - -func PositionGetPositionTokensOwed0Str(positionId uint64) string { - return PositionGetPositionTokensOwed0(positionId).ToString() -} - -func PositionGetPositionTokensOwed1Str(positionId uint64) string { - return PositionGetPositionTokensOwed1(positionId).ToString() -} diff --git a/contract/r/gnoswap/v1/position/gnomod.toml b/contract/r/gnoswap/v1/position/gnomod.toml deleted file mode 100644 index a0d7e4c..0000000 --- a/contract/r/gnoswap/v1/position/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/position" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/position/json.gno b/contract/r/gnoswap/v1/position/json.gno deleted file mode 100644 index eaf7911..0000000 --- a/contract/r/gnoswap/v1/position/json.gno +++ /dev/null @@ -1,149 +0,0 @@ -package position - -import ( - "std" - - "gno.land/p/onbloc/json" - "gno.land/r/gnoswap/v1/gnft" -) - -type JsonResponse interface { - JSON() *json.Node -} - -// helper function for creating JSON response -func makeJsonResponse(stat *ResponseQueryBase, response JsonResponse) string { - node := json.ObjectNode("", map[string]*json.Node{ - "stat": stat.JSON(), - "response": response.JSON(), - }) - - b, err := json.Marshal(node) - if err != nil { - panic(err.Error()) - } - - return string(b) -} - -// Type for Positions response -type PositionsResponse struct { - Positions []RpcPosition -} - -func (pr *PositionsResponse) JSON() *json.Node { - rsps := json.ArrayNode("", []*json.Node{}) - for _, position := range pr.Positions { - owner, err := gnft.OwnerOf(positionIdFrom(position.LpPositionId)) - if err != nil { - owner = zeroAddress - } - rsps.AppendArray(position.JSON(owner)) - } - return rsps -} - -// Type for UnclaimedFee response -type UnclaimedFeesResponse struct { - Fees []RpcUnclaimedFee -} - -func (ur *UnclaimedFeesResponse) JSON() *json.Node { - rsps := json.ArrayNode("", []*json.Node{}) - for _, fee := range ur.Fees { - rsps.AppendArray(fee.JSON()) - } - return rsps -} - -///////////////// RPC TYPES ///////////////// - -type RpcPosition struct { - LpPositionId uint64 `json:"lpPositionId"` - Burned bool `json:"burned"` - Owner string `json:"owner"` - Operator string `json:"operator"` - PoolKey string `json:"poolKey"` - TickLower int32 `json:"tickLower"` - TickUpper int32 `json:"tickUpper"` - Liquidity string `json:"liquidity"` - FeeGrowthInside0LastX128 string `json:"feeGrowthInside0LastX128"` - FeeGrowthInside1LastX128 string `json:"feeGrowthInside1LastX128"` - TokensOwed0 string `json:"token0Owed"` - TokensOwed1 string `json:"token1Owed"` - - Token0Balance string `json:"token0Balance"` - Token1Balance string `json:"token1Balance"` - CalculatedToken0Balance string `json:"calculatedToken0Balance"` - CalculatedToken1Balance string `json:"calculatedToken1Balance"` - FeeUnclaimed0 string `json:"fee0Unclaimed"` - FeeUnclaimed1 string `json:"fee1Unclaimed"` -} - -func (p RpcPosition) JSON(owner std.Address) *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "lpPositionId": json.NumberNode("lpPositionId", float64(p.LpPositionId)), - "burned": json.BoolNode("burned", p.Burned), - "owner": json.StringNode("owner", owner.String()), - "operator": json.StringNode("operator", p.Operator), - "poolKey": json.StringNode("poolKey", p.PoolKey), - "tickLower": json.NumberNode("tickLower", float64(p.TickLower)), - "tickUpper": json.NumberNode("tickUpper", float64(p.TickUpper)), - "liquidity": json.StringNode("liquidity", p.Liquidity), - "feeGrowthInside0LastX128": json.StringNode("feeGrowthInside0LastX128", p.FeeGrowthInside0LastX128), - "feeGrowthInside1LastX128": json.StringNode("feeGrowthInside1LastX128", p.FeeGrowthInside1LastX128), - "token0Owed": json.StringNode("token0Owed", p.TokensOwed0), - "token1Owed": json.StringNode("token1Owed", p.TokensOwed1), - "token0Balance": json.StringNode("token0Balance", p.Token0Balance), - "token1Balance": json.StringNode("token1Balance", p.Token1Balance), - "calculatedToken0Balance": json.StringNode("calculatedToken0Balance", p.CalculatedToken0Balance), - "calculatedToken1Balance": json.StringNode("calculatedToken1Balance", p.CalculatedToken1Balance), - "fee0Unclaimed": json.StringNode("fee0Unclaimed", p.FeeUnclaimed0), - "fee1Unclaimed": json.StringNode("fee1Unclaimed", p.FeeUnclaimed1), - }) -} - -type RpcUnclaimedFee struct { - LpPositionId uint64 `json:"lpPositionId"` - Fee0 string `json:"fee0"` - Fee1 string `json:"fee1"` -} - -func (p RpcUnclaimedFee) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "lpPositionId": json.NumberNode("lpPositionId", float64(p.LpPositionId)), - "fee0": json.StringNode("fee0", p.Fee0), - "fee1": json.StringNode("fee1", p.Fee1), - }) -} - -type ResponseQueryBase struct { - Height int64 `json:"height"` - Timestamp int64 `json:"timestamp"` -} - -func NewResponseQueryBase(height int64, timestamp int64) ResponseQueryBase { - return ResponseQueryBase{ - Height: height, - Timestamp: timestamp, - } -} - -func (r ResponseQueryBase) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "height": json.NumberNode("height", float64(r.Height)), - "timestamp": json.NumberNode("timestamp", float64(r.Timestamp)), - }) -} - -type ResponseApiGetPositions struct { - Stat ResponseQueryBase `json:"stat"` - Response []RpcPosition `json:"response"` -} - -func NewResponseApiGetPositions(stat ResponseQueryBase, response []RpcPosition) ResponseApiGetPositions { - return ResponseApiGetPositions{ - Stat: stat, - Response: response, - } -} diff --git a/contract/r/gnoswap/v1/position/liquidity_management.gno b/contract/r/gnoswap/v1/position/liquidity_management.gno deleted file mode 100644 index 69a14f8..0000000 --- a/contract/r/gnoswap/v1/position/liquidity_management.gno +++ /dev/null @@ -1,67 +0,0 @@ -package position - -import ( - "std" - - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/v1/common" - pl "gno.land/r/gnoswap/v1/pool" -) - -type AddLiquidityParams struct { - poolKey string // poolPath of the pool which has the position - tickLower int32 // lower end of the tick range for the position - tickUpper int32 // upper end of the tick range for the position - amount0Desired *u256.Uint // desired amount of token0 to be minted - amount1Desired *u256.Uint // desired amount of token1 to be minted - amount0Min *u256.Uint // minimum amount of token0 to be minted - amount1Min *u256.Uint // minimum amount of token1 to be minted - caller std.Address // address to call the function -} - -// addLiquidity calculates liquidity amounts and mints position tokens to a pool. -func addLiquidity(params AddLiquidityParams) (*u256.Uint, *u256.Uint, *u256.Uint) { - sqrtPriceX96 := pl.GetSlot0SqrtPriceX96(params.poolKey) - sqrtRatioAX96 := common.TickMathGetSqrtRatioAtTick(params.tickLower) - sqrtRatioBX96 := common.TickMathGetSqrtRatioAtTick(params.tickUpper) - - liquidity := common.GetLiquidityForAmounts( - sqrtPriceX96, - sqrtRatioAX96, - sqrtRatioBX96, - params.amount0Desired, - params.amount1Desired, - ) - - token0, token1, fee := splitOf(params.poolKey) - amount0Str, amount1Str := pl.Mint( - cross, - token0, - token1, - fee, - positionAddr, - params.tickLower, - params.tickUpper, - liquidity.ToString(), - params.caller, - ) - - amount0 := u256.MustFromDecimal(amount0Str) - amount1 := u256.MustFromDecimal(amount1Str) - - amount0Cond := amount0.Gte(params.amount0Min) - amount1Cond := amount1.Gte(params.amount1Min) - - if !(amount0Cond && amount1Cond) { - panic(newErrorWithDetail( - errSlippage, - ufmt.Sprintf( - "Price Slippage Check(amount0(%s) >= amount0Min(%s), amount1(%s) >= amount1Min(%s))", - amount0Str, params.amount0Min.ToString(), amount1Str, params.amount1Min.ToString()), - )) - } - - return liquidity, amount0, amount1 -} diff --git a/contract/r/gnoswap/v1/position/manager.gno b/contract/r/gnoswap/v1/position/manager.gno deleted file mode 100644 index d8adb5e..0000000 --- a/contract/r/gnoswap/v1/position/manager.gno +++ /dev/null @@ -1,95 +0,0 @@ -package position - -import ( - "strconv" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" -) - -var ( - positions = avl.NewTree() // positionId[uint64] -> Position - nextId = uint64(1) -) - -// GetPosition returns a position for a given position ID. -// Returns false if position doesn't exist -func GetPosition(id uint64) (Position, bool) { - idStr := strconv.FormatUint(id, 10) - iPosition, exist := positions.Get(idStr) - if !exist { - return Position{}, false - } - - pos, ok := iPosition.(Position) - if !ok { - panic("cannot cast position to Position") - } - return pos, ok -} - -// MustGetPosition returns a position for a given position ID. -// panics if position doesn't exist -func MustGetPosition(id uint64) Position { - position, exist := GetPosition(id) - if !exist { - panic(newErrorWithDetail( - errPositionDoesNotExist, - ufmt.Sprintf("position with position ID(%d) doesn't exist", id), - )) - } - return position -} - -// ExistPosition checks if a position exists for a given position ID -func ExistPosition(id uint64) bool { - _, exist := GetPosition(id) - return exist -} - -// GetNextId is the next position ID to be minted -func GetNextId() uint64 { - return nextId -} - -// createNewPosition creates a new position with the given position data. -func createNewPosition(id uint64, pos Position) uint64 { - if ExistPosition(id) { - panic(newErrorWithDetail( - errPositionExist, - ufmt.Sprintf("positionId(%d)", id), - )) - } - setPosition(id, pos) - incrementNextId() - return id -} - -// setPosition sets a position for a given position ID. -// Returns true if position is newly created, false if position already exists and just updated. -func setPosition(id uint64, position Position) bool { - posIdStr := strconv.FormatUint(id, 10) - return positions.Set(posIdStr, position) -} - -// mustUpdatePosition updates a position for a given position ID. -func mustUpdatePosition(id uint64, pos Position) { - update := setPosition(id, pos) - if !update { - panic(newErrorWithDetail( - errPositionDoesNotExist, - ufmt.Sprintf("position with position ID(%d) doesn't exist", id), - )) - } -} - -// removePosition removes a position for a given position ID -func removePosition(id uint64) { - posIdStr := strconv.FormatUint(id, 10) - positions.Remove(posIdStr) -} - -// incrementNextId increments the next position ID to be minted -func incrementNextId() { - nextId++ -} diff --git a/contract/r/gnoswap/v1/position/mint.gno b/contract/r/gnoswap/v1/position/mint.gno deleted file mode 100644 index e9afeba..0000000 --- a/contract/r/gnoswap/v1/position/mint.gno +++ /dev/null @@ -1,300 +0,0 @@ -package position - -import ( - "std" - - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/v1/common" - "gno.land/r/gnoswap/v1/gnft" - pl "gno.land/r/gnoswap/v1/pool" -) - -const ( - WRAPPED_WUGNOT string = "gno.land/r/gnoland/wugnot" - UGNOT string = "ugnot" - GNOT string = "gnot" -) - -// mint creates a new liquidity position by adding liquidity to a pool and minting an NFT. -// Panics if position ID already exists or adding liquidity fails. -func mint(params MintParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint) { - poolKey := pl.GetPoolPath(params.token0, params.token1, params.fee) - liquidity, amount0, amount1 := addLiquidity( - AddLiquidityParams{ - poolKey: poolKey, - tickLower: params.tickLower, - tickUpper: params.tickUpper, - amount0Desired: params.amount0Desired, - amount1Desired: params.amount1Desired, - amount0Min: params.amount0Min, - amount1Min: params.amount1Min, - caller: params.caller, - }, - ) - // Ensure liquidity is not zero before minting NFT - if liquidity.IsZero() { - panic(newErrorWithDetail( - errZeroLiquidity, - "Liquidity is zero, cannot mint position.", - )) - } - - id := GetNextId() - if ExistPosition(id) { - panic(newErrorWithDetail( - errPositionExist, - ufmt.Sprintf("positionId(%d)", id), - )) - } - - gnft.Mint(cross, params.mintTo, positionIdFrom(id)) - - positionKey := computePositionKey(params.tickLower, params.tickUpper) - feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(poolKey, positionKey) - - position := Position{ - nonce: u256.Zero(), - operator: zeroAddress, - poolKey: poolKey, - tickLower: params.tickLower, - tickUpper: params.tickUpper, - liquidity: liquidity, - feeGrowthInside0LastX128: feeGrowthInside0LastX128, - feeGrowthInside1LastX128: feeGrowthInside1LastX128, - tokensOwed0: u256.Zero(), - tokensOwed1: u256.Zero(), - token0Balance: amount0, - token1Balance: amount1, - burned: false, - } - - // The position ID should not exist at the time of minting - updated := setPosition(id, position) - if updated { - panic(newErrorWithDetail( - errPositionExist, - ufmt.Sprintf("position ID(%d) already exists", id), - )) - } - incrementNextId() - - return id, liquidity, amount0, amount1 -} - -// processMintInput processes and validates user input for minting liquidity. -// It handles token ordering, amount validation, and native token wrapping. -func processMintInput(input MintInput) (ProcessedMintInput, error) { - assertValidNumberString(input.amount0Desired) - assertValidNumberString(input.amount1Desired) - assertValidNumberString(input.amount0Min) - assertValidNumberString(input.amount1Min) - var result ProcessedMintInput - - // process tokens - token0, token1, token0IsNative, token1IsNative, wrappedAmount, err := processTokens( - input.token0, - input.token1, - input.amount0Desired, - input.amount1Desired, - input.caller, - ) - if err != nil { - return ProcessedMintInput{}, err - } - - pair := TokenPair{ - token0: token0, - token1: token1, - token0IsNative: token0IsNative, - token1IsNative: token1IsNative, - wrappedAmount: wrappedAmount, - } - - // parse amounts - amount0Desired, amount1Desired, amount0Min, amount1Min := parseAmounts(input.amount0Desired, input.amount1Desired, input.amount0Min, input.amount1Min) - - tickLower, tickUpper := input.tickLower, input.tickUpper - - // swap if token1 < token0 - if token1 < token0 { - pair.token0, pair.token1 = pair.token1, pair.token0 - amount0Desired, amount1Desired = amount1Desired, amount0Desired - amount0Min, amount1Min = amount1Min, amount0Min - tickLower, tickUpper = -tickUpper, -tickLower - pair.token0IsNative, pair.token1IsNative = pair.token1IsNative, pair.token0IsNative - } - - poolPath := computePoolPath(pair.token0, pair.token1, input.fee) - - result = ProcessedMintInput{ - tokenPair: pair, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: amount0Min, - amount1Min: amount1Min, - tickLower: tickLower, - tickUpper: tickUpper, - poolPath: poolPath, - } - - return result, nil -} - -// processTokens validates token paths and handles native token wrapping. -// Panics if validation fails or native token wrapping encounters issues. -func processTokens( - token0 string, - token1 string, - amount0Desired string, - amount1Desired string, - caller std.Address, -) (string, string, bool, bool, int64, error) { - err := validateTokenPath(token0, token1) - if err != nil { - panic(newErrorWithDetail(err, ufmt.Sprintf("token0(%s), token1(%s)", token0, token1))) - } - - token0IsNative := false - token1IsNative := false - wrappedAmount := int64(0) - - if isNative(token0) { - token0 = WRAPPED_WUGNOT - token0IsNative = true - - amount0DesiredInt := mustParseInt64(amount0Desired) - wrappedAmount, err = safeWrapNativeToken(amount0DesiredInt, caller) - if err != nil { - return "", "", false, false, 0, err - } - } else if isNative(token1) { - token1 = WRAPPED_WUGNOT - token1IsNative = true - - amount1DesiredInt := mustParseInt64(amount1Desired) - wrappedAmount, err = safeWrapNativeToken(amount1DesiredInt, caller) - if err != nil { - return "", "", false, false, 0, err - } - } - - return token0, token1, token0IsNative, token1IsNative, wrappedAmount, nil -} - -// increaseLiquidity increases the liquidity of an existing position. -func increaseLiquidity(params IncreaseLiquidityParams) (uint64, *u256.Uint, *u256.Uint, *u256.Uint, string, error) { - caller := params.caller - position, exist := GetPosition(params.positionId) - if !exist { - return 0, nil, nil, nil, "", makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("positionId(%d) doesn't exist", params.positionId), - ) - } - - liquidity, amount0, amount1 := addLiquidity( - AddLiquidityParams{ - poolKey: position.poolKey, - tickLower: position.tickLower, - tickUpper: position.tickUpper, - amount0Desired: params.amount0Desired, - amount1Desired: params.amount1Desired, - amount0Min: params.amount0Min, - amount1Min: params.amount1Min, - caller: caller, - }, - ) - - positionKey := computePositionKey(position.tickLower, position.tickUpper) - feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(position.poolKey, positionKey) - - currentSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(position.poolKey) - lowerTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickLower) - upperTickSqrtPriceX96 := common.TickMathGetSqrtRatioAtTick(position.tickUpper) - calculatedToken0BalanceStr, calculatedToken1BalanceStr := common.GetAmountsForLiquidity( - currentSqrtPriceX96, - lowerTickSqrtPriceX96, - upperTickSqrtPriceX96, - position.liquidity, - ) - calculatedToken0Balance := u256.MustFromDecimal(calculatedToken0BalanceStr) - calculatedToken1Balance := u256.MustFromDecimal(calculatedToken1BalanceStr) - - { - diff := u256.Zero().Sub(feeGrowthInside0LastX128, position.feeGrowthInside0LastX128) - mulDiv := u256.MulDiv(diff, u256.Zero().Set(position.liquidity), u256.MustFromDecimal(Q128)) - - position.tokensOwed0 = u256.Zero().Add(position.tokensOwed0, mulDiv) - } - - { - diff := u256.Zero().Sub(feeGrowthInside1LastX128, position.feeGrowthInside1LastX128) - mulDiv := u256.MulDiv(diff, u256.Zero().Set(position.liquidity), u256.MustFromDecimal(Q128)) - - position.tokensOwed1 = u256.Zero().Add(position.tokensOwed1, mulDiv) - } - - position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128 - position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128 - - liquidityAmount, overflow := u256.Zero().AddOverflow(u256.Zero().Set(position.liquidity), liquidity) - if overflow { - return 0, nil, nil, nil, "", errOverflow - } - token0Balance, overflow := u256.Zero().AddOverflow(u256.Zero().Set(calculatedToken0Balance), amount0) - if overflow { - return 0, nil, nil, nil, "", errOverflow - } - token1Balance, overflow := u256.Zero().AddOverflow(u256.Zero().Set(calculatedToken1Balance), amount1) - if overflow { - return 0, nil, nil, nil, "", errOverflow - } - - position.liquidity = liquidityAmount - position.token0Balance = token0Balance - position.token1Balance = token1Balance - position.burned = false - - updated := setPosition(params.positionId, position) - if !updated { - return 0, nil, nil, nil, "", makeErrorWithDetails( - errPositionDoesNotExist, - ufmt.Sprintf("can not increase liquidity for non-existent position(%d)", params.positionId), - ) - } - - return params.positionId, liquidity, amount0, amount1, position.poolKey, nil -} - -// validateTokenPath validates token paths are not identical, not conflicting, and in valid format. -func validateTokenPath(token0, token1 string) error { - if token0 == token1 { - return errInvalidTokenPath - } - if (token0 == GNOT && token1 == WRAPPED_WUGNOT) || - (token0 == WRAPPED_WUGNOT && token1 == GNOT) { - return errInvalidTokenPath - } - if (!isNative(token0) && !isValidTokenPath(token0)) || - (!isNative(token1) && !isValidTokenPath(token1)) { - return errInvalidTokenPath - } - return nil -} - -// isValidTokenPath checks if the token path is registered in the system. -func isValidTokenPath(tokenPath string) bool { - return common.IsRegistered(tokenPath) == nil -} - -// parseAmounts converts amount strings to u256.Uint values. -func parseAmounts(amount0Desired, amount1Desired, amount0Min, amount1Min string) (*u256.Uint, *u256.Uint, *u256.Uint, *u256.Uint) { - return u256.MustFromDecimal(amount0Desired), u256.MustFromDecimal(amount1Desired), u256.MustFromDecimal(amount0Min), u256.MustFromDecimal(amount1Min) -} - -// computePoolPath returns the pool path based on token pair and fee tier. -func computePoolPath(token0, token1 string, fee uint32) string { - return pl.GetPoolPath(token0, token1, fee) -} diff --git a/contract/r/gnoswap/v1/position/native_token.gno b/contract/r/gnoswap/v1/position/native_token.gno deleted file mode 100644 index cf13141..0000000 --- a/contract/r/gnoswap/v1/position/native_token.gno +++ /dev/null @@ -1,171 +0,0 @@ -package position - -import ( - "std" - - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoland/wugnot" -) - -const ( - UGNOT_MIN_DEPOSIT_TO_WRAP = int64(1000) - WUGNOT_PATH = "gno.land/r/gnoland/wugnot" - GNOT_DENOM = "ugnot" -) - -var ( - errFailedToWrapZeroUgnot = "cannot wrap 0 ugnot" - errFailedToWrapBelowMin = "amount(%d) < minimum(%d)" -) - -// wrap wraps the specified amount of the native token ugnot into the wrapped token wugnot. -// Returns an error if ugnotAmount is zero or less than the minimum deposit threshold. -func wrap(ugnotAmount int64, to std.Address) error { - if ugnotAmount == 0 || ugnotAmount < UGNOT_MIN_DEPOSIT_TO_WRAP { - return ufmt.Errorf("amount(%d) < minimum(%d)", ugnotAmount, UGNOT_MIN_DEPOSIT_TO_WRAP) - } - - wugnotAddr := std.DerivePkgAddr(WRAPPED_WUGNOT) - transferUGNOT(positionAddr, wugnotAddr, ugnotAmount) - - wugnot.Deposit(cross) // position has wugnot - wugnot.Transfer(cross, to, ugnotAmount) // send wugnot: position -> user - - return nil -} - -// unwrap converts a specified amount of WUGNOT tokens into UGNOT tokens -// and transfers the resulting UGNOT back to the specified recipient address. -func unwrap(wugnotAmount int64, to std.Address) error { - if wugnotAmount <= 0 { - return ufmt.Errorf("amount(%d) is zero or negative", wugnotAmount) - } - - wugnot.TransferFrom(cross, to, positionAddr, wugnotAmount) // send wugnot: user -> position - wugnot.Withdraw(cross, wugnotAmount) // position has ugnot - transferUGNOT(positionAddr, to, wugnotAmount) // send ugnot: position -> user - return nil -} - -// transferUGNOT transfers a specified amount of UGNOT tokens from one address to another. -// The function ensures that no transaction occurs if the transfer amount is zero. -// Panics if amount is negative or if sender has insufficient balance. -func transferUGNOT(from, to std.Address, amount int64) { - if amount < 0 { - panic(ufmt.Sprintf("amount(%d) is negative", amount)) - } - if amount == 0 { - return - } - - banker := std.NewBanker(std.BankerTypeRealmSend) - fromBalance := banker.GetCoins(from).AmountOf(UGNOT) - if fromBalance < amount { - panic(newErrorWithDetail( - errInsufficientUGNOT, - ufmt.Sprintf("from(%s) balance(%d) is less than amount(%d)", from, fromBalance, amount))) - } - banker.SendCoins(from, to, std.Coins{ - {Denom: UGNOT, Amount: amount}, - }) -} - -// isNative checks whether the given token is a native token. -func isNative(token string) bool { - return token == GNOT -} - -// isWrappedToken checks whether the tokenPath is wrapped token. -func isWrappedToken(tokenPath string) bool { - return tokenPath == WRAPPED_WUGNOT -} - -// safeWrapNativeToken safely wraps the native token ugnot into the wrapped token wugnot for a user. -// Returns the amount of ugnot that was successfully wrapped into wugnot. -// Returns an error if the sent ugnot amount is zero, less than requested, or if wrapping fails. -func safeWrapNativeToken(amount int64, toAddress std.Address) (int64, error) { - // if amount is zero, return 0 - if amount == 0 { - return 0, nil - } - - beforeWrappedBalance := wugnot.BalanceOf(toAddress) - nativeSentAmount := std.OriginSend().AmountOf(UGNOT) - - if nativeSentAmount <= 0 { - return 0, makeErrorWithDetails(errZeroUGNOT, "amount of ugnot is zero") - } - - if nativeSentAmount < amount { - return 0, makeErrorWithDetails(errInsufficientUGNOT, "amount of ugnot is less than desired amount") - } - - // If nativeSentAmount is greater than amount, refund the excess amount. - if nativeSentAmount > amount { - excessAmount := nativeSentAmount - amount - transferUGNOT(positionAddr, toAddress, excessAmount) - - nativeSentAmount = amount - } - - if err := wrapWithTransfer(nativeSentAmount, toAddress); err != nil { - return 0, err - } - - afterWrappedBalance := wugnot.BalanceOf(toAddress) - balanceDiff := afterWrappedBalance - beforeWrappedBalance - - if balanceDiff != nativeSentAmount { - return 0, makeErrorWithDetails( - errWrapUnwrap, - ufmt.Sprintf("amount of ugnot (%d) is not equal to amount of wugnot. (diff: %d)", nativeSentAmount, balanceDiff), - ) - } - - return nativeSentAmount, nil -} - -func wrapWithTransfer(ugnotAmount int64, toAddress std.Address) error { - if ugnotAmount <= 0 { - return makeErrorWithDetails(errWrapUnwrap, errFailedToWrapZeroUgnot) - } - - if ugnotAmount < UGNOT_MIN_DEPOSIT_TO_WRAP { - return makeErrorWithDetails( - errWugnotMinimum, - ufmt.Sprintf(errFailedToWrapBelowMin, ugnotAmount, UGNOT_MIN_DEPOSIT_TO_WRAP), - ) - } - - // wrap it - wugnotAddr := std.DerivePkgAddr(WUGNOT_PATH) - currentRealmAddr := std.CurrentRealm().Address() - - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(currentRealmAddr, wugnotAddr, std.Coins{{"ugnot", ugnotAmount}}) - wugnot.Deposit(cross) // Position has wugnot - - // send wugnot: position -> user - wugnot.Transfer(cross, toAddress, ugnotAmount) - - return nil -} - -func unwrapWithTransferFrom(fromAddress, toAddress std.Address, wugnotAmount int64) error { - if wugnotAmount == 0 { - return nil - } - - currentRealmAddr := std.CurrentRealm().Address() - if fromAddress != currentRealmAddr { - wugnot.TransferFrom(cross, fromAddress, currentRealmAddr, wugnotAmount) - } - - wugnot.Withdraw(cross, wugnotAmount) - - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(currentRealmAddr, toAddress, std.Coins{{"ugnot", wugnotAmount}}) - - return nil -} diff --git a/contract/r/gnoswap/v1/position/position.gno b/contract/r/gnoswap/v1/position/position.gno deleted file mode 100644 index 3322ac2..0000000 --- a/contract/r/gnoswap/v1/position/position.gno +++ /dev/null @@ -1,542 +0,0 @@ -package position - -import ( - "encoding/base64" - "std" - - "gno.land/p/nt/ufmt" - prabc "gno.land/p/gnoswap/rbac" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/rbac" - "gno.land/r/gnoswap/referral" - "gno.land/r/gnoswap/v1/common" - pl "gno.land/r/gnoswap/v1/pool" -) - -var ( - positionAddr = rbac.DefaultRoleAddresses[prabc.ROLE_POSITION] - stakerAddr = rbac.DefaultRoleAddresses[prabc.ROLE_STAKER] -) - -const ( - ZERO_LIQUIDITY_FOR_FEE_COLLECTION = "0" -) - -// Mint creates a new liquidity position NFT. -// -// Parameters: -// - token0, token1: token contract paths -// - fee: pool fee tier -// - tickLower, tickUpper: price range boundaries -// - amount0Desired, amount1Desired: desired token amounts -// - amount0Min, amount1Min: minimum acceptable amounts -// - deadline: transaction deadline -// - mintTo: NFT recipient address -// - caller: transaction initiator -// - referrer: referral address -// -// Returns tokenId, liquidity, amount0, amount1. -// Only callable by users or staker contract. -// Note: Slippage protection via amount0Min/amount1Min. -func Mint( - cur realm, - token0 string, - token1 string, - fee uint32, - tickLower int32, - tickUpper int32, - amount0Desired string, - amount1Desired string, - amount0Min string, - amount1Min string, - deadline int64, - mintTo std.Address, - caller std.Address, - referrer string, -) (uint64, string, string, string) { - halt.AssertIsNotHaltedPosition() - - previousRealm := std.PreviousRealm() - if !previousRealm.IsUser() { - access.AssertIsStaker(previousRealm.Address()) - } else { - assertEqualsAddress(previousRealm.Address(), mintTo) - assertEqualsAddress(previousRealm.Address(), caller) - } - - assertIsNotExpired(deadline) - - referral.TryRegister(cross, caller, referrer) - - emission.MintAndDistributeGns(cross) - - mintInput := MintInput{ - token0: token0, - token1: token1, - fee: fee, - tickLower: tickLower, - tickUpper: tickUpper, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: amount0Min, - amount1Min: amount1Min, - deadline: deadline, - mintTo: mintTo, - caller: caller, - } - processedInput, err := processMintInput(mintInput) - if err != nil { - panic(newErrorWithDetail(errInvalidInput, err.Error())) - } - - // mint liquidity - params := newMintParams(processedInput, mintInput) - id, liquidity, amount0, amount1 := mint(params) - - // refund leftover wrapped tokens - if processedInput.tokenPair.token0IsNative && processedInput.tokenPair.wrappedAmount > safeConvertToInt64(amount0) { - err = unwrap(processedInput.tokenPair.wrappedAmount-safeConvertToInt64(amount0), caller) - if err != nil { - panic(newErrorWithDetail(errWrapUnwrap, err.Error())) - } - } - - if processedInput.tokenPair.token1IsNative && processedInput.tokenPair.wrappedAmount > safeConvertToInt64(amount1) { - err = unwrap(processedInput.tokenPair.wrappedAmount-safeConvertToInt64(amount1), caller) - if err != nil { - panic(newErrorWithDetail(errWrapUnwrap, err.Error())) - } - } - - poolSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(processedInput.poolPath) - - std.Emit( - "Mint", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "tickLower", formatInt(processedInput.tickLower), - "tickUpper", formatInt(processedInput.tickUpper), - "poolPath", processedInput.poolPath, - "mintTo", mintTo.String(), - "caller", caller.String(), - "lpPositionId", formatUint(id), - "liquidity", liquidity.ToString(), - "amount0", amount0.ToString(), - "amount1", amount1.ToString(), - "sqrtPriceX96", poolSqrtPriceX96.ToString(), - "token0Balance", pl.GetBalanceToken0(processedInput.poolPath), - "token1Balance", pl.GetBalanceToken1(processedInput.poolPath), - ) - - return id, liquidity.ToString(), amount0.ToString(), amount1.ToString() -} - -// IncreaseLiquidity increases liquidity of an existing position. -// -// Adds more liquidity to existing NFT position. -// Maintains same price range as original position. -// Calculates optimal token ratio for current price. -// -// Parameters: -// - positionId: NFT token ID to increase -// - amount0DesiredStr: Desired token0 amount -// - amount1DesiredStr: Desired token1 amount -// - amount0MinStr: Minimum token0 (slippage protection) -// - amount1MinStr: Minimum token1 (slippage protection) -// - deadline: Transaction expiration timestamp -// -// Returns: -// - positionId: Same NFT ID -// - liquidity: Total liquidity after increase -// - amount0: Token0 actually deposited -// - amount1: Token1 actually deposited -// - poolPath: Pool identifier -// -// Requirements: -// - Caller must own the position NFT -// - Position must have liquidity -// - Sufficient token balances and approvals -func IncreaseLiquidity( - cur realm, - positionId uint64, - amount0DesiredStr string, - amount1DesiredStr string, - amount0MinStr string, - amount1MinStr string, - deadline int64, -) (uint64, string, string, string, string) { - halt.AssertIsNotHaltedPosition() - - caller := std.PreviousRealm().Address() - assertIsOwnerForToken(positionId, caller) - - assertValidNumberString(amount0DesiredStr) - assertValidNumberString(amount1DesiredStr) - assertValidNumberString(amount0MinStr) - assertValidNumberString(amount1MinStr) - assertIsNotExpired(deadline) - - emission.MintAndDistributeGns(cross) - - position := MustGetPosition(positionId) - token0, token1, _ := splitOf(position.poolKey) - err := validateTokenPath(token0, token1) - if err != nil { - panic(newErrorWithDetail(err, ufmt.Sprintf("token0(%s), token1(%s)", token0, token1))) - } - - wrappedAmount := int64(0) - if isWrappedToken(token0) { - amount0DesiredInt := mustParseInt64(amount0DesiredStr) - wrappedAmount, err = safeWrapNativeToken(amount0DesiredInt, caller) - if err != nil { - panic(err) - } - } else if isWrappedToken(token1) { - amount1DesiredInt := mustParseInt64(amount1DesiredStr) - wrappedAmount, err = safeWrapNativeToken(amount1DesiredInt, caller) - if err != nil { - panic(err) - } - } - - amount0Desired, amount1Desired, amount0Min, amount1Min := parseAmounts(amount0DesiredStr, amount1DesiredStr, amount0MinStr, amount1MinStr) - increaseLiquidityParams := IncreaseLiquidityParams{ - positionId: positionId, - amount0Desired: amount0Desired, - amount1Desired: amount1Desired, - amount0Min: amount0Min, - amount1Min: amount1Min, - deadline: deadline, - caller: caller, - } - - _, liquidity, amount0, amount1, poolPath, err := increaseLiquidity(increaseLiquidityParams) - if err != nil { - panic(err) - } - - if err := unwrapLeftoverWrappedToken(token0, wrappedAmount, safeConvertToInt64(amount0), caller); err != nil { - panic(err) - } - if err := unwrapLeftoverWrappedToken(token1, wrappedAmount, safeConvertToInt64(amount1), caller); err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "IncreaseLiquidity", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "poolPath", poolPath, - "caller", caller.String(), - "lpPositionId", formatUint(positionId), - "liquidity", liquidity.ToString(), - "amount0", amount0.ToString(), - "amount1", amount1.ToString(), - "sqrtPriceX96", pl.GetSlot0SqrtPriceX96(poolPath).ToString(), - "positionLiquidity", PositionGetPositionLiquidityStr(positionId), - "token0Balance", pl.GetBalanceToken0(poolPath), - "token1Balance", pl.GetBalanceToken1(poolPath), - ) - - return positionId, liquidity.ToString(), amount0.ToString(), amount1.ToString(), poolPath -} - -// unwrapLeftoverWrappedToken unwraps leftover wrapped tokens to native tokens. -func unwrapLeftoverWrappedToken(token string, wrapped, amount int64, caller std.Address) error { - unwrappable := isWrappedToken(token) && wrapped > amount - if !unwrappable { - return nil - } - - err := unwrap(wrapped-amount, caller) - if err != nil { - return makeErrorWithDetails(errWrapUnwrap, err.Error()) - } - - return nil -} - -// DecreaseLiquidity decreases liquidity of an existing position. -// -// Removes liquidity but keeps NFT ownership. -// Calculates tokens owed based on current price. -// Two-step: decrease then collect tokens. -// -// Parameters: -// - positionId: NFT token ID -// - liquidityStr: Amount of liquidity to remove -// - amount0MinStr: Min token0 to receive (slippage) -// - amount1MinStr: Min token1 to receive (slippage) -// - deadline: Transaction expiration -// - unwrapResult: Convert WUGNOT to GNOT if true -// -// Returns: -// - positionId: Same NFT ID -// - liquidity: Remaining liquidity -// - fee0, fee1: Fees collected -// - amount0, amount1: Principal collected -// - poolPath: Pool identifier -// -// Note: Applies 1% withdrawal fee on collected amounts. -func DecreaseLiquidity( - cur realm, - positionId uint64, - liquidityStr string, - amount0MinStr string, - amount1MinStr string, - deadline int64, - unwrapResult bool, -) (uint64, string, string, string, string, string, string) { - halt.AssertIsNotHaltedPosition() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - assertIsOwnerForToken(positionId, caller) - assertIsNotExpired(deadline) - assertValidLiquidityAmount(liquidityStr) - - emission.MintAndDistributeGns(cross) - - amount0Min := u256.MustFromDecimal(amount0MinStr) - amount1Min := u256.MustFromDecimal(amount1MinStr) - decreaseLiquidityParams := DecreaseLiquidityParams{ - positionId: positionId, - liquidity: liquidityStr, - amount0Min: amount0Min, - amount1Min: amount1Min, - deadline: deadline, - unwrapResult: unwrapResult, - caller: caller, - } - - positionId, liquidity, fee0, fee1, amount0, amount1, poolPath, err := decreaseLiquidity(decreaseLiquidityParams) - if err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "DecreaseLiquidity", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "lpPositionId", formatUint(positionId), - "poolPath", poolPath, - "decreasedLiquidity", liquidity, - "feeAmount0", fee0, - "feeAmount1", fee1, - "amount0", amount0, - "amount1", amount1, - "unwrapResult", formatBool(unwrapResult), - "sqrtPriceX96", pl.GetSlot0SqrtPriceX96(poolPath).ToString(), - "positionLiquidity", PositionGetPositionLiquidityStr(positionId), - "token0Balance", pl.GetBalanceToken0(poolPath), - "token1Balance", pl.GetBalanceToken1(poolPath), - ) - - return positionId, liquidity, fee0, fee1, amount0, amount1, poolPath -} - -// CollectFee collects swap fee from the position. -// -// Claims accumulated fees without removing liquidity. -// Useful for active positions earning ongoing fees. -// Applies protocol withdrawal fee. -// -// Parameters: -// - positionId: NFT token ID -// - unwrapResult: Convert WUGNOT to GNOT if true -// -// Returns: -// - positionId: Same NFT ID -// - fee0, fee1: Fees collected (after 1% protocol fee) -// - amount0, amount1: Always "0" (no principal) -// - poolPath: Pool identifier -// -// Requirements: -// - Caller must be owner or approved operator -// - Position must have accumulated fees -func CollectFee( - cur realm, - positionId uint64, - unwrapResult bool, -) (uint64, string, string, string, string, string) { - halt.AssertIsNotHaltedPosition() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - assertIsOwnerOrOperatorForToken(positionId, caller) - - return collectFee(positionId, unwrapResult, caller) -} - -// collectFee performs fee collection and withdrawal fee calculation. -func collectFee( - positionId uint64, - unwrapResult bool, - caller std.Address, -) (uint64, string, string, string, string, string) { - emission.MintAndDistributeGns(cross) - - // verify position - position := MustGetPosition(positionId) - token0, token1, fee := splitOf(position.poolKey) - - pl.Burn( - cross, - token0, - token1, - fee, - position.tickLower, - position.tickUpper, - ZERO_LIQUIDITY_FOR_FEE_COLLECTION, // burn '0' liquidity to collect fee - caller, - ) - - currentFeeGrowth, err := getCurrentFeeGrowth(position, caller) - if err != nil { - panic(newErrorWithDetail(err, "failed to get current fee growth")) - } - - tokensOwed0, tokensOwed1 := calculateFees(position, currentFeeGrowth) - - position.feeGrowthInside0LastX128 = u256.Zero().Set(currentFeeGrowth.feeGrowthInside0LastX128) - position.feeGrowthInside1LastX128 = u256.Zero().Set(currentFeeGrowth.feeGrowthInside1LastX128) - - // collect fee - amount0, amount1 := pl.Collect( - cross, - token0, token1, fee, - caller, - position.tickLower, position.tickUpper, - tokensOwed0.ToString(), tokensOwed1.ToString(), - ) - - // sometimes there will be a few less uBase amount than expected due to rounding down in core, but we just subtract the full amount expected - // instead of the actual amount so we can burn the token - position.tokensOwed0 = u256.Zero().Sub(tokensOwed0, u256.MustFromDecimal(amount0)) - position.tokensOwed1 = u256.Zero().Sub(tokensOwed1, u256.MustFromDecimal(amount1)) - mustUpdatePosition(positionId, position) - - withdrawalFeeBps := pl.GetWithdrawalFee() - amount0WithoutFee, fee0 := calculateAmountWithWithdrawalFee(amount0, withdrawalFeeBps) - amount1WithoutFee, fee1 := calculateAmountWithWithdrawalFee(amount1, withdrawalFeeBps) - - poolAddr, ok := access.GetAddress(prabc.ROLE_POOL.String()) - if !ok { - panic("pool address not found") - } - - if isWrappedToken(token0) && unwrapResult { - unwrapWithTransferFrom(poolAddr, caller, amount0WithoutFee) - } else { - common.TransferFrom(cross, token0, poolAddr, caller, amount0WithoutFee) - } - - if isWrappedToken(token1) && unwrapResult { - unwrapWithTransferFrom(poolAddr, caller, amount1WithoutFee) - } else { - common.TransferFrom(cross, token1, poolAddr, caller, amount1WithoutFee) - } - - protocolFeeAddr, ok := access.GetAddress(prabc.ROLE_PROTOCOL_FEE.String()) - if !ok { - panic("protocol fee address not found") - } - - common.TransferFrom(cross, token0, poolAddr, protocolFeeAddr, fee0) - common.TransferFrom(cross, token1, poolAddr, protocolFeeAddr, fee1) - - amount0WithoutFeeStr := formatInt(amount0WithoutFee) - amount1WithoutFeeStr := formatInt(amount1WithoutFee) - - previousRealm := std.PreviousRealm() - std.Emit( - "CollectSwapFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "lpPositionId", formatUint(positionId), - "feeAmount0", amount0WithoutFeeStr, - "feeAmount1", amount1WithoutFeeStr, - "poolPath", position.poolKey, - "unwrapResult", formatBool(unwrapResult), - ) - - std.Emit( - "WithdrawalFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "lpTokenId", formatUint(positionId), - "poolPath", position.poolKey, - "feeAmount0", formatInt(fee0), - "feeAmount1", formatInt(fee1), - "amount0WithoutFee", amount0WithoutFeeStr, - "amount1WithoutFee", amount1WithoutFeeStr, - ) - - return positionId, amount0WithoutFeeStr, amount1WithoutFeeStr, position.poolKey, amount0, amount1 -} - -var feeAmountDivisor = u256.NewUint(10000) - -// calculateAmountWithWithdrawalFee calculates amount after deducting withdrawal fee. -func calculateAmountWithWithdrawalFee(amount string, fee uint64) (int64, int64) { - if fee == 0 { - return safeConvertToInt64(u256.MustFromDecimal(amount)), 0 - } - - amountUint := u256.MustFromDecimal(amount) - feeUint := u256.NewUint(fee) - - feeAmount := u256.Zero().Mul(amountUint, feeUint) - feeAmount = u256.Zero().Div(feeAmount, feeAmountDivisor) - amountWithoutFee := u256.Zero().Sub(amountUint, feeAmount) - - return safeConvertToInt64(amountWithoutFee), safeConvertToInt64(feeAmount) -} - -// SetPositionOperator sets an operator for a position. -// Only staker can call this function. -func SetPositionOperator( - cur realm, - id uint64, - operator std.Address, -) { - halt.AssertIsNotHaltedPosition() - - caller := std.PreviousRealm().Address() - access.AssertIsStaker(caller) - - position := MustGetPosition(id) - position.operator = operator - mustUpdatePosition(id, position) -} - -// computePositionKey generates a unique base64-encoded key for a liquidity position. -func computePositionKey( - tickLower int32, - tickUpper int32, -) string { - currentRealmPath := std.CurrentRealm().PkgPath() - key := ufmt.Sprintf("%s__%d__%d", currentRealmPath, tickLower, tickUpper) - encoded := base64.StdEncoding.EncodeToString([]byte(key)) - return encoded -} - -// getCurrentFeeGrowth retrieves current fee growth values for a position. -func getCurrentFeeGrowth(position Position, owner std.Address) (FeeGrowthInside, error) { - positionKey := computePositionKey(position.tickLower, position.tickUpper) - feeGrowthInside0LastX128, feeGrowthInside1LastX128 := pl.GetPositionFeeGrowthInsideLastX128(position.poolKey, positionKey) - - feeGrowthInside := FeeGrowthInside{ - feeGrowthInside0LastX128: feeGrowthInside0LastX128, - feeGrowthInside1LastX128: feeGrowthInside1LastX128, - } - - return feeGrowthInside, nil -} diff --git a/contract/r/gnoswap/v1/position/reposition.gno b/contract/r/gnoswap/v1/position/reposition.gno deleted file mode 100644 index 49a0cda..0000000 --- a/contract/r/gnoswap/v1/position/reposition.gno +++ /dev/null @@ -1,122 +0,0 @@ -package position - -import ( - "std" - - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" - pl "gno.land/r/gnoswap/v1/pool" -) - -// Reposition adjusts the price range and liquidity of an existing position. -func Reposition( - cur realm, - positionId uint64, - tickLower int32, - tickUpper int32, - amount0DesiredStr string, - amount1DesiredStr string, - amount0MinStr string, - amount1MinStr string, -) (uint64, string, int32, int32, string, string) { - halt.AssertIsNotHaltedPosition() - halt.AssertIsNotHaltedWithdraw() - - caller := std.PreviousRealm().Address() - assertIsOwnerForToken(positionId, caller) - - emission.MintAndDistributeGns(cross) - - // position should be burned to reposition - position := MustGetPosition(positionId) - oldTickLower := position.tickLower - oldTickUpper := position.tickUpper - - if !(position.isClear()) { - panic(newErrorWithDetail( - errNotClear, - ufmt.Sprintf( - "position(%d) isn't clear(liquidity:%s, tokensOwed0:%s, tokensOwed1:%s)", - positionId, - position.liquidity.ToString(), - position.tokensOwed0.ToString(), - position.tokensOwed1.ToString(), - ), - )) - } - - token0, token1, _ := splitOf(position.poolKey) - token0, token1, _, _, _, err := processTokens( - token0, - token1, - amount0DesiredStr, - amount1DesiredStr, - caller, - ) - if err != nil { - panic(err) - } - - liquidity, amount0, amount1 := addLiquidity( - AddLiquidityParams{ - poolKey: position.poolKey, - tickLower: tickLower, - tickUpper: tickUpper, - amount0Desired: u256.MustFromDecimal(amount0DesiredStr), - amount1Desired: u256.MustFromDecimal(amount1DesiredStr), - amount0Min: u256.MustFromDecimal(amount0MinStr), - amount1Min: u256.MustFromDecimal(amount1MinStr), - caller: caller, - }, - ) - - // update position tickLower, tickUpper to new value - // because getCurrentFeeGrowth() uses tickLower, tickUpper - position.tickLower = tickLower - position.tickUpper = tickUpper - - currentFeeGrowth, err := getCurrentFeeGrowth(position, caller) - if err != nil { - panic(newErrorWithDetail(err, "failed to get current fee growth")) - } - position.feeGrowthInside0LastX128 = currentFeeGrowth.feeGrowthInside0LastX128 - position.feeGrowthInside1LastX128 = currentFeeGrowth.feeGrowthInside1LastX128 - - position.liquidity = liquidity - // OBS: do not reset feeGrowthInside1LastX128 and feeGrowthInside1LastX128 to zero - // if so, ( decrease 100% -> reposition ) - // > at this point, that position will have unclaimedFee which isn't intended - position.tokensOwed0 = u256.Zero() - position.tokensOwed1 = u256.Zero() - position.burned = false - mustUpdatePosition(positionId, position) - - poolSqrtPriceX96 := pl.GetSlot0SqrtPriceX96(position.poolKey) - token0Balance := pl.GetBalanceToken0(position.poolKey) - token1Balance := pl.GetBalanceToken1(position.poolKey) - - previousRealm := std.PreviousRealm() - std.Emit( - "Reposition", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "lpPositionId", formatUint(positionId), - "tickLower", formatInt(int64(tickLower)), - "tickUpper", formatInt(int64(tickUpper)), - "liquidity", liquidity.ToString(), - "amount0", amount0.ToString(), - "amount1", amount1.ToString(), - "prevTickLower", formatInt(int64(oldTickLower)), - "prevTickUpper", formatInt(int64(oldTickUpper)), - "poolPath", position.poolKey, - "sqrtPriceX96", poolSqrtPriceX96.ToString(), - "positionLiquidity", PositionGetPositionLiquidityStr(positionId), - "token0Balance", token0Balance, - "token1Balance", token1Balance, - ) - - return positionId, liquidity.ToString(), tickLower, tickUpper, amount0.ToString(), amount1.ToString() -} diff --git a/contract/r/gnoswap/v1/position/type.gno b/contract/r/gnoswap/v1/position/type.gno deleted file mode 100644 index 84885ec..0000000 --- a/contract/r/gnoswap/v1/position/type.gno +++ /dev/null @@ -1,147 +0,0 @@ -package position - -import ( - "std" - - u256 "gno.land/p/gnoswap/uint256" -) - -const ( - Q128 string = "340282366920938463463374607431768211456" // 2 ** 128 -) - -// Previously, we used a different zero address ("g1000000..."), -// but we changed the value because using the address -// below appears to have become the de facto standard practice. -var zeroAddress std.Address = std.Address("") - -// Position represents a liquidity position in a pool. -// Each position tracks the amount of liquidity, fee growth, and tokens owed to the position owner. -type Position struct { - nonce *u256.Uint // nonce for permits - operator std.Address // address that is approved for spending this token - poolKey string // poolPath of the pool which this has lp token - tickLower int32 // the lower tick of the position, bounds are included - tickUpper int32 // the upper tick of the position - liquidity *u256.Uint // liquidity of the position - - // fee growth of the aggregate position as of the last action on the individual position - feeGrowthInside0LastX128 *u256.Uint - feeGrowthInside1LastX128 *u256.Uint - - // how many uncollected tokens are owed to the position, as of the last computation - tokensOwed0 *u256.Uint - tokensOwed1 *u256.Uint - - token0Balance *u256.Uint // token0 balance of the position - token1Balance *u256.Uint // token1 balance of the position - - burned bool // whether the position has been burned (we don't burn the NFT, just mark as burned) -} - -func (p Position) PoolKey() string { return p.poolKey } -func (p Position) Liquidity() *u256.Uint { return p.liquidity } -func (p Position) TickLower() int32 { return p.tickLower } -func (p Position) TickUpper() int32 { return p.tickUpper } -func (p Position) TokensOwed0() *u256.Uint { return p.tokensOwed0 } -func (p Position) TokensOwed1() *u256.Uint { return p.tokensOwed1 } -func (p Position) Token0Balance() *u256.Uint { return p.token0Balance } -func (p Position) Token1Balance() *u256.Uint { return p.token1Balance } - -// isClear reports whether the position is empty -func (p Position) isClear() bool { - return p.liquidity.IsZero() && p.tokensOwed0.IsZero() && p.tokensOwed1.IsZero() -} - -type MintParams struct { - token0 string // token0 path for a specific pool - token1 string // token1 path for a specific pool - fee uint32 // fee for a specific pool - tickLower int32 // lower end of the tick range for the position - tickUpper int32 // upper end of the tick range for the position - amount0Desired *u256.Uint // desired amount of token0 to be minted - amount1Desired *u256.Uint // desired amount of token1 to be minted - amount0Min *u256.Uint // minimum amount of token0 to be minted - amount1Min *u256.Uint // minimum amount of token1 to be minted - deadline int64 // time by which the transaction must be included to effect the change - mintTo std.Address // address to mint lpToken - caller std.Address // address to call the function -} - -// newMintParams creates `MintParams` from processed input data. -func newMintParams(input ProcessedMintInput, mintInput MintInput) MintParams { - return MintParams{ - token0: input.tokenPair.token0, - token1: input.tokenPair.token1, - fee: mintInput.fee, - tickLower: input.tickLower, - tickUpper: input.tickUpper, - amount0Desired: input.amount0Desired, - amount1Desired: input.amount1Desired, - amount0Min: input.amount0Min, - amount1Min: input.amount1Min, - deadline: mintInput.deadline, - mintTo: mintInput.mintTo, - caller: mintInput.caller, - } -} - -type IncreaseLiquidityParams struct { - positionId uint64 // positionId of the position to increase liquidity - amount0Desired *u256.Uint // desired amount of token0 to be minted - amount1Desired *u256.Uint // desired amount of token1 to be minted - amount0Min *u256.Uint // minimum amount of token0 to be minted - amount1Min *u256.Uint // minimum amount of token1 to be minted - deadline int64 // time by which the transaction must be included to effect the change - caller std.Address // address to call the function -} - -type DecreaseLiquidityParams struct { - positionId uint64 // positionId of the position to decrease liquidity - liquidity string // amount of liquidity to decrease - amount0Min *u256.Uint // minimum amount of token0 to be minted - amount1Min *u256.Uint // minimum amount of token1 to be minted - deadline int64 // time by which the transaction must be included to effect the change - unwrapResult bool // whether to unwrap the token if it's wrapped native token - caller std.Address // address to call the function -} - -type MintInput struct { - token0 string - token1 string - fee uint32 - tickLower int32 - tickUpper int32 - amount0Desired string - amount1Desired string - amount0Min string - amount1Min string - deadline int64 - mintTo std.Address - caller std.Address -} - -type TokenPair struct { - token0 string - token1 string - token0IsNative bool - token1IsNative bool - wrappedAmount int64 -} - -type ProcessedMintInput struct { - tokenPair TokenPair - amount0Desired *u256.Uint - amount1Desired *u256.Uint - amount0Min *u256.Uint - amount1Min *u256.Uint - tickLower int32 - tickUpper int32 - poolPath string -} - -// FeeGrowthInside represents fee growth inside ticks -type FeeGrowthInside struct { - feeGrowthInside0LastX128 *u256.Uint - feeGrowthInside1LastX128 *u256.Uint -} diff --git a/contract/r/gnoswap/v1/position/utils.gno b/contract/r/gnoswap/v1/position/utils.gno deleted file mode 100644 index 910639a..0000000 --- a/contract/r/gnoswap/v1/position/utils.gno +++ /dev/null @@ -1,225 +0,0 @@ -package position - -import ( - "std" - "strconv" - "strings" - - "gno.land/p/demo/tokens/grc721" - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/v1/gnft" -) - -// GetOrigPkgAddr returns the original package address. -// In position contract, original package address is the position address. -func GetOrigPkgAddr() std.Address { - return std.CurrentRealm().Address() -} - -// positionIdFrom converts positionId to grc721.TokenID type -// NOTE: input parameter positionId can be string, int, uint64, or grc721.TokenID -// if positionId is nil or not supported, it will panic -// if positionId is not found, it will panic -// input: positionId any -// output: grc721.TokenID -func positionIdFrom(positionId any) grc721.TokenID { - if positionId == nil { - panic(newErrorWithDetail(errInvalidInput, "positionId is nil")) - } - - switch positionId.(type) { - case string: - return grc721.TokenID(positionId.(string)) - case int: - return grc721.TokenID(strconv.Itoa(positionId.(int))) - case uint64: - return grc721.TokenID(strconv.Itoa(int(positionId.(uint64)))) - case grc721.TokenID: - return positionId.(grc721.TokenID) - default: - panic(newErrorWithDetail(errInvalidInput, "unsupported positionId type")) - } -} - -// exists checks whether positionId exists -// If positionId doesn't exist, return false, otherwise return true -// input: positionId uint64 -// output: bool -func exists(positionId uint64) bool { - return gnft.Exists(positionIdFrom(positionId)) -} - -// isOwner checks whether the caller is the owner of the positionId -// If the caller is the owner of the positionId, return true, otherwise return false -// input: positionId uint64, addr std.Address -// output: bool -func isOwner(positionId uint64, addr std.Address) bool { - owner, err := gnft.OwnerOf(positionIdFrom(positionId)) - if err == nil { - if owner == addr { - return true - } - } - - return false -} - -// isOperator checks whether the caller is the approved operator of the positionId -// If the caller is the approved operator of the positionId, return true, otherwise return false -// input: positionId uint64, addr std.Address -// output: bool -func isOperator(positionId uint64, addr std.Address) bool { - return PositionGetPositionOperator(positionId) == addr -} - -// isStaked checks whether positionId is staked -// If positionId is staked, owner of positionId is staker contract -// If positionId is staked, return true, otherwise return false -// input: positionId grc721.TokenID -// output: bool -func isStaked(positionId grc721.TokenID) bool { - exist := gnft.Exists(positionId) - if exist { - owner, err := gnft.OwnerOf(positionId) - if err == nil && owner == stakerAddr { - return true - } - } - return false -} - -// isOwnerOrOperator checks whether the caller is the owner or approved operator of the positionId -// If the caller is the owner or approved operator of the positionId, return true, otherwise return false -// input: addr std.Address, positionId uint64 -// output: bool -func isOwnerOrOperator(positionId uint64, addr std.Address) bool { - if !addr.IsValid() || !exists(positionId) { - return false - } - - if isStaked(positionIdFrom(positionId)) { - return isOperator(positionId, addr) - } - - return isOwner(positionId, addr) -} - -// splitOf divides poolKey into pToken0, pToken1, and pFee -// If poolKey is invalid, it will panic -// -// input: poolKey string -// output: -// - token0Path string -// - token1Path string -// - fee uint32 -func splitOf(poolKey string) (string, string, uint32) { - res := strings.Split(poolKey, ":") - if len(res) != 3 { - panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid poolKey(%s)", poolKey))) - } - - pToken0, pToken1, pFeeStr := res[0], res[1], res[2] - - pFee, err := strconv.Atoi(pFeeStr) - if err != nil { - panic(newErrorWithDetail(errInvalidInput, ufmt.Sprintf("invalid fee(%s)", pFeeStr))) - } - - return pToken0, pToken1, uint32(pFee) -} - -func formatUint(v any) string { - switch v := v.(type) { - case uint8: - return strconv.FormatUint(uint64(v), 10) - case uint32: - return strconv.FormatUint(uint64(v), 10) - case uint64: - return strconv.FormatUint(v, 10) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} - -func formatInt(v any) string { - switch v := v.(type) { - case int32: - return strconv.FormatInt(int64(v), 10) - case int64: - return strconv.FormatInt(v, 10) - case int: - return strconv.Itoa(v) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} - -func formatBool(v bool) string { - return strconv.FormatBool(v) -} - -func mustParseInt64(v string) int64 { - amountInt, err := strconv.ParseInt(v, 10, 64) - if err != nil { - panic(err) - } - - return amountInt -} - -func isTokenOwner(positionId uint64, caller std.Address) bool { - owner, err := gnft.OwnerOf(positionIdFrom(positionId)) - if err != nil { - return false - } - - return owner == caller -} - -func isSlippageExceeded(amount0, amount1, amount0Min, amount1Min *u256.Uint) bool { - return !(amount0.Gte(amount0Min) && amount1.Gte(amount1Min)) -} - -func subUint256(x, y *u256.Uint) *u256.Uint { - if x.Cmp(y) < 0 { - q256 := u256.MustFromDecimal(MAX_UINT256) - diff := u256.Zero().Sub(q256, y) - result := u256.Zero().Add(diff, x) - - // Add 1 to the result since MAX_UINT256 is 2^256 - 1 - return u256.Zero().Add(result, u256.One()) - } - - return u256.Zero().Sub(x, y) -} - -// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. -// -// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds -// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. -// -// Parameters: -// - value (*u256.Uint): The unsigned 256-bit integer to be converted. -// -// Returns: -// - int64: The converted value if it falls within the int64 range. -// -// Panics: -// - If the `value` exceeds the range of int64, the function will panic with an error indicating -// the overflow and the original value. -func safeConvertToInt64(value *u256.Uint) int64 { - const INT64_MAX = 9223372036854775807 - const MAX_INT64 = "9223372036854775807" - - res, overflow := value.Uint64WithOverflow() - if overflow || res > uint64(INT64_MAX) { - panic(ufmt.Sprintf( - "amount(%s) overflows int64 range (max %s)", - value.ToString(), - MAX_INT64, - )) - } - return int64(res) -} diff --git a/contract/r/gnoswap/v1/protocol_fee/README.md b/contract/r/gnoswap/v1/protocol_fee/README.md deleted file mode 100644 index 277f133..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Protocol Fee - -Fee collection and distribution for protocol operations. - -## Overview - -Protocol Fee contract collects fees from various protocol operations and distributes them to xGNS holders and DevOps. - -## Configuration - -- **Router Fee**: 0.15% of swap amount -- **Pool Creation Fee**: 100 GNS -- **Withdrawal Fee**: 1% of LP fees claimed -- **Unstaking Fee**: 1% of staking rewards -- **Distribution**: 100% to xGNS holders (default) - -## Fee Sources - -1. **Swaps**: 0.15% fee on all trades -2. **Pool Creation**: 100 GNS per new pool -3. **LP Withdrawals**: 1% of collected fees -4. **Staking Claims**: 1% of rewards - -## Key Functions - -### `DistributeProtocolFee` -Distributes accumulated fees to recipients. - -### `SetDevOpsPct` -Sets DevOps funding percentage. - -### `SetGovStakerPct` -Sets xGNS holder percentage. - -### `AddToProtocolFee` -Adds fees to distribution queue. - -## Usage - -```go -// Distribute accumulated fees -tokenAmounts := DistributeProtocolFee() - -// Configure distribution -SetDevOpsPct(2000) // 20% to DevOps -SetGovStakerPct(8000) // 80% to xGNS holders - -// View accumulated fees -GetProtocolFee(tokenPath) -``` - -## Security - -- Admin-only configuration changes -- Automatic fee accumulation -- Multi-token support -- Transparent distribution tracking \ No newline at end of file diff --git a/contract/r/gnoswap/v1/protocol_fee/api.gno b/contract/r/gnoswap/v1/protocol_fee/api.gno deleted file mode 100644 index ad4ebb0..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/api.gno +++ /dev/null @@ -1,161 +0,0 @@ -package protocol_fee - -import ( - "std" - "strconv" - "time" - - "gno.land/p/nt/avl" - "gno.land/p/onbloc/json" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/v1/common" -) - -// ApiGetAccuTransferToGovStaker returns accumulated transfers to gov/staker as JSON. -func ApiGetAccuTransferToGovStaker() string { - distributedAmountList := protocolFeeState.accuToGovStaker - if distributedAmountList == nil { - return "" - } - - return marshal(buildByAvlTree(distributedAmountList)) -} - -// ApiGetAccuTransferToDevOps returns accumulated transfers to devOps as JSON. -func ApiGetAccuTransferToDevOps() string { - distributedAmountList := protocolFeeState.accuToDevOps - if distributedAmountList == nil { - return "" - } - - return marshal(buildByAvlTree(distributedAmountList)) -} - -// ApiGetHistoryTransferToGovStaker returns transfer history to gov/staker as JSON. -func ApiGetHistoryTransferToGovStaker() string { - historyTransferList := distributedToGovStakerHistory() - if historyTransferList == nil { - return "" - } - - return marshal(buildByAvlTree(historyTransferList)) -} - -// ApiGetHistoryTransferToDevOps returns transfer history to devOps as JSON. -func ApiGetHistoryTransferToDevOps() string { - historyTransferList := distributedToDevOpsHistory() - if historyTransferList == nil { - return "" - } - return marshal(buildByAvlTree(historyTransferList)) -} - -// buildByAvlTree builds a JSON node from AVL tree data. -func buildByAvlTree(tree *avl.Tree) *json.Node { - data := json.Builder(). - WriteString("height", formatInt(std.ChainHeight())). - WriteString("now", formatInt(time.Now().Unix())) - - tree.Iterate("", "", func(key string, value any) bool { - if iv, ok := value.(int64); !ok { - panic(ufmt.Sprintf("failed to cast value to int64: %T", value)) - } else { - data.WriteString(key, formatInt(iv)) - } - return false - }) - - return data.Node() -} - -// formatUint formats uint64 to string. -func formatUint(v uint64) string { - return strconv.FormatUint(v, 10) -} - -// formatInt64 formats int64 to string. -func formatInt64(v int64) string { - return strconv.FormatInt(v, 10) -} - -// formatInt formats int64 to string. -func formatInt(v int64) string { - return strconv.FormatInt(v, 10) -} - -// marshal converts JSON node to string. -func marshal(node *json.Node) string { - b, err := json.Marshal(node) - if err != nil { - panic(err.Error()) - } - - return string(b) -} - -// ApiGetActualBalance returns all tokens with their current balances (recorded + unrecorded) -func ApiGetActualBalance() string { - tokenMap := make(map[string]int64) - - // get all recorded tokens - for token := range protocolFeeState.tokenListWithAmount { - actualBalance := common.BalanceOf(token, protocolFeeAddr) - tokenMap[token] = actualBalance - } - - // only include tokens that are already recorded. - // check for any tokens that have balance will requires - // iterating through known tokens or having a registry. - data := json.Builder(). - WriteString("height", formatInt(std.ChainHeight())). - WriteString("now", formatInt(time.Now().Unix())) - - for token, balance := range tokenMap { - data.WriteString(token, formatInt64(balance)) - } - - return marshal(data.Node()) -} - -// ApiGetRecordedBalance returns the recorded tokens and their amounts -func ApiGetRecordedBalance() string { - tokenList := GetTokenListWithAmount() - if tokenList == nil { - return "" - } - - data := json.Builder(). - WriteString("height", formatInt(std.ChainHeight())). - WriteString("now", formatInt(time.Now().Unix())) - - for token, amount := range tokenList { - data.WriteString(token, formatInt64(amount)) - } - - return marshal(data.Node()) -} - -// ApiGetUnrecordedBalance returns tokens with unrecorded balances -func ApiGetUnrecordedBalance() string { - unrecordedMap := make(map[string]int64) - - // Check all recorded tokens for discrepancies - for token, recordedAmount := range protocolFeeState.tokenListWithAmount { - actualBalance := common.BalanceOf(token, protocolFeeAddr) - if actualBalance > recordedAmount { - unrecordedAmount := actualBalance - recordedAmount - unrecordedMap[token] = unrecordedAmount - } - } - - data := json.Builder(). - WriteString("height", formatInt(std.ChainHeight())). - WriteString("now", formatInt(time.Now().Unix())) - - for token, unrecordedBalance := range unrecordedMap { - data.WriteString(token, formatInt64(unrecordedBalance)) - } - - return marshal(data.Node()) -} diff --git a/contract/r/gnoswap/v1/protocol_fee/assert.gno b/contract/r/gnoswap/v1/protocol_fee/assert.gno deleted file mode 100644 index 37984d7..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/assert.gno +++ /dev/null @@ -1,46 +0,0 @@ -package protocol_fee - -import ( - "std" - - "gno.land/p/nt/ufmt" - prbac "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoswap/access" -) - -// assertIsPoolOrRouterOrStaker panics if the caller is not the pool, router, or staker contract. -func assertIsPoolOrRouterOrStaker(caller std.Address) { - access.AssertHasAnyRole( - caller, - prbac.ROLE_POOL.String(), - prbac.ROLE_ROUTER.String(), - prbac.ROLE_STAKER.String(), - ) -} - -// assertIsAdminOrGovStaker panics if the caller is not admin or gov/staker. -func assertIsAdminOrGovStaker(caller std.Address) { - access.AssertHasAnyRole( - caller, - prbac.ROLE_ADMIN.String(), - prbac.ROLE_GOV_STAKER.String(), - ) -} - -// assertIsValidPercent panics if the percentage is invalid (not between 0-10000). -func assertIsValidPercent(pct int64) { - if pct > 10000 { - panic(makeErrorWithDetail( - errInvalidPct, - ufmt.Sprintf("pct(%d) should not be bigger than 10000", pct), - )) - } - - if pct < 0 { - panic(makeErrorWithDetail( - errInvalidPct, - ufmt.Sprintf("pct(%d) should not be smaller than 0", pct), - )) - } -} diff --git a/contract/r/gnoswap/v1/protocol_fee/consts.gno b/contract/r/gnoswap/v1/protocol_fee/consts.gno deleted file mode 100644 index 636a9e6..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/consts.gno +++ /dev/null @@ -1,12 +0,0 @@ -package protocol_fee - -import ( - prabc "gno.land/p/gnoswap/rbac" - "gno.land/r/gnoswap/access" -) - -var ( - protocolFeeAddr, _ = access.GetAddress(prabc.ROLE_PROTOCOL_FEE.String()) - govStakerAddr, _ = access.GetAddress(prabc.ROLE_GOV_STAKER.String()) - devOpsAddr, _ = access.GetAddress(prabc.ROLE_DEVOPS.String()) -) diff --git a/contract/r/gnoswap/v1/protocol_fee/doc.gno b/contract/r/gnoswap/v1/protocol_fee/doc.gno deleted file mode 100644 index 5979fb1..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/doc.gno +++ /dev/null @@ -1,6 +0,0 @@ -// Package protocol_fee collects and distributes protocol fees from swaps. -// -// This contract accumulates fees generated from trading activity and -// distributes them to devOps and governance stakers according to -// configurable percentages set through governance. -package protocol_fee diff --git a/contract/r/gnoswap/v1/protocol_fee/errors.gno b/contract/r/gnoswap/v1/protocol_fee/errors.gno deleted file mode 100644 index c438fb4..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/errors.gno +++ /dev/null @@ -1,18 +0,0 @@ -package protocol_fee - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errNoPermission = errors.New("[GNOSWAP-PROTOCOL_FEE-001] caller has no permission") - errInvalidPct = errors.New("[GNOSWAP-PROTOCOL_FEE-002] invalid percentage") - errInvalidAmount = errors.New("[GNOSWAP-PROTOCOL_FEE-003] invalid amount") -) - -// makeErrorWithDetail creates an error with additional context. -func makeErrorWithDetail(err error, detail string) error { - return ufmt.Errorf("%s || %s", err.Error(), detail) -} diff --git a/contract/r/gnoswap/v1/protocol_fee/getter.gno b/contract/r/gnoswap/v1/protocol_fee/getter.gno deleted file mode 100644 index 2d15e6d..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/getter.gno +++ /dev/null @@ -1,99 +0,0 @@ -package protocol_fee - -import ( - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" -) - -// GetDevOpsPct returns the percentage allocated to devOps. -func GetDevOpsPct() int64 { - return protocolFeeState.DevOpsPct() -} - -// GetGovStakerPct returns the percentage allocated to gov/staker. -func GetGovStakerPct() int64 { - return protocolFeeState.GovStakerPct() -} - -// GetTokenListWithAmount returns the token path and amount. -func GetTokenListWithAmount() map[string]int64 { - return protocolFeeState.tokenListWithAmount -} - -// GetAmountOfToken returns the amount of token. -func GetAmountOfToken(tokenPath string) int64 { - amount, exists := protocolFeeState.tokenListWithAmount[tokenPath] - if !exists { - return 0 - } - return amount -} - -// GetAccuTransfersToGovStaker returns all accumulated transfers to gov/staker. -func GetAccuTransfersToGovStaker() map[string]int64 { - accuTransfers := make(map[string]int64) - - protocolFeeState.accuToGovStaker.Iterate("", "", func(key string, value any) bool { - amount, ok := value.(int64) - if !ok { - return false - } - - accuTransfers[key] = amount - return false - }) - - return accuTransfers -} - -// GetAccuTransfersToDevOps returns all accumulated transfers to devOps. -func GetAccuTransfersToDevOps() map[string]int64 { - accuTransfers := make(map[string]int64) - - protocolFeeState.accuToDevOps.Iterate("", "", func(key string, value any) bool { - amount, ok := value.(int64) - if !ok { - return false - } - - accuTransfers[key] = amount - return false - }) - - return accuTransfers -} - -// GetAccuTransferToGovStakerByTokenPath returns the accumulated transfer to gov/staker by token path. -func GetAccuTransferToGovStakerByTokenPath(path string) int64 { - return protocolFeeState.GetAccuTransferToGovStakerByTokenPath(path) -} - -// GetAccuTransferToDevOpsByTokenPath returns the accumulated transfer to devOps by token path. -func GetAccuTransferToDevOpsByTokenPath(path string) int64 { - return protocolFeeState.GetAccuTransferToDevOpsByTokenPath(path) -} - -// GetHistoryOfDistributedToGovStakerByTokenPath returns the history of distributed to gov/staker by token path. -func GetHistoryOfDistributedToGovStakerByTokenPath(path string) int64 { - history := protocolFeeState.distributedToGovStakerHistory - return retrieveHistory(history, path) -} - -// GetHistoryOfDistributedToDevOpsByTokenPath returns the history of distributed to devOps by token path. -func GetHistoryOfDistributedToDevOpsByTokenPath(path string) int64 { - history := protocolFeeState.distributedToDevOpsHistory - return retrieveHistory(history, path) -} - -// retrieveHistory retrieves distribution history amount from AVL tree. -func retrieveHistory(tree *avl.Tree, key string) int64 { - amountI, exists := tree.Get(key) - if !exists { - return 0 - } - res, ok := amountI.(int64) - if !ok { - panic(ufmt.Sprintf("failed to cast amount to int64: %T", amountI)) - } - return res -} diff --git a/contract/r/gnoswap/v1/protocol_fee/gnomod.toml b/contract/r/gnoswap/v1/protocol_fee/gnomod.toml deleted file mode 100644 index b218732..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/protocol_fee" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/protocol_fee/protocol_fee.gno b/contract/r/gnoswap/v1/protocol_fee/protocol_fee.gno deleted file mode 100644 index 951282e..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/protocol_fee.gno +++ /dev/null @@ -1,231 +0,0 @@ -package protocol_fee - -import ( - "std" - "strconv" - "strings" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" -) - -// DistributeProtocolFee distributes collected protocol fees. -// -// Splits fees between devOps and gov/staker based on configured percentages. -// This function processes all accumulated fees since last distribution. -// -// Returns: -// - map[string]int64: Token paths to amounts distributed to gov/staker -// -// Only callable by admin or gov/staker contract. -// Note: Default split is 0% devOps, 100% gov/staker. -func DistributeProtocolFee(cur realm) map[string]int64 { - halt.AssertIsNotHaltedProtocolFee() - - caller := std.PreviousRealm().Address() - assertIsAdminOrGovStaker(caller) - - sentToDevOpsForEvent := make([]string, 0) - sentToGovStakerForEvent := make([]string, 0) - toReturnDistributedToGovStaker := make(map[string]int64) - - for token, amount := range protocolFeeState.tokenListWithAmount { - balance := common.BalanceOf(token, protocolFeeAddr) - - // amount should be less than or equal to balance - if amount > balance { - panic(makeErrorWithDetail( - errInvalidAmount, - ufmt.Sprintf("amount: %d should be less than or equal to balance: %d", amount, balance), - )) - } - - if amount <= 0 { - continue - } - - // Distribute only the recorded amount, not the entire balance - distributeAmount := amount - if distributeAmount > balance { - // This should not happen due to the check above, but safeguard anyway - distributeAmount = balance - } - - toDevOpsAmount := distributeAmount * protocolFeeState.DevOpsPct() / 10000 // default 0% - toGovStakerAmount := distributeAmount - toDevOpsAmount // default 100% - - // Distribute to DevOps - if err := protocolFeeState.distributeToDevOps(token, toDevOpsAmount); err != nil { - panic(err) - } - if toDevOpsAmount > 0 { - sentToDevOpsForEvent = append(sentToDevOpsForEvent, makeEventString(token, toDevOpsAmount)) - } - - // Distribute to Gov/Staker - if err := protocolFeeState.distributeToGovStaker(token, toGovStakerAmount); err != nil { - panic(err) - } - if toGovStakerAmount > 0 { - sentToGovStakerForEvent = append(sentToGovStakerForEvent, makeEventString(token, toGovStakerAmount)) - toReturnDistributedToGovStaker[token] = toGovStakerAmount - } - } - - protocolFeeState.clearTokenListWithAmount() - - previousRealm := std.PreviousRealm() - std.Emit( - "TransferProtocolFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "toDevOps", strings.Join(sentToDevOpsForEvent, ","), - "toGovStaker", strings.Join(sentToGovStakerForEvent, ","), - ) - - return toReturnDistributedToGovStaker -} - -// SetDevOpsPct sets the devOpsPct. -// -// Parameters: -// - pct: percentage for devOps (0-10000, where 10000 = 100%) -// -// Only callable by admin or governance. -// Note: GovStaker percentage is automatically adjusted to (10000 - devOpsPct). -func SetDevOpsPct(cur realm, pct int64) { - halt.AssertIsNotHaltedProtocolFee() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsValidPercent(pct) - - prevDevOpsPct := protocolFeeState.DevOpsPct() - prevGovStakerPct := protocolFeeState.GovStakerPct() - - newDevOpsPct, err := protocolFeeState.setDevOpsPct(pct) - if err != nil { - panic(err) - } - newGovStakerPct := protocolFeeState.GovStakerPct() - - previousRealm := std.PreviousRealm() - std.Emit( - "SetDevOpsPct", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "newDevOpsPct", strconv.FormatInt(newDevOpsPct, 10), - "prevDevOpsPct", strconv.FormatInt(prevDevOpsPct, 10), - "newGovStakerPct", strconv.FormatInt(newGovStakerPct, 10), - "prevGovStakerPct", strconv.FormatInt(prevGovStakerPct, 10), - ) -} - -// SetGovStakerPct sets the stakerPct. -// -// Parameters: -// - pct: percentage for gov/staker (0-10000, where 10000 = 100%) -// -// Only callable by admin or governance. -// Note: DevOps percentage is automatically adjusted to (10000 - govStakerPct). -func SetGovStakerPct(cur realm, pct int64) { - halt.AssertIsNotHaltedProtocolFee() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsValidPercent(pct) - - prevDevOpsPct := protocolFeeState.DevOpsPct() - prevGovStakerPct := protocolFeeState.GovStakerPct() - - newGovStakerPct, err := protocolFeeState.setGovStakerPct(pct) - if err != nil { - panic(err) - } - newDevOpsPct := protocolFeeState.DevOpsPct() - - previousRealm := std.PreviousRealm() - std.Emit( - "SetGovStakerPct", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "newDevOpsPct", strconv.FormatInt(newDevOpsPct, 10), - "prevDevOpsPct", strconv.FormatInt(prevDevOpsPct, 10), - "newGovStakerPct", strconv.FormatInt(newGovStakerPct, 10), - "prevGovStakerPct", strconv.FormatInt(prevGovStakerPct, 10), - ) -} - -// AddToProtocolFee adds the amount to the tokenListWithAmount. -// -// Parameters: -// - tokenPath: token contract path -// - amount: fee amount to add -// -// Only callable by pool, router or staker contracts. -// Note: Accumulated fees are distributed when DistributeProtocolFee is called. -func AddToProtocolFee(cur realm, tokenPath string, amount int64) { - halt.AssertIsNotHaltedProtocolFee() - - caller := std.PreviousRealm().Address() - assertIsPoolOrRouterOrStaker(caller) - - if amount < 0 { - panic(makeErrorWithDetail( - errInvalidAmount, - ufmt.Sprintf("amount(%d) should not be negative", amount), - )) - } - - currentAmount := protocolFeeState.tokenListWithAmount[tokenPath] - - // Check for overflow - if amount > 0 && currentAmount > 0 && currentAmount > (9223372036854775807-amount) { - panic(makeErrorWithDetail( - errInvalidAmount, - ufmt.Sprintf("overflow detected: current(%d) + amount(%d) would exceed int64 max", currentAmount, amount), - )) - } - - protocolFeeState.tokenListWithAmount[tokenPath] += amount -} - -// ClearTokenListWithAmount clears the tokenListWithAmount. -// -// Resets all accumulated token amounts to zero. -// Only callable by gov/staker contract. -// Note: Should be called after successful distribution. -func ClearTokenListWithAmount(cur realm) { - halt.AssertIsNotHaltedProtocolFee() - - caller := std.PreviousRealm().Address() - access.AssertIsGovStaker(caller) - - protocolFeeState.clearTokenListWithAmount() -} - -// ClearAccuTransferToGovStaker clears the accuToGovStaker. -// -// Resets accumulated transfer tracking for gov/staker. -// This allows gov/staker to track distributions between calls. -// -// Only callable by gov/staker contract. -// Note: Should be called after reading accumulated amounts. -func ClearAccuTransferToGovStaker(cur realm) { - halt.AssertIsNotHaltedProtocolFee() - - caller := std.PreviousRealm().Address() - access.AssertIsGovStaker(caller) - - protocolFeeState.accuToGovStaker = avl.NewTree() -} - -func makeEventString(tokenPath string, amount int64) string { - return tokenPath + "*FEE*" + strconv.FormatInt(amount, 10) -} diff --git a/contract/r/gnoswap/v1/protocol_fee/state.gno b/contract/r/gnoswap/v1/protocol_fee/state.gno deleted file mode 100644 index 157f0b6..0000000 --- a/contract/r/gnoswap/v1/protocol_fee/state.gno +++ /dev/null @@ -1,208 +0,0 @@ -package protocol_fee - -import ( - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/v1/common" -) - -// ProtocolFeeState holds all the state variables for protocol fee management -type ProtocolFeeState struct { - // By default, devOps will get 0% of the protocol fee (which means gov/staker will get 100% of the protocol fee) - // This percentage can be modified through governance. - devOpsPct int64 // 0% - - // accumulated amount distributed to gov/staker by token path - accuToGovStaker *avl.Tree - accuToDevOps *avl.Tree - - // distributedToDevOpsHistory and distributedToGovStakerHistory are used to keep track of the distribution history - distributedToGovStakerHistory *avl.Tree - distributedToDevOpsHistory *avl.Tree - - tokenListWithAmount map[string]int64 // tokenPath -> amount -} - -// distributedToGovStakerHistory returns the history of distributed to gov/staker. -func distributedToGovStakerHistory() *avl.Tree { - return protocolFeeState.distributedToGovStakerHistory -} - -// distributedToDevOpsHistory returns the history of distributed to devOps. -func distributedToDevOpsHistory() *avl.Tree { - return protocolFeeState.distributedToDevOpsHistory -} - -// NewProtocolFeeState creates a new instance of ProtocolFeeState with initialized values -func NewProtocolFeeState() *ProtocolFeeState { - return &ProtocolFeeState{ - devOpsPct: 0, // 0% - accuToGovStaker: avl.NewTree(), - accuToDevOps: avl.NewTree(), - distributedToGovStakerHistory: avl.NewTree(), - distributedToDevOpsHistory: avl.NewTree(), - tokenListWithAmount: make(map[string]int64), - } -} - -// Global instance of the protocol fee state -var protocolFeeState = NewProtocolFeeState() - -// DevOpsPct returns the percentage of protocol fees allocated to DevOps. -func (pfs *ProtocolFeeState) DevOpsPct() int64 { return pfs.devOpsPct } - -// GovStakerPct returns the percentage of protocol fees allocated to Gov/Staker. -func (pfs *ProtocolFeeState) GovStakerPct() int64 { return 10000 - pfs.devOpsPct } - -// AccuToGovStaker returns the accumulated amounts distributed to Gov/Staker. -func (pfs *ProtocolFeeState) AccuToGovStaker() *avl.Tree { return pfs.accuToGovStaker } - -// AccuToDevOps returns the accumulated amounts distributed to DevOps. -func (pfs *ProtocolFeeState) AccuToDevOps() *avl.Tree { return pfs.accuToDevOps } - -// distributeToDevOps distributes tokens to DevOps and updates related state. -// Amount should be greater than 0 (already checked in DistributeProtocolFee). -func (pfs *ProtocolFeeState) distributeToDevOps(token string, amount int64) error { - pfs.addAccuToDevOps(token, amount) - pfs.updateDistributedToDevOpsHistory(token, amount) - if err := common.Transfer(cross, token, devOpsAddr, amount); err != nil { - return ufmt.Errorf("transfer failed: token(%s), amount(%d)", token, amount) - } - return nil -} - -// distributeToGovStaker distributes tokens to Gov/Staker and updates related state. -// Amount should be greater than 0 (already checked in DistributeProtocolFee). -func (pfs *ProtocolFeeState) distributeToGovStaker(token string, amount int64) error { - pfs.addAccuToGovStaker(token, amount) - pfs.updateDistributedToGovStakerHistory(token, amount) - if err := common.Transfer(cross, token, govStakerAddr, amount); err != nil { - return ufmt.Errorf("transfer failed: token(%s), amount(%d)", token, amount) - } - return nil -} - -// setDevOpsPct sets the devOpsPct. -func (pfs *ProtocolFeeState) setDevOpsPct(pct int64) (int64, error) { - if pct < 0 { - return 0, makeErrorWithDetail( - errInvalidPct, - ufmt.Sprintf("pct(%d) should not be negative", pct), - ) - } - if pct > 10000 { - return 0, makeErrorWithDetail( - errInvalidPct, - ufmt.Sprintf("pct(%d) should not be bigger than 10000", pct), - ) - } - - pfs.devOpsPct = pct - - return pct, nil -} - -// setGovStakerPct sets the govStakerPct by calculating devOpsPct. -func (pfs *ProtocolFeeState) setGovStakerPct(pct int64) (int64, error) { - if pct < 0 { - return 0, makeErrorWithDetail( - errInvalidPct, - ufmt.Sprintf("pct(%d) should not be negative", pct), - ) - } - devOpsPct := 10000 - pct - if _, err := pfs.setDevOpsPct(devOpsPct); err != nil { - return 0, err - } - - return pct, nil -} - -// addAccuToGovStaker adds the amount to the accuToGovStaker by token path. -func (pfs *ProtocolFeeState) addAccuToGovStaker(tokenPath string, amount int64) { - before := pfs.GetAccuTransferToGovStakerByTokenPath(tokenPath) - - // Check for overflow - if amount > 0 && before > 0 && before > (9223372036854775807-amount) { - panic(makeErrorWithDetail( - errInvalidAmount, - ufmt.Sprintf("overflow detected: before(%d) + amount(%d) would exceed int64 max", before, amount), - )) - } - - after := before + amount - pfs.accuToGovStaker.Set(tokenPath, after) -} - -// addAccuToDevOps adds the amount to the accuToDevOps by token path. -func (pfs *ProtocolFeeState) addAccuToDevOps(tokenPath string, amount int64) { - before := pfs.GetAccuTransferToDevOpsByTokenPath(tokenPath) - - // Check for overflow - if amount > 0 && before > 0 && before > (9223372036854775807-amount) { - panic(makeErrorWithDetail( - errInvalidAmount, - ufmt.Sprintf("overflow detected: before(%d) + amount(%d) would exceed int64 max", before, amount), - )) - } - - after := before + amount - pfs.accuToDevOps.Set(tokenPath, after) -} - -// GetAccuTransferToGovStakerByTokenPath gets the accumulated amount to gov/staker by token path. -func (pfs *ProtocolFeeState) GetAccuTransferToGovStakerByTokenPath(tokenPath string) int64 { - return retrieveAmount(pfs.accuToGovStaker, tokenPath) -} - -// GetAccuTransferToDevOpsByTokenPath gets the accumulated amount to devOps by token path. -func (pfs *ProtocolFeeState) GetAccuTransferToDevOpsByTokenPath(tokenPath string) int64 { - return retrieveAmount(pfs.accuToDevOps, tokenPath) -} - -func retrieveAmount(tree *avl.Tree, key string) int64 { - amountI, exists := tree.Get(key) - if !exists { - return 0 - } - res, ok := amountI.(int64) - if !ok { - panic(ufmt.Sprintf("failed to cast amount to int64: %T", amountI)) - } - return res -} - -// updateDistributedToGovStakerHistory updates the distributedToGovStakerHistory. -func (pfs *ProtocolFeeState) updateDistributedToGovStakerHistory(tokenPath string, amount int64) { - prevAmount := retrievePrevAmount(pfs.distributedToGovStakerHistory, tokenPath) - afterAmount := prevAmount + amount - - pfs.distributedToGovStakerHistory.Set(tokenPath, afterAmount) -} - -// updateDistributedToDevOpsHistory updates the distributedToDevOpsHistory. -func (pfs *ProtocolFeeState) updateDistributedToDevOpsHistory(tokenPath string, amount int64) { - prevAmount := retrievePrevAmount(pfs.distributedToDevOpsHistory, tokenPath) - afterAmount := prevAmount + amount - - pfs.distributedToDevOpsHistory.Set(tokenPath, afterAmount) -} - -func retrievePrevAmount(tree *avl.Tree, key string) (amount int64) { - if prevAmountI, exists := tree.Get(key); !exists { - return 0 - } else { - v, ok := prevAmountI.(int64) - if !ok { - panic(ufmt.Sprintf("failed to cast prevAmount to int64: %T", prevAmountI)) - } - amount = v - } - return -} - -// clearTokenListWithAmount clears the tokenListWithAmount. -func (pfs *ProtocolFeeState) clearTokenListWithAmount() { - pfs.tokenListWithAmount = make(map[string]int64) -} diff --git a/contract/r/gnoswap/v1/router/README.md b/contract/r/gnoswap/v1/router/README.md deleted file mode 100644 index 79b2ab2..0000000 --- a/contract/r/gnoswap/v1/router/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# Router - -Swap routing engine for optimal trade execution across pools. - -## Overview - -Router handles swap execution across multiple pools, finding optimal paths and managing slippage protection for traders. - -## Configuration - -- **Router Fee**: 0.15% on all swaps -- **Max Hops**: 7 pools per route -- **Deadline Buffer**: 5-30 minutes recommended - -## Core Functions - -### `ExactInSwapRoute` -Swaps exact input amount for minimum output. -- Fixed input, variable output -- Reverts if output < amountOutMin -- Supports multi-hop routing - -### `ExactOutSwapRoute` -Swaps for exact output amount with maximum input. -- Fixed output, variable input -- Reverts if input > amountInMax -- Calculates path backwards - -### `DrySwapRoute` -Simulates swap without execution. -- Frontend price quotes -- Slippage calculation -- Gas estimation -- Path validation - -## Technical Details - -### Route Format -Routes encode swap path as colon-separated string: -``` -POOL_PATH,TOKEN0,TOKEN1,FEE:NEXT_POOL... -``` - -Single-hop example: -``` -gno.land/r/demo/bar:gno.land/r/demo/baz:3000,BAR,BAZ,3000 -``` - -Multi-hop example (BAR → BAZ → QUX): -``` -POOL1,BAR,BAZ,3000:POOL2,BAZ,QUX,500 -``` - -### Quote Distribution -Split large trades across routes to minimize impact: -- `quoteArr`: Percentage per route (must sum to 100) -- Example: "30,70" = 30% route1, 70% route2 - -### GNOT Handling -- Auto-wraps GNOT to WUGNOT when specified -- Auto-unwraps on output if needed -- No manual wrapping required - -### Slippage Protection -- Set `amountOutMin = expected * (1 - slippage%)` -- 0.5-1% for stable pairs -- 1-3% for volatile pairs -- Reverts if exceeded - -## Usage - -```go -// Simple exact input swap -amountIn, amountOut := ExactInSwapRoute( - "gno.land/r/demo/bar", // input token - "gno.land/r/demo/baz", // output token - "1000000", // amount (6 decimals) - "POOL,BAR,BAZ,3000", // route - "100", // 100% through route - "950000", // min output - time.Now().Unix() + 300, // deadline - "g1referrer...", // referral -) - -// Multi-hop swap -ExactInSwapRoute( - "gno.land/r/demo/bar", - "gno.land/r/demo/baz", - "1000000", - "POOL1,BAR,WUGNOT,3000:POOL2,WUGNOT,BAZ,3000", - "100", - "900000", - deadline, - "", -) - -// Split route for large trades -ExactInSwapRoute( - "gno.land/r/demo/usdc", - "gnot", - "10000000000", - "POOL1,USDC,WUGNOT,500:POOL2,USDC,WUGNOT,3000", - "60,40", // 60% through 0.05%, 40% through 0.3% - "9500000000", - deadline, - "", -) -``` - -## Security - -- Path validation prevents circular routes -- Deadline prevents stale transactions -- Slippage limits protect against MEV -- Router fees immutable per swap \ No newline at end of file diff --git a/contract/r/gnoswap/v1/router/assert.gno b/contract/r/gnoswap/v1/router/assert.gno deleted file mode 100644 index 2eaaf6f..0000000 --- a/contract/r/gnoswap/v1/router/assert.gno +++ /dev/null @@ -1,19 +0,0 @@ -package router - -import ( - "time" - - "gno.land/p/nt/ufmt" -) - -// assertIsNotExpired ensures the transaction deadline has not passed. -func assertIsNotExpired(deadline int64) { - now := time.Now().Unix() - - if now > deadline { - panic(makeErrorWithDetails( - errExpired, - ufmt.Sprintf("transaction too old, now(%d) > deadline(%d)", now, deadline), - )) - } -} diff --git a/contract/r/gnoswap/v1/router/base.gno b/contract/r/gnoswap/v1/router/base.gno deleted file mode 100644 index 60cfba2..0000000 --- a/contract/r/gnoswap/v1/router/base.gno +++ /dev/null @@ -1,321 +0,0 @@ -package router - -import ( - "errors" - "std" - "strconv" - "strings" - - "gno.land/p/nt/ufmt" - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoland/wugnot" -) - -const ( - SINGLE_HOP_ROUTE int = 1 - - INITIAL_WUGNOT_BALANCE int64 = 0 -) - -// swap can be done by multiple pools -// to separate each pool, we use POOL_SEPARATOR -const ( - POOL_SEPARATOR = "*POOL*" - gnot string = "gnot" - wugnotPath string = "gno.land/r/gnoland/wugnot" -) - -type RouterOperation interface { - Validate() error - Process() (*SwapResult, error) -} - -// executeSwapOperation validates and processes a swap operation. -func executeSwapOperation(op RouterOperation) (*SwapResult, error) { - if err := op.Validate(); err != nil { - return nil, err - } - - result, err := op.Process() - if err != nil { - return nil, err - } - - return result, nil -} - -type BaseSwapParams struct { - InputToken string - OutputToken string - RouteArr string - QuoteArr string - Deadline int64 -} - -// common swap operation -type baseSwapOperation struct { - withUnwrap bool - userBeforeWugnotBalance int64 - userWrappedWugnot int64 - routes []string - quotes []string - amountSpecified *i256.Int -} - -// handleNativeTokenWrapping handles wrapping and unwrapping of native tokens. -func (op *baseSwapOperation) handleNativeTokenWrapping( - inputToken string, - outputToken string, - specifiedAmount *i256.Int, -) error { - // no native token - if inputToken != gnot && outputToken != gnot { - return nil - } - - // save current user's WGNOT amount - op.userBeforeWugnotBalance = wugnot.BalanceOf(std.PreviousRealm().Address()) - - if outputToken == gnot { - op.withUnwrap = true - return nil - } - - sent := std.OriginSend() - ugnotSentByUser := sent.AmountOf("ugnot") - amountSpecified := specifiedAmount.Int64() - - if ugnotSentByUser != amountSpecified { - return ufmt.Errorf("ugnot sent by user(%d) is not equal to amountSpecified(%d)", ugnotSentByUser, amountSpecified) - } - - // wrap user's WUGNOT - if ugnotSentByUser > 0 { - if err := wrapWithTransfer(std.PreviousRealm().Address(), ugnotSentByUser); err != nil { - return err - } - } - - op.userWrappedWugnot = ugnotSentByUser - - return nil -} - -// validateRouteQuote validates and converts a route quote to swap amount. -func (op *baseSwapOperation) validateRouteQuote(quote string, i int) (*i256.Int, error) { - qt, err := strconv.Atoi(quote) - if err != nil { - return nil, ufmt.Errorf("invalid quote(%s) at index(%d)", quote, i) - } - - // calculate amount to swap for this route - toSwap := i256.Zero().Mul(op.amountSpecified, i256.NewInt(int64(qt))) - toSwap = i256.Zero().Div(toSwap, PERCENTAGE_DENOMINATOR) - - return toSwap, nil -} - -// processRoutes processes all swap routes and returns total amounts. -func (op *baseSwapOperation) processRoutes(swapType SwapType) (*u256.Uint, *u256.Uint, error) { - zero := u256.Zero() - resultAmountIn, resultAmountOut := zero, zero - remainRequestAmount := op.amountSpecified - - for i, route := range op.routes { - toSwapAmount := i256.Zero() - - // if it's the last route, use the remaining amount - isLastRoute := i == len(op.routes)-1 - if !isLastRoute { - // calculate the amount to swap for this route - swapAmount, err := op.validateRouteQuote(op.quotes[i], i) - if err != nil { - return nil, nil, err - } - - // update the remaining amount - remainRequestAmount = i256.Zero().Sub(remainRequestAmount, swapAmount) - toSwapAmount = swapAmount - } else { - toSwapAmount = remainRequestAmount - } - - amountIn, amountOut, err := op.processRoute(route, toSwapAmount, swapType) - if err != nil { - return nil, nil, err - } - - amountIn, overflow := u256.Zero().AddOverflow(resultAmountIn, amountIn) - if overflow { - return nil, nil, errOverFlow - } - - amountOut, overflow = u256.Zero().AddOverflow(resultAmountOut, amountOut) - if overflow { - return nil, nil, errOverFlow - } - - resultAmountIn = amountIn - resultAmountOut = amountOut - } - - return resultAmountIn, resultAmountOut, nil -} - -// processRoute processes a single route with specified swap amount. -func (op *baseSwapOperation) processRoute( - route string, - toSwap *i256.Int, - swapType SwapType, -) (amountIn, amountOut *u256.Uint, err error) { - numHops := strings.Count(route, POOL_SEPARATOR) + 1 - assertHopsInRange(numHops) - - switch numHops { - case SINGLE_HOP_ROUTE: - amountIn, amountOut = handleSingleSwap(route, toSwap, op.withUnwrap) - default: - amountIn, amountOut = handleMultiSwap(swapType, route, numHops, toSwap, op.withUnwrap) - } - - if amountIn == nil || amountOut == nil { - return nil, nil, ufmt.Errorf("swap failed to process route(%s)", route) - } - - return amountIn, amountOut, nil -} - -// handleSingleSwap executes a single-hop swap with the specified amount. -func handleSingleSwap(route string, amountSpecified *i256.Int, withUnwrap bool) (*u256.Uint, *u256.Uint) { - input, output, fee := getDataForSinglePath(route) - singleParams := SingleSwapParams{ - tokenIn: input, - tokenOut: output, - fee: fee, - amountSpecified: amountSpecified, - withUnwrap: withUnwrap, - } - - return singleSwap(&singleParams) -} - -// handleMultiSwap processes multi-hop swaps across multiple pools. -func handleMultiSwap( - swapType SwapType, - route string, - numHops int, - amountSpecified *i256.Int, - withUnwrap bool, -) (*u256.Uint, *u256.Uint) { - recipient := routerAddr - - switch swapType { - case ExactIn: - input, output, fee := getDataForMultiPath(route, 0) // first data - sp := newSwapParams(input, output, fee, recipient, amountSpecified, withUnwrap) - return multiSwap(*sp, numHops, route) - case ExactOut: - input, output, fee := getDataForMultiPath(route, numHops-1) // last data - sp := newSwapParams(input, output, fee, recipient, amountSpecified, withUnwrap) - return multiSwapNegative(*sp, numHops, route) - default: - panic(errInvalidSwapType) - } -} - -// SwapRouteParams contains all parameters needed for swap route execution -type SwapRouteParams struct { - inputToken string - outputToken string - routeArr string - quoteArr string - deadline int64 - typ SwapType - exactAmount string // amountIn for ExactIn, amountOut for ExactOut - limitAmount string // amountOutMin for ExactIn, amountInMax for ExactOut -} - -// IsUnwrap checks if the swap output is native token. -func (p *SwapRouteParams) IsUnwrap() bool { - return p.outputToken == gnot -} - -// createSwapOperation creates the appropriate swap operation based on swap type. -func createSwapOperation(params SwapRouteParams) (RouterOperation, error) { - baseParams := BaseSwapParams{ - InputToken: params.inputToken, - OutputToken: params.outputToken, - RouteArr: params.routeArr, - QuoteArr: params.quoteArr, - } - - switch params.typ { - case ExactIn: - pp := NewExactInParams(baseParams, params.exactAmount, params.limitAmount) - return NewExactInSwapOperation(pp), nil - case ExactOut: - pp := NewExactOutParams(baseParams, params.exactAmount, params.limitAmount) - return NewExactOutSwapOperation(pp), nil - default: - msg := addDetailToError(errInvalidSwapType, "unknown swap type") - return nil, errors.New(msg) - } -} - -// extractSwapOperationData extracts common data from swap operation. -func extractSwapOperationData(op RouterOperation) (int64, int64, error) { - var baseOp *baseSwapOperation - switch typedOp := op.(type) { - case *ExactInSwapOperation: - baseOp = &typedOp.baseSwapOperation - case *ExactOutSwapOperation: - baseOp = &typedOp.baseSwapOperation - default: - return 0, 0, ufmt.Errorf("unexpected operation type: %T", op) - } - - return baseOp.userBeforeWugnotBalance, baseOp.userWrappedWugnot, nil -} - -// commonSwapRoute handles the common logic for both ExactIn and ExactOut swaps. -func commonSwapRoute(params SwapRouteParams) (*i256.Int, *i256.Int, error) { - op, err := createSwapOperation(params) - if err != nil { - return i256.Zero(), i256.Zero(), err - } - - result, err := executeSwapOperation(op) - if err != nil { - msg := addDetailToError( - errInvalidInput, - ufmt.Sprintf("invalid %s SwapOperation: %s", params.typ.String(), err.Error()), - ) - return i256.Zero(), i256.Zero(), errors.New(msg) - } - - userBeforeWugnotBalance, userWrappedWugnot, err := extractSwapOperationData(op) - if err != nil { - return i256.Zero(), i256.Zero(), err - } - - limitValue, err := u256.FromDecimal(params.limitAmount) - if err != nil { - return i256.Zero(), i256.Zero(), err - } - - inputAmount, outputAmount := finalizeSwap( - params.inputToken, - params.outputToken, - result.AmountIn, - result.AmountOut, - params.typ, - limitValue, - userBeforeWugnotBalance, - userWrappedWugnot, - result.AmountSpecified.Abs(), - ) - - return inputAmount, outputAmount, nil -} diff --git a/contract/r/gnoswap/v1/router/consts.gno b/contract/r/gnoswap/v1/router/consts.gno deleted file mode 100644 index 8df7577..0000000 --- a/contract/r/gnoswap/v1/router/consts.gno +++ /dev/null @@ -1,16 +0,0 @@ -package router - -import ( - prabc "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoswap/access" -) - -const maxApprove int64 = 9223372036854775807 - -var ( - poolAddr, _ = access.GetAddress(prabc.ROLE_POOL.String()) - routerAddr, _ = access.GetAddress(prabc.ROLE_ROUTER.String()) - positionAddr, _ = access.GetAddress(prabc.ROLE_POSITION.String()) - protocolFeeAddr, _ = access.GetAddress(prabc.ROLE_PROTOCOL_FEE.String()) -) diff --git a/contract/r/gnoswap/v1/router/doc.gno b/contract/r/gnoswap/v1/router/doc.gno deleted file mode 100644 index 3e1e758..0000000 --- a/contract/r/gnoswap/v1/router/doc.gno +++ /dev/null @@ -1,9 +0,0 @@ -// Package router handles token swaps through GnoSwap liquidity pools. -// -// The router provides user-facing swap functions with slippage protection, -// multi-hop routing, and automatic GNOT wrapping/unwrapping. It supports -// both exact input and exact output swap modes. -// -// All swap functions include deadline checks and minimum output validation -// to protect users from unfavorable price movements. -package router diff --git a/contract/r/gnoswap/v1/router/errors.gno b/contract/r/gnoswap/v1/router/errors.gno deleted file mode 100644 index 9157ca1..0000000 --- a/contract/r/gnoswap/v1/router/errors.gno +++ /dev/null @@ -1,37 +0,0 @@ -package router - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errNoPermission = errors.New("[GNOSWAP-ROUTER-001] caller has no permission") - errSlippage = errors.New("[GNOSWAP-ROUTER-002] slippage check failed") - errInvalidRoutesAndQuotes = errors.New("[GNOSWAP-ROUTER-003] invalid routes and quotes") - errExpired = errors.New("[GNOSWAP-ROUTER-004] transaction expired") - errInvalidInput = errors.New("[GNOSWAP-ROUTER-005] invalid input data") - errInvalidPoolFeeTier = errors.New("[GNOSWAP-ROUTER-006] invalid pool fee tier") - errInvalidSwapFee = errors.New("[GNOSWAP-ROUTER-007] invalid swap fee") - errInvalidSwapType = errors.New("[GNOSWAP-ROUTER-008] invalid swap type") - errInvalidPoolPath = errors.New("[GNOSWAP-ROUTER-009] invalid pool path") - errWrapUnwrap = errors.New("[GNOSWAP-ROUTER-010] wrap, unwrap failed") - errWugnotMinimum = errors.New("[GNOSWAP-ROUTER-011] less than minimum amount ") - errQuoteParser = errors.New("[GNOSWAP-ROUTER-012] quote parse failed") - errHopsOutOfRange = errors.New("[GNOSWAP-ROUTER-013] number of hops must be 1~3") - errSameTokenSwap = errors.New("[GNOSWAP-ROUTER-014] cannot swap same token") - errProtocolFeeOverflow = errors.New("[GNOSWAP-ROUTER-015] protocol fee calculation overflow") - errOverFlow = errors.New("[GNOSWAP-ROUTER-016] overflow") -) - -// addDetailToError adds detail to an error message. -func addDetailToError(err error, detail string) string { - finalErr := ufmt.Errorf("%s || %s", err.Error(), detail) - return finalErr.Error() -} - -// makeErrorWithDetails creates an error with additional context. -func makeErrorWithDetails(err error, detail string) error { - return ufmt.Errorf("%s || %s", err.Error(), detail) -} diff --git a/contract/r/gnoswap/v1/router/exact_in.gno b/contract/r/gnoswap/v1/router/exact_in.gno deleted file mode 100644 index 20268c1..0000000 --- a/contract/r/gnoswap/v1/router/exact_in.gno +++ /dev/null @@ -1,175 +0,0 @@ -package router - -import ( - "std" - - "gno.land/p/nt/ufmt" - - i256 "gno.land/p/gnoswap/int256" - - "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/referral" - "gno.land/r/gnoswap/v1/common" -) - -type ExactInSwapOperation struct { - baseSwapOperation - params ExactInParams -} - -func NewExactInSwapOperation(pp ExactInParams) *ExactInSwapOperation { - return &ExactInSwapOperation{ - params: pp, - baseSwapOperation: baseSwapOperation{ - userWrappedWugnot: INITIAL_WUGNOT_BALANCE, - }, - } -} - -// ExactInSwapRoute swaps an exact amount of input tokens for output tokens. -// -// Executes multi-hop swaps through specified route. -// Supports splitting across multiple paths for price optimization. -// Applies slippage protection via minimum output amount. -// -// Parameters: -// - inputToken, outputToken: Token contract paths -// - amountIn: Exact input amount to swap -// - routeArr: Swap route "TOKEN0:TOKEN1:FEE,TOKEN1:TOKEN2:FEE" (max 7 hops) -// - quoteArr: Split percentages "70,30" (must sum to 100) -// - amountOutMin: Minimum acceptable output (slippage protection) -// - deadline: Unix timestamp for expiration -// - referrer: Optional referral address -// -// Route format: -// - Single: "WETH:USDC:3000" -// - Multi-hop: "WETH:GNS:3000,GNS:USDC:500" -// - Multi-path: Use multiple routes with quotes -// -// Returns: -// - amountIn: Actual input consumed -// - amountOut: Actual output received -// -// Reverts if output < amountOutMin or deadline passed. -func ExactInSwapRoute(cur realm, - inputToken string, - outputToken string, - amountIn string, - RouteArr string, - quoteArr string, - amountOutMin string, - deadline int64, - referrer string, -) (string, string) { - halt.AssertIsNotHaltedRouter() - - assertIsNotPassedDeadline(deadline) - - emission.MintAndDistributeGns(cross) - - params := SwapRouteParams{ - inputToken: inputToken, - outputToken: outputToken, - routeArr: RouteArr, - quoteArr: quoteArr, - deadline: deadline, - typ: ExactIn, - exactAmount: amountIn, - limitAmount: amountOutMin, - } - - inputAmount, outputAmount, err := commonSwapRoute(params) - if err != nil { - panic(err) - } - - if params.IsUnwrap() { - err = unwrapWithTransfer(std.PreviousRealm().Address(), outputAmount.Int64()) - if err != nil { - panic(err) - } - } else { - common.Transfer(cross, outputToken, std.PreviousRealm().Address(), outputAmount.Int64()) - } - - // handle referral registration - previousRealm := std.PreviousRealm() - caller := previousRealm.Address() - success := referral.TryRegister(cross, caller, referrer) - actualReferrer := referrer - if !success { - actualReferrer = referral.GetReferral(caller.String()) - } - - inputAmountStr := inputAmount.ToString() - outputAmountStr := i256.Zero().Neg(outputAmount).ToString() - - std.Emit( - "ExactInSwap", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "input", inputToken, - "output", outputToken, - "exactAmount", amountIn, - "route", RouteArr, - "quote", quoteArr, - "resultInputAmount", inputAmountStr, - "resultOutputAmount", outputAmountStr, - "referrer", actualReferrer, - ) - - return inputAmountStr, outputAmountStr -} - -// Validate validates the exact-in swap operation parameters. -func (op *ExactInSwapOperation) Validate() error { - amountIn := i256.MustFromDecimal(op.params.AmountIn) - if amountIn.IsZero() || amountIn.IsNeg() { - return ufmt.Errorf("invalid amountIn(%s), must be positive", amountIn.ToString()) - } - - // when `SwapType` is `ExactIn`, assign `amountSpecified` the `amountIn` - // obtained from above. - op.amountSpecified = amountIn - - routes, quotes, err := validateRoutesAndQuotes(op.params.RouteArr, op.params.QuoteArr) - if err != nil { - return err - } - - op.routes = routes - op.quotes = quotes - - return nil -} - -// Process executes the exact-in swap operation. -func (op *ExactInSwapOperation) Process() (*SwapResult, error) { - if err := op.handleNativeTokenWrapping(); err != nil { - return nil, err - } - - resultAmountIn, resultAmountOut, err := op.processRoutes(ExactIn) - if err != nil { - return nil, err - } - - return &SwapResult{ - AmountIn: resultAmountIn, - AmountOut: resultAmountOut, - Routes: op.routes, - Quotes: op.quotes, - AmountSpecified: op.amountSpecified, - WithUnwrap: op.withUnwrap, - }, nil -} - -// handleNativeTokenWrapping handles the wrapping of native tokens if needed. -func (op *ExactInSwapOperation) handleNativeTokenWrapping() error { - return op.baseSwapOperation.handleNativeTokenWrapping( - op.params.InputToken, - op.params.OutputToken, - op.amountSpecified, - ) -} diff --git a/contract/r/gnoswap/v1/router/exact_out.gno b/contract/r/gnoswap/v1/router/exact_out.gno deleted file mode 100644 index 931d50a..0000000 --- a/contract/r/gnoswap/v1/router/exact_out.gno +++ /dev/null @@ -1,178 +0,0 @@ -package router - -import ( - "std" - - "gno.land/p/nt/ufmt" - - i256 "gno.land/p/gnoswap/int256" - - "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/referral" - "gno.land/r/gnoswap/v1/common" -) - -// ExactOutSwapOperation handles swaps where the output amount is specified. -type ExactOutSwapOperation struct { - baseSwapOperation - params ExactOutParams -} - -// NewExactOutSwapOperation creates a new exact-out swap operation. -func NewExactOutSwapOperation(pp ExactOutParams) *ExactOutSwapOperation { - return &ExactOutSwapOperation{ - params: pp, - baseSwapOperation: baseSwapOperation{ - userWrappedWugnot: INITIAL_WUGNOT_BALANCE, - }, - } -} - -// ExactOutSwapRoute swaps tokens for an exact output amount. -// -// Executes swap to receive exact output tokens. -// Calculates required input working backwards through route. -// Useful for buying specific amounts regardless of price. -// -// Parameters: -// - inputToken, outputToken: Token contract paths -// - amountOut: Exact output amount desired -// - routeArr: Swap route "TOKEN0:TOKEN1:FEE,TOKEN1:TOKEN2:FEE" (max 7 hops) -// - quoteArr: Split percentages "70,30" (must sum to 100) -// - amountInMax: Maximum input to spend (slippage protection) -// - deadline: Unix timestamp for expiration -// - referrer: Optional referral address -// -// Route calculation: -// - Works backwards from output to input -// - Each hop increases required input -// - Multi-path aggregates total input -// -// Returns: -// - amountIn: Actual input consumed -// - amountOut: Exact output received -// -// Reverts if input > amountInMax or deadline passed. -func ExactOutSwapRoute( - cur realm, - inputToken string, - outputToken string, - amountOut string, - routeArr string, - quoteArr string, - amountInMax string, - deadline int64, - referrer string, -) (string, string) { - halt.AssertIsNotHaltedRouter() - - assertIsNotPassedDeadline(deadline) - - emission.MintAndDistributeGns(cross) - - params := SwapRouteParams{ - inputToken: inputToken, - outputToken: outputToken, - routeArr: routeArr, - quoteArr: quoteArr, - deadline: deadline, - typ: ExactOut, - exactAmount: amountOut, - limitAmount: amountInMax, - } - - inputAmount, outputAmount, err := commonSwapRoute(params) - if err != nil { - panic(err) - } - - if params.IsUnwrap() { - err = unwrapWithTransfer(std.PreviousRealm().Address(), outputAmount.Int64()) - if err != nil { - panic(err) - } - } else { - common.Transfer(cross, outputToken, std.PreviousRealm().Address(), outputAmount.Int64()) - } - - // handle referral registration - previousRealm := std.PreviousRealm() - caller := previousRealm.Address() - success := referral.TryRegister(cross, caller, referrer) - actualReferrer := referrer - if !success { - actualReferrer = referral.GetReferral(caller.String()) - } - - inputAmountStr := inputAmount.ToString() - outputAmountStr := i256.Zero().Neg(outputAmount).ToString() - - std.Emit( - "ExactOutSwap", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "input", inputToken, - "output", outputToken, - "exactAmount", amountOut, - "route", routeArr, - "quote", quoteArr, - "resultInputAmount", inputAmountStr, - "resultOutputAmount", outputAmountStr, - "referrer", actualReferrer, - ) - - return inputAmountStr, outputAmountStr -} - -// Validate ensures the exact-out swap parameters are valid. -func (op *ExactOutSwapOperation) Validate() error { - amountOut := i256.MustFromDecimal(op.params.AmountOut) - if amountOut.IsZero() || amountOut.IsNeg() { - return ufmt.Errorf("invalid amountOut(%s), must be positive", amountOut.ToString()) - } - - // assign a signed reversed `amountOut` to `amountSpecified` - // when it's an ExactOut - op.amountSpecified = i256.Zero().Neg(amountOut) - - routes, quotes, err := validateRoutesAndQuotes(op.params.RouteArr, op.params.QuoteArr) - if err != nil { - return err - } - - op.routes = routes - op.quotes = quotes - - return nil -} - -// Process executes the exact-out swap operation. -func (op *ExactOutSwapOperation) Process() (*SwapResult, error) { - if err := op.handleNativeTokenWrapping(); err != nil { - return nil, err - } - - resultAmountIn, resultAmountOut, err := op.processRoutes(ExactOut) - if err != nil { - return nil, err - } - - return &SwapResult{ - AmountIn: resultAmountIn, - AmountOut: resultAmountOut, - Routes: op.routes, - Quotes: op.quotes, - AmountSpecified: op.amountSpecified, - WithUnwrap: op.withUnwrap, - }, nil -} - -// handleNativeTokenWrapping manages native token wrapping for exact-out swaps. -func (op *ExactOutSwapOperation) handleNativeTokenWrapping() error { - return op.baseSwapOperation.handleNativeTokenWrapping( - op.params.InputToken, - op.params.OutputToken, - i256.MustFromDecimal(op.params.AmountInMax), - ) -} diff --git a/contract/r/gnoswap/v1/router/gnomod.toml b/contract/r/gnoswap/v1/router/gnomod.toml deleted file mode 100644 index 8bcfa66..0000000 --- a/contract/r/gnoswap/v1/router/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/router" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/router/protocol_fee_swap.gno b/contract/r/gnoswap/v1/router/protocol_fee_swap.gno deleted file mode 100644 index 95a806c..0000000 --- a/contract/r/gnoswap/v1/router/protocol_fee_swap.gno +++ /dev/null @@ -1,104 +0,0 @@ -package router - -import ( - "std" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" - - "gno.land/p/nt/ufmt" - - u256 "gno.land/p/gnoswap/uint256" - - pf "gno.land/r/gnoswap/v1/protocol_fee" -) - -const ( - defaultSwapFeeBPS = uint64(15) // 0.15% -) - -// swapFee is the fee charged on each swap transaction. -// This parameter can be modified through governance. -var swapFee = defaultSwapFeeBPS - -// GetSwapFee returns the current swap fee rate in basis points. -func GetSwapFee() uint64 { - return swapFee -} - -// SetSwapFee sets the swap fee rate in basis points. -// Only admin or governance can call this function. -func SetSwapFee(cur realm, fee uint64) { - halt.AssertIsNotHaltedRouter() - halt.AssertIsNotHaltedProtocolFee() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - prevSwapFee := swapFee - if err := setSwapFee(fee); err != nil { - panic(err) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "SetSwapFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "newFee", formatUint(fee), - "prevFee", formatUint(prevSwapFee), - ) -} - -// setSwapFee validates and updates the swap fee rate. -func setSwapFee(fee uint64) error { - // 10000 (bps) = 100% - if fee > 10000 { - return ufmt.Errorf( - "%s: fee must be in range 0 to 10000. got %d", - errInvalidSwapFee.Error(), fee, - ) - } - - swapFee = fee - return nil -} - -// handleSwapFee deducts the protocol fee from the swap amount and transfers it to the protocol fee contract. -func handleSwapFee( - outputToken string, - amount *u256.Uint, -) *u256.Uint { - if swapFee <= 0 { - return amount - } - - feeAmount := u256.Zero().Mul(amount, u256.NewUint(swapFee)) - feeAmount = u256.Zero().Div(feeAmount, u256.NewUint(10000)) - feeAmountInt64 := safeConvertToInt64(feeAmount) - - if outputToken == gnot { - outputToken = wugnotPath - } - - common.Transfer(cross, outputToken, protocolFeeAddr, feeAmountInt64) - - pf.AddToProtocolFee(cross, outputToken, feeAmountInt64) - - previousRealm := std.PreviousRealm() - std.Emit( - "SwapRouteFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "tokenPath", outputToken, - "amount", formatInt64(feeAmountInt64), - ) - - toUserAfterProtocol, underflow := u256.Zero().SubOverflow(amount, feeAmount) - if underflow { - panic(errProtocolFeeOverflow) - } - - return toUserAfterProtocol -} diff --git a/contract/r/gnoswap/v1/router/router.gno b/contract/r/gnoswap/v1/router/router.gno deleted file mode 100644 index efd7210..0000000 --- a/contract/r/gnoswap/v1/router/router.gno +++ /dev/null @@ -1,282 +0,0 @@ -package router - -import ( - "std" - "strconv" - - "gno.land/p/nt/ufmt" - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoland/wugnot" -) - -var ( - one = u256.One() - - maxInt64 = int64(^uint64(0) >> 1) -) - -// ErrorMessages define all error message templates used throughout the router -const ( - // slippage validation - errExactOutAmountExceeded = "Received more than requested in [EXACT_OUT] requested=%s, actual=%s" - - // route validation - errInvalidRouteLength = "route length(%d) must be 1~7" - - // quote validation - errRoutesQuotesMismatch = "mismatch between routes(%d) and quotes(%d) length" - errInvalidQuote = "invalid quote(%s) at index(%d)" - errInvalidQuoteValue = "quote(%s) at index(%d) must be positive value" - errQuoteExceedsMax = "quote(%s) at index(%d) must be less than or equal to %d" - errQuoteSumExceedsMax = "quote sum exceeds 100 at index(%d)" - errInvalidQuoteSum = "quote sum(%d) must be 100" - - // balance and overflow validation - errOverflowInBalance = "overflow in balance calculation: beforeBalance(%d) + wrappedAmount(%d)" - errTooMuchWugnotSpent = "too much wugnot spent (wrapped: %d, spend: %d)" - - // swap type validation - errExactInTooFewReceived = "ExactIn: too few received (min:%s, got:%s)" - errExactOutTooMuchSpent = "ExactOut: too much spent (max:%s, used:%s)" - - // route parsing validation - errEmptyRoutes = "routes cannot be empty" -) - -// GnotSwapHandler encapsulates methods for handling GNOT token swaps -type GnotSwapHandler struct { - BeforeBalance int64 - WrappedAmount int64 - NewBalance int64 -} - -// newGnotSwapHandler creates a new handler for GNOT swaps. -func newGnotSwapHandler(beforeBalance, wrappedAmount int64) *GnotSwapHandler { - return &GnotSwapHandler{ - BeforeBalance: beforeBalance, - WrappedAmount: wrappedAmount, - } -} - -// UpdateNewBalance updates the current balance after swap operations. -func (h *GnotSwapHandler) UpdateNewBalance() { - h.NewBalance = wugnot.BalanceOf(std.PreviousRealm().Address()) -} - -// HandleInputSwap manages unwrapping logic for GNOT input tokens. -func (h *GnotSwapHandler) HandleInputSwap() error { - // Check for overflow when adding balances - if h.BeforeBalance > 0 && h.WrappedAmount > 0 { - if h.BeforeBalance > (1<<63-1)-h.WrappedAmount { - return ufmt.Errorf(errOverflowInBalance, - h.BeforeBalance, h.WrappedAmount) - } - } - - totalBefore := h.BeforeBalance + h.WrappedAmount - spend := totalBefore - h.NewBalance - - if spend > h.WrappedAmount { - return ufmt.Errorf(errTooMuchWugnotSpent, - h.WrappedAmount, spend) - } - - toUnwrap := h.WrappedAmount - spend - - caller := std.PreviousRealm().Address() - return unwrapWithTransferFrom(caller, caller, toUnwrap) -} - -// SwapValidator provides validation methods for swap operations -type SwapValidator struct{} - -// exactOutAmount checks if output amount meets specified requirements. -func (v *SwapValidator) exactOutAmount(resultAmount, specifiedAmount *u256.Uint) error { - if resultAmount.Gte(specifiedAmount) { - return nil - } - - diff := u256.Zero().Sub(specifiedAmount, resultAmount) - if diff.Gt(one) { - return ufmt.Errorf(errExactOutAmountExceeded, specifiedAmount.ToString(), resultAmount.ToString()) - } - return nil -} - -// slippage ensures swap amounts meet slippage requirements. -func (v *SwapValidator) slippage(swapType SwapType, amountIn, amountOut, limit *u256.Uint) error { - switch swapType { - case ExactIn: - if amountOut.Lt(limit) { - return ufmt.Errorf(errExactInTooFewReceived, - limit.ToString(), amountOut.ToString()) - } - case ExactOut: - if amountIn.Gt(limit) { - return ufmt.Errorf(errExactOutTooMuchSpent, - limit.ToString(), amountIn.ToString()) - } - default: - return errInvalidSwapType - } - return nil -} - -// swapType ensures the swap type string is valid. -func (v *SwapValidator) swapType(swapTypeStr string) (SwapType, error) { - swapType, err := trySwapTypeFromStr(swapTypeStr) - if err != nil { - return Unknown, errInvalidSwapType - } - return swapType, nil -} - -// amount ensures the amount is properly formatted and positive. -func (v *SwapValidator) amount(amount string) (*i256.Int, error) { - parsedAmount := i256.MustFromDecimal(amount) - if parsedAmount.Lt(i256.Zero()) { - return nil, ufmt.Errorf(ErrInvalidPositiveAmount, amount) - } - return parsedAmount, nil -} - -// amountLimit ensures the amount limit is properly formatted and non-zero. -func (v *SwapValidator) amountLimit(amountLimit string) (*i256.Int, error) { - parsedLimit := i256.MustFromDecimal(amountLimit) - if parsedLimit.IsZero() { - return nil, ufmt.Errorf(ErrInvalidZeroAmountLimit, amountLimit) - } - return parsedLimit, nil -} - -// RouteParser handles parsing and validation of routes and quotes -type RouteParser struct{} - -// NewRouteParser creates a new route parser instance. -func NewRouteParser() *RouteParser { - return &RouteParser{} -} - -// ParseRoutes parses route and quote strings into slices and validates them. -func (p *RouteParser) ParseRoutes(routes, quotes string) ([]string, []string, error) { - // Check for empty routes - if routes == "" || quotes == "" { - return nil, nil, ufmt.Errorf(errEmptyRoutes) - } - - routesArr := splitSingleChar(routes, ',') - quotesArr := splitSingleChar(quotes, ',') - - if err := p.ValidateRoutesAndQuotes(routesArr, quotesArr); err != nil { - return nil, nil, err - } - - return routesArr, quotesArr, nil -} - -// ValidateRoutesAndQuotes ensures routes and quotes meet required criteria. -func (p *RouteParser) ValidateRoutesAndQuotes(routes, quotes []string) error { - rr := len(routes) - qq := len(quotes) - - if rr < 1 || rr > 7 { - return ufmt.Errorf(errInvalidRouteLength, rr) - } - - if rr != qq { - return ufmt.Errorf(errRoutesQuotesMismatch, rr, qq) - } - - return p.ValidateQuoteSum(quotes) -} - -// ValidateQuoteSum ensures all quotes add up to 100%. -func (p *RouteParser) ValidateQuoteSum(quotes []string) error { - const ( - maxQuote int8 = 100 - minQuote int8 = 0 - ) - - var sum int8 - - for i, quote := range quotes { - qt, err := strconv.ParseInt(quote, 10, 8) - if err != nil { - return ufmt.Errorf(errInvalidQuote, quote, i) - } - intQuote := int8(qt) - - // Even if quoteArr itself contains 0, there's no problem as long as the sum equals 100, - // but since quote generally won't be 0, we check if it's less than or equal to minQuote. - if intQuote <= minQuote { - return ufmt.Errorf(errInvalidQuoteValue, quote, i) - } - - if intQuote > maxQuote { - return ufmt.Errorf(errQuoteExceedsMax, quote, i, maxQuote) - } - - if sum > maxQuote-intQuote { - return ufmt.Errorf(errQuoteSumExceedsMax, i) - } - - sum += intQuote - } - - if sum != maxQuote { - return ufmt.Errorf(errInvalidQuoteSum, sum) - } - - return nil -} - -// finalizeSwap handles post-swap operations and validations. -func finalizeSwap( - inputToken, outputToken string, - resultAmountIn, resultAmountOut *u256.Uint, - swapType SwapType, - tokenAmountLimit *u256.Uint, - userBeforeWugnotBalance, userWrappedWugnot int64, - amountSpecified *u256.Uint, -) (*i256.Int, *i256.Int) { - validator := &SwapValidator{} - - // Validate exact out amount if applicable - if swapType == ExactOut { - if err := validator.exactOutAmount(resultAmountOut, amountSpecified); err != nil { - panic(addDetailToError(errSlippage, err.Error())) - } - } - - // Handle swap fee - resultAmountOutWithoutFee := handleSwapFee(outputToken, resultAmountOut) - - // Handle GNOT token swaps - handler := newGnotSwapHandler(userBeforeWugnotBalance, userWrappedWugnot) - handler.UpdateNewBalance() - - var err error - if inputToken == gnot { - err = handler.HandleInputSwap() - } - - if err != nil { - panic(addDetailToError(errSlippage, err.Error())) - } - - if err := validator.slippage(swapType, resultAmountIn, resultAmountOutWithoutFee, tokenAmountLimit); err != nil { - panic(addDetailToError(errSlippage, err.Error())) - } - - // calculate final amounts - intAmountOut := i256.FromUint256(resultAmountOutWithoutFee) - - return i256.FromUint256(resultAmountIn), intAmountOut -} - -// validateRoutesAndQuotes is a convenience function that parses and validates routes in one call. -func validateRoutesAndQuotes(routes, quotes string) ([]string, []string, error) { - return NewRouteParser().ParseRoutes(routes, quotes) -} diff --git a/contract/r/gnoswap/v1/router/router_dry.gno b/contract/r/gnoswap/v1/router/router_dry.gno deleted file mode 100644 index 0826820..0000000 --- a/contract/r/gnoswap/v1/router/router_dry.gno +++ /dev/null @@ -1,250 +0,0 @@ -package router - -import ( - "std" - "strconv" - "strings" - - "gno.land/p/nt/ufmt" - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/v1/common" -) - -var PERCENTAGE_DENOMINATOR = i256.NewInt(100) - -// QuoteConstraints defines the valid range for swap quote percentages -const ( - MaxQuotePercentage = 100 - MinQuotePercentage = 0 -) - -// ErrorMessages for DrySwapRoute operations -const ( - ErrOverflowResultAmountIn = "overflow in resultAmountIn" - ErrOverflowResultAmountOut = "overflow in resultAmountOut" - ErrUnknownSwapType = "unknown swapType(%s)" - ErrInvalidPositiveAmount = "invalid amount(%s), must be positive" - ErrInvalidZeroAmountLimit = "invalid amountLimit(%s), should not be zero" - ErrInvalidQuoteRange = "quote(%d) must be %d~%d" - ErrOverflowCalculateSwapAmount = "overflow in calculateSwapAmount" -) - -// SwapProcessor handles the execution of swap operations -type SwapProcessor struct{} - -// ProcessSwapAmount calculates the exact amount to swap based on quote percentage. -func (p *SwapProcessor) ProcessSwapAmount(amountSpecified *i256.Int, quote int) (*i256.Int, error) { - if quote < MinQuotePercentage || quote > MaxQuotePercentage { - return nil, ufmt.Errorf(ErrInvalidQuoteRange, quote, MinQuotePercentage, MaxQuotePercentage) - } - - toSwap := i256.Zero().Mul(amountSpecified, i256.NewInt(int64(quote))) - if toSwap.IsOverflow() { - return nil, ufmt.Errorf(ErrOverflowCalculateSwapAmount) - } - - return i256.Zero().Div(toSwap, PERCENTAGE_DENOMINATOR), nil -} - -// ProcessSingleSwap handles a single-hop swap simulation. -func (p *SwapProcessor) ProcessSingleSwap(route string, amountSpecified *i256.Int) (amountIn, amountOut *u256.Uint, err error) { - input, output, fee := getDataForSinglePath(route) - singleParams := SingleSwapParams{ - tokenIn: input, - tokenOut: output, - fee: fee, - amountSpecified: amountSpecified, - } - - amountIn, amountOut = singleDrySwap(&singleParams) - return amountIn, amountOut, nil -} - -// ProcessMultiSwap handles a multi-hop swap simulation. -func (p *SwapProcessor) ProcessMultiSwap( - swapType SwapType, - route string, - numHops int, - amountSpecified *i256.Int, -) (*u256.Uint, *u256.Uint, error) { - recipient := std.PreviousRealm().Address() - pathIndex := getPathIndex(swapType, numHops) - - input, output, fee := getDataForMultiPath(route, pathIndex) - swapParams := newSwapParams(input, output, fee, recipient, amountSpecified, false) - - switch swapType { - case ExactIn: - return multiDrySwap(*swapParams, numHops, route) - case ExactOut: - return multiDrySwapNegative(*swapParams, numHops, route) - default: - return nil, nil, ufmt.Errorf(ErrUnknownSwapType, swapType) - } -} - -// ValidateSwapResults checks if the swap results meet the required constraints. -func (p *SwapProcessor) ValidateSwapResults( - swapType SwapType, - resultAmountIn, resultAmountOut *u256.Uint, - amountSpecified, amountLimit *i256.Int, -) (amountIn, amountOut string, success bool) { - if resultAmountIn.IsZero() || resultAmountOut.IsZero() { - return "0", "0", false - } - - switch swapType { - case ExactIn: - if i256.FromUint256(resultAmountIn).Gt(amountSpecified) { - return resultAmountIn.ToString(), resultAmountOut.ToString(), false - } - if i256.FromUint256(resultAmountOut).Lt(amountLimit) { - return resultAmountIn.ToString(), resultAmountOut.ToString(), false - } - return resultAmountIn.ToString(), resultAmountOut.ToString(), true - - case ExactOut: - if i256.FromUint256(resultAmountOut).Lt(amountSpecified) { - return resultAmountIn.ToString(), resultAmountOut.ToString(), false - } - if i256.FromUint256(resultAmountIn).Gt(amountLimit) { - return resultAmountIn.ToString(), resultAmountOut.ToString(), false - } - return resultAmountIn.ToString(), resultAmountOut.ToString(), true - - default: - // This should never happen since we validate the swap type earlier - return "", "", false - } -} - -// AddSwapResults safely adds swap result amounts, checking for overflow. -func (p *SwapProcessor) AddSwapResults( - resultAmountIn, resultAmountOut, amountIn, amountOut *u256.Uint, -) (*u256.Uint, *u256.Uint, error) { - newAmountIn := u256.Zero().Add(resultAmountIn, amountIn) - if newAmountIn.IsOverflow() { - return nil, nil, ufmt.Errorf(ErrOverflowResultAmountIn) - } - - newAmountOut := u256.Zero().Add(resultAmountOut, amountOut) - if newAmountOut.IsOverflow() { - return nil, nil, ufmt.Errorf(ErrOverflowResultAmountOut) - } - - return newAmountIn, newAmountOut, nil -} - -// DrySwapRoute simulates a token swap route without executing the swap. -// It calculates the expected outcome based on the current state of liquidity pools. -func DrySwapRoute( - inputToken, outputToken string, - specifiedAmount string, - swapTypeStr string, - strRouteArr, quoteArr string, - tokenAmountLimit string, -) (string, string, bool) { - drySwapRouteWithCrossFn := func(cur realm) (string, string, bool) { - return drySwapRoute(inputToken, outputToken, specifiedAmount, swapTypeStr, strRouteArr, quoteArr, tokenAmountLimit) - } - - return drySwapRouteWithCrossFn(cross) -} - -// drySwapRoute is a function for applying cross realm. -func drySwapRoute( - inputToken, outputToken string, - specifiedAmount string, - swapTypeStr string, - strRouteArr, quoteArr string, - tokenAmountLimit string, -) (string, string, bool) { - common.MustRegistered(inputToken, outputToken) - // initialize components - validator := &SwapValidator{} - processor := &SwapProcessor{} - parser := &RouteParser{} - - // validate and parse inputs - swapType, err := validator.swapType(swapTypeStr) - if err != nil { - panic(addDetailToError(errInvalidSwapType, err.Error())) - } - - amountSpecified, err := validator.amount(specifiedAmount) - if err != nil { - panic(addDetailToError(errInvalidInput, err.Error())) - } - - amountLimit, err := validator.amountLimit(tokenAmountLimit) - if err != nil { - panic(addDetailToError(errInvalidInput, err.Error())) - } - - routes, quotes, err := parser.ParseRoutes(strRouteArr, quoteArr) - if err != nil { - panic(addDetailToError(errInvalidRoutesAndQuotes, err.Error())) - } - - // adjust amount sign for exact out swaps - if swapType == ExactOut { - amountSpecified = i256.Zero().Neg(amountSpecified) - } - - // initialize accumulators for swap results - resultAmountIn, resultAmountOut := zero, zero - - // Process each route - for i, route := range routes { - // calculate the amount to swap for this route - quoteValue, err := strconv.Atoi(quotes[i]) - if err != nil { - panic(addDetailToError(errInvalidInput, err.Error())) - } - - toSwap, err := processor.ProcessSwapAmount(amountSpecified, quoteValue) - if err != nil { - panic(addDetailToError(errInvalidInput, err.Error())) - } - - // determine the number of hops and validate - numHops := strings.Count(route, POOL_SEPARATOR) + 1 - assertHopsInRange(numHops) - - // execute the appropriate swap type - var amountIn, amountOut *u256.Uint - if numHops == 1 { - amountIn, amountOut, err = processor.ProcessSingleSwap(route, toSwap) - } else { - amountIn, amountOut, err = processor.ProcessMultiSwap(swapType, route, numHops, toSwap) - } - - if err != nil { - panic(addDetailToError(errInvalidSwapType, err.Error())) - } - - // update accumulated results - resultAmountIn, resultAmountOut, err = processor.AddSwapResults(resultAmountIn, resultAmountOut, amountIn, amountOut) - if err != nil { - panic(addDetailToError(errInvalidInput, err.Error())) - } - } - - return processor.ValidateSwapResults(swapType, resultAmountIn, resultAmountOut, amountSpecified, amountLimit) -} - -// getPathIndex returns the path index based on swap type and number of hops. -func getPathIndex(swapType SwapType, numHops int) int { - switch swapType { - case ExactIn: - // first data for exact input swaps - return 0 - case ExactOut: - // last data for exact output swaps - return numHops - 1 - default: - panic("should not happen") - } -} diff --git a/contract/r/gnoswap/v1/router/swap_inner.gno b/contract/r/gnoswap/v1/router/swap_inner.gno deleted file mode 100644 index 0fadfdc..0000000 --- a/contract/r/gnoswap/v1/router/swap_inner.gno +++ /dev/null @@ -1,186 +0,0 @@ -package router - -import ( - "std" - - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/v1/common" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - pl "gno.land/r/gnoswap/v1/pool" -) - -const ( - MIN_SQRT_RATIO string = "4295128739" // same as TickMathGetSqrtRatioAtTick(MIN_TICK) - MAX_SQRT_RATIO string = "1461446703485210103287273052203988822378723970342" // same as TickMathGetSqrtRatioAtTick(MAX_TICK) -) - -// swapInner executes the core swap logic by interacting with the pool contract. -// Returns poolRecv (tokens received by pool) and poolOut (tokens sent by pool). -func swapInner( - amountSpecified *i256.Int, - recipient std.Address, - sqrtPriceLimitX96 *u256.Uint, - data SwapCallbackData, -) (poolRecv, poolOut *u256.Uint) { - zeroForOne := data.tokenIn < data.tokenOut - - sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96) - - amount0Str, amount1Str := pl.Swap( - cross, - data.tokenIn, - data.tokenOut, - data.fee, - recipient, - zeroForOne, - amountSpecified.ToString(), - sqrtPriceLimitX96.ToString(), - data.payer, - ) - - amount0 := i256.MustFromDecimal(amount0Str) - amount1 := i256.MustFromDecimal(amount1Str) - - poolOut, poolRecv = i256MinMax(amount0, amount1) - if poolRecv.IsOverflow() || poolOut.IsOverflow() { - panic("overflow in swapInner") - } - - // approves pool as spender - if data.hasNext { - common.Approve(cross, data.tokenOut, poolAddr, poolOut.Int64()) - } - - return poolRecv, poolOut -} - -// RealSwapExecutor implements SwapExecutor for actual swaps. -type RealSwapExecutor struct{} - -// execute performs the actual swap execution. -func (e *RealSwapExecutor) execute(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { - caller := std.PreviousRealm().Address() - recipient := routerAddr - - return swapInner( - p.amountSpecified, - recipient, // if single swap => user will receive - zero, // sqrtPriceLimitX96 - newSwapCallbackData(p, caller, false), - ) -} - -// swapDryInner performs a dry-run of a swap operation without executing it. -func swapDryInner( - amountSpecified *i256.Int, - sqrtPriceLimitX96 *u256.Uint, - data SwapCallbackData, -) (poolRecv, poolOut *u256.Uint) { - zeroForOne := data.tokenIn < data.tokenOut - sqrtPriceLimitX96 = calculateSqrtPriceLimitForSwap(zeroForOne, data.fee, sqrtPriceLimitX96) - - // check possible - amount0Str, amount1Str, ok := pl.DrySwap( - data.tokenIn, - data.tokenOut, - data.fee, - zeroForOne, - amountSpecified.ToString(), - sqrtPriceLimitX96.ToString(), - ) - if !ok { - return zero, zero - } - - amount0 := i256.MustFromDecimal(amount0Str) - amount1 := i256.MustFromDecimal(amount1Str) - - poolOut, poolRecv = i256MinMax(amount0, amount1) - if poolRecv.IsOverflow() || poolOut.IsOverflow() { - panic("overflow in swapDryInner") - } - - return poolRecv, poolOut -} - -// DrySwapExecutor implements SwapExecutor for dry swaps. -type DrySwapExecutor struct{} - -// execute performs the dry swap execution. -func (e *DrySwapExecutor) execute(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { - previousRealmAddr := std.PreviousRealm().Address() - - return swapDryInner( - p.amountSpecified, - zero, - newSwapCallbackData(p, previousRealmAddr, false), - ) -} - -// calculateSqrtPriceLimitForSwap calculates the price limit for a swap operation. -func calculateSqrtPriceLimitForSwap(zeroForOne bool, fee uint32, sqrtPriceLimitX96 *u256.Uint) *u256.Uint { - if !sqrtPriceLimitX96.IsZero() { - return sqrtPriceLimitX96 - } - - if zeroForOne { - minTick := getMinTick(fee) + 1 - sqrtPriceLimitX96 = u256.Zero().Set(common.TickMathGetSqrtRatioAtTick(minTick)) - if sqrtPriceLimitX96.IsZero() { - sqrtPriceLimitX96 = u256.MustFromDecimal(MIN_SQRT_RATIO) - } - return u256.Zero().Add(sqrtPriceLimitX96, one) - } - - maxTick := getMaxTick(fee) - 1 - sqrtPriceLimitX96 = u256.Zero().Set(common.TickMathGetSqrtRatioAtTick(maxTick)) - if sqrtPriceLimitX96.IsZero() { - sqrtPriceLimitX96 = u256.MustFromDecimal(MAX_SQRT_RATIO) - } - return u256.Zero().Sub(sqrtPriceLimitX96, one) -} - -// getMinTick returns the minimum tick value for a given fee tier. -// The implementation follows Uniswap V3's tick spacing rules where -// lower fee tiers allow for finer price granularity. -func getMinTick(fee uint32) int32 { - switch fee { - case 100: - return -887272 - case 500: - return -887270 - case 3000: - return -887220 - case 10000: - return -887200 - default: - panic(addDetailToError( - errInvalidPoolFeeTier, - ufmt.Sprintf("unknown fee(%d)", fee), - )) - } -} - -// getMaxTick returns the maximum tick value for a given fee tier. -// The max tick values are the exact negatives of min tick values. -func getMaxTick(fee uint32) int32 { - switch fee { - case 100: - return 887272 - case 500: - return 887270 - case 3000: - return 887220 - case 10000: - return 887200 - default: - panic(addDetailToError( - errInvalidPoolFeeTier, - ufmt.Sprintf("unknown fee(%d)", fee), - )) - } -} diff --git a/contract/r/gnoswap/v1/router/swap_multi.gno b/contract/r/gnoswap/v1/router/swap_multi.gno deleted file mode 100644 index 49ff662..0000000 --- a/contract/r/gnoswap/v1/router/swap_multi.gno +++ /dev/null @@ -1,259 +0,0 @@ -package router - -import ( - "std" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -// SwapDirection represents the direction of swap execution in multi-hop swaps. -// It determines whether swaps are processed in forward order (first to last pool) -// or backward order (last to first pool). -type SwapDirection int - -const ( - _ SwapDirection = iota - // Forward indicates a swap processing direction from the first pool to the last pool. - // Used primarily for exactIn swaps where the input amount is known. - Forward - - // Backward indicates a swap processing direction from the last pool to the first pool. - // Used primarily for exactOut swaps where the output amount is known and input amounts - // Need to be calculated in reverse order. - Backward -) - -// MultiSwapExecutor defines the interface for multi-hop swap operation execution. -type MultiSwapExecutor interface { - // Run performs the swap operation and returns pool received and pool output amounts. - Run(p SwapParams, data SwapCallbackData, recipient std.Address) (*u256.Uint, *u256.Uint) -} - -// DryMultiSwapExecutor implements MultiSwapExecutor for dry run simulations. -type DryMultiSwapExecutor struct{} - -// Run performs a dry swap operation without changing state. -func (e *DryMultiSwapExecutor) Run(p SwapParams, data SwapCallbackData, _ std.Address) (*u256.Uint, *u256.Uint) { - return swapDryInner(p.amountSpecified, zero, data) -} - -// RealMultiSwapExecutor implements MultiSwapExecutor for actual swap operations. -type RealMultiSwapExecutor struct{} - -// Run performs a real swap operation with state changes. -func (e *RealMultiSwapExecutor) Run(p SwapParams, data SwapCallbackData, recipient std.Address) (*u256.Uint, *u256.Uint) { - return swapInner(p.amountSpecified, recipient, zero, data) -} - -// MultiSwapProcessor handles the execution flow for multi-hop swaps. -type MultiSwapProcessor struct { - executor MultiSwapExecutor - direction SwapDirection - isSimulate bool -} - -var ( - _ MultiSwapExecutor = (*DryMultiSwapExecutor)(nil) - _ MultiSwapExecutor = (*RealMultiSwapExecutor)(nil) -) - -// NewMultiSwapProcessor creates a new MultiSwapProcessor with the specified configuration. -func NewMultiSwapProcessor(isSimulate bool, direction SwapDirection) *MultiSwapProcessor { - var executor MultiSwapExecutor - if isSimulate { - executor = &DryMultiSwapExecutor{} - } else { - executor = &RealMultiSwapExecutor{} - } - - return &MultiSwapProcessor{ - executor: executor, - direction: direction, - isSimulate: isSimulate, - } -} - -// processForwardSwap handles forward direction swaps (exactIn). -func (p *MultiSwapProcessor) processForwardSwap(sp SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint, error) { - payer := std.PreviousRealm().Address() // Initial payer is the user - - firstAmountIn := zero - currentPoolIndex := 0 - - for { - currentPoolIndex++ - - // Execute the swap operation - callbackData := newSwapCallbackData(sp, payer, currentPoolIndex != numPools) - amountIn, amountOut := p.executor.Run(sp, callbackData, sp.recipient) - - // Record the first hop's input amount - if currentPoolIndex == 1 { - firstAmountIn = amountIn - } - - // Check if we've processed all hops - if currentPoolIndex >= numPools { - if p.isSimulate { - return firstAmountIn, amountOut, nil - } - return firstAmountIn, amountOut, nil - } - - // Update parameters for the next hop - payer = routerAddr - nextInput, nextOutput, nextFee := getDataForMultiPath(swapPath, currentPoolIndex) - sp.tokenIn = nextInput - sp.tokenOut = nextOutput - sp.fee = nextFee - sp.amountSpecified = i256.FromUint256(amountOut) - } -} - -// processBackwardSwap handles backward direction swaps (exactOut). -func (p *MultiSwapProcessor) processBackwardSwap(sp SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint, error) { - if !p.isSimulate { - return p.processBackwardRealSwap(sp, numPools, swapPath) - } - return p.processBackwardDrySwap(sp, numPools, swapPath) -} - -// processBackwardDrySwap handles backward simulated swaps. -func (p *MultiSwapProcessor) processBackwardDrySwap(sp SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint, error) { - firstAmountIn := u256.Zero() - currentPoolIndex := numPools - 1 - payer := routerAddr - - for { - callbackData := newSwapCallbackData(sp, payer, false) - amountIn, amountOut := p.executor.Run(sp, callbackData, sp.recipient) - - if currentPoolIndex == 0 { - firstAmountIn = amountIn - } - - currentPoolIndex-- - - if currentPoolIndex == -1 { - return firstAmountIn, amountOut, nil - } - - // Update parameters for the next hop - nextInput, nextOutput, nextFee := getDataForMultiPath(swapPath, currentPoolIndex) - intAmountIn := i256.FromUint256(amountIn) - - sp.amountSpecified = i256.Zero().Neg(intAmountIn) - sp.tokenIn = nextInput - sp.tokenOut = nextOutput - sp.fee = nextFee - } -} - -// processBackwardRealSwap handles backward real swaps. -func (p *MultiSwapProcessor) processBackwardRealSwap(sp SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint, error) { - // First collect all swap information by simulating backward - swapInfo := p.collectBackwardSwapInfo(sp, numPools, swapPath) - - // Then execute swaps in forward order - return p.executeCollectedSwaps(swapInfo, sp.recipient, sp.withUnwrap) -} - -// collectBackwardSwapInfo simulates swaps backward to collect parameters. -func (p *MultiSwapProcessor) collectBackwardSwapInfo(sp SwapParams, numPools int, swapPath string) []SingleSwapParams { - swapInfo := make([]SingleSwapParams, 0, numPools-1) - currentPoolIndex := numPools - 1 - - for currentPoolIndex >= 0 { - thisSwap := SingleSwapParams{ - tokenIn: sp.tokenIn, - tokenOut: sp.tokenOut, - fee: sp.fee, - amountSpecified: sp.amountSpecified, - } - - // dry simulation to calculate input amount - amountIn, _ := singleDrySwap(&thisSwap) - swapInfo = append(swapInfo, thisSwap) - - if currentPoolIndex == 0 { - break - } - currentPoolIndex-- - - // Update parameters for the next simulation - nextInput, nextOutput, nextFee := getDataForMultiPath(swapPath, currentPoolIndex) - - sp.tokenIn = nextInput - sp.tokenOut = nextOutput - sp.fee = nextFee - sp.amountSpecified = i256.Zero().Neg(i256.FromUint256(amountIn)) - } - - return swapInfo -} - -// executeCollectedSwaps performs the collected swaps in forward order. -func (p *MultiSwapProcessor) executeCollectedSwaps(swapInfo []SingleSwapParams, recipient std.Address, withUnwrap bool) (*u256.Uint, *u256.Uint, error) { - firstAmountIn := zero - currentPoolIndex := len(swapInfo) - 1 - payer := std.PreviousRealm().Address() // Initial payer is the user - - for currentPoolIndex >= 0 { - // Execute the swap - callbackData := newSwapCallbackData( - swapInfo[currentPoolIndex], - payer, - currentPoolIndex != 0, - ) - - amountIn, amountOut := swapInner( - swapInfo[currentPoolIndex].amountSpecified, - recipient, - zero, - callbackData, - ) - - // Record the first hop's input amount - if currentPoolIndex == len(swapInfo)-1 { - firstAmountIn = amountIn - } - - if currentPoolIndex == 0 { - return firstAmountIn, amountOut, nil - } - - // Update parameters for the next swap - swapInfo[currentPoolIndex-1].amountSpecified = i256.FromUint256(amountOut) - payer = routerAddr - currentPoolIndex-- - } - - return firstAmountIn, zero, nil -} - -// multiSwap performs a multi-hop swap in forward direction. -func multiSwap(p SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint) { - result, output, _ := NewMultiSwapProcessor(false, Forward). - processForwardSwap(p, numPools, swapPath) - return result, output -} - -// multiSwapNegative performs a multi-hop swap in backward direction. -func multiSwapNegative(p SwapParams, numPools int, swapPath string) (*u256.Uint, *u256.Uint) { - result, output, _ := NewMultiSwapProcessor(false, Backward). - processBackwardSwap(p, numPools, swapPath) - return result, output -} - -// multiDrySwap simulates a multi-hop swap in forward direction. -func multiDrySwap(p SwapParams, numPool int, swapPath string) (*u256.Uint, *u256.Uint, error) { - return NewMultiSwapProcessor(true, Forward). - processForwardSwap(p, numPool, swapPath) -} - -// multiDrySwapNegative simulates a multi-hop swap in backward direction. -func multiDrySwapNegative(p SwapParams, numPool int, swapPath string) (*u256.Uint, *u256.Uint, error) { - return NewMultiSwapProcessor(true, Backward). - processBackwardSwap(p, numPool, swapPath) -} diff --git a/contract/r/gnoswap/v1/router/swap_single.gno b/contract/r/gnoswap/v1/router/swap_single.gno deleted file mode 100644 index c9dc5f3..0000000 --- a/contract/r/gnoswap/v1/router/swap_single.gno +++ /dev/null @@ -1,43 +0,0 @@ -package router - -import ( - u256 "gno.land/p/gnoswap/uint256" - "gno.land/r/gnoswap/v1/common" -) - -var zero = u256.Zero() - -// SwapExecutor defines the interface for executing swaps. -type SwapExecutor interface { - // execute performs the swap operation. - execute(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) -} - -// executeSwap is the common logic for both real and dry swaps. -func executeSwap(executor SwapExecutor, p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { - if p.tokenIn == p.tokenOut { - panic(errSameTokenSwap) - } - - common.MustRegistered(p.tokenIn, p.tokenOut) - - return executor.execute(p) -} - -var ( - _ SwapExecutor = (*RealSwapExecutor)(nil) - _ SwapExecutor = (*DrySwapExecutor)(nil) -) - -// singleSwap executes a swap within a single pool using the provided parameters. -// It processes a token swap within two assets using a specific fee tier and -// automatically sets the recipient to the caller's address. -func singleSwap(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { - return executeSwap(&RealSwapExecutor{}, p) -} - -// singleDrySwap simulates a single-token swap operation without executing it. -// It performs a dry run simulation and does not alter the state. -func singleDrySwap(p *SingleSwapParams) (amountIn, amountOut *u256.Uint) { - return executeSwap(&DrySwapExecutor{}, p) -} diff --git a/contract/r/gnoswap/v1/router/type.gno b/contract/r/gnoswap/v1/router/type.gno deleted file mode 100644 index 7c062f1..0000000 --- a/contract/r/gnoswap/v1/router/type.gno +++ /dev/null @@ -1,189 +0,0 @@ -package router - -import ( - "std" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/p/nt/ufmt" -) - -const ( - rawUnknown = "UNKNOWN" - rawExactIn = "EXACT_IN" - rawExactOut = "EXACT_OUT" -) - -type SwapType string - -const ( - Unknown SwapType = rawUnknown - // ExactIn represents a swap type where the input amount is exact and the output amount may vary. - // Used when a user wants to swap a specific amount of input tokens. - ExactIn SwapType = rawExactIn - - // ExactOut represents a swap type where the output amount is exact and the input amount may vary. - // Used when a user wants to swap a specific amount of output tokens. - ExactOut SwapType = rawExactOut -) - -// trySwapTypeFromStr attempts to convert a string into a SwapType. -// It validates and converts string representations of swap types into their corresponding enum values. -func trySwapTypeFromStr(swapType string) (SwapType, error) { - switch swapType { - case rawExactIn: - return ExactIn, nil - case rawExactOut: - return ExactOut, nil - default: - return "", ufmt.Errorf("unknown swapType: expected ExactIn or ExactOut, got %s", swapType) - } -} - -// String returns the string representation of SwapType. -func (s SwapType) String() string { - switch s { - case ExactIn: - return rawExactIn - case ExactOut: - return rawExactOut - default: - return "" - } -} - -// SingleSwapParams contains parameters for executing a single pool swap. -// It represents the simplest form of swap that occurs within a single liquidity pool. -type SingleSwapParams struct { - tokenIn string // token to spend - tokenOut string // token to receive - fee uint32 // fee of the pool used to swap - withUnwrap bool // whether to unwrap the token - - // Amount specified for the swap: - // - Positive: exact input amount (tokenIn) - // - Negative: exact output amount (tokenOut) - amountSpecified *i256.Int -} - -// TokenIn returns the input token address. -func (p SingleSwapParams) TokenIn() string { return p.tokenIn } - -// TokenOut returns the output token address. -func (p SingleSwapParams) TokenOut() string { return p.tokenOut } - -// Fee returns the pool fee tier. -func (p SingleSwapParams) Fee() uint32 { return p.fee } - -// SwapParams contains parameters for executing a multi-hop swap operation. -type SwapParams struct { - SingleSwapParams - recipient std.Address // address to receive the token - withUnwrap bool // whether to unwrap the token -} - -// TokenIn returns the input token address. -func (p SwapParams) TokenIn() string { return p.tokenIn } - -// TokenOut returns the output token address. -func (p SwapParams) TokenOut() string { return p.tokenOut } - -// Fee returns the pool fee tier. -func (p SwapParams) Fee() uint32 { return p.fee } - -// Recipient returns the recipient address. -func (p SwapParams) Recipient() std.Address { return p.recipient } - -// newSwapParams creates a new SwapParams instance with the provided parameters. -func newSwapParams(tokenIn, tokenOut string, fee uint32, recipient std.Address, amountSpecified *i256.Int, withUnwrap bool) *SwapParams { - return &SwapParams{ - SingleSwapParams: SingleSwapParams{ - tokenIn: tokenIn, - tokenOut: tokenOut, - fee: fee, - amountSpecified: amountSpecified, - }, - withUnwrap: withUnwrap, - recipient: recipient, - } -} - -// SwapResult encapsulates the outcome of a swap operation. -type SwapResult struct { - Routes []string - Quotes []string - AmountIn *u256.Uint - AmountOut *u256.Uint - AmountSpecified *i256.Int - WithUnwrap bool -} - -// SwapParamsI defines the common interface for swap parameters. -type SwapParamsI interface { - TokenIn() string - TokenOut() string - Fee() uint32 -} - -// SwapCallbackData contains the callback data required for swap execution. -// This type is used to pass necessary information during the swap callback process, -// ensuring proper token transfers and pool data updates. -type SwapCallbackData struct { - tokenIn string // token to spend - tokenOut string // token to receive - fee uint32 // fee of the pool used to swap - payer std.Address // address to spend the token - hasNext bool // whether there is a next swap -} - -// newSwapCallbackData creates a new SwapCallbackData from a SwapParamsI. -func newSwapCallbackData(params SwapParamsI, payer std.Address, hasNext bool) SwapCallbackData { - return SwapCallbackData{ - tokenIn: params.TokenIn(), - tokenOut: params.TokenOut(), - fee: params.Fee(), - payer: payer, - hasNext: hasNext, - } -} - -// ExactInParams contains parameters for exact input swaps. -type ExactInParams struct { - BaseSwapParams - AmountIn string - AmountOutMin string -} - -// NewExactInParams creates a new ExactInParams instance. -func NewExactInParams( - baseParams BaseSwapParams, - amountIn string, - amountOutMin string, -) ExactInParams { - return ExactInParams{ - BaseSwapParams: baseParams, - AmountIn: amountIn, - AmountOutMin: amountOutMin, - } -} - -// ExactOutParams contains parameters for exact output swaps. -type ExactOutParams struct { - BaseSwapParams - AmountOut string - AmountInMax string -} - -// NewExactOutParams creates a new ExactOutParams instance. -func NewExactOutParams( - baseParams BaseSwapParams, - amountOut string, - amountInMax string, -) ExactOutParams { - return ExactOutParams{ - BaseSwapParams: baseParams, - AmountOut: amountOut, - AmountInMax: amountInMax, - } -} diff --git a/contract/r/gnoswap/v1/router/utils.gno b/contract/r/gnoswap/v1/router/utils.gno deleted file mode 100644 index 20af180..0000000 --- a/contract/r/gnoswap/v1/router/utils.gno +++ /dev/null @@ -1,167 +0,0 @@ -package router - -import ( - "bytes" - "strconv" - "strings" - "time" - - "gno.land/p/nt/ufmt" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -var ( - errRouterHalted = "router contract operations are currently disabled" - errTxExpired = "transaction too old, now(%d) > deadline(%d)" -) - -// assertHopsInRange ensures the number of hops is within the valid range of 1-3. -func assertHopsInRange(hops int) { - switch hops { - case 1, 2, 3: - return - default: - panic(errHopsOutOfRange) - } -} - -// assertIsNotPassedDeadline ensures the transaction deadline has not expired. -func assertIsNotPassedDeadline(deadline int64) { - if err := checkDeadline(deadline); err != nil { - errMsg := addDetailToError(errExpired, err.Error()) - panic(errMsg) - } -} - -// getDataForSinglePath extracts token addresses and fee from a single pool path. -func getDataForSinglePath(poolPath string) (token0, token1 string, fee uint32) { - poolPathSplit := strings.Split(poolPath, ":") - if len(poolPathSplit) != 3 { - panic(addDetailToError( - errInvalidPoolPath, - ufmt.Sprintf("len(poolPathSplit) != 3, poolPath: %s", poolPath), - )) - } - - f, err := strconv.Atoi(poolPathSplit[2]) - if err != nil { - panic(ufmt.Sprintf("invalid fee: %s", poolPathSplit[2])) - } - - return poolPathSplit[0], poolPathSplit[1], uint32(f) -} - -// getDataForMultiPath extracts token addresses and fee from a multi-hop path at specified index. -func getDataForMultiPath(possiblePath string, poolIdx int) (token0, token1 string, fee uint32) { - pools := strings.Split(possiblePath, POOL_SEPARATOR) - - switch poolIdx { - case 0: - return getDataForSinglePath(pools[0]) - case 1: - return getDataForSinglePath(pools[1]) - case 2: - return getDataForSinglePath(pools[2]) - default: - return "", "", uint32(0) - } -} - -// i256MinMax returns the absolute values of x and y in min-max order. -func i256MinMax(x, y *i256.Int) (min, max *u256.Uint) { - if x.Lt(y) || x.Eq(y) { - return x.Abs(), y.Abs() - } - return y.Abs(), x.Abs() -} - -// checkDeadline verifies that the transaction deadline has not passed. -func checkDeadline(deadline int64) error { - now := time.Now().Unix() - if now <= deadline { - return nil - } - - return ufmt.Errorf(errTxExpired, now, deadline) -} - -// splitSingleChar splits a string by a single character separator. -// This function is optimized for splitting strings with a single-byte separator -// and is more memory efficient than strings.Split for this use case. -func splitSingleChar(s string, sep byte) []string { - if s == "" { - return []string{""} - } - - result := make([]string, 0, bytes.Count([]byte(s), []byte{sep})+1) - start := 0 - for i := range s { - if s[i] == sep { - result = append(result, s[start:i]) - start = i + 1 - } - } - result = append(result, s[start:]) - return result -} - -// formatUint formats an unsigned integer to string. -func formatUint(v any) string { - switch v := v.(type) { - case uint8: - return strconv.FormatUint(uint64(v), 10) - case uint32: - return strconv.FormatUint(uint64(v), 10) - case uint64: - return strconv.FormatUint(v, 10) - default: - panic(ufmt.Sprintf("invalid type: %T", v)) - } -} - -// formatInt64 formats a signed integer to string. -func formatInt64(v any) string { - switch v := v.(type) { - case int8: - return strconv.FormatInt(int64(v), 10) - case int16: - return strconv.FormatInt(int64(v), 10) - case int32: - return strconv.FormatInt(int64(v), 10) - case int64: - return strconv.FormatInt(v, 10) - default: - panic(ufmt.Sprintf("invalid type %T", v)) - } -} - -// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. -// -// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds -// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. -// -// Parameters: -// - value (*u256.Uint): The unsigned 256-bit integer to be converted. -// -// Returns: -// - int64: The converted value if it falls within the int64 range. -// -// Panics: -// - If the `value` exceeds the range of int64, the function will panic with an error indicating -// the overflow and the original value. -func safeConvertToInt64(value *u256.Uint) int64 { - const INT64_MAX = 9223372036854775807 - const MAX_INT64 = "9223372036854775807" - - res, overflow := value.Uint64WithOverflow() - if overflow || res > uint64(INT64_MAX) { - panic(ufmt.Sprintf( - "amount(%s) overflows int64 range (max %s)", - value.ToString(), - MAX_INT64, - )) - } - return int64(res) -} diff --git a/contract/r/gnoswap/v1/router/wrap_unwrap.gno b/contract/r/gnoswap/v1/router/wrap_unwrap.gno deleted file mode 100644 index 85fa364..0000000 --- a/contract/r/gnoswap/v1/router/wrap_unwrap.gno +++ /dev/null @@ -1,98 +0,0 @@ -package router - -import ( - "std" - - "gno.land/r/gnoland/wugnot" - - "gno.land/p/nt/ufmt" -) - -const ( - UGNOT_MIN_DEPOSIT_TO_WRAP int64 = 1000 - WUGNOT_PATH = "gno.land/r/gnoland/wugnot" - GNOT = "gnot" - GNOT_DENOM = "ugnot" -) - -var ( - errFailedToWrapZeroUgnot = "cannot wrap 0 ugnot" - errFailedToWrapBelowMin = "amount(%d) < minimum(%d)" -) - -// wrapWithTransfer wraps GNOT into WUGNOT and transfers it to the specified address. -func wrapWithTransfer(toAddress std.Address, amount int64) error { - if amount <= 0 { - return nil - } - - if amount < UGNOT_MIN_DEPOSIT_TO_WRAP { - return makeErrorWithDetails( - errWugnotMinimum, - ufmt.Sprintf("amount(%d) < minimum(%d)", amount, UGNOT_MIN_DEPOSIT_TO_WRAP), - ) - } - - // transfer ugnot from fromAddress to current realm - currentRealmAddr := std.CurrentRealm().Address() - - sentCoins := std.OriginSend() - ugnotSent := sentCoins.AmountOf(GNOT_DENOM) - if ugnotSent != amount { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("user(%s) sent ugnot(%d) amount not equal to rewardAmount(%d)", toAddress.String(), ugnotSent, amount), - ) - } - - // wrap gnot to wugnot - wugnotAddr := std.DerivePkgAddr(WUGNOT_PATH) - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(currentRealmAddr, wugnotAddr, sentCoins) - wugnot.Deposit(cross) - - // if to address is not current realm, transfer wugnot to to address - if toAddress != currentRealmAddr { - wugnot.Transfer(cross, toAddress, amount) - } - - return nil -} - -// unwrapWithTransferFrom transfers WUGNOT from a source address, unwraps it to GNOT, and sends it to the target. -func unwrapWithTransferFrom(fromAddress, toAddress std.Address, wugnotAmount int64) error { - if wugnotAmount == 0 { - return nil - } - - currentRealmAddr := std.CurrentRealm().Address() - if fromAddress != currentRealmAddr { - wugnot.TransferFrom(cross, fromAddress, currentRealmAddr, wugnotAmount) - } - - wugnot.Withdraw(cross, wugnotAmount) - - sendCoins := std.Coins{{Denom: GNOT_DENOM, Amount: wugnotAmount}} - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(currentRealmAddr, toAddress, sendCoins) - - return nil -} - -// unwrapWithTransfer unwraps WUGNOT to GNOT and sends it to the specified address. -func unwrapWithTransfer(toAddress std.Address, amount int64) error { - if amount <= 0 { - return nil - } - - // unwrap wugnot to gnot - wugnot.Withdraw(cross, amount) - - // send gnot to user - sendCoins := std.Coins{{Denom: GNOT_DENOM, Amount: amount}} - banker := std.NewBanker(std.BankerTypeRealmSend) - currentRealmAddr := std.CurrentRealm().Address() - banker.SendCoins(currentRealmAddr, toAddress, sendCoins) - - return nil -} diff --git a/contract/r/gnoswap/v1/staker/README.md b/contract/r/gnoswap/v1/staker/README.md deleted file mode 100644 index 8935f93..0000000 --- a/contract/r/gnoswap/v1/staker/README.md +++ /dev/null @@ -1,182 +0,0 @@ -# Staker - -Liquidity mining and reward distribution for LP positions. - -## Overview - -Staker manages distribution of internal (GNS emission) and external (user-provided) rewards to staked LP positions, with time-weighted rewards and warmup periods. - -## Configuration - -- **Deposit GNS Amount**: 1,000 GNS for external incentives (default) -- **Minimum Reward Amount**: 1,000 tokens (default) -- **Unstaking Fee**: 1% (default) -- **Pool Tiers**: 1, 2, or 3 (assigned per pool) -- **Warmup Schedule**: 30/50/70/100% over 30/60/90 days -- **External Token Whitelist**: Approved reward tokens - -## Core Features - -### Internal Rewards (GNS Emission) -- Allocated to tiered pools (tiers 1, 2, 3) -- Split across tiers by TierRatio -- Distributed proportionally to in-range liquidity -- Unclaimed rewards go to community pool - -### External Rewards (User Incentives) -- Created for specific pools -- Constant reward per block -- Proportional to staked liquidity -- Unclaimed rewards returned to creator - -### Warmup Periods -Every staked position progresses through warmup periods: -- 0-30 days: 30% rewards (70% to community/creator) -- 30-60 days: 50% rewards (50% to community/creator) -- 60-90 days: 70% rewards (30% to community/creator) -- 90+ days: 100% rewards - -## Key Functions - -### `StakeToken` -Stakes LP position NFT to earn rewards. - -### `UnStakeToken` -Unstakes position and collects all rewards. - -### `CollectReward` -Collects accumulated rewards without unstaking. - -### `MintAndStake` -Mints new position and stakes in single transaction. - -### `CreateExternalIncentive` -Creates external reward program for specific pool. - -### `EndExternalIncentive` -Ends incentive program and returns unused rewards. - -## Reward Calculation Logic - -### Tier Ratio Distribution - -Emission split across tiers based on active pools: - -``` -If only tier 1 has pools: [100%, 0%, 0%] -If tiers 1 & 3 have pools: [80%, 0%, 20%] -If tiers 1 & 2 have pools: [70%, 30%, 0%] -If all tiers have pools: [50%, 30%, 20%] -``` - -Mathematical representation: -```math -TierRatio(t) = - [1, 0, 0] if Count(2) = 0 ∧ Count(3) = 0 - [0.8, 0, 0.2] if Count(2) = 0 - [0.7, 0.3, 0] if Count(3) = 0 - [0.5, 0.3, 0.2] otherwise -``` - -### Pool Reward Formula - -```math -poolReward(pool) = (emission × TierRatio[tier(pool)]) / Count(tier(pool)) -``` - -Where emission is calculated as: -```math -emission = GNSEmissionPerSecond × (avgMsPerBlock/1000) × StakerEmissionRatio -``` - -### Position Reward Calculation - -The reward for each position is calculated through: - -1. **Cache pool rewards** up to current block -2. **Retrieve position state** from deposit records -3. **Calculate internal rewards** if pool is tiered -4. **Calculate external rewards** for active incentives -5. **Apply warmup penalties** based on stake duration - -Mathematical formula for total reward ratio: -```math -TotalRewardRatio(s,e) = Σ[i=0 to m-1] ΔRaw(αᵢ, βᵢ) × rᵢ - -where: - αᵢ = max(s, Hᵢ₋₁) - βᵢ = min(e, Hᵢ) - -ΔRaw(a, b) = CalcRaw(b) - CalcRaw(a) - -CalcRaw(h) = - L(h) - U(h) if tick(h) < ℓ - U(h) - L(h) if tick(h) ≥ u - G(h) - (L(h) + U(h)) otherwise - -where: - L(h) = tickLower.OutsideAccumulation(h) - U(h) = tickUpper.OutsideAccumulation(h) - G(h) = globalRewardRatioAccumulation(h) - ℓ = tickLower.id - u = tickUpper.id -``` - -Final position reward: -```math -finalReward = TotalRewardRatio × poolReward × positionLiquidity - = ∫[s to e] (poolReward × positionLiquidity) / TotalStakedLiquidity(h) dh -``` - -### Tick Cross Hook - -When price crosses an initialized tick with staked positions: - -1. **Updates staked liquidity** - Adjusts total staked liquidity -2. **Updates reward accumulation** - Recalculates `globalRewardRatioAccumulation` -3. **Manages unclaimable periods** - Starts/ends periods with no in-range liquidity -4. **Updates tick accumulation** - Adjusts `CurrentOutsideAccumulation` - -The `globalRewardRatioAccumulation` tracks the integral: -```math -globalRewardRatioAccumulation = ∫ 1/TotalStakedLiquidity(h) dh -``` - -This integral is only computed when `TotalStakedLiquidity(h) ≠ 0`, enabling precise reward calculation even as liquidity changes. - -### Reward State Tracking - -The system maintains: -- **Global accumulation**: Tracks reward ratio across all positions -- **Tick accumulation**: Tracks rewards "outside" each tick -- **Position state**: Individual reward calculation parameters - -## Usage - -```go -// Stake existing position -StakeToken(123, "g1referrer...") - -// Create external incentive -CreateExternalIncentive( - "gno.land/r/demo/bar:gno.land/r/demo/baz:3000", - "gno.land/r/demo/reward", - "1000000000", // 1000 tokens - startTime, - endTime, -) - -// Collect rewards without unstaking -CollectReward(123) - -// Unstake and collect all rewards -UnStakeToken(123) -``` - -## Security - -- Positions locked during staking -- External incentives require GNS deposit -- Warmup periods prevent gaming -- Unclaimed rewards properly redirected -- Hook integration ensures accurate tracking \ No newline at end of file diff --git a/contract/r/gnoswap/v1/staker/api.gno b/contract/r/gnoswap/v1/staker/api.gno deleted file mode 100644 index 232d1f5..0000000 --- a/contract/r/gnoswap/v1/staker/api.gno +++ /dev/null @@ -1,310 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/p/onbloc/json" - "gno.land/p/nt/ufmt" -) - -// ApiGetRewardTokensByPoolPath returns all reward tokens for a specific pool. -func ApiGetRewardTokensByPoolPath(targetPoolPath string) string { - rewardTokens := []RewardToken{} - - pool, ok := pools.Get(targetPoolPath) - if !ok { - return "" - } - - thisPoolRewardTokens := []string{} - - // HANDLE INTERNAL - if poolTier.IsInternallyIncentivizedPool(pool.poolPath) { - thisPoolRewardTokens = append(thisPoolRewardTokens, GNS_PATH) - } - - // HANDLE EXTERNAL - if pool.IsExternallyIncentivizedPool() { - pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { - ictv := value.(*ExternalIncentive) - if ictv.RewardToken() == "" { - return false - } - thisPoolRewardTokens = append(thisPoolRewardTokens, ictv.RewardToken()) - return false - }) - } - - rt := newRewardToken(pool.poolPath, thisPoolRewardTokens) - rewardTokens = append(rewardTokens, rt) - - rsps := make([]JsonResponse, len(rewardTokens)) - for i := range rewardTokens { - rsps[i] = rewardTokens[i] - } - - return makeApiResponse(rsps) -} - -// ApiGetExternalIncentives returns all external incentives across all pools. -func ApiGetExternalIncentives() string { - apiExternalIncentives := []ApiExternalIncentive{} - - pools.tree.Iterate("", "", func(key string, value any) bool { - pool := value.(*Pool) - pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { - ictv := value.(*ExternalIncentive) - externalIctv := newApiExternalIncentive(ictv) - apiExternalIncentives = append(apiExternalIncentives, externalIctv) - return false - }) - return false - }) - - rsps := make([]JsonResponse, len(apiExternalIncentives)) - for i := range apiExternalIncentives { - rsps[i] = apiExternalIncentives[i] - } - - return makeApiResponse(rsps) -} - -// ApiGetExternalIncentiveById returns a specific external incentive by pool path and incentive ID. -func ApiGetExternalIncentiveById(poolPath, incentiveId string) string { - apiExternalIncentives := []ApiExternalIncentive{} - - pool, ok := pools.Get(poolPath) - if !ok { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("pool(%s) not found", poolPath), - )) - } - - incentive, exist := pool.incentives.GetByIncentiveId(incentiveId) - if !exist { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("incentive(%s) not found", incentiveId), - )) - } - - externalictv := newApiExternalIncentive(incentive) - apiExternalIncentives = append(apiExternalIncentives, externalictv) - - rsps := make([]JsonResponse, len(apiExternalIncentives)) - for i := range apiExternalIncentives { - rsps[i] = apiExternalIncentives[i] - } - - return makeApiResponse(rsps) -} - -// ApiGetExternalIncentivesByPoolPath returns all external incentives for a specific pool. -func ApiGetExternalIncentivesByPoolPath(targetPoolPath string) string { - apiExternalIncentives := []ApiExternalIncentive{} - - pool, ok := pools.Get(targetPoolPath) - if !ok { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("pool(%s) not found", targetPoolPath), - )) - } - - pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { - incentive, ok := value.(*ExternalIncentive) - if !ok { - panic("failed to cast value to *ExternalIncentive") - } - if incentive.targetPoolPath != targetPoolPath { - return false - } - - externalIctv := newApiExternalIncentive(incentive) - apiExternalIncentives = append(apiExternalIncentives, externalIctv) - - return false - }) - - rsps := make([]JsonResponse, len(apiExternalIncentives)) - for i := range apiExternalIncentives { - rsps[i] = apiExternalIncentives[i] - } - - return makeApiResponse(rsps) -} - -// ApiGetInternalIncentives returns all internal incentives across all pools. -func ApiGetInternalIncentives() string { - apiInternalIncentives := []ApiInternalIncentive{} - - poolTier.membership.Iterate("", "", func(key string, value any) bool { - poolPath := key - internalTier, ok := value.(uint64) - if !ok { - panic(ufmt.Sprintf("failed to cast value to uint64: %T", value)) - } - internalIctv := newApiInternalIncentive(poolPath, internalTier) - apiInternalIncentives = append(apiInternalIncentives, internalIctv) - return false - }) - - rsps := make([]JsonResponse, len(apiInternalIncentives)) - for i := range apiInternalIncentives { - rsps[i] = apiInternalIncentives[i] - } - - return makeApiResponse(rsps) -} - -// ApiGetInternalIncentivesByPoolPath returns internal incentives for a specific pool. -func ApiGetInternalIncentivesByPoolPath(targetPoolPath string) string { - apiInternalIncentives := []ApiInternalIncentive{} - - tier := poolTier.CurrentTier(targetPoolPath) - if tier == 0 { - return "" - } - - internalIctv := newApiInternalIncentive(targetPoolPath, tier) - apiInternalIncentives = append(apiInternalIncentives, internalIctv) - - rsps := make([]JsonResponse, len(apiInternalIncentives)) - for i := range apiInternalIncentives { - rsps[i] = apiInternalIncentives[i] - } - - return makeApiResponse(rsps) -} - -// ApiGetInternalIncentivesByTiers returns all internal incentives for a specific tier. -func ApiGetInternalIncentivesByTiers(targetTier uint64) string { - apiInternalIncentives := []ApiInternalIncentive{} - - poolTier.membership.Iterate("", "", func(key string, value any) bool { - poolPath := key - internalTier := value.(uint64) - if internalTier != targetTier { - return false - } - - internalIctv := newApiInternalIncentive(poolPath, internalTier) - apiInternalIncentives = append(apiInternalIncentives, internalIctv) - - return false - }) - - rsps := make([]JsonResponse, len(apiInternalIncentives)) - for i := range apiInternalIncentives { - rsps[i] = apiInternalIncentives[i] - } - - return makeApiResponse(rsps) -} - -// makeRewardTokensArray creates a JSON array of reward tokens. -func makeRewardTokensArray(rewardsTokenList []string) []*json.Node { - rewardsTokenArray := make([]*json.Node, len(rewardsTokenList)) - for i, rewardToken := range rewardsTokenList { - rewardsTokenArray[i] = json.StringNode("", rewardToken) - } - return rewardsTokenArray -} - -// calculateInternalRewardPerSecondByPoolPath calculates the internal reward per second for a pool. -func calculateInternalRewardPerSecondByPoolPath(poolPath string) string { - reward := poolTier.CurrentRewardPerPool(poolPath) - return ufmt.Sprintf("%d", reward) -} - -// ResponseQueryBase contains basic information about a query response. -type ResponseQueryBase struct { - Height int64 `json:"height"` // The block height at the time of the query - Timestamp int64 `json:"timestamp"` // The timestamp at the time of the query -} - -// ResponseApiGetRewards represents the API response for getting rewards. -type ResponseApiGetRewards struct { - Stat ResponseQueryBase `json:"stat"` // Basic query information - Response []LpTokenReward `json:"response"` // A slice of LpTokenReward structs -} - -// ResponseApiGetRewardByLpTokenId represents the API response for getting rewards for a specific LP token. -type ResponseApiGetRewardByLpTokenId struct { - Stat ResponseQueryBase `json:"stat"` // Basic query information - Response LpTokenReward `json:"response"` // The LpTokenReward for the specified LP token -} - -// ApiGetRewardsByLpTokenId returns all rewards for a specific LP token ID. -func ApiGetRewardsByLpTokenId(targetLpTokenId uint64) string { - deposit := deposits.get(targetLpTokenId) - - reward := calcPositionReward(std.ChainHeight(), time.Now().Unix(), targetLpTokenId) - - rewards := []ApiReward{} - - if reward.Internal > 0 { - rewards = append(rewards, ApiReward{ - IncentiveType: "INTERNAL", - IncentiveId: "", - TargetPoolPath: deposit.targetPoolPath, - RewardTokenPath: GNS_PATH, - RewardTokenAmount: reward.Internal, - StakeTimestamp: deposit.stakeTimestamp, - StakeTime: deposit.stakeTime, - IncentiveStart: deposit.stakeTimestamp, - }) - } - - for incentiveId, externalReward := range reward.External { - if externalReward == 0 { - continue - } - incentive := externalIncentives.get(incentiveId) - rewards = append(rewards, ApiReward{ - IncentiveType: "EXTERNAL", - IncentiveId: incentiveId, - TargetPoolPath: incentive.targetPoolPath, - RewardTokenPath: incentive.rewardToken, - RewardTokenAmount: externalReward, - StakeTimestamp: deposit.stakeTimestamp, - StakeTime: deposit.stakeTime, - IncentiveStart: incentive.startTimestamp, - }) - } - - rsps := make([]JsonResponse, len(rewards)) - for i := range rewards { - rsps[i] = rewards[i] - } - - return makeApiResponse(rsps) -} - -// ApiGetStakesByLpTokenId returns stake information for a specific LP token ID. -func ApiGetStakesByLpTokenId(targetLpTokenId uint64) string { - stakes := []ApiStake{} - - deposit := deposits.get(targetLpTokenId) - stk := newApiStake(targetLpTokenId, deposit) - stakes = append(stakes, stk) - - rsps := make([]JsonResponse, len(stakes)) - for i := range stakes { - rsps[i] = stakes[i] - } - - return makeApiResponse(rsps) -} - -// IsStaked checks if a position ID is currently staked. -func IsStaked(positionId uint64) bool { - return deposits.Has(positionId) -} - -// formatInt formats an int64 value to string. -func formatInt(value int64) string { - return ufmt.Sprintf("%d", value) -} diff --git a/contract/r/gnoswap/v1/staker/assert.gno b/contract/r/gnoswap/v1/staker/assert.gno deleted file mode 100644 index 57188ee..0000000 --- a/contract/r/gnoswap/v1/staker/assert.gno +++ /dev/null @@ -1,196 +0,0 @@ -package staker - -import ( - "std" - "strconv" - "strings" - "time" - - "gno.land/p/nt/ufmt" - "gno.land/r/gnoswap/v1/gnft" - pl "gno.land/r/gnoswap/v1/pool" -) - -// assertIsValidAmount ensures the amount is non-negative. -func assertIsValidAmount(amount int64) { - if amount < 0 { - panic(makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("amount(%d) must be positive", amount), - )) - } -} - -// assertIsValidRewardAmountFormat ensures the reward amount string is formatted as "tokenPath:amount". -func assertIsValidRewardAmountFormat(rewardAmountStr string) { - parts := strings.SplitN(rewardAmountStr, ":", 2) - if len(parts) != 2 { - panic(makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("invalid format for SetTokenMinimumRewardAmount params: expected 'tokenPath:amount', got '%s'", rewardAmountStr), - )) - } -} - -// assertIsDepositor ensures the caller is the owner of the deposit. -func assertIsDepositor(caller std.Address, positionId uint64) { - deposit := deposits.get(positionId) - if deposit == nil { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("positionId(%d) not found", positionId), - )) - } - - if caller != deposit.owner { - panic(makeErrorWithDetails( - errNoPermission, - ufmt.Sprintf("caller(%s) is not depositor(%s)", caller.String(), deposit.owner.String()), - )) - } -} - -// assertIsNotStaked ensures the position is not already staked. -func assertIsNotStaked(positionId uint64) { - if deposits.Has(positionId) { - panic(makeErrorWithDetails( - errAlreadyStaked, - ufmt.Sprintf("positionId(%d) already staked", positionId), - )) - } -} - -// assertIsPositionOwner ensures the caller owns the position NFT. -func assertIsPositionOwner(positionId uint64, caller std.Address) { - owner := gnft.MustOwnerOf(positionIdFrom(positionId)) - if owner != caller { - panic(makeErrorWithDetails( - errNoPermission, - ufmt.Sprintf("caller(%s) is not owner(%s)", caller.String(), owner.String()), - )) - } -} - -// assertIsPoolExists ensures the pool exists. -func assertIsPoolExists(poolPath string) { - if !pl.ExistsPoolPath(poolPath) { - panic(makeErrorWithDetails( - errInvalidPoolPath, - ufmt.Sprintf("pool(%s) does not exist", poolPath), - )) - } -} - -// assertIsValidPoolTier ensures the tier is within valid range. -func assertIsValidPoolTier(tier uint64) { - if tier >= AllTierCount { - panic(makeErrorWithDetails( - errInvalidPoolTier, - ufmt.Sprintf("tier(%d) must be less than %d", tier, AllTierCount), - )) - } -} - -// assertIsGreaterThanMinimumRewardAmount ensures the reward amount meets minimum requirements. -func assertIsGreaterThanMinimumRewardAmount(rewardToken string, rewardAmount int64) { - minReward := GetMinimumRewardAmountForToken(rewardToken) - if rewardAmount < minReward { - panic(makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("rewardAmount(%d) is less than minimum required amount(%d)", rewardAmount, minReward), - )) - } -} - -// assertIsAllowedForExternalReward ensures the token is allowed for external rewards. -func assertIsAllowedForExternalReward(poolPath, tokenPath string) { - token0, token1, _ := poolPathDivide(poolPath) - - if tokenPath == token0 || tokenPath == token1 { - return - } - - allowed := contains(allowedTokens, tokenPath) - if allowed { - return - } - - panic(makeErrorWithDetails( - errNotAllowedForExternalReward, - ufmt.Sprintf("tokenPath(%s) is not allowed for external reward for poolPath(%s)", tokenPath, poolPath), - )) -} - -// assertIsValidFeeRate ensures the fee rate is within valid range (0-10000 basis points). -func assertIsValidFeeRate(fee int64) { - if fee < 0 || fee > 10000 { - panic(makeErrorWithDetails( - errInvalidUnstakingFee, - ufmt.Sprintf("fee(%d) must be in range 0 ~ 10000", fee), - )) - } -} - -// assertIsValidIncentiveStartTime ensures the incentive starts at midnight of a future date. -func assertIsValidIncentiveStartTime(startTimestamp int64) { - // must be in seconds format, not milliseconds - // REF: https://stackoverflow.com/a/23982005 - numStr := strconv.Itoa(int(startTimestamp)) - - if len(numStr) >= 13 { - panic(makeErrorWithDetails( - errInvalidIncentiveStartTime, - ufmt.Sprintf("startTimestamp(%d) must be in seconds format, not milliseconds", startTimestamp), - )) - } - - // must be at least +1 day midnight - tomorrowMidnight := time.Now().AddDate(0, 0, 1).Truncate(24 * time.Hour).Unix() - if startTimestamp < tomorrowMidnight { - panic(makeErrorWithDetails( - errInvalidIncentiveStartTime, - ufmt.Sprintf("startTimestamp(%d) must be at least +1 day midnight(%d)", startTimestamp, tomorrowMidnight), - )) - } - - // must be midnight of the day - startTime := time.Unix(startTimestamp, 0) - if !isMidnight(startTime) { - panic(makeErrorWithDetails( - errInvalidIncentiveStartTime, - ufmt.Sprintf("startTime(%d = %s) must be midnight of the day", startTimestamp, startTime.String()), - )) - } -} - -// assertIsValidIncentiveEndTime ensures the end timestamp is within valid epoch range. -func assertIsValidIncentiveEndTime(endTimestamp int64) { - if endTimestamp >= MAX_UNIX_EPOCH_TIME { - panic(makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("endTimestamp(%d) cannot be later than 253402300799 (9999-12-31 23:59:59)", endTimestamp), - )) - } -} - -// assertIsValidIncentiveDuration ensures the duration is 90, 180, or 365 days. -func assertIsValidIncentiveDuration(externalDuration int64) { - switch externalDuration { - case TIMESTAMP_90DAYS, TIMESTAMP_180DAYS, TIMESTAMP_365DAYS: - return - } - - panic(makeErrorWithDetails( - errInvalidIncentiveDuration, - ufmt.Sprintf("externalDuration(%d) must be 90, 180, 365 days", externalDuration), - )) -} - -// isMidnight checks if a time represents midnight (00:00:00). -func isMidnight(startTime time.Time) bool { - hour := startTime.Hour() - minute := startTime.Minute() - second := startTime.Second() - - return hour == 0 && minute == 0 && second == 0 -} diff --git a/contract/r/gnoswap/v1/staker/calculate_pool_position_reward.gno b/contract/r/gnoswap/v1/staker/calculate_pool_position_reward.gno deleted file mode 100644 index dc10f54..0000000 --- a/contract/r/gnoswap/v1/staker/calculate_pool_position_reward.gno +++ /dev/null @@ -1,153 +0,0 @@ -package staker - -import ( - u256 "gno.land/p/gnoswap/uint256" -) - -// Q96 -var _q96 = u256.MustFromDecimal("79228162514264337593543950336") - -func isAbleToCalculateEmissionReward(prev int64, current int64) bool { - if prev >= current { - return false - } - return true -} - -// Reward is a struct for storing reward for a position. -// Internal reward is the GNS reward, external reward is the reward for other incentives. -// Penalties are the amount that is deducted from the reward due to the position's warmup. -type Reward struct { - Internal int64 - InternalPenalty int64 - External map[string]int64 // Incentive ID -> TokenAmount - ExternalPenalty map[string]int64 // Incentive ID -> TokenAmount -} - -// calculate total position rewards and penalties -func calcPositionReward(currentHeight, currentTimestamp int64, positionId uint64) Reward { - rewards := calculatePositionReward(CalcPositionRewardParam{ - CurrentHeight: currentHeight, - CurrentTime: currentTimestamp, - Deposits: deposits, - Pools: pools, - PoolTier: poolTier, - PositionId: positionId, - }) - - internal := int64(0) - for _, reward := range rewards { - internal += reward.Internal - } - - internalPenalty := int64(0) - for _, reward := range rewards { - internalPenalty += reward.InternalPenalty - } - - externalReward := make(map[string]int64) - for _, reward := range rewards { - if reward.External != nil { - for incentive, reward := range reward.External { - externalReward[incentive] += reward - } - } - } - - externalPenalty := make(map[string]int64) - for _, reward := range rewards { - if reward.ExternalPenalty != nil { - for incentive, penalty := range reward.ExternalPenalty { - externalPenalty[incentive] += penalty - } - } - } - - return Reward{ - Internal: internal, - InternalPenalty: internalPenalty, - External: externalReward, - ExternalPenalty: externalPenalty, - } -} - -// CalcPositionRewardParam is a struct for calculating position reward -type CalcPositionRewardParam struct { - // Environmental variables - CurrentHeight int64 - CurrentTime int64 - Deposits *Deposits - Pools *Pools - PoolTier *PoolTier - - // Position variables - PositionId uint64 -} - -func calculatePositionReward(param CalcPositionRewardParam) []Reward { - // cache per-pool rewards in the internal incentive(tiers) - param.PoolTier.cacheReward(param.CurrentHeight, param.CurrentTime, param.Pools) - - deposit := param.Deposits.get(param.PositionId) - poolPath := deposit.targetPoolPath - - pool, ok := param.Pools.Get(poolPath) - if !ok { - pool = NewPool(poolPath, param.CurrentTime) - param.Pools.set(poolPath, pool) - } - - lastCollectTime := deposit.lastCollectTime - - // Initializes reward/penalty arrays for rewards and penalties for each warmup - internalRewards := make([]int64, len(deposit.warmups)) - internalPenalties := make([]int64, len(deposit.warmups)) - externalRewards := make([]map[string]int64, len(deposit.warmups)) - externalPenalties := make([]map[string]int64, len(deposit.warmups)) - - if param.PoolTier.CurrentTier(poolPath) != 0 { - // Internal incentivized pool. - // Calculate reward for each warmup - internalRewards, internalPenalties = pool.RewardStateOf(deposit).calculateInternalReward(lastCollectTime, param.CurrentTime) - } - - // All active incentives - allIncentives := pool.incentives.GetAllInTimestamps(lastCollectTime, param.CurrentTime) - - for i := range externalRewards { - externalRewards[i] = make(map[string]int64) - externalPenalties[i] = make(map[string]int64) - } - - for incentiveId, incentive := range allIncentives { - // External incentivized pool. - // Calculate reward for each warmup - externalReward, externalPenalty := pool.RewardStateOf(deposit).calculateExternalReward(lastCollectTime, param.CurrentTime, incentive) - - for i := range externalReward { - externalRewards[i][incentiveId] = externalReward[i] - externalPenalties[i][incentiveId] = externalPenalty[i] - } - } - - rewards := make([]Reward, len(internalRewards)) - for i := range internalRewards { - rewards[i] = Reward{ - Internal: internalRewards[i], - InternalPenalty: internalPenalties[i], - External: externalRewards[i], - ExternalPenalty: externalPenalties[i], - } - } - - return rewards -} - -// calculates internal unclaimable reward for the pool -func processUnClaimableReward(poolPath string, endTimestamp int64) int64 { - pool, ok := pools.Get(poolPath) - if !ok { - return 0 - } - return pool.processUnclaimableReward(poolTier, endTimestamp) -} diff --git a/contract/r/gnoswap/v1/staker/consts.gno b/contract/r/gnoswap/v1/staker/consts.gno deleted file mode 100644 index 1c31cef..0000000 --- a/contract/r/gnoswap/v1/staker/consts.gno +++ /dev/null @@ -1,13 +0,0 @@ -package staker - -// WRAP & UNWRAP -const ( - GNOT string = "gnot" - GNOT_DENOM string = "ugnot" - - // ref: https://github.com/gnolang/gno/blob/81a88a2976ba9f2f9127ebbe7fb7d1e1f7fa4bd4/examples/gno.land/r/gnoland/wugnot/wugnot.gno#L19 - UGNOT_MIN_DEPOSIT_TO_WRAP int64 = 1000 - - GNS_PATH string = "gno.land/r/gnoswap/gns" - WUGNOT_PATH string = "gno.land/r/gnoland/wugnot" -) diff --git a/contract/r/gnoswap/v1/staker/counter.gno b/contract/r/gnoswap/v1/staker/counter.gno deleted file mode 100644 index 998d7e1..0000000 --- a/contract/r/gnoswap/v1/staker/counter.gno +++ /dev/null @@ -1,21 +0,0 @@ -package staker - -type Counter struct { - id int64 -} - -func NewCounter() *Counter { - return &Counter{ - id: 0, - } -} - -func (c *Counter) next() int64 { - c.id++ - - return c.id -} - -func (c *Counter) Get() int64 { - return c.id -} diff --git a/contract/r/gnoswap/v1/staker/doc.gno b/contract/r/gnoswap/v1/staker/doc.gno deleted file mode 100644 index 15fe6bd..0000000 --- a/contract/r/gnoswap/v1/staker/doc.gno +++ /dev/null @@ -1,9 +0,0 @@ -// Package staker manages liquidity mining rewards for GnoSwap positions. -// -// The staker distributes GNS emissions and external incentives to liquidity -// providers based on their position size, price range, and staking duration. -// It supports both internal GNS rewards and external token incentives. -// -// Rewards are calculated per-tick and accumulate over time, with automatic -// compounding and fee collection integration. -package staker diff --git a/contract/r/gnoswap/v1/staker/errors.gno b/contract/r/gnoswap/v1/staker/errors.gno deleted file mode 100644 index ae05b89..0000000 --- a/contract/r/gnoswap/v1/staker/errors.gno +++ /dev/null @@ -1,48 +0,0 @@ -package staker - -import ( - "errors" - - "gno.land/p/nt/ufmt" -) - -var ( - errNoPermission = errors.New("[GNOSWAP-STAKER-001] caller has no permission") - errPoolNotFound = errors.New("[GNOSWAP-STAKER-002] pool not found") - errAlreadyRegistered = errors.New("[GNOSWAP-STAKER-003] already registered token") - errInsufficientReward = errors.New("[GNOSWAP-STAKER-004] insufficient reward") - errWrapUnwrap = errors.New("[GNOSWAP-STAKER-005] wrap, unwrap failed") - errWugnotMinimum = errors.New("[GNOSWAP-STAKER-006] can not wrapless than minimum amount") - errInvalidInput = errors.New("[GNOSWAP-STAKER-007] invalid input data") - errInvalidUnstakingFee = errors.New("[GNOSWAP-STAKER-008] invalid unstaking fee") - errAlreadyStaked = errors.New("[GNOSWAP-STAKER-009] already staked position") - errNonIncentivizedPool = errors.New("[GNOSWAP-STAKER-010] pool is not incentivized") - errOutOfRange = errors.New("[GNOSWAP-STAKER-011] out of range") - errCannotEndIncentive = errors.New("[GNOSWAP-STAKER-012] can not end incentive") - errInvalidIncentiveStartTime = errors.New("[GNOSWAP-STAKER-013] invalid incentive start time") - errInvalidIncentiveEndTime = errors.New("[GNOSWAP-STAKER-014] invalid incentive end time") - errCannotUseForExternalReward = errors.New("[GNOSWAP-STAKER-015] can not use for external reward") - errMinTier = errors.New("[GNOSWAP-STAKER-016] emission minimum tier is 1") - errDefaultPoolTier1 = errors.New("[GNOSWAP-STAKER-017] can not delete default pool tier 1") - errDefaultExternalToken = errors.New("[GNOSWAP-STAKER-018] can not delete default external token") - errInvalidPoolPath = errors.New("[GNOSWAP-STAKER-019] invalid pool path") - errInvalidPoolTier = errors.New("[GNOSWAP-STAKER-020] invalid pool tier") - errAlreadyHasTier = errors.New("[GNOSWAP-STAKER-021] pool already has emission target") - errDataNotFound = errors.New("[GNOSWAP-STAKER-022] requested data not found") - errCalculationError = errors.New("[GNOSWAP-STAKER-023] unexpected calculation error") - errZeroLiquidity = errors.New("[GNOSWAP-STAKER-024] zero liquidity") - errInvalidIncentiveDuration = errors.New("[GNOSWAP-STAKER-025] invalid incentive duration") - errNotAllowedForExternalReward = errors.New("[GNOSWAP-STAKER-026] not allowed for external reward") - errInvalidWarmUpPercent = errors.New("[GNOSWAP-STAKER-027] invalid warm-up duration") - errInvalidTickCross = errors.New("[GNOSWAP-STAKER-028] invalid tick cross") - errIncentiveAlreadyExists = errors.New("[GNOSWAP-STAKER-029] incentive already exists") - errIncentiveNotFound = errors.New("[GNOSWAP-STAKER-030] incentive not found") - errWarmUpAmountNotFound = errors.New("[GNOSWAP-STAKER-031] warm-up amount not found") - errOverflow = errors.New("[GNOSWAP-STAKER-032] overflow") - errUnauthorized = errors.New("[GNOSWAP-STAKER-033] unauthorized access") - errAddExistingToken = errors.New("[GNOSWAP-STAKER-034] can not add existing token") -) - -func makeErrorWithDetails(err error, details string) error { - return ufmt.Errorf("%s || %s", err.Error(), details) -} diff --git a/contract/r/gnoswap/v1/staker/external_deposit_fee.gno b/contract/r/gnoswap/v1/staker/external_deposit_fee.gno deleted file mode 100644 index 9550634..0000000 --- a/contract/r/gnoswap/v1/staker/external_deposit_fee.gno +++ /dev/null @@ -1,173 +0,0 @@ -package staker - -import ( - "std" - "strconv" - "strings" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" -) - -var ( - // depositGnsAmount is the amount of GNS required to create an external incentive. - // This parameter can be modified through governance. - depositGnsAmount = int64(1_000_000_000) // 1_000 GNS - - // minimumRewardAmount is the default minimum reward amount for external incentives. - // This parameter can be modified through governance. - minimumRewardAmount = int64(1_000_000_000) // Default 1000 (ugnot equivalent for GNS) -) - -// tokenSpecificMinimumRewards stores minimum reward amounts for specific tokens. -// Key: tokenPath (string), Value: minimumAmount (int64) -var tokenSpecificMinimumRewards = avl.NewTree() - -// GetDepositGnsAmount returns the current deposit amount in GNS. -func GetDepositGnsAmount() int64 { - return depositGnsAmount -} - -// GetMinimumRewardAmount returns the default minimum reward amount required for external incentives. -func GetMinimumRewardAmount() int64 { - return minimumRewardAmount -} - -// GetMinimumRewardAmountForToken returns the minimum reward amount for a specific token. -func GetMinimumRewardAmountForToken(tokenPath string) int64 { - amountI, found := tokenSpecificMinimumRewards.Get(tokenPath) - if found { - return amountI.(int64) - } - // Fallback to default if not found - return GetMinimumRewardAmount() -} - -// GetSpecificTokenMinimumRewardAmount returns the explicitly set minimum reward amount for a token. -func GetSpecificTokenMinimumRewardAmount(tokenPath string) (int64, bool) { - amountI, found := tokenSpecificMinimumRewards.Get(tokenPath) - if !found { - return 0, false - } - v, ok := amountI.(int64) - if !ok { - panic("failed to cast amount to int64") - } - return v, true -} - -// SetDepositGnsAmount sets the GNS deposit amount required for creating external incentives. -// Only admin or governance can call this function. -func SetDepositGnsAmount(cur realm, amount int64) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsValidAmount(amount) - - prevDepositGnsAmount := getDepositGnsAmount() - setDepositGnsAmount(amount) - - previousRealm := std.PreviousRealm() - std.Emit( - "SetDepositGnsAmount", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "prevAmount", formatInt(prevDepositGnsAmount), - "newAmount", formatInt(amount), - ) -} - -// SetMinimumRewardAmount sets the default minimum reward amount for external incentives. -// Only admin or governance can call this function. -func SetMinimumRewardAmount(cur realm, amount int64) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsValidAmount(amount) - - prevMinimumRewardAmount := getMinimumRewardAmount() - setMinimumRewardAmount(amount) - - previousRealm := std.PreviousRealm() - std.Emit( - "SetMinimumRewardAmount", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "prevAmount", formatInt(prevMinimumRewardAmount), - "newAmount", formatInt(amount), - ) -} - -// SetTokenMinimumRewardAmount sets the minimum reward amount for a specific token. -// Only admin or governance can call this function. -func SetTokenMinimumRewardAmount(cur realm, paramsStr string) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsValidRewardAmountFormat(paramsStr) - - // Parse the paramsStr - parts := strings.SplitN(paramsStr, ":", 2) - tokenPath := parts[0] - amountStr := parts[1] - amount64, err := strconv.ParseInt(amountStr, 10, 64) - if err != nil { - panic(makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("invalid amount format in params '%s': %v", paramsStr, err), - )) - } - - prevAmount, found := GetSpecificTokenMinimumRewardAmount(tokenPath) - - // If amount is 0, remove the entry; otherwise, set it. - if amount64 == 0 { - // Only attempt removal if an entry actually existed - if found { - tokenSpecificMinimumRewards.Remove(tokenPath) - } - } else { - tokenSpecificMinimumRewards.Set(tokenPath, amount64) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "SetTokenMinimumRewardAmount", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "paramsStr", paramsStr, // Log the raw input string - "tokenPath", tokenPath, - "prevAmountFound", formatBool(found), - "prevAmount", formatInt(prevAmount), // Will be 0 if !found - "newAmount", formatInt(amount64), - ) -} - -// setDepositGnsAmount internally updates the deposit GNS amount. -func setDepositGnsAmount(amount int64) { - depositGnsAmount = amount -} - -// setMinimumRewardAmount internally updates the minimum reward amount. -func setMinimumRewardAmount(amount int64) { - minimumRewardAmount = amount -} - -// getDepositGnsAmount internally retrieves the deposit GNS amount. -func getDepositGnsAmount() int64 { - return depositGnsAmount -} - -// getMinimumRewardAmount internally retrieves the minimum reward amount. -func getMinimumRewardAmount() int64 { - return minimumRewardAmount -} diff --git a/contract/r/gnoswap/v1/staker/external_incentive.gno b/contract/r/gnoswap/v1/staker/external_incentive.gno deleted file mode 100644 index 00df855..0000000 --- a/contract/r/gnoswap/v1/staker/external_incentive.gno +++ /dev/null @@ -1,224 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/p/nt/ufmt" - prbac "gno.land/p/gnoswap/rbac" - - "gno.land/r/gnoswap/access" - en "gno.land/r/gnoswap/emission" - "gno.land/r/gnoswap/gns" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" -) - -// CreateExternalIncentive creates an external incentive program for a pool. -// -// Parameters: -// - targetPoolPath: pool to incentivize -// - rewardToken: reward token path -// - rewardAmount: total reward amount -// - startTimestamp, endTimestamp: incentive period -// -// Only callable by users. -func CreateExternalIncentive( - cur realm, - targetPoolPath string, - rewardToken string, // token path should be registered - rewardAmount int64, - startTimestamp int64, - endTimestamp int64, -) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsUser(std.PreviousRealm()) - - assertIsPoolExists(targetPoolPath) - assertIsGreaterThanMinimumRewardAmount(rewardToken, rewardAmount) - assertIsAllowedForExternalReward(targetPoolPath, rewardToken) - assertIsValidIncentiveStartTime(startTimestamp) - assertIsValidIncentiveEndTime(endTimestamp) - assertIsValidIncentiveDuration(endTimestamp - startTimestamp) - - en.MintAndDistributeGns(cross) - - // transfer reward token from user to staker - if rewardToken == GNOT { - rewardToken = WUGNOT_PATH - err := wrapWithTransfer(stakerAddr, rewardAmount) - if err != nil { - panic(err) - } - } else { - err := common.TransferFrom(cross, rewardToken, caller, stakerAddr, rewardAmount) - if err != nil { - panic(err) - } - } - - // deposit gns amount - gns.TransferFrom(cross, caller, stakerAddr, depositGnsAmount) - - currentTime := time.Now().Unix() - currentHeight := std.ChainHeight() - incentiveId := nextIncentiveID(caller, currentTime) - pool := pools.GetOrCreate(targetPoolPath) - - incentive := NewExternalIncentive( - incentiveId, - targetPoolPath, - rewardToken, - rewardAmount, - startTimestamp, - endTimestamp, - caller, - currentHeight, - depositGnsAmount, - currentTime, - ) - - if externalIncentives.Has(incentiveId) { - panic(makeErrorWithDetails( - errIncentiveAlreadyExists, - ufmt.Sprintf("incentiveId(%s)", incentiveId), - )) - } - // store external incentive information for each incentiveId - externalIncentives.set(incentiveId, incentive) - - pool.incentives.create(caller, incentive) - - previousRealm := std.PreviousRealm() - std.Emit( - "CreateExternalIncentive", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "incentiveId", incentiveId, - "targetPoolPath", targetPoolPath, - "rewardToken", rewardToken, - "rewardAmount", formatInt(rewardAmount), - "startTimestamp", formatInt(startTimestamp), - "endTimestamp", formatInt(endTimestamp), - "depositGnsAmount", formatInt(depositGnsAmount), - "currentHeight", formatInt(currentHeight), - "currentTime", formatInt(currentTime), - ) -} - -// EndExternalIncentive ends an external incentive and refunds remaining rewards. -// -// Finalizes incentive program after end timestamp. -// Returns unallocated rewards and GNS deposit. -// Calculates unclaimable rewards for refund. -// -// Parameters: -// - targetPoolPath: Pool with the incentive -// - incentiveId: Unique incentive identifier -// -// Process: -// 1. Validates incentive end time reached -// 2. Calculates remaining and unclaimable rewards -// 3. Refunds rewards to original creator -// 4. Returns 100 GNS deposit -// 5. Removes incentive from active list -// -// Only callable by Refundee or Admin. -func EndExternalIncentive(cur realm, targetPoolPath, incentiveId string) { - halt.AssertIsNotHaltedStaker() - halt.AssertIsNotHaltedWithdraw() - - assertIsPoolExists(targetPoolPath) - - pool, exists := pools.Get(targetPoolPath) - if !exists { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("targetPoolPath(%s) does not exist", targetPoolPath), - )) - } - - caller := std.PreviousRealm().Address() - currentTime := time.Now().Unix() - incentive, refund, err := endExternalIncentive(pool, incentiveId, caller, currentTime) - if err != nil { - panic(err) - } - - poolLeftExternalRewardAmount := common.BalanceOf(incentive.rewardToken, stakerAddr) - if poolLeftExternalRewardAmount < refund { - refund = poolLeftExternalRewardAmount - } - - // unwrap if wugnot - isUnwrap := incentive.rewardToken == WUGNOT_PATH - if isUnwrap { - err = unwrapWithTransfer(incentive.refundee, refund) - } else { - err = common.Transfer(cross, incentive.rewardToken, incentive.refundee, refund) - } - - if err != nil { - panic(err) - } - - // also refund deposit gns amount - gns.Transfer(cross, incentive.refundee, incentive.depositGnsAmount) - - pool.incentives.update(incentive.refundee, incentive) - - previousRealm := std.PreviousRealm() - std.Emit( - "EndExternalIncentive", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "incentiveId", incentiveId, - "targetPoolPath", targetPoolPath, - "refundee", incentive.refundee.String(), - "refundToken", incentive.rewardToken, - "refundAmount", formatInt(refund), - "refundGnsAmount", formatInt(incentive.depositGnsAmount), - "isRequestUnwrap", formatBool(incentive.rewardToken == WUGNOT_PATH), - "externalIncentiveEndBy", previousRealm.Address().String(), - ) -} - -// endExternalIncentive processes the end of an external incentive program. -func endExternalIncentive(pool *Pool, incentiveId string, caller std.Address, currentTime int64) (*ExternalIncentive, int64, error) { - incentive, exists := pool.incentives.Get(incentiveId) - if !exists { - return nil, 0, makeErrorWithDetails( - errCannotEndIncentive, - ufmt.Sprintf("cannot end non existent incentive(%s)", incentiveId), - ) - } - - if currentTime < incentive.endTimestamp { - return nil, 0, makeErrorWithDetails( - errCannotEndIncentive, - ufmt.Sprintf("cannot end incentive before endTime(%d), current(%d)", incentive.endTimestamp, currentTime), - ) - } - - // only refundee or admin can end incentive - if !access.IsAuthorized(prbac.ROLE_ADMIN.String(), caller) && caller != incentive.refundee { - return nil, 0, makeErrorWithDetails( - errNoPermission, - ufmt.Sprintf( - "only refundee(%s) or admin(%s) can end incentive, but called from %s", - incentive.refundee, adminAddr.String(), caller, - ), - ) - } - - refund := int64(incentive.rewardLeft) - - if !incentive.unclaimableRefunded { - refund += int64(pool.incentives.calculateUnclaimableReward(incentive.incentiveId)) - incentive.setUnClaimableRefunded(true) - } - - return incentive, refund, nil -} diff --git a/contract/r/gnoswap/v1/staker/external_token_list.gno b/contract/r/gnoswap/v1/staker/external_token_list.gno deleted file mode 100644 index efdbd5d..0000000 --- a/contract/r/gnoswap/v1/staker/external_token_list.gno +++ /dev/null @@ -1,132 +0,0 @@ -package staker - -import ( - "std" - - "gno.land/p/nt/ufmt" - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" -) - -// defaultAllowed is the list of default allowed tokens to create external incentive -var defaultAllowed = []string{GNS_PATH, GNOT} - -// allowedTokens is a slice of all allowed token paths, including the default and added tokens. -var allowedTokens = make([]string, 0, len(defaultAllowed)) - -func init() { - allowedTokens = defaultAllowed -} - -// AddToken adds a new token path to the list of allowed tokens -// Only the admin can add a new token. -// -// Parameters: -// - tokenPath (string): The path of the token to add -// -// Panics: -// - If the caller is not the admin -func AddToken(cur realm, tokenPath string) { - caller := std.PreviousRealm().Address() - halt.AssertIsNotHaltedStaker() - access.AssertIsAdminOrGovernance(caller) - - if err := modifyTokenList(tokenPath, addTokenValidator, addTokenExecutor); err != nil { - panic(err.Error()) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "AddToken", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "tokenPath", tokenPath, - ) -} - -// RemoveToken removes a token path from the list of allowed tokens. -// Only the admin can remove a token. -// -// Default tokens can not be removed. -// -// Parameters: -// - tokenPath (string): The path of the token to remove -// -// Panics: -// - If the caller is not the admin -func RemoveToken(cur realm, tokenPath string) { - caller := std.PreviousRealm().Address() - halt.AssertIsNotHaltedStaker() - access.AssertIsAdminOrGovernance(caller) - - if err := modifyTokenList(tokenPath, removeTokenValidator, removeTokenExecutor); err != nil { - panic(err.Error()) - } - - previousRealm := std.PreviousRealm() - std.Emit( - "RemoveToken", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "tokenPath", tokenPath, - ) -} - -// TokenValidator is a function type that validates a token -type TokenValidator func(tokenPath string) error - -// TokenExecutor is a token manipulation function type -type TokenExecutor func(tokenPath string, tokens []string) []string - -// modifyTokenList handles common token modification logics, such as admin check, validation, and execution. -func modifyTokenList(tokenPath string, validator TokenValidator, executor TokenExecutor) error { - // validate token operation if validator is provided - if validator != nil { - if err := validator(tokenPath); err != nil { - return err - } - } - - allowedTokens = executor(tokenPath, allowedTokens) - return nil -} - -// addTokenExecutor executes token append operation -func addTokenExecutor(tokenPath string, tokens []string) []string { - if contains(tokens, tokenPath) { - return tokens - } - - return append(tokens, tokenPath) -} - -// addTokenValidator validates token addition operation -func addTokenValidator(tokenPath string) error { - if contains(defaultAllowed, tokenPath) { - return ufmt.Errorf("%v: can not add existing token(%s)", errAddExistingToken, tokenPath) - } - - return nil -} - -// removeTokenExecutor executes token removal operation -func removeTokenExecutor(tokenPath string, tokens []string) []string { - // find and remove token - for i, t := range tokens { - if t == tokenPath { - return append(tokens[:i], tokens[i+1:]...) - } - } - - // if token not found, return the original list - return tokens -} - -// removeTokenValidator validates token removal operation -func removeTokenValidator(tokenPath string) error { - if contains(defaultAllowed, tokenPath) { - return ufmt.Errorf("%v: can not remove default token(%s)", errDefaultExternalToken, tokenPath) - } - - return nil -} diff --git a/contract/r/gnoswap/v1/staker/getter.gno b/contract/r/gnoswap/v1/staker/getter.gno deleted file mode 100644 index 7b56e2f..0000000 --- a/contract/r/gnoswap/v1/staker/getter.gno +++ /dev/null @@ -1,410 +0,0 @@ -package staker - -import ( - "std" - "strconv" - "time" - - "gno.land/p/onbloc/json" - "gno.land/p/nt/ufmt" - - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/gns" -) - -// getPoolByPoolPath retrieves the pool by its path. -func getPoolByPoolPath(poolPath string) *Pool { - pool, ok := pools.Get(poolPath) - if !ok { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("poolPath(%s) pool does not exist", poolPath)), - ) - } - - return pool -} - -// GetPoolIncentiveIdList returns all incentive IDs for a pool. -func GetPoolIncentiveIdList(poolPath string) []string { - pool := getPoolByPoolPath(poolPath) - - ids := []string{} - pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { - ids = append(ids, key) - return true - }) - - return ids -} - -// getIncentive retrieves an external incentive by ID. -func getIncentive(poolPath string, incentiveId string) *ExternalIncentive { - pool := getPoolByPoolPath(poolPath) - - incentive, exist := pool.incentives.incentives.Get(incentiveId) - if !exist { - panic(ufmt.Sprintf("incentiveId(%s) incentive does not exist", incentiveId)) - } - - ictv, ok := incentive.(*ExternalIncentive) - if !ok { - panic(ufmt.Sprintf("failed to cast incentive to *ExternalIncentive: %T", incentive)) - } - return ictv -} - -// GetIncentiveStartTimestamp returns the start timestamp of an incentive. -func GetIncentiveStartTimestamp(poolPath string, incentiveId string) int64 { - incentive := getIncentive(poolPath, incentiveId) - - return incentive.startTimestamp -} - -// GetIncentiveEndTimestamp returns the end timestamp of an incentive. -func GetIncentiveEndTimestamp(poolPath string, incentiveId string) int64 { - incentive := getIncentive(poolPath, incentiveId) - - return incentive.endTimestamp -} - -// GetTargetPoolPathByIncentiveId returns the target pool path of an incentive. -func GetTargetPoolPathByIncentiveId(poolPath string, incentiveId string) string { - incentive := getIncentive(poolPath, incentiveId) - - return incentive.targetPoolPath -} - -// GetCreatedHeightOfIncentive returns the creation height of an incentive. -func GetCreatedHeightOfIncentive(poolPath string, incentiveId string) int64 { - incentive := getIncentive(poolPath, incentiveId) - - return incentive.createdHeight -} - -// GetIncentiveRewardToken returns the reward token of an incentive. -func GetIncentiveRewardToken(poolPath string, incentiveId string) string { - incentive := getIncentive(poolPath, incentiveId) - - return incentive.rewardToken -} - -// GetIncentiveRewardAmount returns the reward amount of an incentive. -func GetIncentiveRewardAmount(poolPath string, incentiveId string) *u256.Uint { - incentive := getIncentive(poolPath, incentiveId) - - return u256.NewUintFromInt64(incentive.rewardAmount) -} - -// GetIncentiveRewardAmountAsString returns the reward amount of an incentive as string. -func GetIncentiveRewardAmountAsString(poolPath string, incentiveId string) string { - rewardAmount := GetIncentiveRewardAmount(poolPath, incentiveId) - - return rewardAmount.ToString() -} - -// GetIncentiveRewardPerSecond returns the reward per second of an incentive. -func GetIncentiveRewardPerSecond(poolPath string, incentiveId string) int64 { - incentive := getIncentive(poolPath, incentiveId) - - return incentive.rewardPerSecond -} - -// GetIncentiveRefundee returns the refundee address of an incentive. -func GetIncentiveRefundee(poolPath string, incentiveId string) std.Address { - incentive := getIncentive(poolPath, incentiveId) - - return incentive.refundee -} - -// getDeposit retrieves a deposit by LP token ID. -func getDeposit(lpTokenId uint64) *Deposit { - deposit := deposits.get(lpTokenId) - if deposit == nil { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("lpTokenId(%d) deposit does not exist", lpTokenId)), - ) - } - - return deposit -} - -// GetDepositOwner returns the owner of a deposit. -func GetDepositOwner(lpTokenId uint64) std.Address { - deposit := getDeposit(lpTokenId) - - return deposit.owner -} - -// GetDepositStakeTimestamp returns the stake timestamp of a deposit. -func GetDepositStakeTimestamp(lpTokenId uint64) int64 { - deposit := getDeposit(lpTokenId) - - return deposit.stakeTimestamp -} - -// GetDepositStakeTime returns the stake time of a deposit. -func GetDepositStakeTime(lpTokenId uint64) int64 { - deposit := getDeposit(lpTokenId) - - return deposit.stakeTime -} - -// GetDepositTargetPoolPath returns the target pool path of a deposit. -func GetDepositTargetPoolPath(lpTokenId uint64) string { - deposit := getDeposit(lpTokenId) - - return deposit.targetPoolPath -} - -// GetDepositTickLower returns the lower tick of a deposit. -func GetDepositTickLower(lpTokenId uint64) int32 { - deposit := getDeposit(lpTokenId) - - return deposit.tickLower -} - -// GetDepositTickUpper returns the upper tick of a deposit. -func GetDepositTickUpper(lpTokenId uint64) int32 { - deposit := getDeposit(lpTokenId) - - return deposit.tickUpper -} - -// GetDepositLiquidity returns the liquidity of a deposit. -func GetDepositLiquidity(lpTokenId uint64) *u256.Uint { - deposit := getDeposit(lpTokenId) - - return deposit.liquidity.Clone() -} - -// GetDepositLiquidityAsString returns the liquidity of a deposit as string. -func GetDepositLiquidityAsString(lpTokenId uint64) string { - liquidity := GetDepositLiquidity(lpTokenId) - - return liquidity.ToString() -} - -// GetDepositLastCollectTimestamp returns the last collect timestamp of a deposit. -func GetDepositLastCollectTimestamp(lpTokenId uint64) int64 { - deposit := getDeposit(lpTokenId) - - return deposit.lastCollectTime -} - -// GetDepositWarmUp returns the warm-up records of a deposit. -func GetDepositWarmUp(lpTokenId uint64) []Warmup { - deposit := getDeposit(lpTokenId) - - return deposit.warmups -} - -// GetPoolTier returns the tier of a pool. -func GetPoolTier(poolPath string) uint64 { - return poolTier.CurrentTier(poolPath) -} - -// GetPoolTierRatio returns the current reward ratio for a pool's tier. -func GetPoolTierRatio(poolPath string) uint64 { - tier := GetPoolTier(poolPath) - return poolTier.tierRatio.Get(tier) -} - -// GetPoolTierCount returns the number of pools in a tier. -func GetPoolTierCount(tier uint64) uint64 { - if tier == 0 { - return 0 - } - return uint64(poolTier.CurrentCount(tier)) -} - -// GetPoolReward returns the current reward amount for a tier. -func GetPoolReward(tier uint64) int64 { - return poolTier.CurrentReward(tier) -} - -// GetExternalIncentiveByPoolPath returns all external incentives for a pool. -func GetExternalIncentiveByPoolPath(poolPath string) []ExternalIncentive { - incentives := []ExternalIncentive{} - externalIncentives.tree.Iterate("", "", func(key string, value any) bool { - incentive := value.(*ExternalIncentive) - if incentive.targetPoolPath == poolPath { - incentives = append(incentives, *incentive) - } - return false - }) - - return incentives -} - -// GetPrintExternalInfo returns a JSON representation of external incentive debug information. -func GetPrintExternalInfo() string { - externalDebug := ApiExternalDebugInfo{ - Height: std.ChainHeight(), - Time: time.Now().Unix(), - } - - externalPositions := []ApiExternalDebugPosition{} - deposits.Iterate(uint64(0), uint64(deposits.Size()), func(positionId uint64, deposit *Deposit) bool { - externalPosition := ApiExternalDebugPosition{ - PositionId: positionId, - StakedTime: deposit.stakeTime, - StakedTimestamp: deposit.stakeTimestamp, - } - - externalIncentivesList := []ApiExternalDebugIncentive{} - externalIncentives.tree.Iterate("", "", func(key string, value any) bool { - incentive, ok := value.(*ExternalIncentive) - if !ok { - panic("failed to cast value to *ExternalIncentive") - } - if incentive.targetPoolPath == deposit.targetPoolPath { - externalIncentive := ApiExternalDebugIncentive{ - PoolPath: incentive.targetPoolPath, - IncentiveId: key, - RewardToken: incentive.rewardToken, - RewardAmount: strconv.FormatInt(incentive.rewardAmount, 10), - RewardLeft: strconv.FormatInt(incentive.rewardLeft, 10), - StartTimestamp: incentive.startTimestamp, - EndTimestamp: incentive.endTimestamp, - RewardPerSecond: strconv.FormatInt(incentive.rewardPerSecond, 10), - Refundee: incentive.refundee, - TokenAmountFull: incentive.depositGnsAmount, - TokenAmountToGive: incentive.RewardSpent(time.Now().Unix()), - } - - externalIncentivesList = append(externalIncentivesList, externalIncentive) - } - return false - }) - - externalPosition.Incentive = externalIncentivesList - externalPositions = append(externalPositions, externalPosition) - return false - }) - - externalDebug.Position = externalPositions - - // JSON Serialization - node := json.ObjectNode("", map[string]*json.Node{ - "height": json.NumberNode("", float64(externalDebug.Height)), - "time": json.NumberNode("", float64(externalDebug.Time)), - "position": json.ArrayNode("", makeExternalPositionsNode(externalDebug.Position)), - }) - - b, err := json.Marshal(node) - if err != nil { - return "JSON MARSHAL ERROR" - } - - return string(b) -} - -// makeExternalPositionsNode creates JSON nodes for external position data. -func makeExternalPositionsNode(positions []ApiExternalDebugPosition) []*json.Node { - externalPositions := make([]*json.Node, 0) - - for _, externalPosition := range positions { - incentives := make([]*json.Node, 0) - for _, incentive := range externalPosition.Incentive { - stakedOrExternalDuration := std.ChainHeight() - max(incentive.StartHeight, externalPosition.StakedTime) - - incentives = append(incentives, json.ObjectNode("", map[string]*json.Node{ - "poolPath": json.StringNode("poolPath", incentive.PoolPath), - "rewardToken": json.StringNode("rewardToken", incentive.RewardToken), - "rewardAmount": json.StringNode("rewardAmount", incentive.RewardAmount), - "rewardLeft": json.StringNode("rewardLeft", incentive.RewardLeft), - "startTimestamp": json.NumberNode("startTimestamp", float64(incentive.StartTimestamp)), - "endTimestamp": json.NumberNode("endTimestamp", float64(incentive.EndTimestamp)), - "rewardPerSecond": json.StringNode("rewardPerSecond", incentive.RewardPerSecond), - "stakedOrExternalDuration": json.NumberNode("stakedOrExternalDuration", float64(stakedOrExternalDuration)), - "tokenAmountFull": json.NumberNode("tokenAmountFull", float64(incentive.TokenAmountFull)), - "tokenAmountToGive": json.NumberNode("tokenAmountToGive", float64(incentive.TokenAmountToGive)), - })) - } - - externalPositions = append(externalPositions, json.ObjectNode("", map[string]*json.Node{ - "lpTokenId": json.NumberNode("lpTokenId", float64(externalPosition.PositionId)), - "stakedTime": json.NumberNode("stakedTime", float64(externalPosition.StakedTime)), - "stakedTimestamp": json.NumberNode("stakedTimestamp", float64(externalPosition.StakedTimestamp)), - "incentive": json.ArrayNode("", incentives), - })) - } - - return externalPositions -} - -type currentExternalInfo struct { - height int64 - time int64 - externalIncentives []ExternalIncentive -} - -type ApiExternalDebugInfo struct { - Height int64 `json:"height"` - Time int64 `json:"time"` - Position []ApiExternalDebugPosition `json:"pool"` -} - -type ApiExternalDebugPosition struct { - PositionId uint64 `json:"positionId"` - StakedTime int64 `json:"stakedTime"` - StakedTimestamp int64 `json:"stakedTimestamp"` - Incentive []ApiExternalDebugIncentive `json:"incentive"` -} - -type ApiExternalDebugIncentive struct { - PoolPath string `json:"poolPath"` - IncentiveId string `json:"incentiveId"` - RewardToken string `json:"rewardToken"` - RewardAmount string `json:"rewardAmount"` - RewardLeft string `json:"rewardLeft"` - StartTimestamp int64 `json:"startTimestamp"` - EndTimestamp int64 `json:"endTimestamp"` - RewardPerSecondX96 string `json:"rewardPerSecondX96"` - RewardPerSecond string `json:"rewardPerSecond"` - Refundee std.Address `json:"refundee"` - StartHeight int64 `json:"startHeight"` - EndHeight int64 `json:"endHeight"` - // FROM positionExternal -> externalRewards - TokenAmountX96 *u256.Uint `json:"tokenAmountX96"` - TokenAmount int64 `json:"tokenAmount"` - TokenAmountFull int64 `json:"tokenAmountFull"` - TokenAmountToGive int64 `json:"tokenAmountToGive"` - // FROM externalWarmUpAmount - Full30 int64 `json:"full30"` - Give30 int64 `json:"give30"` - Full50 int64 `json:"full50"` - Give50 int64 `json:"give50"` - Full70 int64 `json:"full70"` - Give70 int64 `json:"give70"` - Full100 int64 `json:"full100"` -} - -// DEBUG INTERNAL (GNS EMISSION) -type currentInfo struct { - height int64 - time int64 - gnsStaker int64 - gnsDevOps int64 - gnsCommunityPool int64 - gnsGovStaker int64 - gnsProtocolFee int64 - gnsADMIN int64 -} - -// getCurrentInfo returns current GNS balance information for system addresses. -func getCurrentInfo() currentInfo { - return currentInfo{ - height: std.ChainHeight(), - time: time.Now().Unix(), - gnsStaker: gns.BalanceOf(stakerAddr), - gnsDevOps: gns.BalanceOf(devOpsAddr), - gnsCommunityPool: gns.BalanceOf(communityPoolAddr), - gnsGovStaker: gns.BalanceOf(govStakerAddr), - gnsProtocolFee: gns.BalanceOf(protocolFeeAddr), - gnsADMIN: gns.BalanceOf(adminAddr), - } -} diff --git a/contract/r/gnoswap/v1/staker/gnomod.toml b/contract/r/gnoswap/v1/staker/gnomod.toml deleted file mode 100644 index 5c38b8f..0000000 --- a/contract/r/gnoswap/v1/staker/gnomod.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "gno.land/r/gnoswap/v1/staker" -gno = "0.9" diff --git a/contract/r/gnoswap/v1/staker/incentive_id.gno b/contract/r/gnoswap/v1/staker/incentive_id.gno deleted file mode 100644 index cb9f381..0000000 --- a/contract/r/gnoswap/v1/staker/incentive_id.gno +++ /dev/null @@ -1,21 +0,0 @@ -package staker - -import ( - "std" - - "gno.land/p/nt/ufmt" -) - -// Counter for generating unique incentive IDs -var incentiveCounter = NewCounter() - -// nextIncentiveID generates a new unique incentive ID using creator address, timestamp and counter -func nextIncentiveID(creator std.Address, timestamp int64) string { - return makeIncentiveID(creator, timestamp, incentiveCounter.next()) -} - -// makeIncentiveID formats an incentive ID string from the given components -// incentive id format: creator:timestamp:index -func makeIncentiveID(creator std.Address, timestamp int64, index int64) string { - return ufmt.Sprintf("%s:%d:%d", creator.String(), timestamp, index) -} diff --git a/contract/r/gnoswap/v1/staker/json.gno b/contract/r/gnoswap/v1/staker/json.gno deleted file mode 100644 index c403b20..0000000 --- a/contract/r/gnoswap/v1/staker/json.gno +++ /dev/null @@ -1,276 +0,0 @@ -package staker - -import ( - "std" - "strconv" - "time" - - "gno.land/p/onbloc/json" -) - -// JsonResponse is an interface that all JSON response types must implement. -type JsonResponse interface { - JSON() *json.Node -} - -type RewardToken struct { - PoolPath string `json:"poolPath"` - RewardsTokenList []string `json:"rewardsTokenList"` -} - -func newRewardToken(poolPath string, tokens []string) RewardToken { - return RewardToken{ - PoolPath: poolPath, - RewardsTokenList: tokens, - } -} - -func (r RewardToken) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "poolPath": json.StringNode("poolPath", r.PoolPath), - "tokens": json.ArrayNode("tokens", makeRewardTokensArray(r.RewardsTokenList)), - }) -} - -type ApiExternalIncentive struct { - IncentiveId string `json:"incentiveId"` - PoolPath string `json:"poolPath"` - RewardToken string `json:"rewardToken"` - RewardAmount int64 `json:"rewardAmount"` - RewardLeft int64 `json:"rewardLeft"` - StartTimestamp int64 `json:"startTimestamp"` - EndTimestamp int64 `json:"endTimestamp"` - Active bool `json:"active"` - RewardPerSecond int64 `json:"rewardPerSecond"` - Refundee string `json:"refundee"` - CreatedHeight int64 `json:"createdHeight"` - DepositGnsAmount int64 `json:"depositGnsAmount"` - UnClaimableRefunded bool `json:"unclaimableRefunded"` -} - -func newApiExternalIncentive(ictv *ExternalIncentive) ApiExternalIncentive { - now := time.Now().Unix() - isActive := false - if now >= ictv.startTimestamp && now <= ictv.endTimestamp { - isActive = true - } - return ApiExternalIncentive{ - IncentiveId: ictv.incentiveId, - PoolPath: ictv.targetPoolPath, - RewardToken: ictv.rewardToken, - RewardAmount: ictv.rewardAmount, - RewardLeft: ictv.RewardLeft(now), - StartTimestamp: ictv.startTimestamp, - EndTimestamp: ictv.endTimestamp, - Active: isActive, - RewardPerSecond: ictv.rewardPerSecond, - Refundee: ictv.refundee.String(), - CreatedHeight: ictv.createdHeight, - DepositGnsAmount: ictv.depositGnsAmount, - UnClaimableRefunded: ictv.unclaimableRefunded, - } -} - -func (r ApiExternalIncentive) JSON() *json.Node { - active := false - if time.Now().Unix() >= r.StartTimestamp && time.Now().Unix() <= r.EndTimestamp { - active = true - } - - return json.ObjectNode("", map[string]*json.Node{ - "incentiveId": json.StringNode("incentiveId", r.IncentiveId), - "poolPath": json.StringNode("poolPath", r.PoolPath), - "rewardToken": json.StringNode("rewardToken", r.RewardToken), - "rewardAmount": json.StringNode("rewardAmount", strconv.FormatInt(r.RewardAmount, 10)), - "rewardLeft": json.StringNode("rewardLeft", strconv.FormatInt(r.RewardLeft, 10)), - "startTimestamp": json.NumberNode("startTimestamp", float64(r.StartTimestamp)), - "endTimestamp": json.NumberNode("endTimestamp", float64(r.EndTimestamp)), - "active": json.BoolNode("active", active), - "rewardPerSecond": json.StringNode("rewardPerSecond", strconv.FormatInt(r.RewardPerSecond, 10)), - "refundee": json.StringNode("refundee", r.Refundee), - "createdHeight": json.NumberNode("createdHeight", float64(r.CreatedHeight)), - "depositGnsAmount": json.NumberNode("depositGnsAmount", float64(r.DepositGnsAmount)), - "unclaimableRefunded": json.BoolNode("unclaimableRefunded", r.UnClaimableRefunded), - }) -} - -type ApiInternalIncentive struct { - PoolPath string `json:"poolPath"` - Tier uint64 `json:"tier"` - StartTimestamp int64 `json:"startTimestamp"` - RewardPerSecond string `json:"rewardPerSecond"` -} - -func newApiInternalIncentive(poolPath string, tier uint64) ApiInternalIncentive { - perSecond := calculateInternalRewardPerSecondByPoolPath(poolPath) - return ApiInternalIncentive{ - PoolPath: poolPath, - Tier: tier, - RewardPerSecond: perSecond, - } -} - -func (r ApiInternalIncentive) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "poolPath": json.StringNode("poolPath", r.PoolPath), - "rewardToken": json.StringNode("rewardToken", GNS_PATH), - "tier": json.NumberNode("tier", float64(r.Tier)), - "rewardPerSecond": json.StringNode("rewardPerSecond", r.RewardPerSecond), - }) -} - -// LpTokenReward represents the rewards associated with a specific LP token -type LpTokenReward struct { - LpTokenId uint64 `json:"lpTokenId"` // The ID of the LP token - Address string `json:"address"` // The address associated with the LP token - Rewards []ApiReward `json:"rewards"` -} - -func newLpTokenReward(lpTokenId uint64, address string, rewards []ApiReward) LpTokenReward { - return LpTokenReward{ - LpTokenId: lpTokenId, - Address: address, - Rewards: rewards, - } -} - -func (r LpTokenReward) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "lpTokenId": json.NumberNode("lpTokenId", float64(r.LpTokenId)), - "address": json.StringNode("address", r.Address), - "rewards": json.ArrayNode("rewards", makeRewardsArray(r.Rewards)), - }) -} - -// Stake represents a single stake -type ApiStake struct { - PositionId uint64 `json:"positionId"` // The ID of the staked LP token - Owner std.Address `json:"owner"` // The address of the owner of the staked LP token - NumberOfStakes uint64 `json:"numberOfStakes"` // The number of times this LP token has been staked - StakeTimestamp int64 `json:"stakeTimestamp"` // The timestamp when the LP token was staked - StakeTime int64 `json:"stakeTime"` // The time when the LP token was staked - TargetPoolPath string `json:"targetPoolPath"` // The path of the target pool for the stake - StakeDuration uint64 `json:"stakeDuration"` // The duration of the stake -} - -func newApiStake(tokenId uint64, deposit *Deposit) ApiStake { - stakeDuration := uint64(0) - if std.ChainHeight() > deposit.stakeTime { - stakeDuration = uint64(std.ChainHeight() - deposit.stakeTime) // TODO: should use time difference - } - - return ApiStake{ - PositionId: tokenId, - Owner: deposit.owner, - StakeTimestamp: deposit.stakeTimestamp, - StakeTime: deposit.stakeTime, - TargetPoolPath: deposit.targetPoolPath, - StakeDuration: stakeDuration, - } -} - -func (s ApiStake) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "positionId": json.NumberNode("positionId", float64(s.PositionId)), - "owner": json.StringNode("owner", s.Owner.String()), - "stakeTimestamp": json.NumberNode("stakeTimestamp", float64(s.StakeTimestamp)), - "stakeTime": json.NumberNode("stakeTime", float64(s.StakeTime)), - "targetPoolPath": json.StringNode("targetPoolPath", s.TargetPoolPath), - "stakeDuration": json.NumberNode("stakeDuration", float64(s.StakeDuration)), - }) -} - -type statNode struct { - height int64 - timestamp int64 -} - -func newStatNode() statNode { - return statNode{ - height: std.ChainHeight(), - timestamp: time.Now().Unix(), - } -} - -func (s statNode) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "height": json.NumberNode("height", float64(s.height)), - "timestamp": json.NumberNode("timestamp", float64(s.timestamp)), - }) -} - -// Reward represents a single reward for a staked LP token -type ApiReward struct { - IncentiveType string `json:"incentiveType"` // The type of incentive (INTERNAL or EXTERNAL) - IncentiveId string `json:"incentiveId"` // The unique identifier of the incentive - TargetPoolPath string `json:"targetPoolPath"` // The path of the target pool for the reward - RewardTokenPath string `json:"rewardTokenPath"` // The pathe of the reward token - RewardTokenAmount int64 `json:"rewardTokenAmount"` // The amount of the reward token - StakeTimestamp int64 `json:"stakeTimestamp"` // The timestamp when the LP token was staked - StakeTime int64 `json:"stakeTime"` // The time when the LP token was staked - IncentiveStart int64 `json:"incentiveStart"` // The timestamp when the incentive started -} - -func (r ApiReward) JSON() *json.Node { - return json.ObjectNode("", map[string]*json.Node{ - "incentiveType": json.StringNode("incentiveType", r.IncentiveType), - "incentiveId": json.StringNode("incentiveId", r.IncentiveId), - "targetPoolPath": json.StringNode("targetPoolPath", r.TargetPoolPath), - "rewardTokenPath": json.StringNode("rewardTokenPath", r.RewardTokenPath), - "rewardTokenAmount": json.NumberNode("rewardTokenAmount", float64(r.RewardTokenAmount)), - "stakeTimestamp": json.NumberNode("stakeTimestamp", float64(r.StakeTimestamp)), - "stakeTime": json.NumberNode("stakeTime", float64(r.StakeTime)), - "incentiveStart": json.NumberNode("incentiveStart", float64(r.IncentiveStart)), - }) -} - -///////////////////// Response ///////////////////// - -type ApiResponse struct { - Stat statNode `json:"stat"` - Response []JsonResponse `json:"response"` -} - -func (r ApiResponse) JSON() *json.Node { - rspsNodes := make([]*json.Node, len(r.Response)) - for i, item := range r.Response { - rspsNodes[i] = item.JSON() - } - - return json.ObjectNode("", map[string]*json.Node{ - "stat": r.Stat.JSON(), - "response": json.ArrayNode("response", rspsNodes), - }) -} - -func makeApiResponse(rs []JsonResponse) string { - resp := ApiResponse{ - Stat: newStatNode(), - Response: rs, - } - - b, err := json.Marshal(resp.JSON()) - if err != nil { - panic(err.Error()) - } - - return string(b) -} - -func makeRewardsArray(rewards []ApiReward) []*json.Node { - rewardsArray := make([]*json.Node, len(rewards)) - - for i, reward := range rewards { - rewardsArray[i] = json.ObjectNode("", map[string]*json.Node{ - "incentiveType": json.StringNode("incentiveType", reward.IncentiveType), - "incentiveId": json.StringNode("incentiveId", reward.IncentiveId), - "targetPoolPath": json.StringNode("targetPoolPath", reward.TargetPoolPath), - "rewardTokenPath": json.StringNode("rewardTokenPath", reward.RewardTokenPath), - "rewardTokenAmount": json.NumberNode("rewardTokenAmount", float64(reward.RewardTokenAmount)), - "stakeTimestamp": json.NumberNode("stakeTimestamp", float64(reward.StakeTimestamp)), - "stakeTime": json.NumberNode("stakeTime", float64(reward.StakeTime)), - "incentiveStart": json.NumberNode("incentiveStart", float64(reward.IncentiveStart)), - }) - } - return rewardsArray -} diff --git a/contract/r/gnoswap/v1/staker/manage_pool_tier_and_warmup.gno b/contract/r/gnoswap/v1/staker/manage_pool_tier_and_warmup.gno deleted file mode 100644 index 56e1042..0000000 --- a/contract/r/gnoswap/v1/staker/manage_pool_tier_and_warmup.gno +++ /dev/null @@ -1,173 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - - en "gno.land/r/gnoswap/emission" - pl "gno.land/r/gnoswap/v1/pool" -) - -const ( - NOT_EMISSION_TARGET_TIER uint64 = 0 -) - -// SetPoolTier assigns a tier level to a pool for internal GNS emission rewards. -// Only admin or governance can call this function. -func SetPoolTier(cur realm, poolPath string, tier uint64) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsPoolExists(poolPath) - assertIsValidPoolTier(tier) - - currentTime := time.Now().Unix() - setPoolTier(poolPath, tier, currentTime) - - previousRealm := std.PreviousRealm() - std.Emit( - "SetPoolTier", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "poolPath", poolPath, - "tier", formatUint(tier), - "currentTime", formatInt(currentTime), - "currentHeight", formatInt(std.ChainHeight()), - ) -} - -// ChangePoolTier modifies the tier level of an existing pool. -// Only admin or governance can call this function. -func ChangePoolTier(cur realm, poolPath string, tier uint64) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsPoolExists(poolPath) - assertIsValidPoolTier(tier) - - currentTime := time.Now().Unix() - previousTier, newTier := changePoolTier(poolPath, tier, currentTime) - - previousRealm := std.PreviousRealm() - std.Emit( - "ChangePoolTier", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "poolPath", poolPath, - "prevTier", formatUint(previousTier), - "newTier", formatUint(newTier), - "currentTime", formatInt(currentTime), - "currentHeight", formatInt(std.ChainHeight()), - ) -} - -// RemovePoolTier removes a pool from internal GNS emission rewards. -// Only admin or governance can call this function. -func RemovePoolTier(cur realm, poolPath string) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsPoolExists(poolPath) - - currentTime := time.Now().Unix() - removePoolTier(poolPath, currentTime) - - previousRealm := std.PreviousRealm() - std.Emit( - "RemovePoolTier", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "poolPath", poolPath, - "currentTime", formatInt(currentTime), - "currentHeight", formatInt(std.ChainHeight()), - ) -} - -// SetWarmUp configures the warm-up percentage and duration for rewards. -// Only admin or governance can call this function. -func SetWarmUp(cur realm, pct, timeDuration int64) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - setWarmUp(pct, timeDuration) - - previousRealm := std.PreviousRealm() - std.Emit( - "SetWarmUp", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "pct", formatInt(pct), - "timeDuration", formatInt(timeDuration), - ) -} - -// setPoolTier internally sets the pool tier. -func setPoolTier(poolPath string, tier uint64, currentTime int64) { - en.MintAndDistributeGns(cross) - - pools.GetOrCreate(poolPath) - poolTier.changeTier(std.ChainHeight(), currentTime, pools, poolPath, tier) -} - -// changePoolTier internally changes the pool tier and returns old and new tiers. -func changePoolTier(poolPath string, tier uint64, currentTime int64) (uint64, uint64) { - en.MintAndDistributeGns(cross) - previousTier := poolTier.CurrentTier(poolPath) - - poolTier.changeTier(std.ChainHeight(), currentTime, pools, poolPath, tier) - - return previousTier, tier -} - -// removePoolTier internally removes the pool from tier system. -func removePoolTier(poolPath string, currentTime int64) { - en.MintAndDistributeGns(cross) - - poolTier.changeTier(std.ChainHeight(), currentTime, pools, poolPath, NOT_EMISSION_TARGET_TIER) -} - -// setWarmUp internally sets the warm-up parameters. -func setWarmUp(pct, timeDuration int64) { - en.MintAndDistributeGns(cross) - - modifyWarmup(pctToIndex(pct), timeDuration) -} - -// pctToIndex converts percentage to warmup index. -func pctToIndex(pct int64) int { - switch pct { - case 30: - return 0 - case 50: - return 1 - case 70: - return 2 - case 100: - return 3 - default: - panic("staker.gno__pctToIndex() || pct is not valid") - } -} - -// assertPoolMustExist panics if the pool doesn't exist. -func assertPoolMustExist(poolPath string) { - if !pl.ExistsPoolPath(poolPath) { - panic(makeErrorWithDetails( - errInvalidPoolPath, - ufmt.Sprintf("pool(%s) does not exist", poolPath), - )) - } -} diff --git a/contract/r/gnoswap/v1/staker/mint_stake.gno b/contract/r/gnoswap/v1/staker/mint_stake.gno deleted file mode 100644 index cb9f92c..0000000 --- a/contract/r/gnoswap/v1/staker/mint_stake.gno +++ /dev/null @@ -1,93 +0,0 @@ -package staker - -import ( - "std" - - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/halt" - - pn "gno.land/r/gnoswap/v1/position" -) - -// MintAndStake mints a new liquidity position and immediately stakes it. -// -// Atomic operation combining position creation and staking. -// Saves gas by avoiding separate mint and stake transactions. -// Position NFT transferred directly to staker contract. -// -// Parameters: -// - token0, token1: Token contract paths -// - fee: Pool fee tier (500=0.05%, 3000=0.3%, 10000=1%) -// - tickLower, tickUpper: Price range boundaries -// - amount0Desired, amount1Desired: Target token amounts -// - amount0Min, amount1Min: Minimum amounts (slippage protection) -// - deadline: Transaction expiration timestamp -// - referrer: Optional referral address -// -// Native token support: -// - Accepts GNOT via std.OriginSend() -// - Auto-wraps to WUGNOT for liquidity -// - Minimum 1 GNOT required -// -// Returns: -// - positionId: New NFT token ID -// - liquidity: Amount of liquidity minted -// - amount0, amount1: Actual tokens deposited -// - poolPath: Pool identifier -func MintAndStake( - cur realm, - token0 string, - token1 string, - fee uint32, - tickLower int32, - tickUpper int32, - amount0Desired string, - amount1Desired string, - amount0Min string, - amount1Min string, - deadline int64, - referrer string, -) (uint64, string, string, string, string) { - halt.AssertIsNotHaltedStaker() - - // if one click native - if token0 == GNOT || token1 == GNOT { - // check sent ugnot - sent := std.OriginSend() - ugnotSent := sent.AmountOf("ugnot") - - // not enough ugnot sent - if ugnotSent < UGNOT_MIN_DEPOSIT_TO_WRAP { - panic(ufmt.Errorf( - "%v: too less ugnot sent(%d), minimum:%d", - errWugnotMinimum, ugnotSent, UGNOT_MIN_DEPOSIT_TO_WRAP, - )) - } - - // send it over to position to wrap - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(stakerAddr, positionAddr, std.Coins{{Denom: "ugnot", Amount: ugnotSent}}) - } - - positionId, liquidity, amount0, amount1 := pn.Mint( - cross, - token0, - token1, - fee, - tickLower, - tickUpper, - amount0Desired, - amount1Desired, - amount0Min, - amount1Min, - deadline, - stakerAddr, - std.PreviousRealm().Address(), - referrer, - ) - - poolPath, _, _ := StakeToken(cur, positionId, "") - - return positionId, liquidity, amount0, amount1, poolPath -} diff --git a/contract/r/gnoswap/v1/staker/protocol_fee_unstaking.gno b/contract/r/gnoswap/v1/staker/protocol_fee_unstaking.gno deleted file mode 100644 index 647d7bb..0000000 --- a/contract/r/gnoswap/v1/staker/protocol_fee_unstaking.gno +++ /dev/null @@ -1,84 +0,0 @@ -package staker - -import ( - "std" - - "gno.land/p/nt/ufmt" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" - - pf "gno.land/r/gnoswap/v1/protocol_fee" -) - -const FEE_PRECISION = 10000 - -// unstakingFee is the fee charged when unstaking positions. -// This parameter can be modified through governance. -var unstakingFee = int64(100) // 1% - -// GetUnstakingFee returns the current unstaking fee rate in basis points. -func GetUnstakingFee() int64 { return unstakingFee } - -// handleUnStakingFee calculates and applies the unstaking fee. -func handleUnStakingFee( - tokenPath string, - amount int64, - internal bool, - positionId uint64, - poolPath string, -) (int64, int64, error) { - if unstakingFee == 0 { - return amount, 0, nil - } - - // Do not change the order of the operation. - feeAmount := (amount * unstakingFee) / FEE_PRECISION - if feeAmount < 0 { - return 0, 0, ufmt.Errorf("fee amount cannot be negative") - } - - if feeAmount == 0 { - return amount, 0, nil - } - - if internal { - tokenPath = GNS_PATH - } - - // external contract has fee - common.Transfer(cross, tokenPath, protocolFeeAddr, feeAmount) - pf.AddToProtocolFee(cross, tokenPath, feeAmount) - - return amount - feeAmount, feeAmount, nil -} - -// SetUnStakingFee sets the unstaking fee rate in basis points. -// Only admin or governance can call this function. -func SetUnStakingFee(cur realm, fee int64) { - halt.AssertIsNotHaltedStaker() - - caller := std.PreviousRealm().Address() - access.AssertIsAdminOrGovernance(caller) - - assertIsValidFeeRate(fee) - - prevUnStakingFee := GetUnstakingFee() - - setUnStakingFee(fee) - - previousRealm := std.PreviousRealm() - std.Emit( - "SetUnStakingFee", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "prevFee", formatAnyInt(prevUnStakingFee), - "newFee", formatAnyInt(fee), - ) -} - -// setUnStakingFee internally updates the unstaking fee. -func setUnStakingFee(fee int64) { - unstakingFee = fee -} diff --git a/contract/r/gnoswap/v1/staker/query.gno b/contract/r/gnoswap/v1/staker/query.gno deleted file mode 100644 index 7436681..0000000 --- a/contract/r/gnoswap/v1/staker/query.gno +++ /dev/null @@ -1,127 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" -) - -type PoolData struct { - PoolPath string - Tier uint64 - ActiveIncentives []string - StakedLiquidity *u256.Uint -} - -type IncentiveData struct { - IncentiveID string - StartTimestamp int64 - EndTimestamp int64 - RewardToken string - RewardAmount *u256.Uint - Refundee std.Address - PoolPath string -} - -type DepositData struct { - TokenID uint64 - Owner std.Address - TargetPoolPath string - StakeTimestamp int64 - Liquidity *u256.Uint - WarmupCount int -} - -// QueryPoolData returns combined pool data including tier, incentives and current staked liquidity -func QueryPoolData(poolPath string) (*PoolData, error) { - pool, exist := pools.Get(poolPath) - if !exist { - return nil, ufmt.Errorf("pool %s not found", poolPath) - } - - currentTimestamp := time.Now().Unix() - tier := poolTier.CurrentTier(poolPath) - - ictvIds := filterActiveIncentives(pool, currentTimestamp) - - return &PoolData{ - PoolPath: poolPath, - Tier: tier, - ActiveIncentives: ictvIds, - StakedLiquidity: pool.CurrentStakedLiquidity(currentTimestamp), - }, nil -} - -// QueryIncentiveData returns detailed information about a specific incentive -func QueryIncentiveData(incentiveId string) (*IncentiveData, error) { - var found bool - var data IncentiveData - - pools.tree.Iterate("", "", func(key string, value any) bool { - pool := value.(*Pool) - - pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { - if key == incentiveId { - ictv := value.(*ExternalIncentive) - data = IncentiveData{ - IncentiveID: incentiveId, - StartTimestamp: ictv.startTimestamp, - EndTimestamp: ictv.endTimestamp, - RewardToken: ictv.rewardToken, - RewardAmount: u256.NewUintFromInt64(ictv.rewardAmount), - Refundee: ictv.refundee, - PoolPath: pool.poolPath, - } - found = true - return true - } - return false - }) - - return found - }) - - if !found { - return nil, ufmt.Errorf("incentiveId(%s) incentive does not exist", incentiveId) - } - - return &data, nil -} - -// QueryDepositData returns detailed information about a specific deposit -func QueryDepositData(lpTokenId uint64) (*DepositData, error) { - deposit := deposits.get(lpTokenId) - if deposit == nil { - return nil, ufmt.Errorf("positionId(%d) deposit does not exist", lpTokenId) - } - - return &DepositData{ - TokenID: lpTokenId, - Owner: deposit.owner, - TargetPoolPath: deposit.targetPoolPath, - StakeTimestamp: deposit.stakeTimestamp, - Liquidity: deposit.liquidity, - WarmupCount: len(deposit.warmups), - }, nil -} - -func filterActiveIncentives(pool *Pool, currentTimestamp int64) []string { - ictvIds := make([]string, 0) - - pool.incentives.incentives.Iterate("", "", func(key string, value any) bool { - ictv, ok := value.(*ExternalIncentive) - if !ok { - return false - } - - if ictv.startTimestamp <= currentTimestamp && currentTimestamp < ictv.endTimestamp { - ictvIds = append(ictvIds, ictv.incentiveId) - } - - return false - }) - - return ictvIds -} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation.gno b/contract/r/gnoswap/v1/staker/reward_calculation.gno deleted file mode 100644 index 9c49a3a..0000000 --- a/contract/r/gnoswap/v1/staker/reward_calculation.gno +++ /dev/null @@ -1,61 +0,0 @@ -package staker - -import ( - "gno.land/p/nt/ufmt" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -// liquidityMathAddDelta calculates the new liquidity by applying the delta liquidity to the current liquidity. -// If delta liquidity is negative, it subtracts the absolute value of delta liquidity from the current liquidity. -// If delta liquidity is positive, it adds the absolute value of delta liquidity to the current liquidity. -// -// Parameters: -// - x: The current liquidity as a uint256 value. -// - y: The delta liquidity as a signed int256 value. -// -// Returns: -// - The new liquidity as a uint256 value. -// -// Notes: -// - If `x` or `y` is nil, the function panics with an appropriate error message. -// - If `y` is negative, its absolute value is subtracted from `x`. -// - The result must be less than `x`. Otherwise, the function panics to prevent underflow. -// -// - If `y` is positive, it is added to `x`. -// - The result must be greater than or equal to `x`. Otherwise, the function panics to prevent overflow. -// -// - The function ensures correctness by validating the results of the arithmetic operations. -func liquidityMathAddDelta(x *u256.Uint, y *i256.Int) *u256.Uint { - if x == nil || y == nil { - panic(makeErrorWithDetails( - errInvalidInput, - "x or y is nil", - )) - } - - var z *u256.Uint - - // Subtract or add based on the sign of y - if y.Lt(i256.Zero()) { - absDelta := y.Abs() - z = u256.Zero().Sub(x, absDelta) - if z.Gte(x) { - panic(makeErrorWithDetails( - errCalculationError, - ufmt.Sprintf("Condition failed: (z must be < x) (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString()), - )) - } - } else { - z = u256.Zero().Add(x, y.Abs()) - if z.Lt(x) { - panic(makeErrorWithDetails( - errCalculationError, - ufmt.Sprintf("Condition failed: (z must be >= x) (x: %s, y: %s, z:%s)", x.ToString(), y.ToString(), z.ToString()), - )) - } - } - - return z -} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_incentives.gno b/contract/r/gnoswap/v1/staker/reward_calculation_incentives.gno deleted file mode 100644 index 8cf0aca..0000000 --- a/contract/r/gnoswap/v1/staker/reward_calculation_incentives.gno +++ /dev/null @@ -1,223 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/p/nt/avl" -) - -// Incentives represents a collection of external incentives for a specific pool. -// -// Fields: -// -// - incentives: AVL tree storing ExternalIncentive objects indexed by incentiveId -// The incentiveId serves as the key to efficiently lookup incentive details -// -// - targetPoolPath: String identifier for the pool this incentive collection belongs to -// Used to associate incentives with their corresponding liquidity pool -// -// - unclaimablePeriods: Tree storing periods when rewards cannot be claimed -// Maps start timestamp (key) to end timestamp (value) -// An end timestamp of 0 indicates an ongoing unclaimable period -// Used to track intervals when staking rewards are not claimable -type Incentives struct { - incentives *avl.Tree // (incentiveId) => ExternalIncentive - - targetPoolPath string // The target pool path for this incentive collection - - unclaimablePeriods *UintTree // startTimestamp => endTimestamp -} - -// NewIncentives creates a new Incentives instance. -func NewIncentives(targetPoolPath string) Incentives { - result := Incentives{ - targetPoolPath: targetPoolPath, - unclaimablePeriods: NewUintTree(), - incentives: avl.NewTree(), - } - - // initial unclaimable period starts, as there cannot be any staked positions yet. - currentTimestamp := time.Now().Unix() - result.unclaimablePeriods.set(currentTimestamp, int64(0)) - return result -} - -// Get incentive by incentiveId -func (self *Incentives) Get(incentiveId string) (*ExternalIncentive, bool) { - return retrieveIncentive(self.incentives, incentiveId) -} - -// Get incentive by full incentiveId(by time) -func (self *Incentives) GetByIncentiveId(incentiveId string) (*ExternalIncentive, bool) { - return retrieveIncentive(self.incentives, incentiveId) -} - -func retrieveIncentive(tree *avl.Tree, id string) (*ExternalIncentive, bool) { - value, ok := tree.Get(id) - if !ok { - return nil, false - } - v, ok := value.(*ExternalIncentive) - if !ok { - panic("failed to cast value to *ExternalIncentive") - } - return v, true -} - -// Get all incentives that is active in given [startTimestamp, endTimestamp) -func (self *Incentives) GetAllInTimestamps(startTimestamp, endTimestamp int64) map[string]*ExternalIncentive { - incentives := make(map[string]*ExternalIncentive) - - // Iterate all incentives that has start timestamp less than endTimestamp - self.incentives.Iterate("", "", func(key string, value any) bool { - incentive, ok := value.(*ExternalIncentive) - if !ok { - return false - } - - // incentive is not active - if incentive.startTimestamp > endTimestamp || incentive.endTimestamp < startTimestamp { - return false - } - - incentives[incentive.incentiveId] = incentive - - return false - }) - - return incentives -} - -// Create a new external incentive -// Panics if the incentive already exists. -func (self *Incentives) create( - creator std.Address, - incentive *ExternalIncentive, -) { - self.incentives.Set(incentive.incentiveId, incentive) -} - -// starts incentive unclaimable period for this pool -func (self *Incentives) update( - creator std.Address, - incentive *ExternalIncentive, -) { - self.incentives.Set(incentive.incentiveId, incentive) -} - -// starts incentive unclaimable period for this pool -func (self *Incentives) startUnclaimablePeriod(startTimestamp int64) { - self.unclaimablePeriods.set(startTimestamp, int64(0)) -} - -// ends incentive unclaimable period for this pool -// ignores if currently not in unclaimable period -func (self *Incentives) endUnclaimablePeriod(endTimestamp int64) { - startTimestamp := int64(0) - self.unclaimablePeriods.ReverseIterate(0, endTimestamp, func(key int64, value any) bool { - if value.(int64) != 0 { - // Already ended, no need to update - // keeping startTimestamp as 0 to indicate this - return true - } - startTimestamp = key - return true - }) - - if startTimestamp == 0 { - // No ongoing unclaimable period found - return - } - - if startTimestamp == endTimestamp { - self.unclaimablePeriods.remove(startTimestamp) - } else { - self.unclaimablePeriods.set(startTimestamp, endTimestamp) - } -} - -// calculate unclaimable reward by checking unclaimable periods -func (self *Incentives) calculateUnclaimableReward(incentiveId string) int64 { - incentive, ok := self.GetByIncentiveId(incentiveId) - if !ok { - return 0 - } - - timeDiff := int64(0) - - // Find unclaimable periods that end before or at incentive start - self.unclaimablePeriods.ReverseIterate(0, incentive.startTimestamp, func(key int64, value any) bool { - startTimestamp := key - endTimestamp := value.(int64) - if endTimestamp == 0 { - endTimestamp = incentive.endTimestamp - } - - if endTimestamp <= incentive.startTimestamp { - return true - } - - // Calculate duration of unclaimable period that overlaps with incentive period - duration := calculateUnClaimableDuration( - startTimestamp, - endTimestamp, - incentive.startTimestamp, - incentive.endTimestamp, - ) - - timeDiff += duration - - return true - }) - - // Find unclaimable periods that start within incentive period - self.unclaimablePeriods.Iterate(incentive.startTimestamp, incentive.endTimestamp, func(key int64, value any) bool { - startTimestamp := key - endTimestamp, ok := value.(int64) - if !ok { - panic("failed to cast value to int64") - } - - if endTimestamp == 0 { - endTimestamp = incentive.endTimestamp - } - - // Calculate duration of unclaimable period that overlaps with incentive period - duration := calculateUnClaimableDuration( - startTimestamp, - endTimestamp, - incentive.startTimestamp, - incentive.endTimestamp, - ) - - timeDiff += duration - - return false - }) - - return timeDiff * incentive.rewardPerSecond -} - -// calculateUnClaimableDuration calculates the duration of overlap between an unclaimable period and incentive period -func calculateUnClaimableDuration(unclaimableStart, unclaimableEnd, incentiveStartTimestamp, incentiveEndTimestamp int64) int64 { - // Use later timestamp between unclaimable start and incentive start - startTime := unclaimableStart - if startTime < incentiveStartTimestamp { - startTime = incentiveStartTimestamp - } - - // Use earlier timestamp between unclaimable end and incentive end - endTime := unclaimableEnd - if endTime > incentiveEndTimestamp { - endTime = incentiveEndTimestamp - } - - // Return 0 if no overlap - if endTime < startTime { - return 0 - } - - // Calculate overlap duration - return endTime - startTime -} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_pool.gno b/contract/r/gnoswap/v1/staker/reward_calculation_pool.gno deleted file mode 100644 index 3046673..0000000 --- a/contract/r/gnoswap/v1/staker/reward_calculation_pool.gno +++ /dev/null @@ -1,523 +0,0 @@ -package staker - -import ( - "time" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" -) - -var ( - // Q128 is 2^128 - q128 = u256.MustFromDecimal("340282366920938463463374607431768211456") - // Q192 is 2^192 - q192 = u256.MustFromDecimal("6277101735386680763835789423207666416102355444464034512896") - - // pools is the global pool storage - pools *Pools -) - -func init() { - pools = NewPools() -} - -// Pools represents the global pool storage -type Pools struct { - tree *avl.Tree // string poolPath -> pool -} - -func NewPools() *Pools { - return &Pools{ - tree: avl.NewTree(), - } -} - -// Get returns the pool for the given poolPath -func (self *Pools) Get(poolPath string) (*Pool, bool) { - v, ok := self.tree.Get(poolPath) - if !ok { - return nil, false - } - p, ok := v.(*Pool) - if !ok { - panic(ufmt.Sprintf("failed to cast v to *Pool: %T", v)) - } - return p, true -} - -// GetOrCreate returns the pool for the given poolPath, or creates a new pool if it does not exist -func (self *Pools) GetOrCreate(poolPath string) *Pool { - pool, ok := self.Get(poolPath) - if !ok { - pool = NewPool(poolPath, time.Now().Unix()) - self.set(poolPath, pool) - } - return pool -} - -// set sets the pool for the given poolPath -func (self *Pools) set(poolPath string, pool *Pool) { - self.tree.Set(poolPath, pool) -} - -// Has returns true if the pool exists for the given poolPath -func (self *Pools) Has(poolPath string) bool { - return self.tree.Has(poolPath) -} - -func (self *Pools) IterateAll(fn func(key string, pool *Pool) bool) { - self.tree.Iterate("", "", func(key string, value any) bool { - return fn(key, value.(*Pool)) - }) -} - -// Pool is a struct for storing an incentivized pool information -// Each pool stores Incentives and Ticks associated with it. -// -// Fields: -// - poolPath: The path of the pool. -// -// - currentStakedLiquidity: -// The current total staked liquidity of the in-range positions for the pool. -// Updated when tick cross happens or stake/unstake happens. -// Used to calculate the global reward ratio accumulation or -// decide whether to enter/exit unclaimable period. -// -// - lastUnclaimableTime: -// The time at which the unclaimable period started. -// Set to 0 when the pool is not in an unclaimable period. -// -// - unclaimableAcc: -// The accumulated undisributed unclaimable reward. -// Reset to 0 when processUnclaimableReward is called and sent to community pool. -// -// - rewardCache: -// The cached per-second reward emitted for this pool. -// Stores new entry only when the reward is changed. -// PoolTier.cacheReward() updates this. -// -// - incentives: The external incentives associated with the pool. -// -// - ticks: The Ticks associated with the pool. -// -// - globalRewardRatioAccumulation: -// Global ratio of Time / TotalStake accumulation(since the pool creation) -// Stores new entry only when tick cross or stake/unstake happens. -// It is used to calculate the reward for a staked position at certain time. -// -// - historicalTick: -// The historical tick for the pool at a given time. -// It does not reflect the exact tick at the timestamp, -// but it provides correct ordering for the staked position's ticks. -// Therefore, you should not compare it for equality, only for ordering. -// Set when tick cross happens or a new position is created. -type Pool struct { - poolPath string - - stakedLiquidity *UintTree // uint64 timestamp -> *u256.Uint(Q128) - - lastUnclaimableTime int64 - unclaimableAcc int64 - - rewardCache *UintTree // uint64 timestamp -> int64 gnsReward - - incentives Incentives - - ticks Ticks // int32 tickId -> Tick tick - - globalRewardRatioAccumulation *UintTree // uint64 timestamp -> *u256.Uint(Q128) rewardRatioAccumulation - - historicalTick *UintTree // uint64 timestamp -> int32 tickId -} - -// NewPool creates a new pool with the given poolPath and currentHeight. -func NewPool(poolPath string, currentTime int64) *Pool { - pool := &Pool{ - poolPath: poolPath, - stakedLiquidity: NewUintTree(), - lastUnclaimableTime: currentTime, - unclaimableAcc: 0, - rewardCache: NewUintTree(), - incentives: NewIncentives(poolPath), - ticks: NewTicks(), - globalRewardRatioAccumulation: NewUintTree(), - historicalTick: NewUintTree(), - } - - pool.globalRewardRatioAccumulation.set(currentTime, u256.Zero()) - pool.rewardCache.set(currentTime, int64(0)) - pool.stakedLiquidity.set(currentTime, u256.Zero()) - - return pool -} - -// Get the latest global reward ratio accumulation in [0, currentTime] range. -// Returns the time and the accumulation. -func (self *Pool) CurrentGlobalRewardRatioAccumulation(currentTime int64) (time int64, acc *u256.Uint) { - self.globalRewardRatioAccumulation.ReverseIterate(0, currentTime, func(key int64, value any) bool { - time = key - v, ok := value.(*u256.Uint) - if !ok { - panic(ufmt.Sprintf("failed to cast value to *u256.Uint: %T", value)) - } - acc = v - return true - }) - if acc == nil { - panic("should not happen, globalRewardRatioAccumulation must be set when pool is created") - } - return -} - -// Get the latest tick in [0, currentTime] range. -// Returns the tick. -func (self *Pool) CurrentTick(currentTime int64) (tick int32) { - self.historicalTick.ReverseIterate(0, currentTime, func(key int64, value any) bool { - res, ok := value.(int32) - if !ok { - panic(ufmt.Sprintf("failed to cast value to int32: %T", value)) - } - tick = res - return true - }) - return -} - -func (self *Pool) CurrentStakedLiquidity(currentTime int64) (liquidity *u256.Uint) { - self.stakedLiquidity.ReverseIterate(0, currentTime, func(key int64, value any) bool { - res, ok := value.(*u256.Uint) - if !ok { - panic(ufmt.Sprintf("failed to cast value to *u256.Uint: %T", value)) - } - liquidity = res - return true - }) - return -} - -// IsExternallyIncentivizedPool returns true if the pool has any external incentives. -func (self *Pool) IsExternallyIncentivizedPool() bool { - return self.incentives.incentives.Size() > 0 -} - -// Get the latest reward in [0, currentTime] range. -// Returns the reward. -func (self *Pool) CurrentReward(currentTime int64) (reward int64) { - self.rewardCache.ReverseIterate(0, currentTime, func(key int64, value any) bool { - res, ok := value.(int64) - if !ok { - panic(ufmt.Sprintf("failed to cast value to int64: %T", value)) - } - reward = res - return true - }) - return -} - -// cacheReward sets the current reward for the pool -// If the pool is in unclaimable period, it will end the unclaimable period, updates the reward, and start the unclaimable period again. -func (self *Pool) cacheReward(currentTime int64, currentTierReward int64) { - oldTierReward := self.CurrentReward(currentTime) - if oldTierReward == currentTierReward { - return - } - - isInUnclaimable := self.CurrentStakedLiquidity(currentTime).IsZero() - if isInUnclaimable { - self.endUnclaimablePeriod(currentTime) - } - - self.rewardCache.set(currentTime, currentTierReward) - - if isInUnclaimable { - self.startUnclaimablePeriod(currentTime) - } -} - -// cacheInternalReward caches the current emission and updates the global reward ratio accumulation. -func (self *Pool) cacheInternalReward(currentTime int64, currentEmission int64) { - self.cacheReward(currentTime, currentEmission) - - currentStakedLiquidity := self.CurrentStakedLiquidity(currentTime) - if currentStakedLiquidity.IsZero() { - self.endUnclaimablePeriod(currentTime) - self.startUnclaimablePeriod(currentTime) - } - - self.updateGlobalRewardRatioAccumulation(currentTime, currentStakedLiquidity) -} - -func (self *Pool) calculateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint { - oldAccTime, oldAcc := self.CurrentGlobalRewardRatioAccumulation(currentTime) - timeDiff := currentTime - oldAccTime - if timeDiff == 0 { - return oldAcc.Clone() - } - if timeDiff < 0 { - panic("time cannot go backwards") - } - - if currentStakedLiquidity.IsZero() { - return oldAcc.Clone() - } - - acc := u256.MulDiv( - u256.NewUintFromInt64(timeDiff), - q128, - currentStakedLiquidity, - ) - return u256.Zero().Add(oldAcc, acc) -} - -// updateGlobalRewardRatioAccumulation updates the global reward ratio accumulation and returns the new accumulation. -func (self *Pool) updateGlobalRewardRatioAccumulation(currentTime int64, currentStakedLiquidity *u256.Uint) *u256.Uint { - newAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, currentStakedLiquidity) - - self.globalRewardRatioAccumulation.set(currentTime, newAcc) - return newAcc -} - -// RewardState is a struct for storing the intermediate state for reward calculation. -type RewardState struct { - pool *Pool - deposit *Deposit - - // accumulated rewards for each warmup - rewards []int64 - penalties []int64 -} - -// RewardStateOf initializes a new RewardState for the given deposit. -func (self *Pool) RewardStateOf(deposit *Deposit) *RewardState { - result := &RewardState{ - pool: self, - deposit: deposit, - rewards: make([]int64, len(deposit.warmups)), - penalties: make([]int64, len(deposit.warmups)), - } - - for i := range result.rewards { - result.rewards[i] = 0 - result.penalties[i] = 0 - } - - return result -} - -// calculateInternalReward calculates the internal reward for the deposit. -// It calls rewardPerWarmup for each rewardCache interval, applies warmup, and returns the rewards and penalties. -func (self *RewardState) calculateInternalReward(startTime, endTime int64) ([]int64, []int64) { - currentReward := self.pool.CurrentReward(startTime) - self.pool.rewardCache.Iterate(startTime, endTime, func(key int64, value any) bool { - // we calculate per-position reward - err := self.rewardPerWarmup(startTime, key, currentReward) - if err != nil { - panic(err) - } - - reward, ok := value.(int64) - if !ok { - panic(ufmt.Sprintf("failed to cast value to int64: %T", value)) - } - startTime = key - currentReward = reward - return false - }) - - if startTime < endTime { - err := self.rewardPerWarmup(startTime, endTime, currentReward) - if err != nil { - panic(err) - } - } - - self.applyWarmup() - - return self.rewards, self.penalties -} - -// calculateExternalReward calculates the external reward for the deposit. -// It calls rewardPerWarmup for startTime to endTime(clamped to the incentive period), applies warmup and returns the rewards and penalties. -func (self *RewardState) calculateExternalReward(startTime, endTime int64, incentive *ExternalIncentive) ([]int64, []int64) { - if startTime < self.deposit.lastCollectTime { - // This must not happen, but adding some guards just in case. - startTime = self.deposit.lastCollectTime - } - - if endTime < incentive.startTimestamp { - return nil, nil // Not started yet - } - - if startTime < incentive.startTimestamp { - startTime = incentive.startTimestamp - } - - if endTime > incentive.endTimestamp { - endTime = incentive.endTimestamp - } - - if startTime > incentive.endTimestamp { - return nil, nil // Already ended - } - - err := self.rewardPerWarmup(startTime, endTime, incentive.rewardPerSecond) - if err != nil { - panic(err) - } - - self.applyWarmup() - - return self.rewards, self.penalties -} - -// applyWarmup applies the warmup to the rewards and calculate penalties. -func (self *RewardState) applyWarmup() { - for i, warmup := range self.deposit.warmups { - refactorReward := self.rewards[i] - self.rewards[i] = safeMulInt64(refactorReward, int64(warmup.WarmupRatio)) / 100 - self.penalties[i] = safeSubInt64(refactorReward, self.rewards[i]) - } -} - -// rewardPerWarmup calculates the reward for each warmup, adds to the RewardState's rewards array. -func (self *RewardState) rewardPerWarmup(startTime, endTime int64, rewardPerSecond int64) error { - for i, warmup := range self.deposit.warmups { - if startTime >= warmup.NextWarmupTime { - // passed the warmup - continue - } - - if endTime < warmup.NextWarmupTime { - rewardAcc := self.pool.CalculateRewardForPosition( - startTime, - self.pool.CurrentTick(startTime), - endTime, - self.pool.CurrentTick(endTime), - self.deposit, - ) - - rewardAcc = u256.Zero().Mul(rewardAcc, self.deposit.liquidity) - rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128) - self.rewards[i] += safeConvertToInt64(rewardAcc) - - break - } - - rewardAcc := self.pool.CalculateRewardForPosition( - startTime, - self.pool.CurrentTick(startTime), - warmup.NextWarmupTime, - self.pool.CurrentTick(warmup.NextWarmupTime), - self.deposit, - ) - - rewardAcc = u256.Zero().Mul(rewardAcc, self.deposit.liquidity) - rewardAcc = u256.MulDiv(rewardAcc, u256.NewUintFromInt64(rewardPerSecond), q128) - self.rewards[i] += safeConvertToInt64(rewardAcc) - - startTime = warmup.NextWarmupTime - } - - return nil -} - -// modifyDeposit updates the pool's staked liquidity and returns the new staked liquidity. -// updates when there is a change in the staked liquidity(tick cross, stake, unstake) -func (self *Pool) modifyDeposit(delta *i256.Int, currentTime int64, nextTick int32) *u256.Uint { - // update staker side pool info - lastStakedLiquidity := self.CurrentStakedLiquidity(currentTime) - deltaApplied := liquidityMathAddDelta(lastStakedLiquidity, delta) - result := self.updateGlobalRewardRatioAccumulation(currentTime, lastStakedLiquidity) - - // historical tick does NOT actually reflect the tick at the timestamp, but it provides correct ordering for the staked positions - // because TickCrossHook is assured to be called for the staked-initialized ticks - self.historicalTick.set(currentTime, nextTick) - - switch deltaApplied.Sign() { - case -1: - panic("stakedLiquidity is less than 0, should not happen") - case 0: - if lastStakedLiquidity.Sign() == 1 { - // StakedLiquidity moved from positive to zero, start unclaimable period - self.startUnclaimablePeriod(currentTime) - self.incentives.startUnclaimablePeriod(currentTime) - } - case 1: - if lastStakedLiquidity.Sign() == 0 { - // StakedLiquidity moved from zero to positive, end unclaimable period - self.endUnclaimablePeriod(currentTime) - self.incentives.endUnclaimablePeriod(currentTime) - } - } - - self.stakedLiquidity.set(currentTime, deltaApplied) - - return result -} - -// startUnclaimablePeriod starts the unclaimable period. -func (self *Pool) startUnclaimablePeriod(currentTime int64) { - if self.lastUnclaimableTime == 0 { - // We set only if it's the first time entering(0 indicates not set yet) - self.lastUnclaimableTime = currentTime - } -} - -// endUnclaimablePeriod ends the unclaimable period. -// Accumulates to unclaimableAcc and resets lastUnclaimableTime to 0. -func (self *Pool) endUnclaimablePeriod(currentTime int64) { - if self.lastUnclaimableTime == 0 { - // This should not happen, but guarding just in case - return - } - unclaimableDuration := currentTime - self.lastUnclaimableTime - self.unclaimableAcc += unclaimableDuration * self.CurrentReward(self.lastUnclaimableTime) - self.lastUnclaimableTime = 0 -} - -// processUnclaimableReward processes the unclaimable reward and returns the accumulated reward. -// It resets unclaimableAcc to 0 and updates lastUnclaimableTime to endTime. -func (self *Pool) processUnclaimableReward(poolTier *PoolTier, endTime int64) int64 { - internalUnClaimable := self.unclaimableAcc - self.unclaimableAcc = 0 - self.lastUnclaimableTime = endTime - return internalUnClaimable -} - -// Calculates reward for a position *without* considering debt or warmup -// It calculates the theoretical total reward for the position if it has been staked since the pool creation -func (self *Pool) CalculateRawRewardForPosition(currentTime int64, currentTick int32, deposit *Deposit) *u256.Uint { - var rewardAcc *u256.Uint - - globalAcc := self.calculateGlobalRewardRatioAccumulation(currentTime, self.CurrentStakedLiquidity(currentTime)) - lowerAcc := self.ticks.Get(deposit.tickLower).CurrentOutsideAccumulation(currentTime) - upperAcc := self.ticks.Get(deposit.tickUpper).CurrentOutsideAccumulation(currentTime) - if currentTick < deposit.tickLower { - rewardAcc = u256.Zero().Sub(lowerAcc, upperAcc) - } else if currentTick >= deposit.tickUpper { - rewardAcc = u256.Zero().Sub(upperAcc, lowerAcc) - } else { - rewardAcc = u256.Zero().Sub(globalAcc, lowerAcc) - rewardAcc = rewardAcc.Sub(rewardAcc, upperAcc) - } - - return rewardAcc -} - -// Calculate actual reward in [startTime, endTime) for a position by -// subtracting the startTime's raw reward from the endTime's raw reward -func (self *Pool) CalculateRewardForPosition( - startTime int64, - startTick int32, - endTime int64, - endTick int32, - deposit *Deposit, -) *u256.Uint { - rewardAcc := self.CalculateRawRewardForPosition(endTime, endTick, deposit) - debtAcc := self.CalculateRawRewardForPosition(startTime, startTick, deposit) - - return u256.Zero().Sub(rewardAcc, debtAcc) -} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_pool_tier.gno b/contract/r/gnoswap/v1/staker/reward_calculation_pool_tier.gno deleted file mode 100644 index e531590..0000000 --- a/contract/r/gnoswap/v1/staker/reward_calculation_pool_tier.gno +++ /dev/null @@ -1,326 +0,0 @@ -package staker - -import ( - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" -) - -const ( - AllTierCount = 4 // 0, 1, 2, 3 - Tier1 = 1 - Tier2 = 2 - Tier3 = 3 -) - -// 100%, 0%, 0% if no tier2 and tier3 -// 80%, 0%, 20% if no tier2 -// 70%, 30%, 0% if no tier3 -// 50%, 30%, 20% if has tier2 and tier3 -type TierRatio struct { - Tier1 uint64 - Tier2 uint64 - Tier3 uint64 -} - -// TierRatioFromCounts calculates the ratio distribution for each tier based on pool counts. -// -// Parameters: -// - tier1Count (uint64): Number of pools in tier 1. -// - tier2Count (uint64): Number of pools in tier 2. -// - tier3Count (uint64): Number of pools in tier 3. -// -// Returns: -// - TierRatio: The ratio distribution across tier 1, 2, and 3, scaled up by 100. -func TierRatioFromCounts(tier1Count, tier2Count, tier3Count uint64) TierRatio { - // tier1 always exists - if tier2Count == 0 && tier3Count == 0 { - return TierRatio{ - Tier1: 100, - Tier2: 0, - Tier3: 0, - } - } - if tier2Count == 0 { - return TierRatio{ - Tier1: 80, - Tier2: 0, - Tier3: 20, - } - } - if tier3Count == 0 { - return TierRatio{ - Tier1: 70, - Tier2: 30, - Tier3: 0, - } - } - return TierRatio{ - Tier1: 50, - Tier2: 30, - Tier3: 20, - } -} - -// Get returns the ratio(scaled up by 100) for the given tier. -func (self *TierRatio) Get(tier uint64) uint64 { - switch tier { - case Tier1: - return self.Tier1 - case Tier2: - return self.Tier2 - case Tier3: - return self.Tier3 - default: - panic(makeErrorWithDetails( - errInvalidPoolTier, ufmt.Sprintf("unsupported tier(%d)", tier))) - } -} - -// PoolTier manages pool counts, ratios, and rewards for different tiers. -// -// Fields: -// - membership: Tracks which tier a pool belongs to (poolPath -> blockNumber -> tier). -// -// Methods: -// - CurrentCount: Returns the current count of pools in a tier at a specific timestamp. -// - CurrentRatio: Returns the current ratio for a tier at a specific timestamp. -// - CurrentTier: Returns the tier of a specific pool at a given timestamp. -// - CurrentReward: Retrieves the reward for a tier at a specific timestamp. -// - changeTier: Updates the tier of a pool and recalculates ratios. -type PoolTier struct { - membership *avl.Tree // poolPath -> tier(1, 2, 3) - - tierRatio TierRatio - - counts [AllTierCount]uint64 - - lastRewardCacheTimestamp int64 - lastRewardCacheHeight int64 - - currentEmission int64 - - // returns current emission. - getEmission func() int64 - // Returns a list of halving timestamps and their emission amounts within the interval [start, end) in ascending order. - // The first return value is a list of timestamps where halving occurs. - // The second return value is a list of emission amounts corresponding to each halving timestamp. - getHalvingBlocksInRange func(start, end int64) ([]int64, []int64) -} - -// NewPoolTier creates a new PoolTier instance with single initial 1 tier pool. -// -// Parameters: -// - pools: The pool collection. -// - currentHeight: The current block height. -// - initialPoolPath: The path of the initial pool. -// - getEmission: A function that returns the current emission to the staker contract. -// - getHalvingBlocksInRange: A function that returns a list of halving blocks within the interval [start, end) in ascending order. -// -// Returns: -// - *PoolTier: The new PoolTier instance. -func NewPoolTier(pools *Pools, currentHeight int64, currentTime int64, initialPoolPath string, getEmission func() int64, getHalvingBlocksInRange func(start, end int64) ([]int64, []int64)) *PoolTier { - result := &PoolTier{ - membership: avl.NewTree(), - tierRatio: TierRatioFromCounts(1, 0, 0), - lastRewardCacheTimestamp: currentTime + 1, - lastRewardCacheHeight: currentHeight + 1, - getEmission: getEmission, - getHalvingBlocksInRange: getHalvingBlocksInRange, - currentEmission: getEmission(), - } - - pools.set(initialPoolPath, NewPool(initialPoolPath, currentTime+1)) - result.changeTier(currentHeight+1, currentTime+1, pools, initialPoolPath, 1) - return result -} - -// CurrentReward returns the current per-pool reward for the given tier. -func (self *PoolTier) CurrentReward(tier uint64) int64 { - currentEmission := self.getEmission() - tierRatio := int64(self.tierRatio.Get(tier)) - count := int64(self.CurrentCount(tier)) - - // Check for zero count to prevent division by zero - if count == 0 { - return 0 - } - - return currentEmission * tierRatio / count / 100 -} - -// CurrentCount returns the current count of pools in the given tier. -func (self *PoolTier) CurrentCount(tier uint64) int { - if tier >= AllTierCount { - return 0 - } - return int(self.counts[tier]) -} - -// CurrentAllTierCounts returns the current count of pools in each tier. -func (self *PoolTier) CurrentAllTierCounts() []uint64 { - out := make([]uint64, AllTierCount) - copy(out, self.counts[:]) - return out // returning snapshot -} - -// CurrentTier returns the tier of the given pool. -func (self *PoolTier) CurrentTier(poolPath string) (tier uint64) { - if tierI, ok := self.membership.Get(poolPath); !ok { - return 0 - } else { - tier, ok = tierI.(uint64) - if !ok { - panic("failed to cast tier to uint64") - } - return - } -} - -// changeTier updates the tier of a pool, recalculates ratios, and applies -// updated per-pool reward to each of the pools. -func (self *PoolTier) changeTier(currentHeight int64, currentTime int64, pools *Pools, poolPath string, nextTier uint64) { - self.cacheReward(currentHeight, currentTime, pools) - // same as prev. no need to update - currentTier := self.CurrentTier(poolPath) - if currentTier == nextTier { - // no change, return - return - } - - // decrement count from current tier if it exists - if currentTier > 0 { - if self.counts[currentTier] == 0 { - panic("counts underflow: removing from empty tier") - } - self.counts[currentTier]-- - } - - if nextTier == 0 { - // removed from the tier - self.membership.Remove(poolPath) - pool, ok := pools.Get(poolPath) - if !ok { - panic("changeTier: pool not found") - } - pool.cacheReward(currentTime, int64(0)) - } else { - // handle all move/add operations - self.membership.Set(poolPath, nextTier) - self.counts[nextTier]++ - } - - self.tierRatio = TierRatioFromCounts(self.counts[Tier1], self.counts[Tier2], self.counts[Tier3]) - currentEmission := self.getEmission() - - // Cache updated reward for each tiered pool - self.membership.Iterate("", "", func(key string, value any) bool { - pool, ok := pools.Get(key) - if !ok { - panic("changeTier: pool not found") - } - tier, ok := value.(uint64) - if !ok { - panic("failed to cast value to uint64") - } - - tierRatio := int64(self.tierRatio.Get(tier)) - tierCount := int64(self.counts[tier]) - if tierCount == 0 { - return false // Skip if no pools in tier - } - - poolReward := currentEmission * tierRatio / tierCount / 100 - pool.cacheReward(currentTime, poolReward) - return false - }) - - self.currentEmission = currentEmission -} - -// cacheReward MUST be called before calculating any position reward -// cacheReward updates the reward cache for each pools, accounting for any halving event in between the last cached height and the current height. -func (self *PoolTier) cacheReward(currentHeight int64, currentTimestamp int64, pools *Pools) { - lastTimestamp := self.lastRewardCacheTimestamp - - if currentTimestamp <= lastTimestamp { - // no need to check - return - } - - // find halving blocks in range - halvingTimestamps, halvingEmissions := self.getHalvingBlocksInRange(lastTimestamp, currentTimestamp) - - if len(halvingTimestamps) == 0 { - self.applyCacheToAllPools(pools, currentTimestamp, self.currentEmission) - self.lastRewardCacheTimestamp = currentTimestamp - return - } - - for i, hvTimestamp := range halvingTimestamps { - emission := halvingEmissions[i] - // caching: [lastTimestamp, hvTimestamp) - self.applyCacheToAllPools(pools, hvTimestamp, emission) - - // halve emissions when halvingBlock is reached - self.currentEmission = emission - } - - // remaining range [lastTimestamp, currentTimestamp) - self.applyCacheToAllPools(pools, currentTimestamp, self.currentEmission) - - // update lastRewardCacheHeight and currentEmission - self.lastRewardCacheTimestamp = currentTimestamp - self.lastRewardCacheHeight = currentHeight -} - -// applyCacheToAllPools applies the cached reward to all tiered pools. -func (self *PoolTier) applyCacheToAllPools(pools *Pools, currentTimestamp, emissionInThisInterval int64) { - // calculate denominator and number of pools in each tier - counts := self.CurrentAllTierCounts() - - // apply cache to all pools - self.membership.Iterate("", "", func(key string, value any) bool { - tierNum := value.(uint64) - pool, ok := pools.Get(key) - if !ok { - return false - } - - // Calculate real reward with overflow check - tierRatio := int64(self.tierRatio.Get(tierNum)) - tierCount := int64(counts[tierNum]) - - if tierCount == 0 { - return false // Skip if no pools in tier - } - - reward := emissionInThisInterval * tierRatio / 100 / tierCount - // accumulate the reward for the interval (startBlock to endBlock) in the Pool - pool.cacheInternalReward(currentTimestamp, reward) - return false - }) -} - -// IsInternallyIncentivizedPool returns true if the pool is in a tier. -func (self *PoolTier) IsInternallyIncentivizedPool(poolPath string) bool { - return self.CurrentTier(poolPath) > 0 -} - -func (self *PoolTier) CurrentRewardPerPool(poolPath string) int64 { - emission := self.getEmission() - counts := self.CurrentAllTierCounts() - tierNum := self.CurrentTier(poolPath) - - if tierNum == 0 { - return 0 // Pool not in any tier - } - - tierRatio := int64(self.tierRatio.Get(tierNum)) - tierCount := int64(counts[tierNum]) - - if tierCount == 0 { - return 0 // No pools in tier - } - - return emission * tierRatio / 100 / tierCount -} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_tick.gno b/contract/r/gnoswap/v1/staker/reward_calculation_tick.gno deleted file mode 100644 index 9797a10..0000000 --- a/contract/r/gnoswap/v1/staker/reward_calculation_tick.gno +++ /dev/null @@ -1,364 +0,0 @@ -package staker - -import ( - "strconv" - "strings" - - "gno.land/p/nt/avl" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - pl "gno.land/r/gnoswap/v1/pool" -) - -var ( - zeroUint256 = u256.Zero() - zeroInt256 = i256.Zero() -) - -// Global batch processor for current swap -var currentSwapBatch *SwapBatchProcessor - -// EncodeInt takes an int32 and returns a zero-padded decimal string -// with up to 10 digits for the absolute value. -// If the number is negative, the '-' sign comes first, followed by zeros, then digits. -func EncodeInt(num int32) string { - // Convert the absolute value to a decimal string. - absValue := int64(num) - isNegative := false - if num < 0 { - isNegative = true - absValue = -absValue // Safely negate into int64 to avoid overflow. - } - - s := strconv.FormatInt(absValue, 10) - - // Zero-pad to a total of 10 digits for the absolute value. - // (The '-' sign will be added later if needed.) - zerosNeeded := 10 - len(s) - if zerosNeeded < 0 { - zerosNeeded = 0 - } - - padded := strings.Repeat("0", zerosNeeded) + s - - // If the original number was negative, prepend '-'. - if isNegative { - return "-" + padded - } - return padded -} - -// Tick mapping for each pool -type Ticks struct { - tree *avl.Tree // int32 tickId -> tick -} - -func NewTicks() Ticks { - return Ticks{ - tree: avl.NewTree(), - } -} - -func (self *Ticks) Get(tickId int32) *Tick { - v, ok := self.tree.Get(EncodeInt(tickId)) - if !ok { - tick := &Tick{ - id: tickId, - stakedLiquidityGross: u256.Zero(), - stakedLiquidityDelta: i256.Zero(), - outsideAccumulation: NewUintTree(), - } - self.tree.Set(EncodeInt(tickId), tick) - return tick - } - - tick, ok := v.(*Tick) - if !ok { - panic("failed to cast value to *Tick") - } - return tick -} - -func (self *Ticks) set(tickId int32, tick *Tick) { - if tick.stakedLiquidityGross.IsZero() { - self.tree.Remove(EncodeInt(tickId)) - return - } - self.tree.Set(EncodeInt(tickId), tick) -} - -func (self *Ticks) Has(tickId int32) bool { - return self.tree.Has(EncodeInt(tickId)) -} - -// Tick represents the state of a specific tick in a pool. -// -// Fields: -// - id (int32): The ID of the tick. -// - stakedLiquidityGross (*u256.Uint): Total gross staked liquidity at this tick. -// - stakedLiquidityDelta (*i256.Int): Net change in staked liquidity at this tick. -// - outsideAccumulation (*UintTree): RewardRatioAccumulation outside the tick. -type Tick struct { - id int32 - - // conceptually equal with Pool.liquidityGross but only for the staked positions - stakedLiquidityGross *u256.Uint - - // conceptually equal with Pool.liquidityNet but only for the staked positions - stakedLiquidityDelta *i256.Int - - // currentOutsideAccumulation is the accumulation of the time / TotalStake outside the tick. - // It is calculated by subtracting the current tick's currentOutsideAccumulation from the global reward ratio accumulation. - outsideAccumulation *UintTree // timestamp -> *u256.Uint -} - -// CurrentOutsideAccumulation returns the latest outside accumulation for the tick -func (self *Tick) CurrentOutsideAccumulation(timestamp int64) *u256.Uint { - acc := u256.Zero() - self.outsideAccumulation.ReverseIterate(0, timestamp, func(key int64, value any) bool { - acc = value.(*u256.Uint) - return true - }) - if acc == nil { - acc = u256.Zero() - } - return acc -} - -// modifyDepositLower updates the tick's liquidity info by treating the deposit as a lower tick -func (self *Tick) modifyDepositLower(currentTime int64, liquidity *i256.Int) { - // update staker side tick info - self.stakedLiquidityGross = liquidityMathAddDelta(self.stakedLiquidityGross, liquidity) - if self.stakedLiquidityGross.Lt(zeroUint256) { - panic("stakedLiquidityGross is negative") - } - self.stakedLiquidityDelta = i256.Zero().Add(self.stakedLiquidityDelta, liquidity) -} - -// modifyDepositUpper updates the tick's liquidity info by treating the deposit as an upper tick -func (self *Tick) modifyDepositUpper(currentTime int64, liquidity *i256.Int) { - self.stakedLiquidityGross = liquidityMathAddDelta(self.stakedLiquidityGross, liquidity) - if self.stakedLiquidityGross.Lt(zeroUint256) { - panic("stakedLiquidityGross is negative") - } - self.stakedLiquidityDelta = i256.Zero().Sub(self.stakedLiquidityDelta, liquidity) -} - -// updateCurrentOutsideAccumulation updates the tick's outside accumulation -// It "flips" the accumulation's inside/outside by subtracting the current outside accumulation from the global accumulation -func (self *Tick) updateCurrentOutsideAccumulation(timestamp int64, acc *u256.Uint) { - currentOutsideAccumulation := self.CurrentOutsideAccumulation(timestamp) - newOutsideAccumulation := u256.Zero().Sub(acc, currentOutsideAccumulation) - self.outsideAccumulation.set(timestamp, newOutsideAccumulation) -} - -// SwapTickCross stores information about a tick cross during a swap -// This struct is used to accumulate tick cross events during a single swap transaction -// for batch processing to optimize gas usage and computational efficiency -type SwapTickCross struct { - tickId int32 // The tick index that was crossed - zeroForOne bool // Direction of the swap (true: token0->token1, false: token1->token0) - delta *i256.Int // Pre-calculated liquidity delta for this tick cross -} - -// SwapBatchProcessor processes tick crosses in batch for a swap -// This processor accumulates all tick crosses that occur during a single swap -// and processes them together at the end, reducing redundant calculations -// and state updates that would occur with individual tick processing -type SwapBatchProcessor struct { - poolPath string // The pool path identifier for this swap - pool *Pool // Reference to the pool being swapped in - crosses []SwapTickCross // Accumulated tick crosses during the swap - timestamp int64 // Timestamp when the swap started - isActive bool // Flag to prevent accumulation after swap ends -} - -// swapStartHook is called when a swap starts -// This hook initializes the batch processor for accumulating tick crosses -func swapStartHook(pools *Pools) func(poolPath string, timestamp int64) { - return func(poolPath string, timestamp int64) { - func(cur realm) { - pool, ok := pools.Get(poolPath) - if !ok { - return - } - - // Initialize batch processor for this swap - // This will accumulate all tick crosses until swap completion - currentSwapBatch = &SwapBatchProcessor{ - poolPath: poolPath, - pool: pool, - crosses: make([]SwapTickCross, 0), // Pre-allocate slice for tick crosses - timestamp: timestamp, - isActive: true, // Enable accumulation mode - } - }(cross) - } -} - -// swapEndHook is called when a swap ends -// This hook processes all accumulated tick crosses in a single batch operation -// and cleans up the batch processor. The batch processing approach provides: -// 1. O(1) pool state updates instead of O(n) where n = number of tick crosses -// 2. Reduced computational overhead for reward calculations -// 3. Atomic processing ensuring consistency across all tick updates -func swapEndHook(pools *Pools) func(poolPath string) error { - return func(poolPath string) error { - return func(cur realm) error { - // Validate batch processor state - if currentSwapBatch == nil || !currentSwapBatch.isActive || currentSwapBatch.poolPath != poolPath { - return nil - } - - // Disable further accumulation - currentSwapBatch.isActive = false - - // Process all accumulated tick crosses in a single batch - // This is where the optimization happens - instead of processing - // each tick cross individually, we calculate cumulative effects - err := processBatchedTickCrosses() - if err != nil { - return err - } - - // Clean up batch processor - currentSwapBatch = nil - - return nil - }(cross) - } -} - -// tickCrossHook is called when a tick is crossed -// This hook implements intelligent routing between batch processing and immediate processing: -// - During swaps: accumulates tick crosses for batch processing at swap end -// - Outside swaps: processes tick crosses immediately for real-time updates -// The hybrid approach optimizes for both swap performance and non-swap responsiveness -func tickCrossHook(pools *Pools) func(poolPath string, tickId int32, zeroForOne bool, timestamp int64) { - return func(poolPath string, tickId int32, zeroForOne bool, timestamp int64) { - func(cur realm) { - pool, ok := pools.Get(poolPath) - if !ok { - return - } - - tick := pool.ticks.Get(tickId) - // Skip ticks with zero staked liquidity (no reward impact) - if tick.stakedLiquidityDelta.Sign() == 0 { - return - } - - // Batch processing path: accumulate tick crosses during active swap - if currentSwapBatch != nil && currentSwapBatch.isActive && currentSwapBatch.poolPath == poolPath { - // Pre-calculate liquidity delta with direction consideration - // zeroForOne swap: liquidity delta is negated (liquidity being removed from current tick) - liquidityDelta := tick.stakedLiquidityDelta - if zeroForOne { - liquidityDelta = i256.Zero().Neg(liquidityDelta) - } - - // Accumulate this tick cross for batch processing - currentSwapBatch.crosses = append(currentSwapBatch.crosses, SwapTickCross{ - tickId: tickId, - zeroForOne: zeroForOne, - delta: liquidityDelta, // Store pre-calculated delta for efficiency - }) - return - } - - // Immediate processing path: handle tick crosses outside of swap context - // This ensures real-time updates for non-swap operations (e.g., position modifications) - processTickCrossImmediate(pool, tick, tickId, zeroForOne, timestamp) - }(cross) - } -} - -// processTickCrossImmediate processes a single tick cross immediately -// This function handles individual tick crosses for non-swap operations -// where batch processing is not applicable (e.g., position modifications, liquidations) -func processTickCrossImmediate(pool *Pool, tick *Tick, tickId int32, zeroForOne bool, timestamp int64) { - // Calculate the effective tick position after crossing - // For zeroForOne swaps, liquidity becomes effective one tick lower - nextTick := tickId - if zeroForOne { - nextTick-- // Move to the lower tick where liquidity becomes active - } - - // Calculate liquidity delta with direction consideration - liquidityDelta := tick.stakedLiquidityDelta - if zeroForOne { - // Negate delta for zeroForOne direction (liquidity being removed from current range) - liquidityDelta = i256.Zero().Neg(liquidityDelta) - } - - // Update pool's cumulative deposit with the liquidity change - newAcc := pool.modifyDeposit(liquidityDelta, timestamp, nextTick) - - // Update the tick's outside accumulation for reward calculations - // This ensures proper reward distribution tracking across tick boundaries - tick.updateCurrentOutsideAccumulation(timestamp, newAcc) -} - -// processBatchedTickCrosses processes all accumulated tick crosses at once -// This is the core optimization function that processes multiple tick crosses in a single operation. -// Instead of updating pool state for each tick cross individually (O(n) operations), -// it calculates the cumulative effect and applies it once (O(1) pool updates + O(n) tick updates). -func processBatchedTickCrosses() error { - // Early exit for empty batches - if currentSwapBatch == nil || len(currentSwapBatch.crosses) == 0 { - return nil - } - - // Validate pool reference - if currentSwapBatch.pool == nil { - return errPoolNotFound - } - - batch := currentSwapBatch - timestamp := batch.timestamp - - // Phase 1: Calculate cumulative liquidity delta across all tick crosses - // This replaces multiple individual pool updates with a single cumulative update - cumulativeDelta := i256.Zero() - for _, tickCross := range batch.crosses { - newDelta := cumulativeDelta.Add(cumulativeDelta, tickCross.delta) - cumulativeDelta = newDelta - } - - // Phase 2: Determine the effective tick position for pool state update - // Use the last crossed tick as the reference point for cumulative changes - lastCross := batch.crosses[len(batch.crosses)-1] - lastTick := lastCross.tickId - if lastCross.zeroForOne { - lastTick-- // Adjust for zeroForOne direction - } - - // Phase 3: Apply cumulative changes to pool state in a single operation - // This is the key optimization - one pool update instead of many - newAcc := batch.pool.modifyDeposit(cumulativeDelta, timestamp, lastTick) - - // Phase 4: Update individual tick outside accumulations for reward tracking - // While we optimize pool updates, each tick still needs its accumulation updated - // for proper reward distribution calculations - for _, tickCross := range batch.crosses { - tick := batch.pool.ticks.Get(tickCross.tickId) - tick.updateCurrentOutsideAccumulation(timestamp, newAcc) - } - - return nil -} - -func setHooks() { - // Set tick cross hook for pool contract - pl.SetTickCrossHook(cross, tickCrossHook(pools)) - - // Set swap start/end hooks for batch processing - pl.SetSwapStartHook(cross, swapStartHook(pools)) - pl.SetSwapEndHook(cross, swapEndHook(pools)) -} - -func init() { - setHooks() -} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_types.gno b/contract/r/gnoswap/v1/staker/reward_calculation_types.gno deleted file mode 100644 index 103c700..0000000 --- a/contract/r/gnoswap/v1/staker/reward_calculation_types.gno +++ /dev/null @@ -1,117 +0,0 @@ -package staker - -import ( - "strconv" - "strings" - - "gno.land/p/nt/avl" -) - -// EncodeUint converts a uint64 number into a zero-padded 20-character string. -// -// Parameters: -// - num (uint64): The number to encode. -// -// Returns: -// - string: A zero-padded string representation of the number. -// -// Example: -// Input: 12345 -// Output: "00000000000000012345" -func EncodeUint(num uint64) string { - // Convert the value to a decimal string. - s := strconv.FormatUint(num, 10) - - // Zero-pad to a total length of 20 characters. - zerosNeeded := 20 - len(s) - return strings.Repeat("0", zerosNeeded) + s -} - -func EncodeInt64(num int64) string { - s := strconv.FormatInt(num, 10) - zerosNeeded := 20 - len(s) - return strings.Repeat("0", zerosNeeded) + s -} - -// DecodeUint converts a zero-padded string back into a uint64 number. -// -// Parameters: -// - s (string): The zero-padded string. -// -// Returns: -// - uint64: The decoded number. -// -// Panics: -// - If the string cannot be parsed into a uint64. -// -// Example: -// Input: "00000000000000012345" -// Output: 12345 -func DecodeUint(s string) uint64 { - num, err := strconv.ParseUint(s, 10, 64) - if err != nil { - panic(err) - } - return num -} - -func DecodeInt64(s string) int64 { - num, err := strconv.ParseInt(s, 10, 64) - if err != nil { - panic(err) - } - return num -} - -// UintTree is a wrapper around an AVL tree for storing block timestamps as strings. -// Since block timestamps are defined as int64, we take int64 and convert it to uint64 for the tree. -// -// Methods: -// - Get: Retrieves a value associated with a uint64 key. -// - set: Stores a value with a uint64 key. -// - Has: Checks if a uint64 key exists in the tree. -// - remove: Removes a uint64 key and its associated value. -// - Iterate: Iterates over keys and values in a range. -// - ReverseIterate: Iterates in reverse order over keys and values in a range. -type UintTree struct { - tree *avl.Tree // blockTimestamp -> any -} - -// NewUintTree creates a new UintTree instance. -func NewUintTree() *UintTree { - return &UintTree{ - tree: avl.NewTree(), - } -} - -func (self *UintTree) Get(key int64) (any, bool) { - v, ok := self.tree.Get(EncodeInt64(key)) - if !ok { - return nil, false - } - return v, true -} - -func (self *UintTree) set(key int64, value any) { - self.tree.Set(EncodeInt64(key), value) -} - -func (self *UintTree) Has(key int64) bool { - return self.tree.Has(EncodeInt64(key)) -} - -func (self *UintTree) remove(key int64) { - self.tree.Remove(EncodeInt64(key)) -} - -func (self *UintTree) Iterate(start, end int64, fn func(key int64, value any) bool) { - self.tree.Iterate(EncodeInt64(start), EncodeInt64(end), func(key string, value any) bool { - return fn(DecodeInt64(key), value) - }) -} - -func (self *UintTree) ReverseIterate(start, end int64, fn func(key int64, value any) bool) { - self.tree.ReverseIterate(EncodeInt64(start), EncodeInt64(end), func(key string, value any) bool { - return fn(DecodeInt64(key), value) - }) -} diff --git a/contract/r/gnoswap/v1/staker/reward_calculation_warmup.gno b/contract/r/gnoswap/v1/staker/reward_calculation_warmup.gno deleted file mode 100644 index efbabb9..0000000 --- a/contract/r/gnoswap/v1/staker/reward_calculation_warmup.gno +++ /dev/null @@ -1,118 +0,0 @@ -package staker - -import ( - "math" - - "gno.land/p/nt/ufmt" - u256 "gno.land/p/gnoswap/uint256" -) - -type Warmup struct { - Index int - TimeDuration int64 - NextWarmupTime int64 // time when this warmup period ends - WarmupRatio uint64 -} - -// warmupTemplate defines the warmup periods for staking rewards. -// These parameters can be modified through governance via SetWarmUp. -var warmupTemplate []Warmup = DefaultWarmupTemplate() - -func DefaultWarmupTemplate() []Warmup { - secondsInDay := int64(86400) - secondsIn5Days := int64(5 * secondsInDay) - secondsIn10Days := int64(10 * secondsInDay) - secondsIn30Days := int64(30 * secondsInDay) - - // NextWarmupTime is set to 0 for template. - // They will be set by InstantiateWarmup() - return []Warmup{ - { - Index: 0, - TimeDuration: secondsIn5Days, - // NextWarmupTime will be set based on currentTime - // NextWarmupTime: currentTime + secondsIn5Days, - WarmupRatio: 30, - }, - { - Index: 1, - TimeDuration: secondsIn10Days, - // NextWarmupTime will be set based on currentTime - // NextWarmupTime: currentTime + secondsIn10Days, - WarmupRatio: 50, - }, - { - Index: 2, - TimeDuration: secondsIn30Days, - // NextWarmupTime will be set based on currentTime - // NextWarmupTime: currentTime + secondsIn30Days, - WarmupRatio: 70, - }, - { - Index: 3, - TimeDuration: math.MaxInt64, - // NextWarmupTime will be set to math.MaxInt64 - // NextWarmupTime: math.MaxInt64, - WarmupRatio: 100, - }, - } -} - -// expected to be called by governance -func modifyWarmup(index int, timeDuration int64) { - if index >= len(warmupTemplate) { - panic(ufmt.Sprintf("index(%d) is out of range", index)) - } - - warmupTemplate[index].TimeDuration = timeDuration -} - -func instantiateWarmup(currentTime int64) []Warmup { - warmups := make([]Warmup, 0) - for _, warmup := range warmupTemplate { - nextWarmupTime := currentTime + warmup.TimeDuration - if nextWarmupTime < 0 { - nextWarmupTime = math.MaxInt64 - } - - warmups = append(warmups, Warmup{ - Index: warmup.Index, - TimeDuration: warmup.TimeDuration, - NextWarmupTime: nextWarmupTime, - WarmupRatio: warmup.WarmupRatio, - }) - currentTime += warmup.TimeDuration - } - return warmups -} - -func (warmup *Warmup) apply(poolReward int64, positionLiquidity, stakedLiquidity *u256.Uint) (int64, int64) { - if stakedLiquidity.IsZero() { - return 0, 0 - } - - divisor := u256.NewUint(100) - poolRewardUint := u256.NewUintFromInt64(poolReward) - perPositionReward := u256.Zero().Mul(poolRewardUint, positionLiquidity) - perPositionReward = u256.Zero().Div(perPositionReward, stakedLiquidity) - rewardRatio := u256.NewUint(warmup.WarmupRatio) - penaltyRatio := u256.NewUint(100 - warmup.WarmupRatio) - totalReward := u256.Zero().Mul(perPositionReward, rewardRatio) - totalReward = u256.Zero().Div(totalReward, divisor) - totalPenalty := u256.Zero().Mul(perPositionReward, penaltyRatio) - totalPenalty = u256.Zero().Div(totalPenalty, divisor) - return safeConvertToInt64(totalReward), safeConvertToInt64(totalPenalty) -} - -func (self *Deposit) FindWarmup(currentTime int64) int { - for i, warmup := range self.warmups { - if currentTime < warmup.NextWarmupTime { - return i - } - } - return len(self.warmups) - 1 -} - -func (self *Deposit) GetWarmup(index int) Warmup { - return self.warmups[index] -} diff --git a/contract/r/gnoswap/v1/staker/staker.gno b/contract/r/gnoswap/v1/staker/staker.gno deleted file mode 100644 index 8ace704..0000000 --- a/contract/r/gnoswap/v1/staker/staker.gno +++ /dev/null @@ -1,789 +0,0 @@ -package staker - -import ( - "std" - "time" - - "gno.land/p/nt/avl" - "gno.land/p/nt/ufmt" - - prbac "gno.land/p/gnoswap/rbac" - "gno.land/r/gnoswap/halt" - "gno.land/r/gnoswap/v1/common" - - "gno.land/r/gnoswap/gns" - "gno.land/r/gnoswap/v1/gnft" - - en "gno.land/r/gnoswap/emission" - pl "gno.land/r/gnoswap/v1/pool" - pn "gno.land/r/gnoswap/v1/position" - - i256 "gno.land/p/gnoswap/int256" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/referral" -) - -const ZERO_ADDRESS = std.Address("") - -var ( - stakerAddr = getRoleAddress(prbac.ROLE_STAKER) - devOpsAddr = getRoleAddress(prbac.ROLE_DEVOPS) - communityPoolAddr = getRoleAddress(prbac.ROLE_COMMUNITY_POOL) - govStakerAddr = getRoleAddress(prbac.ROLE_GOV_STAKER) - protocolFeeAddr = getRoleAddress(prbac.ROLE_PROTOCOL_FEE) - adminAddr = getRoleAddress(prbac.ROLE_ADMIN) - positionAddr = getRoleAddress(prbac.ROLE_POSITION) -) - -// Deposits manages all staked positions. -type Deposits struct { - tree *avl.Tree -} - -// NewDeposits creates a new Deposits instance. -func NewDeposits() *Deposits { - return &Deposits{ - tree: avl.NewTree(), // positionId -> *Deposit - } -} - -// Has checks if a position ID exists in deposits. -func (self *Deposits) Has(positionId uint64) bool { - return self.tree.Has(EncodeUint(positionId)) -} - -// Iterate traverses deposits within the specified range. -func (self *Deposits) Iterate(start uint64, end uint64, fn func(positionId uint64, deposit *Deposit) bool) { - self.tree.Iterate(EncodeUint(start), EncodeUint(end), func(positionId string, depositI any) bool { - dpst := retrieveDeposit(depositI) - return fn(DecodeUint(positionId), dpst) - }) -} - -// Size returns the number of deposits. -func (self *Deposits) Size() int { - return self.tree.Size() -} - -// get retrieves a deposit by position ID. -func (self *Deposits) get(positionId uint64) *Deposit { - depositI, ok := self.tree.Get(EncodeUint(positionId)) - if !ok { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("positionId(%d) not found", positionId), - )) - } - return retrieveDeposit(depositI) -} - -// retrieveDeposit safely casts data to Deposit type. -func retrieveDeposit(data any) *Deposit { - deposit, ok := data.(*Deposit) - if !ok { - panic("failed to cast value to *Deposit") - } - return deposit -} - -// set stores a deposit for a position ID. -func (self *Deposits) set(positionId uint64, deposit *Deposit) { - self.tree.Set(EncodeUint(positionId), deposit) -} - -// remove deletes a deposit by position ID. -func (self *Deposits) remove(positionId uint64) { - self.tree.Remove(EncodeUint(positionId)) -} - -// ExternalIncentives manages external incentive programs. -type ExternalIncentives struct { - tree *avl.Tree -} - -// NewExternalIncentives creates a new ExternalIncentives instance. -func NewExternalIncentives() *ExternalIncentives { - return &ExternalIncentives{ - tree: avl.NewTree(), - } -} - -// Has checks if an incentive ID exists. -func (self *ExternalIncentives) Has(incentiveId string) bool { return self.tree.Has(incentiveId) } - -// Size returns the number of external incentives. -func (self *ExternalIncentives) Size() int { return self.tree.Size() } - -// get retrieves an external incentive by ID. -func (self *ExternalIncentives) get(incentiveId string) *ExternalIncentive { - incentiveI, ok := self.tree.Get(incentiveId) - if !ok { - panic(makeErrorWithDetails( - errDataNotFound, - ufmt.Sprintf("incentiveId(%s) not found", incentiveId), - )) - } - - incentive, ok := incentiveI.(*ExternalIncentive) - if !ok { - panic("failed to cast value to *ExternalIncentive") - } - return incentive -} - -// set stores an external incentive. -func (self *ExternalIncentives) set(incentiveId string, incentive *ExternalIncentive) { - self.tree.Set(incentiveId, incentive) -} - -// remove deletes an external incentive by ID. -func (self *ExternalIncentives) remove(incentiveId string) { - self.tree.Remove(incentiveId) -} - -// Stakers manages deposits by staker address. -type Stakers struct { - tree *avl.Tree // address -> depositId -> *Deposit -} - -// NewStakers creates a new Stakers instance. -func NewStakers() *Stakers { - return &Stakers{ - tree: avl.NewTree(), - } -} - -// IterateAll traverses all deposits for a specific address. -func (self *Stakers) IterateAll(address std.Address, fn func(depositId uint64, deposit *Deposit) bool) { - depositTreeI, ok := self.tree.Get(address.String()) - if !ok { - return - } - depositTree := retrieveDepositTree(depositTreeI) - depositTree.Iterate("", "", func(depositId string, depositI any) bool { - deposit, ok := depositI.(*Deposit) - if !ok { - panic("failed to cast value to *Deposit") - } - return fn(DecodeUint(depositId), deposit) - }) -} - -// addDeposit adds a deposit for a staker address. -func (self *Stakers) addDeposit(address std.Address, depositId uint64, deposit *Deposit) { - depositTreeI, ok := self.tree.Get(address.String()) - if !ok { - depositTree := avl.NewTree() - self.tree.Set(address.String(), depositTree) - depositTreeI = depositTree - } - - depositTree := retrieveDepositTree(depositTreeI) - depositTree.Set(EncodeUint(depositId), deposit) -} - -// removeDeposit removes a deposit for a staker address. -func (self *Stakers) removeDeposit(address std.Address, depositId uint64) { - depositTreeI, ok := self.tree.Get(address.String()) - if !ok { - return - } - - depositTree := retrieveDepositTree(depositTreeI) - depositTree.Remove(EncodeUint(depositId)) -} - -// retrieveDepositTree safely casts data to AVL tree type. -func retrieveDepositTree(data any) *avl.Tree { - depositTree, ok := data.(*avl.Tree) - if !ok { - panic("failed to cast depositTree to *avl.Tree") - } - return depositTree -} - -var ( - // deposits stores deposit information for each positionId - deposits *Deposits = NewDeposits() - - // externalIncentives stores external incentive information for each incentiveId - externalIncentives *ExternalIncentives = NewExternalIncentives() - - // stakers stores staker information for each address - stakers *Stakers = NewStakers() - - // poolTier stores pool tier information - poolTier *PoolTier - - // totalEmissionSent is the total amount of GNS emission sent from staker to user(and community pool if penalty exists) - // which includes following - // 1. reward sent to user (which also includes protocol_fee) - // 2. penalty sent to community pool - // 3. unclaimable reward - totalEmissionSent int64 -) - -const ( - TIMESTAMP_90DAYS = int64(7776000) - TIMESTAMP_180DAYS = int64(15552000) - TIMESTAMP_365DAYS = int64(31536000) - - MAX_UNIX_EPOCH_TIME = 253402300799 // 9999-12-31 23:59:59 - - MUST_EXISTS_IN_TIER_1 = "gno.land/r/gnoland/wugnot:gno.land/r/gnoswap/gns:3000" - - INTERNAL = true - EXTERNAL = false -) - -// init initializes the staker contract with tier 1 pool. -func init() { - // Initialize tier 1 with GNOT:GNS 0.3% pool - - pools.GetOrCreate(MUST_EXISTS_IN_TIER_1) - - poolTier = NewPoolTier( - pools, - std.ChainHeight(), - time.Now().Unix(), - MUST_EXISTS_IN_TIER_1, - en.GetStakerEmissionAmountPerSecond, - en.GetStakerEmissionAmountPerSecondInRange, - ) -} - -// StakeToken stakes an LP position NFT to earn rewards. -// -// Transfers position NFT to staker and begins reward accumulation. -// Eligible for internal incentives (GNS emission) and external rewards. -// Position must have liquidity and be in eligible pool tier. -// -// Parameters: -// - positionId: LP position NFT token ID to stake -// - referrer: Optional referral address for tracking -// -// Returns: -// - poolPath: Pool identifier (token0:token1:fee) -// - token0Amount: Current token0 balance in position -// - token1Amount: Current token1 balance in position -// -// Requirements: -// - Caller must own the position NFT -// - Position must have active liquidity -// - Pool must be in tier 1, 2, or 3 -// - Position not already staked -// -// Note: Out-of-range positions earn no rewards but can be staked. -func StakeToken(cur realm, positionId uint64, referrer string) (string, string, string) { - halt.AssertIsNotHaltedStaker() - - assertIsNotStaked(positionId) - - en.MintAndDistributeGns(cross) - - previousRealm := std.PreviousRealm() - caller := previousRealm.Address() - owner := gnft.MustOwnerOf(positionIdFrom(positionId)) - currentTime := time.Now().Unix() - - success := referral.TryRegister(cross, caller, referrer) - actualReferrer := referrer - if !success { - actualReferrer = referral.GetReferral(caller.String()) - } - - token0Amount, token1Amount, err := getPositionStakeTokenAmount(positionId, owner, caller) - if err != nil { - panic(err.Error()) - } - - // check pool path from positionId - poolPath := pn.PositionGetPositionPoolKey(positionId) - pool, ok := pools.Get(poolPath) - if !ok { - panic(makeErrorWithDetails( - errNonIncentivizedPool, - ufmt.Sprintf("can not stake position to non existing pool(%s)", poolPath), - )) - } - liquidity := getLiquidity(positionId) - - tickLower, tickUpper := getTickOf(positionId) - - // staked status - deposit := &Deposit{ - owner: caller, - stakeTimestamp: currentTime, - stakeTime: currentTime, - targetPoolPath: poolPath, - tickLower: tickLower, - tickUpper: tickUpper, - liquidity: liquidity, - lastCollectTime: currentTime, - warmups: instantiateWarmup(currentTime), - } - - currentTick := pl.GetSlot0Tick(poolPath) - - deposits.set(positionId, deposit) - stakers.addDeposit(caller, positionId, deposit) - - // transfer NFT ownership to staker contract - if err := transferDeposit(positionId, owner, caller, stakerAddr); err != nil { - panic(err.Error()) - } - - // after transfer, set caller(user) as position operator (to collect fee and reward) - pn.SetPositionOperator(cross, positionId, caller) - - signedLiquidity := i256.FromUint256(liquidity) - isInRange := false - poolTier.cacheReward(currentTime, currentTime, pools) - - if pn.PositionIsInRange(positionId) { - isInRange = true - pool.modifyDeposit(signedLiquidity, currentTime, currentTick) - } - // historical tick must be set regardless of the deposit's range - pool.historicalTick.set(currentTime, currentTick) - - // This could happen because of how position stores the ticks. - // Ticks are negated if the token1 < token0. - upperTick := pool.ticks.Get(tickUpper) - lowerTick := pool.ticks.Get(tickLower) - - upperTick.modifyDepositUpper(currentTime, signedLiquidity) - lowerTick.modifyDepositLower(currentTime, signedLiquidity) - - std.Emit( - "StakeToken", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "positionId", formatUint(positionId), - "poolPath", poolPath, - "amount0", token0Amount, - "amount1", token1Amount, - "liquidity", liquidity.ToString(), - "positionUpperTick", formatAnyInt(tickUpper), - "positionLowerTick", formatAnyInt(tickLower), - "currentTick", formatAnyInt(currentTick), - "isInRange", formatBool(isInRange), - "referrer", actualReferrer, - ) - - return poolPath, token0Amount, token1Amount -} - -// getPositionStakeTokenAmount validates staking requirements and returns token amounts. -func getPositionStakeTokenAmount(positionId uint64, owner, caller std.Address) (string, string, error) { - exist := deposits.Has(positionId) - if exist { - return "", "", errAlreadyStaked - } - - if err := hasTokenOwnership(owner, caller); err != nil { - return "", "", err - } - - if err := tokenHasLiquidity(positionId); err != nil { - return "", "", err - } - - poolPath := pn.PositionGetPositionPoolKey(positionId) - if err := poolHasIncentives(poolPath); err != nil { - return "", "", err - } - - token0Amount, token1Amount := getTokenPairBalanceFromPosition(poolPath, positionId) - - return token0Amount, token1Amount, nil -} - -// transferDeposit transfers deposit ownership to a new address. -// -// Manages NFT custody during staking operations. -// Transfers ownership to staker contract for reward eligibility. -// Handles special cases for mint-and-stake operations. -// -// Parameters: -// - positionId: The ID of the position NFT to transfer -// - owner: The current owner of the position -// - caller: The entity initiating the transfer -// - to: The recipient address (usually staker contract) -// -// Security Features: -// - Prevents self-transfer exploits -// - Validates ownership before transfer -// - Atomic operation with staking -// - No transfer if owner == to (mint & stake case) -// -// Returns: -// - nil: If owner and recipient are same (mint-and-stake) -// - error: If caller unauthorized or transfer fails -// -// NFT remains locked in staker until unstaking. -// Otherwise delegates the transfer to `gnft.TransferFrom`. -func transferDeposit(positionId uint64, owner, caller, to std.Address) error { - // if owner is the same as to, when mint and stake, it will be the same address - if owner == to { - return nil - } - - if caller == to { - return ufmt.Errorf( - "%v: only owner(%s) can transfer positionId(%d), called from %s", - errNoPermission, owner, positionId, caller, - ) - } - - // transfer NFT ownership - return gnft.TransferFrom(cross, owner, to, positionIdFrom(positionId)) -} - -// CollectReward harvests accumulated rewards for a staked position. This includes both -// internal GNS emission and external incentive rewards. -// -// State Transition: -// 1. Warm-up amounts are clears for both internal and external rewards -// 2. Reward tokens are transferred to the owner -// 3. Penalty fees are transferred to protocol/community addresses -// 4. GNS balance is recalculated -// -// Requirements: -// - Contract must not be halted -// - Caller must be the position owner -// - Position must be staked (have a deposit record) -// -// Parameters: -// CollectReward claims accumulated rewards without unstaking. -// -// Parameters: -// - positionId: LP position NFT token ID -// - unwrapResult: if true, unwraps WUGNOT to GNOT -// -// Returns poolPath, gnsAmount, externalRewards map, externalPenalties map. -func CollectReward(cur realm, positionId uint64, unwrapResult bool) (string, string, map[string]int64, map[string]int64) { - caller := std.PreviousRealm().Address() - halt.AssertIsNotHaltedStaker() - halt.AssertIsNotHaltedWithdraw() - - assertIsDepositor(caller, positionId) - - deposit := deposits.get(positionId) - - en.MintAndDistributeGns(cross) - - currentTime := time.Now().Unix() - blockHeight := std.ChainHeight() - previousRealm := std.PreviousRealm() - // get all internal and external rewards - reward := calcPositionReward(blockHeight, currentTime, positionId) - - // update lastCollectTime to current time - deposit.lastCollectTime = currentTime - - // transfer external rewards to user - externalReward := reward.External - toUserExternalReward := make(map[string]int64) - toUserExternalPenalty := make(map[string]int64) - for incentiveId, rewardAmount := range externalReward { - incentive := externalIncentives.get(incentiveId).Clone() - if !incentive.IsStarted(currentTime) { - continue - } - - if incentive.rewardAmount < rewardAmount { - // Emit event for insufficient reward and skip this incentive - std.Emit( - "InsufficientExternalReward", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "positionId", formatUint(positionId), - "incentiveId", incentiveId, - "requiredAmount", formatInt(rewardAmount), - "availableAmount", formatInt(incentive.rewardAmount), - "currentTime", formatInt(currentTime), - "currentHeight", formatInt(blockHeight), - ) - continue - } - - // process external reward to user - incentive.rewardAmount = safeSubInt64(incentive.rewardAmount, rewardAmount) - rewardToken := incentive.rewardToken - toUserExternalReward[rewardToken] = safeAddInt64(toUserExternalReward[rewardToken], rewardAmount) - toUser, feeAmount, err := handleUnStakingFee(rewardToken, rewardAmount, false, positionId, incentive.targetPoolPath) - if err != nil { - panic(err.Error()) - } - - std.Emit( - "ProtocolFeeExternalReward", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "fromPositionId", formatUint(positionId), - "fromPoolPath", incentive.targetPoolPath, - "feeTokenPath", rewardToken, - "feeAmount", formatInt(feeAmount), - "currentTime", formatInt(currentTime), - "currentHeight", formatInt(blockHeight), - ) - if toUser > 0 { - if unwrapResult { - tErr := unwrapWithTransfer(deposit.owner, toUser) - if tErr != nil { - panic(tErr) - } - } else { - common.Transfer(cross, rewardToken, deposit.owner, toUser) - } - } - - // process external penalty - externalPenalty := reward.ExternalPenalty[incentiveId] - incentive.rewardAmount = safeSubInt64(incentive.rewardAmount, externalPenalty) - incentive.rewardLeft = safeAddInt64(incentive.rewardLeft, externalPenalty) - toUserExternalPenalty[rewardToken] = safeAddInt64(toUserExternalPenalty[rewardToken], externalPenalty) - - // update - externalIncentives.set(incentiveId, incentive) - - std.Emit( - "CollectReward", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "positionId", formatUint(positionId), - "poolPath", deposit.targetPoolPath, - "recipient", deposit.owner.String(), - "incentiveId", incentiveId, - "rewardToken", rewardToken, - "rewardAmount", formatInt(rewardAmount), - "rewardToUser", formatInt(toUser), - "rewardToFee", formatInt(rewardAmount-toUser), - "rewardPenalty", formatInt(externalPenalty), - "isRequestUnwrap", formatBool(unwrapResult), - "currentTime", formatInt(currentTime), - "currentHeight", formatInt(blockHeight), - ) - } - - communityPoolAddr := getRoleAddress(prbac.ROLE_COMMUNITY_POOL) - - // internal reward to user - toUser, feeAmount, err := handleUnStakingFee(GNS_PATH, reward.Internal, true, positionId, deposit.targetPoolPath) - if err != nil { - panic(err.Error()) - } - - std.Emit( - "ProtocolFeeInternalReward", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "fromPositionId", formatUint(positionId), - "fromPoolPath", deposit.targetPoolPath, - "feeTokenPath", GNS_PATH, - "feeAmount", formatInt(feeAmount), - ) - - if toUser > 0 { - // internal reward to user - totalEmissionSent = safeAddInt64(totalEmissionSent, toUser) - gns.Transfer(cross, deposit.owner, toUser) - - // internal penalty to community pool - totalEmissionSent = safeAddInt64(totalEmissionSent, reward.InternalPenalty) - gns.Transfer(cross, communityPoolAddr, reward.InternalPenalty) - } - - unClaimableInternal := processUnClaimableReward(deposit.targetPoolPath, currentTime) - if unClaimableInternal > 0 { - // internal unClaimable to community pool - totalEmissionSent = safeAddInt64(totalEmissionSent, unClaimableInternal) - gns.Transfer(cross, communityPoolAddr, unClaimableInternal) - } - - rewardToUser := formatInt(toUser) - rewardPenalty := formatInt(reward.InternalPenalty) - - std.Emit( - "CollectReward", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "positionId", formatUint(positionId), - "poolPath", deposit.targetPoolPath, - "recipient", deposit.owner.String(), - "rewardToken", GNS_PATH, - "rewardAmount", formatInt(reward.Internal), - "rewardToUser", rewardToUser, - "rewardToFee", formatInt(reward.Internal-toUser), - "rewardPenalty", rewardPenalty, - "rewardUnClaimableAmount", formatInt(unClaimableInternal), - "currentTime", formatInt(currentTime), - ) - - return rewardToUser, rewardPenalty, toUserExternalReward, toUserExternalPenalty -} - -// UnStakeToken withdraws an LP token from staking, collecting all pending rewards -// and returning the token to its original owner. -// -// Parameters: -// - positionId: LP position NFT token ID to unstake -// - unwrapResult: Convert WUGNOT to GNOT if true -// -// Process: -// 1. Collects all pending rewards (GNS + external) -// 2. Transfers NFT ownership back to original owner -// 3. Clears position operator rights -// 4. Removes from reward tracking systems -// 5. Cleans up all staking metadata -// -// Returns: -// - poolPath: Pool identifier where position was staked -// - token0Amount: Current token0 balance in position -// - token1Amount: Current token1 balance in position -// -// Requirements: -// - Caller must be the depositor -// - Position must be currently staked -func UnStakeToken(cur realm, positionId uint64, unwrapResult bool) (string, string, string) { // poolPath, token0Amount, token1Amount - caller := std.PreviousRealm().Address() - halt.AssertIsNotHaltedStaker() - halt.AssertIsNotHaltedWithdraw() - assertIsDepositor(caller, positionId) - - deposit := deposits.get(positionId) - - // unStaked status - poolPath := deposit.targetPoolPath - - // claim All Rewards - CollectReward(cur, positionId, unwrapResult) - token0Amount, token1Amount := getTokenPairBalanceFromPosition(poolPath, positionId) - - if err := applyUnStake(positionId); err != nil { - panic(err) - } - - // transfer NFT ownership to origin owner - gnft.TransferFrom(cross, stakerAddr, deposit.owner, positionIdFrom(positionId)) - pn.SetPositionOperator(cross, positionId, ZERO_ADDRESS) - - previousRealm := std.PreviousRealm() - std.Emit( - "UnStakeToken", - "prevAddr", previousRealm.Address().String(), - "prevRealm", previousRealm.PkgPath(), - "positionId", formatUint(positionId), - "poolPath", poolPath, - "isRequestUnwrap", formatBool(unwrapResult), - "from", stakerAddr.String(), - "to", deposit.owner.String(), - "amount0", token0Amount, - "amount1", token1Amount, - ) - - return poolPath, token0Amount, token1Amount -} - -func applyUnStake(positionId uint64) error { - deposit := deposits.get(positionId) - pool, ok := pools.Get(deposit.targetPoolPath) - if !ok { - return ufmt.Errorf( - "%v: pool(%s) does not exist", - errDataNotFound, deposit.targetPoolPath, - ) - } - - currentTime := time.Now().Unix() - currentTick := pl.GetSlot0Tick(deposit.targetPoolPath) - signedLiquidity := i256.Zero().Neg(i256.FromUint256(deposit.liquidity)) - if pn.PositionIsInRange(positionId) { - pool.modifyDeposit(signedLiquidity, currentTime, currentTick) - } - - upperTick := pool.ticks.Get(deposit.tickUpper) - lowerTick := pool.ticks.Get(deposit.tickLower) - upperTick.modifyDepositUpper(currentTime, signedLiquidity) - lowerTick.modifyDepositLower(currentTime, signedLiquidity) - - deposits.remove(positionId) - stakers.removeDeposit(deposit.owner, positionId) - - owner := gnft.MustOwnerOf(positionIdFrom(positionId)) - caller := std.PreviousRealm().Address() - - _, _, err := getPositionStakeTokenAmount(positionId, owner, caller) - if err != nil { - return err - } - - return nil -} - -// hasTokenOwnership validates that the caller has permission to operate the token. -func hasTokenOwnership(owner, caller std.Address) error { - isCallerOwner := owner == caller - isStakerOwner := owner == stakerAddr - - if !isCallerOwner && !isStakerOwner { - return errNoPermission - } - - return nil -} - -// poolHasIncentives checks if the pool has any active incentives (internal or external). -func poolHasIncentives(poolPath string) error { - pool, ok := pools.Get(poolPath) - if !ok { - return ufmt.Errorf( - "%v: can not stake position to non existent pool(%s)", - errNonIncentivizedPool, poolPath, - ) - } - hasInternal := poolTier.IsInternallyIncentivizedPool(poolPath) - hasExternal := pool.IsExternallyIncentivizedPool() - if hasInternal == false && hasExternal == false { - return ufmt.Errorf( - "%v: can not stake position to non incentivized pool(%s)", - errNonIncentivizedPool, poolPath, - ) - } - return nil -} - -// tokenHasLiquidity checks if the target positionId has non-zero liquidity -func tokenHasLiquidity(positionId uint64) error { - liquidity := getLiquidity(positionId) - - if liquidity.Lte(u256.Zero()) { - return ufmt.Errorf( - "%v: positionId(%d) has no liquidity", - errZeroLiquidity, positionId, - ) - } - return nil -} - -func getLiquidity(positionId uint64) *u256.Uint { - liq := pn.PositionGetPositionLiquidityStr(positionId) - return u256.MustFromDecimal(liq) -} - -func getTokenPairBalanceFromPosition(poolPath string, positionId uint64) (string, string) { - position := pn.MustGetPosition(positionId) - - return position.Token0Balance().ToString(), position.Token1Balance().ToString() -} - -func getTickOf(positionId uint64) (int32, int32) { - tickLower := pn.PositionGetPositionTickLower(positionId) - tickUpper := pn.PositionGetPositionTickUpper(positionId) - if tickUpper < tickLower { - panic(ufmt.Sprintf("tickUpper(%d) is less than tickLower(%d)", tickUpper, tickLower)) - } - return tickLower, tickUpper -} diff --git a/contract/r/gnoswap/v1/staker/type.gno b/contract/r/gnoswap/v1/staker/type.gno deleted file mode 100644 index 6732b66..0000000 --- a/contract/r/gnoswap/v1/staker/type.gno +++ /dev/null @@ -1,204 +0,0 @@ -package staker - -import ( - "math" - "std" - - u256 "gno.land/p/gnoswap/uint256" -) - -// ExternalIncentive is a struct for storing external incentive information. -type ExternalIncentive struct { - incentiveId string // incentive id - startTimestamp int64 // start time for external reward - endTimestamp int64 // end time for external reward - createdHeight int64 // block height when the incentive was created - depositGnsAmount int64 // deposited gns amount - targetPoolPath string // external reward target pool path - rewardToken string // external reward token path - rewardAmount int64 // total reward amount - rewardLeft int64 // remaining reward amount - rewardPerSecond int64 // reward per second - refundee std.Address // refundee address - - unclaimableRefunded bool // whether unclaimable reward is refunded -} - -func (e ExternalIncentive) IsStarted(currentTimestamp int64) bool { - return currentTimestamp >= e.startTimestamp -} - -// safeMulInt64 performs safe multiplication of int64 values, panicking on overflow -func safeMulInt64(a, b int64) int64 { - if a == 0 || b == 0 { - return 0 - } - if a > 0 && b > 0 { - if a > math.MaxInt64/b { - panic("int64 multiplication overflow") - } - } else if a < 0 && b < 0 { - if a < math.MaxInt64/b { - panic("int64 multiplication overflow") - } - } else if a > 0 && b < 0 { - if b < math.MinInt64/a { - panic("int64 multiplication underflow") - } - } else { // a < 0 && b > 0 - if a < math.MinInt64/b { - panic("int64 multiplication underflow") - } - } - return a * b -} - -// safeAddInt64 performs safe addition of int64 values, panicking on overflow -func safeAddInt64(a, b int64) int64 { - if a > 0 && b > math.MaxInt64-a { - panic("int64 addition overflow") - } - if a < 0 && b < math.MinInt64-a { - panic("int64 addition underflow") - } - return a + b -} - -// safeSubInt64 performs safe subtraction of int64 values, panicking on underflow -func safeSubInt64(a, b int64) int64 { - if b > 0 && a < math.MinInt64+b { - panic("int64 subtraction underflow") - } - if b < 0 && a > math.MaxInt64+b { - panic("int64 subtraction overflow") - } - return a - b -} - -func (e ExternalIncentive) StartTimestamp() int64 { - return e.startTimestamp -} - -func (e ExternalIncentive) EndTimestamp() int64 { - return e.endTimestamp -} - -func (e ExternalIncentive) RewardToken() string { - return e.rewardToken -} - -func (e ExternalIncentive) RewardAmount() int64 { - return e.rewardAmount -} - -func (self *ExternalIncentive) RewardSpent(currentTimestamp int64) int64 { - // Still check timestamps for state validation - if currentTimestamp < self.startTimestamp { - return 0 - } - - if currentTimestamp > self.endTimestamp { - return int64(self.rewardAmount) - } - - // But use time for calculation - if currentTimestamp < self.startTimestamp { - return 0 - } - - if currentTimestamp > self.endTimestamp { - return int64(self.rewardAmount) - } - - timeDuration := currentTimestamp - self.startTimestamp - rewardSpent := safeMulInt64(timeDuration, self.rewardPerSecond) - return rewardSpent -} - -func (self *ExternalIncentive) RewardLeft(currentTimestamp int64) int64 { - // Still check timestamps for state validation - if currentTimestamp <= self.startTimestamp { - return int64(self.rewardAmount) - } - - if currentTimestamp > self.endTimestamp { - return 0 - } - - // But use time for calculation - if currentTimestamp <= self.startTimestamp { - return int64(self.rewardAmount) - } - - if currentTimestamp > self.endTimestamp { - return 0 - } - - timeDuration := self.endTimestamp - currentTimestamp - rewardLeft := safeMulInt64(timeDuration, self.rewardPerSecond) - return rewardLeft -} - -func (self *ExternalIncentive) Clone() *ExternalIncentive { - return &ExternalIncentive{ - incentiveId: self.incentiveId, - startTimestamp: self.startTimestamp, - endTimestamp: self.endTimestamp, - createdHeight: self.createdHeight, - depositGnsAmount: self.depositGnsAmount, - targetPoolPath: self.targetPoolPath, - rewardToken: self.rewardToken, - rewardAmount: self.rewardAmount, - rewardLeft: self.rewardLeft, - rewardPerSecond: self.rewardPerSecond, - refundee: self.refundee, - unclaimableRefunded: self.unclaimableRefunded, - } -} - -func (self *ExternalIncentive) setUnClaimableRefunded(unClaimableRefunded bool) { - self.unclaimableRefunded = unClaimableRefunded -} - -// NewExternalIncentive creates a new external incentive -func NewExternalIncentive( - incentiveId string, - targetPoolPath string, - rewardToken string, - rewardAmount int64, - startTimestamp int64, // timestamp is in unix time(seconds) - endTimestamp int64, - refundee std.Address, - createdHeight int64, - depositGnsAmount int64, - currentTime int64, // current time in unix time(seconds) -) *ExternalIncentive { - incentiveDuration := endTimestamp - startTimestamp - rewardPerSecond := rewardAmount / incentiveDuration - - return &ExternalIncentive{ - incentiveId: incentiveId, - targetPoolPath: targetPoolPath, - rewardToken: rewardToken, - rewardAmount: rewardAmount, - startTimestamp: startTimestamp, - endTimestamp: endTimestamp, - rewardPerSecond: rewardPerSecond, - refundee: refundee, - createdHeight: createdHeight, - depositGnsAmount: depositGnsAmount, - unclaimableRefunded: false, - } -} - -type Deposit struct { - owner std.Address // owner address - stakeTimestamp int64 // staked time - stakeTime int64 // staked time (same as stakeTimestamp) - targetPoolPath string // staked position's pool path - tickLower int32 // tick lower - tickUpper int32 // tick upper - liquidity *u256.Uint // liquidity - lastCollectTime int64 // last collect time - warmups []Warmup // warmup information -} diff --git a/contract/r/gnoswap/v1/staker/utils.gno b/contract/r/gnoswap/v1/staker/utils.gno deleted file mode 100644 index c98f48e..0000000 --- a/contract/r/gnoswap/v1/staker/utils.gno +++ /dev/null @@ -1,168 +0,0 @@ -package staker - -import ( - "std" - "strconv" - "strings" - - "gno.land/p/demo/tokens/grc721" - "gno.land/p/nt/ufmt" - prbac "gno.land/p/gnoswap/rbac" - u256 "gno.land/p/gnoswap/uint256" - - "gno.land/r/gnoswap/access" - "gno.land/r/gnoswap/rbac" -) - -// GetOrigPkgAddr returns the original package address. -func GetOrigPkgAddr() std.Address { - return stakerAddr -} - -// poolPathAlign ensures pool path tokens are in lexicographical order. -func poolPathAlign(poolPath string) string { - pToken0, pToken1, fee := poolPathDivide(poolPath) - - if pToken0 < pToken1 { - return ufmt.Sprintf("%s:%s:%s", pToken0, pToken1, fee) - } - - return ufmt.Sprintf("%s:%s:%s", pToken1, pToken0, fee) -} - -// poolPathDivide splits a pool path into token addresses and fee tier. -func poolPathDivide(poolPath string) (string, string, string) { - res := strings.Split(poolPath, ":") - if len(res) != 3 { - panic(errInvalidPoolPath) - } - - pToken0, pToken1, fee := res[0], res[1], res[2] - return pToken0, pToken1, fee -} - -// positionIdFrom converts various types to grc721.TokenID. -func positionIdFrom(positionId any) grc721.TokenID { - if positionId == nil { - panic(makeErrorWithDetails( - errDataNotFound, - "positionId is nil", - )) - } - - switch positionId.(type) { - case string: - return grc721.TokenID(positionId.(string)) - case int: - return grc721.TokenID(strconv.Itoa(positionId.(int))) - case uint64: - return grc721.TokenID(strconv.Itoa(int(positionId.(uint64)))) - case grc721.TokenID: - return positionId.(grc721.TokenID) - default: - panic(makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("unsupported positionId type(%T)", positionId), - )) - } -} - -// max returns the larger of two int64 values. -func max(x, y int64) int64 { - if x > y { - return x - } - return y -} - -// min returns the smaller of two uint64 values. -func min(x, y uint64) uint64 { - if x < y { - return x - } - return y -} - -// contains checks if a string exists in a slice. -func contains(slice []string, item string) bool { - // We can use strings.EqualFold here, but this function should be case-sensitive. - // So, it is better to compare strings directly. - for _, element := range slice { - if element == item { - return true - } - } - return false -} - -// formatUint formats an unsigned integer to string. -func formatUint(v any) string { - switch v := v.(type) { - case uint8: - return strconv.FormatUint(uint64(v), 10) - case uint32: - return strconv.FormatUint(uint64(v), 10) - case uint64: - return strconv.FormatUint(v, 10) - default: - panic(ufmt.Sprintf("invalid type for Unsigned: %T", v)) - } -} - -// formatAnyInt formats a signed integer to string. -func formatAnyInt(v any) string { - switch v := v.(type) { - case int32: - return strconv.FormatInt(int64(v), 10) - case int64: - return strconv.FormatInt(v, 10) - case int: - return strconv.Itoa(v) - default: - panic(ufmt.Sprintf("invalid type for Signed: %T", v)) - } -} - -// formatBool formats a boolean to string. -func formatBool(v bool) string { - return strconv.FormatBool(v) -} - -// getRoleAddress retrieves the address for a system role. -func getRoleAddress(role prbac.SystemRole) std.Address { - addr, exists := access.GetAddress(role.String()) - if !exists { - return rbac.DefaultRoleAddresses[role] - } - - return addr -} - -// safeConvertToInt64 safely converts a *u256.Uint value to an int64, ensuring no overflow. -// -// This function attempts to convert the given *u256.Uint value to an int64. If the value exceeds -// the maximum allowable range for int64 (`2^63 - 1`), it triggers a panic with a descriptive error message. -// -// Parameters: -// - value (*u256.Uint): The unsigned 256-bit integer to be converted. -// -// Returns: -// - int64: The converted value if it falls within the int64 range. -// -// Panics: -// - If the `value` exceeds the range of int64, the function will panic with an error indicating -// the overflow and the original value. -func safeConvertToInt64(value *u256.Uint) int64 { - const INT64_MAX = 9223372036854775807 - const MAX_INT64 = "9223372036854775807" - - res, overflow := value.Uint64WithOverflow() - if overflow || res > uint64(INT64_MAX) { - panic(ufmt.Sprintf( - "amount(%s) overflows int64 range (max %s)", - value.ToString(), - MAX_INT64, - )) - } - return int64(res) -} diff --git a/contract/r/gnoswap/v1/staker/wrap_unwrap.gno b/contract/r/gnoswap/v1/staker/wrap_unwrap.gno deleted file mode 100644 index ec582fd..0000000 --- a/contract/r/gnoswap/v1/staker/wrap_unwrap.gno +++ /dev/null @@ -1,82 +0,0 @@ -package staker - -import ( - "std" - - "gno.land/r/gnoland/wugnot" - - "gno.land/p/nt/ufmt" -) - -// wrapWithTransfer wraps GNOT into WUGNOT and transfers it to the specified address. -func wrapWithTransfer(toAddress std.Address, amount int64) error { - if amount <= 0 { - return nil - } - - if amount < UGNOT_MIN_DEPOSIT_TO_WRAP { - return makeErrorWithDetails( - errWugnotMinimum, - ufmt.Sprintf("amount(%d) < minimum(%d)", amount, UGNOT_MIN_DEPOSIT_TO_WRAP), - ) - } - - // transfer ugnot from fromAddress to current realm - currentRealmAddr := std.CurrentRealm().Address() - - sentCoins := std.OriginSend() - ugnotSent := sentCoins.AmountOf(GNOT_DENOM) - if ugnotSent != amount { - return makeErrorWithDetails( - errInvalidInput, - ufmt.Sprintf("user(%s) sent ugnot(%d) amount not equal to rewardAmount(%d)", toAddress.String(), ugnotSent, amount), - ) - } - - // wrap gnot to wugnot - wugnotAddr := std.DerivePkgAddr(WUGNOT_PATH) - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(currentRealmAddr, wugnotAddr, sentCoins) - wugnot.Deposit(cross) - - // if to address is not current realm, transfer wugnot to to address - if toAddress != currentRealmAddr { - wugnot.Transfer(cross, toAddress, amount) - } - - return nil -} - -// unwrapWithTransfer unwraps WUGNOT to GNOT and sends it to the specified address. -func unwrapWithTransfer(toAddress std.Address, wugnotAmount int64) error { - if wugnotAmount == 0 { - return nil - } - - wugnot.Withdraw(cross, wugnotAmount) - - currentRealmAddr := std.CurrentRealm().Address() - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(currentRealmAddr, toAddress, std.Coins{{"ugnot", int64(wugnotAmount)}}) - - return nil -} - -// unwrapWithTransferFrom transfers WUGNOT from a source address, unwraps it to GNOT, and sends it to the target. -func unwrapWithTransferFrom(fromAddress, toAddress std.Address, wugnotAmount int64) error { - if wugnotAmount == 0 { - return nil - } - - currentRealmAddr := std.CurrentRealm().Address() - if fromAddress != currentRealmAddr { - wugnot.TransferFrom(cross, fromAddress, currentRealmAddr, wugnotAmount) - } - - wugnot.Withdraw(cross, wugnotAmount) - - banker := std.NewBanker(std.BankerTypeRealmSend) - banker.SendCoins(currentRealmAddr, toAddress, std.Coins{{"ugnot", int64(wugnotAmount)}}) - - return nil -} From 5ce348a8c6a7078c02daf78412fffa943948d4e5 Mon Sep 17 00:00:00 2001 From: stefann-01 Date: Tue, 9 Sep 2025 12:10:22 +0200 Subject: [PATCH 4/4] update docker --- contract/Dockerfile | 7 ++---- contract/r/gnoswap/rbac/consts.gno | 40 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 contract/r/gnoswap/rbac/consts.gno diff --git a/contract/Dockerfile b/contract/Dockerfile index e01238d..9ef1d54 100644 --- a/contract/Dockerfile +++ b/contract/Dockerfile @@ -25,11 +25,8 @@ COPY --from=builder /go/bin/gnokey /app/gnokey COPY --from=builder /app/src /app/src -COPY ./r/gnoswap/v1/test_token/bar/ /app/src/examples/gno.land/r/gnoswap/v1/test_token/bar/ -COPY ./p/gnoswap/consts/ /app/src/examples/gno.land/p/gnoswap/consts/ - -# temporary -COPY ./r/volos/core/volos.gno /app/src/examples/gno.land/r/volos/core/volos.gno +COPY ./r/gnoswap/test_token/ /app/src/examples/gno.land/r/gnoswap/test_token/ +COPY ./r/gnoswap/rbac/consts.gno /app/src/examples/gno.land/r/gnoswap/rbac/consts.gno COPY ./keys.db /root/.config/gno/data/keys.db diff --git a/contract/r/gnoswap/rbac/consts.gno b/contract/r/gnoswap/rbac/consts.gno new file mode 100644 index 0000000..2f4f54e --- /dev/null +++ b/contract/r/gnoswap/rbac/consts.gno @@ -0,0 +1,40 @@ +package rbac + +import "std" + +// Initial addresses for protocol roles. +const ( + // ADMIN is the initial admin address for RBAC management. + ADMIN std.Address = "g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42" + // DEV_OPS is the initial DevOps address for operational tasks. + DEV_OPS std.Address = "g1e9mkmle8rgx4jy2398dal9320uul7g00tkyh42" +) + +// Derived addresses for GnoSwap protocol packages. +var ( + // GNS_ADDR is the derived address for the GNS token package. + GNS_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/gns") + // EMISSION_ADDR is the derived address for the emission package. + EMISSION_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/emission") + + // POOL_ADDR is the derived address for the pool package. + POOL_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/pool") + // POSITION_ADDR is the derived address for the position package. + POSITION_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/position") + // ROUTER_ADDR is the derived address for the router package. + ROUTER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/router") + // STAKER_ADDR is the derived address for the staker package. + STAKER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/staker") + // PROTOCOL_FEE_ADDR is the derived address for the protocol fee package. + PROTOCOL_FEE_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/protocol_fee") + // COMMUNITY_POOL_ADDR is the derived address for the community pool package. + COMMUNITY_POOL_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/community_pool") + // GOV_GOVERNANCE_ADDR is the derived address for the governance package. + GOV_GOVERNANCE_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/governance") + // GOV_STAKER_ADDR is the derived address for the governance staker package. + GOV_STAKER_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/staker") + // GOV_XGNS_ADDR is the derived address for the xGNS governance package. + GOV_XGNS_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/gov/xgns") + // LAUNCHPAD_ADDR is the derived address for the launchpad package. + LAUNCHPAD_ADDR std.Address = std.DerivePkgAddr("gno.land/r/gnoswap/v1/launchpad") +)