diff --git a/.gitignore b/.gitignore
index e866c60bc9..584c479389 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,4 @@ yarn-debug.log*
yarn-error.log*
.vscode/
.editorconfig
+logs
diff --git a/__tests__/components/bounty/create-bounty/token-amount/token-amount.controller.test.tsx b/__tests__/components/bounty/create-bounty/token-amount/token-amount.controller.test.tsx
new file mode 100644
index 0000000000..0befff60e7
--- /dev/null
+++ b/__tests__/components/bounty/create-bounty/token-amount/token-amount.controller.test.tsx
@@ -0,0 +1,145 @@
+import { NumberFormatValues } from "react-number-format";
+
+import { toSmartContractDecimals } from "@taikai/dappkit/dist/src/utils/numbers";
+import { fireEvent } from "@testing-library/dom";
+import BigNumber from "bignumber.js";
+
+import CreateBountyTokenAmount, {
+ ZeroNumberFormatValues,
+} from "components/bounty/create-bounty/token-amount/controller";
+
+import { Network } from "interfaces/network";
+import { DistributionsProps } from "interfaces/proposal";
+import { Token } from "interfaces/token";
+
+import { render } from "__tests__/utils/custom-render";
+
+jest.mock("x-hooks/use-bepro", () => () => ({}));
+
+const mockCurrentToken: Token = {
+ address: "0x1234567890123456789012345678901234567890",
+ name: "Mock Token",
+ symbol: "MOCK",
+};
+
+const mockCurrentNetwork: Network = {
+ id: 1,
+ name: "Mock Network",
+ updatedAt: new Date(),
+ createdAt: new Date(),
+ description: "Mock Description",
+ networkAddress: "0x1234567890123456789012345678901234567890",
+ creatorAddress: "0x1234567890123456789012345678901234567890",
+ openBounties: 0,
+ totalBounties: 0,
+ allowCustomTokens: false,
+ councilMembers: [],
+ banned_domains: [],
+ closeTaskAllowList: [],
+ allow_list: [],
+ mergeCreatorFeeShare: 0.05,
+ proposerFeeShare: 0.5,
+ chain: {
+ chainId: 1,
+ chainRpc: "https://mock-rpc.com",
+ name: "Mock Chain",
+ chainName: "Mock Chain",
+ chainShortName: "MOCK",
+ chainCurrencySymbol: "MOCK",
+ chainCurrencyDecimals: "18",
+ chainCurrencyName: "Mock Token",
+ blockScanner: "https://mock-scanner.com",
+ registryAddress: "0x1234567890123456789012345678901234567890",
+ eventsApi: "https://mock-events.com",
+ isDefault: true,
+ closeFeePercentage: 10,
+ cancelFeePercentage: 1.0,
+ networkCreationFeePercentage: 0.5,
+ },
+};
+
+describe("TokenAmountController", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("fuzzes total input and ensures internal adjusted total is divisible by 100 in contract units", () => {
+ let executions = 0;
+
+ let issueAmount = ZeroNumberFormatValues;
+ let previewAmount = ZeroNumberFormatValues;
+
+ const setPreviewAmount = jest.fn((value: NumberFormatValues) => {
+ previewAmount = value;
+ });
+ const updateIssueAmount = jest.fn((value: NumberFormatValues) => {
+ issueAmount = value;
+ });
+
+ while (executions < 100) {
+ const decimals = Math.floor(Math.random() * 13) + 6;
+ const randomValue = parseFloat((Math.random() * 499999 + 1).toFixed(decimals));
+
+ const result = render();
+
+ const totalAmountInput = result.getAllByTestId("total-amount-input")[0];
+
+ const valueString = randomValue.toString();
+
+ fireEvent.change(totalAmountInput, { target: { value: valueString } });
+
+ result.rerender();
+
+ const newValueContract = toSmartContractDecimals(issueAmount.value, decimals);
+
+ expect(Number(BigInt(newValueContract) % BigInt(100))).toBe(0);
+
+ jest.clearAllMocks();
+ result.unmount();
+
+ executions += 1;
+ }
+ });
+});
diff --git a/components/bounty/create-bounty/token-amount/controller.tsx b/components/bounty/create-bounty/token-amount/controller.tsx
index 1c83fc1642..d19c2bfd1e 100644
--- a/components/bounty/create-bounty/token-amount/controller.tsx
+++ b/components/bounty/create-bounty/token-amount/controller.tsx
@@ -1,6 +1,7 @@
import {useEffect, useState} from "react";
import {NumberFormatValues} from "react-number-format";
+import { fromSmartContractDecimals, toSmartContractDecimals } from "@taikai/dappkit/dist/src/utils/numbers";
import BigNumber from "bignumber.js";
import {useDebouncedCallback} from "use-debounce";
@@ -14,7 +15,7 @@ import {useUserStore} from "x-hooks/stores/user/user.store";
import CreateBountyTokenAmountView from "./view";
-const ZeroNumberFormatValues = {
+export const ZeroNumberFormatValues = {
value: "",
formattedValue: "",
floatValue: 0,
@@ -101,8 +102,8 @@ export default function CreateBountyTokenAmount({
}
const handleNumberFormat = (v: BigNumber) => ({
- value: v.decimalPlaces(5, 0).toFixed(),
- floatValue: v.toNumber(),
+ value: v.decimalPlaces(Math.min(10, decimals), 0).toFixed(),
+ floatValue: +v.decimalPlaces(Math.min(10, decimals), 0).toFixed(),
formattedValue: v.decimalPlaces(Math.min(10, decimals), 0).toFixed()
});
@@ -139,15 +140,22 @@ export default function CreateBountyTokenAmount({
return;
}
- const amountOfType =
+ let totalAmount =
BigNumber(type === "reward"
? _calculateTotalAmountFromGivenReward(value)
: value);
+
+ const contractTotalAmount = +toSmartContractDecimals(totalAmount.toString(), decimals);
+
+ if (contractTotalAmount % 100 !== 0) {
+ const adjustedContractTotalAmount = Math.ceil(contractTotalAmount / 100) * 100;
+ totalAmount = BigNumber(fromSmartContractDecimals(adjustedContractTotalAmount, decimals));
+ }
const initialDistributions = calculateDistributedAmounts( chain.closeFeePercentage,
mergeCreatorFeeShare,
proposerFeeShare,
- amountOfType,
+ totalAmount,
[
{
recipient: currentUser?.walletAddress,
@@ -166,24 +174,32 @@ export default function CreateBountyTokenAmount({
const _distributions = { totalServiceFees, ...initialDistributions}
if(type === 'reward'){
- const total = BigNumber(_calculateTotalAmountFromGivenReward(value));
- updateIssueAmount(handleNumberFormat(total))
- if (amountIsGtBalance(total.toNumber(), tokenBalance) && !isFunding) {
+ updateIssueAmount(handleNumberFormat(totalAmount))
+ const rewardValue = BigNumber(calculateRewardAmountGivenTotalAmount(totalAmount.toNumber()));
+
+ if (rewardValue !== value) {
+ setPreviewAmount(handleNumberFormat(rewardValue));
+ }
+
+ if (amountIsGtBalance(totalAmount.toNumber(), tokenBalance) && !isFunding) {
setInputError("bounty:errors.exceeds-allowance");
sethasAmountError(true);
}
}
if(type === 'total'){
- const rewardValue = BigNumber(calculateRewardAmountGivenTotalAmount(value));
+ const rewardValue = BigNumber(calculateRewardAmountGivenTotalAmount(totalAmount.toNumber()));
setPreviewAmount(handleNumberFormat(rewardValue));
+ if (totalAmount.toFixed() !== value.toString()) {
+ updateIssueAmount(handleNumberFormat(totalAmount));
+ }
+
if (rewardValue.isLessThan(BigNumber(currentToken?.minimum))) {
setInputError("bounty:errors.exceeds-minimum-amount");
sethasAmountError(true);
}
}
-
setDistributions(_distributions);
}
@@ -223,8 +239,8 @@ export default function CreateBountyTokenAmount({
setInputError("");
sethasAmountError(false);
}
- debouncedDistributionsUpdater(values.value, type);
setType(handleNumberFormat(BigNumber(values.value)));
+ debouncedDistributionsUpdater(values.value, type);
}
}
diff --git a/components/pages/task/create-task/controller.tsx b/components/pages/task/create-task/controller.tsx
index 91a845196c..8e2bd57164 100644
--- a/components/pages/task/create-task/controller.tsx
+++ b/components/pages/task/create-task/controller.tsx
@@ -466,7 +466,11 @@ export default function CreateTaskPage({
function handleUpdateToken(e: Token, type: 'transactional' | 'reward') {
const ERC20 = type === 'transactional' ? transactionalERC20 : rewardERC20
const setToken = type === 'transactional' ? setTransactionalToken : setRewardToken
- setToken(e)
+ setToken(e);
+ setIssueAmount(ZeroNumberFormatValues);
+ setRewardAmount(ZeroNumberFormatValues);
+ setPreviewAmount(ZeroNumberFormatValues);
+ setDistributions(undefined);
ERC20.setAddress(e.address)
}
diff --git a/helpers/analytic-events.ts b/helpers/analytic-events.ts
index e42ea71c43..1609ef7dd4 100644
--- a/helpers/analytic-events.ts
+++ b/helpers/analytic-events.ts
@@ -11,4 +11,5 @@ export const analyticEvents: AnalyticEvents = {
create_task_approve_amount: [analytic("ga4")],
create_pre_task: [analytic("ga4")],
created_task: [analytic("ga4")],
+ user_logged_in: [analytic("ga4")],
}
\ No newline at end of file
diff --git a/scripts/deploy-multichain.js b/scripts/deploy-multichain.js
index 4470187cc3..7d84ac0844 100644
--- a/scripts/deploy-multichain.js
+++ b/scripts/deploy-multichain.js
@@ -260,9 +260,9 @@ async function main(option = 0) {
isDefault: false,
color: "#29b6af",
lockAmountForNetworkCreation: DEPLOY_LOCK_AMOUNT_FOR_NETWORK_CREATION,
- networkCreationFeePercentage: DEPLOY_LOCK_FEE_PERCENTAGE || DEFAULT_LOCK_FEE_PERCENTAGE / DIVISOR,
- closeFeePercentage: DEPLOY_CLOSE_BOUNTY_FEE || DEFAULT_LOCK_FEE_PERCENTAGE / DIVISOR,
- cancelFeePercentage: DEPLOY_CANCEL_BOUNTY_FEE || DEFAULT_LOCK_FEE_PERCENTAGE / DIVISOR,
+ networkCreationFeePercentage: (+DEPLOY_LOCK_FEE_PERCENTAGE || DEFAULT_LOCK_FEE_PERCENTAGE) / DIVISOR,
+ closeFeePercentage: (+DEPLOY_CLOSE_BOUNTY_FEE || DEFAULT_CLOSE_BOUNTY_FEE) / DIVISOR,
+ cancelFeePercentage: (+DEPLOY_CANCEL_BOUNTY_FEE || DEFAULT_CANCEL_BOUNTY_FEE) / DIVISOR,
icon: "QmZ8dSeJp9pZn2TFy2gp7McfMj9HapqnPW3mwnnrDLKtZs",
}
});