From 73565d0695651a14fc0b4f02862c13f79bc774bf Mon Sep 17 00:00:00 2001 From: Graham Dixon Date: Wed, 8 Dec 2021 15:17:04 +0000 Subject: [PATCH 1/5] fix: apply the cap after normalising the distribution (#9846) * fix: apply the cap after normalising the distribution * fix: adds flag to estimate_clr so that we can skip saving the results * fix: expose id and title to make it easier to compare results * fix: apply Adityas comments * add logging * add more logs * add logging * fix: adds id to totals * fix: clean up logs * fix: ensures we dont spread the remainder if we havent reached the saturation point * fix: debug output Co-authored-by: Aditya Anand M C --- app/grants/clr.py | 72 +++++++++++++++---- .../management/commands/estimate_clr.py | 6 +- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/app/grants/clr.py b/app/grants/clr.py index a1ec5ae4a59..a952e5e07e4 100644 --- a/app/grants/clr.py +++ b/app/grants/clr.py @@ -200,7 +200,7 @@ def get_totals_by_pair(contrib_dict): return pair_totals -def calculate_clr(curr_agg, trust_dict, pair_totals, v_threshold, total_pot, match_cap_per_grant): +def calculate_clr(curr_agg, trust_dict, pair_totals, v_threshold, total_pot): ''' calculates the clr amount at the given threshold and total pot args: @@ -245,17 +245,13 @@ def calculate_clr(curr_agg, trust_dict, pair_totals, v_threshold, total_pot, mat if type(tot) == complex: tot = float(tot.real) - # ensure CLR match for a grant in CLR round does not exceed 2.5 of the total pot - if total_pot != match_cap_per_grant and tot > match_cap_per_grant: - tot = match_cap_per_grant - bigtot += tot - totals[proj] = {'number_contributions': _num, 'contribution_amount': _sum, 'clr_amount': tot} + totals[proj] = {'id': proj, 'number_contributions': _num, 'contribution_amount': _sum, 'clr_amount': tot} return bigtot, totals -def calculate_clr_for_prediction(bigtot, totals, curr_agg, trust_dict, v_threshold, total_pot, grant_id, amount): +def calculate_clr_for_prediction(bigtot, totals, curr_agg, trust_dict, v_threshold, total_pot, grant_id, amount, match_cap_per_grant): ''' clubbed function that runs all calculation functions and returns the result for a single grant_id @@ -319,10 +315,10 @@ def calculate_clr_for_prediction(bigtot, totals, curr_agg, trust_dict, v_thresho if type(tot) == complex: tot = float(tot.real) - totals[grant_id] = {'number_contributions': _num, 'contribution_amount': _sum, 'clr_amount': tot} + totals[grant_id] = {'id': grant_id, 'number_contributions': _num, 'contribution_amount': _sum, 'clr_amount': tot} # normalise the result - grants_clr = normalise(bigtot + tot, totals, total_pot) + grants_clr = normalise(bigtot + tot, totals, total_pot, match_cap_per_grant) # find grant we added the contribution to and get the new clr amount grant_clr = grants_clr.get(grant_id) @@ -339,7 +335,7 @@ def calculate_clr_for_prediction(bigtot, totals, curr_agg, trust_dict, v_thresho return (grants_clr, 0.0, 0, 0.0) -def normalise(bigtot, totals, total_pot): +def normalise(bigtot, totals, total_pot, match_cap_per_grant): ''' given the total amount distributed (bigtot) and the total_pot size normalise the distribution args: @@ -351,8 +347,10 @@ def normalise(bigtot, totals, total_pot): [{'id': proj, 'number_contributions': _num, 'contribution_amount': _sum, 'clr_amount': tot}] ''' + # check if saturation is reached + is_saturated = bigtot >= total_pot # check for saturation and normalise if reached - if bigtot >= total_pot: + if is_saturated: # print(f'saturation reached. Total Pot: ${total_pot} | Total Allocated ${bigtot}. Normalizing') for key, t in totals.items(): t['clr_amount'] = ((t['clr_amount'] / bigtot) * total_pot) @@ -363,6 +361,48 @@ def normalise(bigtot, totals, total_pot): for key, t in totals.items(): t['clr_amount'] = t['clr_amount'] * (1 + percentage_increase) + # apply the match cap post-normalisation + totals = apply_cap(totals, match_cap_per_grant, is_saturated) + + return totals + + +def apply_cap(totals, match_cap_per_grant, should_spread): + # work out how much of the pool is remaining after capping each grant + remainder = 0 # amount left to be redistributed after cap + uncapped = 0 # total amount matched for grants which haven't capped + + # cap each of the clr_amounts + for key, t in totals.items(): + if t['clr_amount'] >= match_cap_per_grant: + # grant has exceeded the cap + # - so cap the clr_amount + # - add the extra funds to remainder + remainder += t['clr_amount'] - match_cap_per_grant + t['clr_amount'] = match_cap_per_grant + + else: + # grant has not exceed cap so add amount to uncapped + uncapped += t['clr_amount'] + + # check that we have both capped and uncapped grants and that we should be spreading the remainder + if should_spread and remainder > 0 and uncapped > 0: + # div so we can spread the remainder proportionally + per_remainder = remainder / uncapped + # reset remainder to check if any grants enter the cap region after spreading the remainder + remainder = 0 + # spread the remainder + for key, t in totals.items(): + if t['clr_amount'] < match_cap_per_grant: + t['clr_amount'] += per_remainder * t['clr_amount'] + # check if the cap is hit after spreading the remainder + if t['clr_amount'] >= match_cap_per_grant: + remainder += t['clr_amount'] - match_cap_per_grant + + # apply the cap again (recursively) + if remainder > 0: + apply_cap(totals, match_cap_per_grant, should_spread) + return totals @@ -404,11 +444,13 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn pair_totals = get_totals_by_pair(curr_agg) grant_clr_percentage_cap = clr_round.grant_clr_percentage_cap if clr_round.grant_clr_percentage_cap else 100 + bigtot, totals = calculate_clr(curr_agg, trust_dict, pair_totals, v_threshold, total_pot) + + # $ value of the percentage cap match_cap_per_grant = total_pot * (float(grant_clr_percentage_cap) / 100) - bigtot, totals = calculate_clr(curr_agg, trust_dict, pair_totals, v_threshold, total_pot, match_cap_per_grant) # normalise against a deepcopy of the totals to avoid mutations - curr_grants_clr = normalise(bigtot, copy.deepcopy(totals), total_pot) + curr_grants_clr = normalise(bigtot, copy.deepcopy(totals), total_pot, match_cap_per_grant) # for slim calc - only update the current distribution and skip calculating predictions if what == 'slim': @@ -474,7 +516,7 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn else: # calculate clr with additional donation amount grants_clr, predicted_clr, _, _ = calculate_clr_for_prediction( - bigtot, totals, curr_agg, trust_dict, v_threshold, total_pot, grant.id, amount + bigtot, totals, curr_agg, trust_dict, v_threshold, total_pot, grant.id, amount, match_cap_per_grant ) # record each point of the prediction @@ -501,7 +543,7 @@ def predict_clr(save_to_db=False, from_date=None, clr_round=None, network='mainn if from_date > (clr_calc_start_time - timezone.timedelta(hours=1)): grant.save() - debug_output.append({'grant': grant.id, "clr_prediction_curve": (potential_donations, potential_clr), "grants_clr": grants_clr}) + debug_output.append({'grant': grant.id, "title": grant.title, "clr_prediction_curve": (potential_donations, potential_clr), "grants_clr": grants_clr}) print(f"\nTotal execution time: {(timezone.now() - clr_calc_start_time)}\n") diff --git a/app/grants/management/commands/estimate_clr.py b/app/grants/management/commands/estimate_clr.py index 26857a9871d..c37b11ff5b3 100644 --- a/app/grants/management/commands/estimate_clr.py +++ b/app/grants/management/commands/estimate_clr.py @@ -38,6 +38,7 @@ def add_arguments(self, parser): parser.add_argument('what', type=str, default="full") parser.add_argument('sync', type=str, default="false") parser.add_argument('--use-sql', type=bool, default=False) + parser.add_argument('--skip-save', type=bool, default=False) # slim = just run 0 contribution match upcate calcs # full, run [0, 1, 10, 100, calcs across all grants] @@ -48,6 +49,7 @@ def handle(self, *args, **options): what = options['what'] sync = options['sync'] use_sql = options['use_sql'] + skip_save = options['skip_save'] print (network, clr_pk, what, sync, use_sql) if clr_pk and clr_pk.isdigit(): @@ -60,7 +62,7 @@ def handle(self, *args, **options): if sync == 'true': # run it sync -> useful for payout / debugging predict_clr( - save_to_db=True, + save_to_db=True if not skip_save else False, from_date=timezone.now(), clr_round=clr_round, network=network, @@ -70,7 +72,7 @@ def handle(self, *args, **options): else: # runs it as celery task. process_predict_clr( - save_to_db=True, + save_to_db=True if not skip_save else False, from_date=timezone.now(), clr_round=clr_round, network=network, From b0a6c6c0621b47b66e831588a174ca7d4bb25420 Mon Sep 17 00:00:00 2001 From: Graham Dixon Date: Wed, 8 Dec 2021 15:55:32 +0000 Subject: [PATCH 2/5] fix: enable search in GrantCLRCalculation admin --- app/grants/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/grants/admin.py b/app/grants/admin.py index 8e4e9909e97..165b778dc90 100644 --- a/app/grants/admin.py +++ b/app/grants/admin.py @@ -85,7 +85,9 @@ class GrantCLRCalculationAdmin(admin.ModelAdmin): 'grant','grantclr','clr_prediction_curve' ] search_fields = [ - 'grantclr' + 'grant__title', + 'grantclr__round_num', + 'grantclr__pk' ] class CLRMatchAdmin(admin.ModelAdmin): From 625f2bafa9bd0c4c06283bdb2bcaf6ebe16242a4 Mon Sep 17 00:00:00 2001 From: Graham Dixon Date: Wed, 8 Dec 2021 19:53:18 +0000 Subject: [PATCH 3/5] fix: ensures props are available for str of questAttempt --- app/quests/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/quests/models.py b/app/quests/models.py index 91f5012221f..ed6aae9799e 100644 --- a/app/quests/models.py +++ b/app/quests/models.py @@ -302,8 +302,10 @@ class QuestAttempt(SuperModel): state = models.IntegerField(default=0) def __str__(self): + handle = self.profile.handle if self.profile else 'deleted profile' + title = self.quest.title if self.quest else 'deleted quest' """Return the string representation of this obj.""" - return f'{self.pk}, {self.profile.handle} => {self.quest.title} state: {self.state} success: {self.success}' + return f'{self.pk}, {handle} => {title} state: {self.state} success: {self.success}' class QuestFeedback(SuperModel): From a0889548036ab49b5d54d55b6a667d2b13caba49 Mon Sep 17 00:00:00 2001 From: Graham Dixon Date: Thu, 9 Dec 2021 15:19:20 +0000 Subject: [PATCH 4/5] fix: prevents bigNumber overflow (#9862) --- app/assets/v2/js/cart-ethereum-polygon.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/v2/js/cart-ethereum-polygon.js b/app/assets/v2/js/cart-ethereum-polygon.js index fd59d4b026c..e965588ac5a 100644 --- a/app/assets/v2/js/cart-ethereum-polygon.js +++ b/app/assets/v2/js/cart-ethereum-polygon.js @@ -467,7 +467,7 @@ Vue.component('grantsCartEthereumPolygon', { const tokenIsMatic = tokenDetails && tokenDetails.name === 'MATIC'; // Check user matic balance against required amount - if (userMaticBalance.toNumber() && userMaticBalance.lt(requiredAmounts[tokenSymbol].amount) && tokenIsMatic) { + if (userMaticBalance.toString() !== "0" && userMaticBalance.lt(requiredAmounts[tokenSymbol].amount) && tokenIsMatic) { requiredAmounts[tokenSymbol].isBalanceSufficient = false; requiredAmounts[tokenSymbol].amount = parseFloat((( requiredAmounts[tokenSymbol].amount - userMaticBalance @@ -511,7 +511,7 @@ Vue.component('grantsCartEthereumPolygon', { .balanceOf(userAddress) .call({ from: userAddress })); - if (userTokenBalance.toNumber() && userTokenBalance.lt(requiredAmounts[tokenSymbol].amount)) { + if (userTokenBalance.toString() !== "0" && userTokenBalance.lt(requiredAmounts[tokenSymbol].amount)) { requiredAmounts[tokenSymbol].isBalanceSufficient = false; requiredAmounts[tokenSymbol].amount = parseFloat((( requiredAmounts[tokenSymbol].amount - userTokenBalance From e319883633bd99f14872bf8275f14b874cbb8e3f Mon Sep 17 00:00:00 2001 From: Aditya Anand M C Date: Fri, 10 Dec 2021 12:57:52 +0530 Subject: [PATCH 5/5] allow disabling checkout via admin --- app/assets/v2/js/cart-ethereum-polygon.js | 4 ++-- app/assets/v2/js/cart.js | 4 ++-- app/grants/templates/grants/cart-vue.html | 5 +++++ app/grants/views.py | 12 ++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/assets/v2/js/cart-ethereum-polygon.js b/app/assets/v2/js/cart-ethereum-polygon.js index e965588ac5a..1729c666417 100644 --- a/app/assets/v2/js/cart-ethereum-polygon.js +++ b/app/assets/v2/js/cart-ethereum-polygon.js @@ -467,7 +467,7 @@ Vue.component('grantsCartEthereumPolygon', { const tokenIsMatic = tokenDetails && tokenDetails.name === 'MATIC'; // Check user matic balance against required amount - if (userMaticBalance.toString() !== "0" && userMaticBalance.lt(requiredAmounts[tokenSymbol].amount) && tokenIsMatic) { + if (userMaticBalance.toString() !== '0' && userMaticBalance.lt(requiredAmounts[tokenSymbol].amount) && tokenIsMatic) { requiredAmounts[tokenSymbol].isBalanceSufficient = false; requiredAmounts[tokenSymbol].amount = parseFloat((( requiredAmounts[tokenSymbol].amount - userMaticBalance @@ -511,7 +511,7 @@ Vue.component('grantsCartEthereumPolygon', { .balanceOf(userAddress) .call({ from: userAddress })); - if (userTokenBalance.toString() !== "0" && userTokenBalance.lt(requiredAmounts[tokenSymbol].amount)) { + if (userTokenBalance.toString() !== '0' && userTokenBalance.lt(requiredAmounts[tokenSymbol].amount)) { requiredAmounts[tokenSymbol].isBalanceSufficient = false; requiredAmounts[tokenSymbol].amount = parseFloat((( requiredAmounts[tokenSymbol].amount - userTokenBalance diff --git a/app/assets/v2/js/cart.js b/app/assets/v2/js/cart.js index 1f1520d2c97..56a8c2138a2 100644 --- a/app/assets/v2/js/cart.js +++ b/app/assets/v2/js/cart.js @@ -73,8 +73,8 @@ Vue.component('grants-cart', { zkSyncEstimatedGasCost: undefined, // Used to tell user which checkout method is cheaper polygonSupportedTokens: [], // Used to inform user which tokens in their cart are on Polygon polygonEstimatedGasCost: undefined, // Used to tell user which checkout method is cheaper - isZkSyncDown: false, // disable zkSync when true - isPolygonDown: false, // disable polygon when true + isZkSyncDown: document.disableZksync, // disable zkSync when true + isPolygonDown: document.disablePolygon, // disable polygon when true isPolkadotExtInstalled: false, chainScripts: { 'POLKADOT': [ diff --git a/app/grants/templates/grants/cart-vue.html b/app/grants/templates/grants/cart-vue.html index 4cf81642b3b..c1e19deb277 100644 --- a/app/grants/templates/grants/cart-vue.html +++ b/app/grants/templates/grants/cart-vue.html @@ -219,6 +219,11 @@

Discover Grants

document.authenticated = {{ authenticated | yesno:"true,false" }}; document.isFullyVerified = '{{is_fully_verified}}' === 'True'; document.polygonGasPrice = "{{ gas_prices|get_item:'polygon' }}" + + // Checkout Status + document.disableZksync = '{{ disableZksync }}' === 'true'; + document.disablePolygon = '{{ disablePolygon }}' === 'true'; + {% bundle merge_js file qrcode %} diff --git a/app/grants/views.py b/app/grants/views.py index 070c9542b79..f4eb2b4ecb7 100644 --- a/app/grants/views.py +++ b/app/grants/views.py @@ -2417,10 +2417,22 @@ def get_replaced_tx(request): def grants_cart_view(request): + + try: + checkout_status = StaticJsonEnv.objects.get(key='L2_CHECKOUT_STATUS').data + disableZksync = checkout_status['disableZksync'] + disablePolygon = checkout_status['disablePolygon'] + except: + disableZksync = 'false' + disablePolygon = 'false' + context = { 'title': 'Grants Cart', 'EMAIL_ACCOUNT_VALIDATION': EMAIL_ACCOUNT_VALIDATION, + 'disableZksync': disableZksync, + 'disablePolygon': disablePolygon } + if request.user.is_authenticated: profile = request.user.profile context['username'] = profile.username