Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GIX-1890: Check transaction fee disburse maturity #3331

Merged
merged 9 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
import TestIdWrapper from "$lib/components/common/TestIdWrapper.svelte";
import { formatMaturity } from "$lib/utils/neuron.utils";
import { replacePlaceholders } from "$lib/utils/i18n.utils";
import { nonNullish } from "@dfinity/utils";
import Tooltip from "../ui/Tooltip.svelte";

export let availableMaturityE8s: bigint;
export let percentage: number;
export let buttonText: string;
export let disabled = false;
export let disabledText: string | undefined = undefined;

let selectedMaturityE8s: bigint;
$: selectedMaturityE8s = (availableMaturityE8s * BigInt(percentage)) / 100n;
Expand Down Expand Up @@ -59,14 +62,27 @@
<button class="secondary" on:click={() => dispatcher("nnsCancel")}>
{$i18n.core.cancel}
</button>
<button
data-tid="select-maturity-percentage-button"
class="primary"
on:click={selectPercentage}
{disabled}
>
{buttonText}
</button>
{#if nonNullish(disabledText)}
<Tooltip id="disabled-disburse-button-modal" text={disabledText}>
<button
data-tid="select-maturity-percentage-button"
class="primary"
on:click={selectPercentage}
{disabled}
>
{buttonText}
</button>
</Tooltip>
{:else}
<button
data-tid="select-maturity-percentage-button"
class="primary"
on:click={selectPercentage}
{disabled}
>
{buttonText}
</button>
lmuntaner marked this conversation as resolved.
Show resolved Hide resolved
{/if}
</div>
</TestIdWrapper>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { E8S_PER_ICP } from "$lib/constants/icp.constants";
import {
MIN_NEURON_STAKE,
SPAWN_VARIANCE_PERCENTAGE,
MATURITY_MODULATION_VARIANCE_PERCENTAGE,
} from "$lib/constants/neurons.constants";
import { i18n } from "$lib/stores/i18n";
import { formatNumber, formatPercentage } from "$lib/utils/format.utils";
Expand Down Expand Up @@ -49,17 +49,22 @@
$i18n.neuron_detail.spawn_neuron_disabled_tooltip,
{
$amount: formatNumber(
MIN_NEURON_STAKE / E8S_PER_ICP / SPAWN_VARIANCE_PERCENTAGE,
MIN_NEURON_STAKE /
E8S_PER_ICP /
MATURITY_MODULATION_VARIANCE_PERCENTAGE,
{ minFraction: 4, maxFraction: 4 }
),
$min: formatNumber(MIN_NEURON_STAKE / E8S_PER_ICP, {
minFraction: 0,
maxFraction: 0,
}),
$varibility: formatPercentage(SPAWN_VARIANCE_PERCENTAGE, {
minFraction: 0,
maxFraction: 0,
}),
$variability: formatPercentage(
MATURITY_MODULATION_VARIANCE_PERCENTAGE,
{
minFraction: 0,
maxFraction: 0,
}
),
}
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<script lang="ts">
import { hasEnoughMaturityToDisburse } from "$lib/utils/sns-neuron.utils";
import {
hasEnoughMaturityToDisburse,
minimumAmountToDisburseMaturity,
} from "$lib/utils/sns-neuron.utils";
import { openSnsNeuronModal } from "$lib/utils/modals.utils";
import type { SnsNeuron } from "@dfinity/sns";
import DisburseMaturityButton from "$lib/components/neuron-detail/actions/DisburseMaturityButton.svelte";
Expand All @@ -17,7 +20,11 @@
$: disabledText = !enoughMaturity
? replacePlaceholders(
$i18n.neuron_detail.disburse_maturity_disabled_tooltip,
{ $fee: formatToken({ value: feeE8s }) }
{
$amount: formatToken({
value: minimumAmountToDisburseMaturity(feeE8s),
}),
}
)
: undefined;

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/constants/neurons.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { E8S_PER_ICP } from "./icp.constants";
export const MAX_NEURONS_MERGED = 2;
export const MIN_NEURON_STAKE = E8S_PER_ICP;
export const MAX_CONCURRENCY = 10;
export const SPAWN_VARIANCE_PERCENTAGE = 0.95;
export const MATURITY_MODULATION_VARIANCE_PERCENTAGE = 0.95;
// Neuron ids are random u64. Max digits of a u64 is 20.
export const MAX_NEURON_ID_DIGITS = 20;

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,7 @@
"spawn_neuron": "Spawn Neuron",
"spawn": "Spawn",
"stake_maturity_disabled_tooltip": "Currently, you do not have any maturity available to stake into this neuron.",
"disburse_maturity_disabled_tooltip": "You do not have enough maturity to disburse. The minimum is: $fee.",
"disburse_maturity_disabled_tooltip": "You do not have enough maturity to disburse. The minimum is: $amount.",
"stake_maturity_tooltip": "Merge Maturity has been replaced by Stake Maturity. <a href=\"https://wiki.internetcomputer.org/wiki/NNS_neuron_operations_related_to_maturity\" rel=\"noopener noreferrer\" aria-label=\"Find more information about the new stake maturity\" target=\"_blank\">Learn more</a>.",
"start_dissolve_description": "This will cause your neuron to lose its age bonus.\nAre you sure you wish to continue?",
"stop_dissolve_description": "Are you sure you want to stop the dissolve process?",
Expand All @@ -628,7 +628,7 @@
"disburse_neuron_title": "Disburse Neuron",
"split_neuron_success": "Your neuron has successfully been split.",
"split_neuron_disabled_tooltip": "Neuron needs a minimum stake of $amount $token to be splittable.",
"spawn_neuron_disabled_tooltip": "You need at least the equivalent of $amount ICP ($min / $varibility) to spawn a new neuron from maturity.",
"spawn_neuron_disabled_tooltip": "You need at least the equivalent of $amount ICP ($min / $variability) to spawn a new neuron from maturity.",
"hotkeys_title": "Hotkeys",
"add_hotkey": "Add Hotkey",
"no_notkeys": "No hotkeys",
Expand Down
21 changes: 20 additions & 1 deletion frontend/src/lib/modals/neurons/DisburseMaturityModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
export let availableMaturityE8s: bigint;
export let tokenSymbol: string;

export let minimumAmountE8s: bigint;

const steps: WizardSteps = [
{
name: "SelectPercentage",
Expand All @@ -33,6 +35,22 @@
let modal: WizardModal;

let percentageToDisburse = 0;
let selectedMaturityE8s: bigint;
$: selectedMaturityE8s =
(availableMaturityE8s * BigInt(percentageToDisburse)) / 100n;

let disableDisburse = false;
$: disableDisburse = selectedMaturityE8s < minimumAmountE8s;
lmuntaner marked this conversation as resolved.
Show resolved Hide resolved

// Show the text only if the selected percentage is greater than 0.
let disabledText: string | undefined = undefined;
$: disabledText =
disableDisburse && percentageToDisburse > 0
? replacePlaceholders(
$i18n.neuron_detail.disburse_maturity_disabled_tooltip,
{ $amount: formatToken({ value: minimumAmountE8s }) }
)
: undefined;

const dispatcher = createEventDispatcher();
const disburseNeuronMaturity = () =>
Expand Down Expand Up @@ -74,7 +92,8 @@
on:nnsSelectPercentage={goToConfirm}
on:nnsCancel={close}
bind:percentage={percentageToDisburse}
disabled={percentageToDisburse === 0}
disabled={disableDisburse}
{disabledText}
>
<div class="percentage-container" slot="description">
<span class="description">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
import type { SnsNeuron, SnsNeuronId } from "@dfinity/sns";
import type { Principal } from "@dfinity/principal";
import { disburseMaturity as disburseMaturityService } from "$lib/services/sns-neurons.services";
import { formattedMaturity } from "$lib/utils/sns-neuron.utils";
import {
formattedMaturity,
minimumAmountToDisburseMaturity,
} from "$lib/utils/sns-neuron.utils";
import DisburseMaturityModal from "$lib/modals/neurons/DisburseMaturityModal.svelte";
import { snsProjectMainAccountStore } from "$lib/derived/sns/sns-project-accounts.derived";
import { shortenWithMiddleEllipsis } from "$lib/utils/format.utils";
import { tokensStore } from "$lib/stores/tokens.store";
import type { Token } from "@dfinity/utils";
import type { IcrcTokenMetadata } from "$lib/types/icrc";

export let neuron: SnsNeuron;
export let neuronId: SnsNeuronId;
Expand All @@ -25,9 +28,14 @@
$snsProjectMainAccountStore?.identifier ?? ""
);

let token: Token | undefined;
let token: IcrcTokenMetadata | undefined;
$: token = $tokensStore[rootCanisterId.toText()]?.token;

let minimumAmountE8s: bigint;
// Token is loaded with all the projects from the aggregator.
// Therefore, if the user made it here, it's present.
$: minimumAmountE8s = minimumAmountToDisburseMaturity(token?.fee ?? 0n);

const dispatcher = createEventDispatcher();
const close = () => dispatcher("nnsClose");

Expand Down Expand Up @@ -57,6 +65,7 @@

<DisburseMaturityModal
availableMaturityE8s={neuron.maturity_e8s_equivalent}
{minimumAmountE8s}
tokenSymbol={token?.symbol ?? ""}
on:nnsDisburseMaturity={disburseMaturity}
on:nnsClose
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/lib/utils/neuron.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
import {
AGE_MULTIPLIER,
DISSOLVE_DELAY_MULTIPLIER,
MATURITY_MODULATION_VARIANCE_PERCENTAGE,
MAX_NEURONS_MERGED,
MIN_NEURON_STAKE,
SPAWN_VARIANCE_PERCENTAGE,
TOPICS_TO_FOLLOW_NNS,
} from "$lib/constants/neurons.constants";
import { DEPRECATED_TOPICS } from "$lib/constants/proposals.constants";
Expand Down Expand Up @@ -499,7 +499,10 @@ export const isEnoughMaturityToSpawn = ({
const maturitySelected = Math.floor(
(Number(fullNeuron.maturityE8sEquivalent) * percentage) / 100
);
return maturitySelected >= MIN_NEURON_STAKE / SPAWN_VARIANCE_PERCENTAGE;
return (
maturitySelected >=
MIN_NEURON_STAKE / MATURITY_MODULATION_VARIANCE_PERCENTAGE
);
};

export const isSpawning = (neuron: NeuronInfo): boolean =>
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/lib/utils/sns-neuron.utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MATURITY_MODULATION_VARIANCE_PERCENTAGE } from "$lib/constants/neurons.constants";
import {
HOTKEY_PERMISSIONS,
MANAGE_HOTKEY_PERMISSIONS,
Expand Down Expand Up @@ -505,7 +506,7 @@ export const hasEnoughMaturityToStake = (
): boolean => (neuron?.maturity_e8s_equivalent ?? BigInt(0)) > BigInt(0);

/**
* Is the maturity of the neuron bigger than the transaction fee?
* Is the maturity of the neuron bigger than the minimum amount to disburse?
* @param {SnsNeuron} neuron
* @param {bigint} feeE8s
*/
Expand All @@ -515,7 +516,8 @@ export const hasEnoughMaturityToDisburse = ({
}: {
feeE8s: bigint;
neuron: SnsNeuron;
}): boolean => maturity_e8s_equivalent >= feeE8s;
}): boolean =>
maturity_e8s_equivalent >= minimumAmountToDisburseMaturity(feeE8s);

/**
* Does the neuron has staked maturity?
Expand Down Expand Up @@ -981,3 +983,11 @@ export const totalDisbursingMaturity = ({
(acc, disbursement) => acc + disbursement.amount_e8s,
BigInt(0)
);

/**
* The governance canister checks that the amount to disburse in the worst case (of the maturity modulation) is bigger than the transaction fee.
*
* Source: https://sourcegraph.com/github.com/dfinity/ic/-/blob/rs/sns/governance/src/governance.rs?L1651
*/
export const minimumAmountToDisburseMaturity = (fee: bigint): bigint =>
BigInt(Math.ceil(Number(fee) / MATURITY_MODULATION_VARIANCE_PERCENTAGE));
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("SnsDisburseMaturityButton", () => {
const po = renderComponent(
{
...mockSnsNeuron,
maturity_e8s_equivalent: fee + 10n,
maturity_e8s_equivalent: fee * 2n,
staked_maturity_e8s_equivalent: [],
},
fee
Expand All @@ -50,7 +50,7 @@ describe("SnsDisburseMaturityButton", () => {

expect(await po.isDisabled()).toBe(true);
expect(await po.getTooltipText()).toBe(
"You do not have enough maturity to disburse. The minimum is: 0.0001."
"You do not have enough maturity to disburse. The minimum is: 0.00010527."
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ describe("SnsDisburseMaturityModal", () => {
expect(await po.isNextButtonDisabled()).toBe(false);
});

it("should disable next button if amount of maturity is less than transaction fee", async () => {
const fee = 100_000_000n;
const neuron = createMockSnsNeuron({
id: [1],
maturity: fee * 2n,
});
tokensStore.setToken({
canisterId: rootCanisterId,
token: {
fee,
...mockSnsToken,
},
});
// Maturity is 2x the fee, so 10% of maturity is not enough to cover the fee
const percentage = 10;
const po = await renderSnsDisburseMaturityModal(neuron);
await po.setPercentage(percentage);
expect(await po.isNextButtonDisabled()).toBe(false);
});

it("should display selected percentage and total maturity", async () => {
const neuron = createMockSnsNeuron({
id: [1],
Expand Down
19 changes: 15 additions & 4 deletions frontend/src/tests/lib/utils/sns-neuron.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
isUserHotkey,
isVesting,
minNeuronSplittable,
minimumAmountToDisburseMaturity,
needsRefresh,
neuronAge,
nextMemo,
Expand Down Expand Up @@ -1391,10 +1392,10 @@ describe("sns-neuron utils", () => {

describe("hasEnoughMaturityToDisburse", () => {
const feeE8s = 10_000n;
it("should return true if maturity is more than fee", () => {
it("should return true if maturity is more than fee in worst modulation scenario", () => {
const neuron = {
...mockSnsNeuron,
maturity_e8s_equivalent: feeE8s + 1n,
maturity_e8s_equivalent: 10526n + 1n,
};
expect(hasEnoughMaturityToDisburse({ neuron, feeE8s })).toBe(true);
});
Expand All @@ -1407,12 +1408,12 @@ describe("sns-neuron utils", () => {
expect(hasEnoughMaturityToDisburse({ neuron, feeE8s })).toBe(false);
});

it("should return true if maturity is same as fee", () => {
it("should return false if maturity is same as fee", () => {
const neuron = {
...mockSnsNeuron,
maturity_e8s_equivalent: feeE8s,
};
expect(hasEnoughMaturityToDisburse({ neuron, feeE8s })).toBe(true);
expect(hasEnoughMaturityToDisburse({ neuron, feeE8s })).toBe(false);
});
});

Expand Down Expand Up @@ -2492,4 +2493,14 @@ describe("sns-neuron utils", () => {
expect(totalDisbursingMaturity(neuron)).toBe(0n);
});
});

describe("minimumAmountToDisburseMaturity", () => {
it("returns worst case of maturity modulation", () => {
expect(minimumAmountToDisburseMaturity(10_000n)).toBe(10527n);
});

it("returns 0 if fee is 0", () => {
expect(minimumAmountToDisburseMaturity(0n)).toBe(0n);
});
});
});