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", } });