diff --git a/src/assets/abis/erc20.json b/src/assets/abis/erc20.json new file mode 100644 index 000000000..2cb66870e --- /dev/null +++ b/src/assets/abis/erc20.json @@ -0,0 +1,37 @@ +{ + "abi": [ + { + "constant": false, + "inputs": [ + { "name": "spender", "type": "address" }, + { "name": "value", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { "name": "owner", "type": "address" }, + { "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "name": "", "type": "uint256" }], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{ "name": "", "type": "uint8" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ] +} \ No newline at end of file diff --git a/src/modules/etherlink/bridge/TokenBridge.tsx b/src/modules/etherlink/bridge/TokenBridge.tsx index fb8979dd8..ff9c30fc6 100644 --- a/src/modules/etherlink/bridge/TokenBridge.tsx +++ b/src/modules/etherlink/bridge/TokenBridge.tsx @@ -1,5 +1,5 @@ import React, { useState, useContext } from "react" -import { ethers } from "ethers" +import { ethers, parseUnits } from "ethers" import { EtherlinkContext } from "services/wagmi/context" import { Box, styled, Typography, Tabs, Tab, CircularProgress, Link } from "@material-ui/core" import { Button } from "components/ui/Button" @@ -7,1009 +7,13 @@ import { StyledTextField } from "components/ui/StyledTextField" import { useTezos } from "services/beacon/hooks/useTezos" import CheckCircleIcon from "@material-ui/icons/CheckCircle" import ErrorIcon from "@material-ui/icons/Error" +import ERC20_ABI from "assets/abis/erc20.json" +import WRAPPED_TOKEN_ABI from "assets/abis/hb_wrapped_evm.json" async function sleep(ms: number) { await new Promise(resolve => setTimeout(resolve, ms)) } -const ERC20_ABI = [ - { - constant: false, - inputs: [ - { name: "spender", type: "address" }, - { name: "value", type: "uint256" } - ], - name: "approve", - outputs: [{ name: "", type: "bool" }], - payable: false, - stateMutability: "nonpayable", - type: "function" - } -] - -const wrapperContractAbiJson = [ - { - inputs: [ - { - internalType: "contract IERC20", - name: "underlyingToken", - type: "address" - }, - { - internalType: "string", - name: "name_", - type: "string" - }, - { - internalType: "string", - name: "symbol_", - type: "string" - } - ], - stateMutability: "nonpayable", - type: "constructor" - }, - { - inputs: [], - name: "CheckpointUnorderedInsertion", - type: "error" - }, - { - inputs: [], - name: "ECDSAInvalidSignature", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "length", - type: "uint256" - } - ], - name: "ECDSAInvalidSignatureLength", - type: "error" - }, - { - inputs: [ - { - internalType: "bytes32", - name: "s", - type: "bytes32" - } - ], - name: "ECDSAInvalidSignatureS", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "increasedSupply", - type: "uint256" - }, - { - internalType: "uint256", - name: "cap", - type: "uint256" - } - ], - name: "ERC20ExceededSafeSupply", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "spender", - type: "address" - }, - { - internalType: "uint256", - name: "allowance", - type: "uint256" - }, - { - internalType: "uint256", - name: "needed", - type: "uint256" - } - ], - name: "ERC20InsufficientAllowance", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "sender", - type: "address" - }, - { - internalType: "uint256", - name: "balance", - type: "uint256" - }, - { - internalType: "uint256", - name: "needed", - type: "uint256" - } - ], - name: "ERC20InsufficientBalance", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "approver", - type: "address" - } - ], - name: "ERC20InvalidApprover", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "receiver", - type: "address" - } - ], - name: "ERC20InvalidReceiver", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "sender", - type: "address" - } - ], - name: "ERC20InvalidSender", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "spender", - type: "address" - } - ], - name: "ERC20InvalidSpender", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "token", - type: "address" - } - ], - name: "ERC20InvalidUnderlying", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "deadline", - type: "uint256" - } - ], - name: "ERC2612ExpiredSignature", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "signer", - type: "address" - }, - { - internalType: "address", - name: "owner", - type: "address" - } - ], - name: "ERC2612InvalidSigner", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "timepoint", - type: "uint256" - }, - { - internalType: "uint48", - name: "clock", - type: "uint48" - } - ], - name: "ERC5805FutureLookup", - type: "error" - }, - { - inputs: [], - name: "ERC6372InconsistentClock", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - }, - { - internalType: "uint256", - name: "currentNonce", - type: "uint256" - } - ], - name: "InvalidAccountNonce", - type: "error" - }, - { - inputs: [], - name: "InvalidShortString", - type: "error" - }, - { - inputs: [ - { - internalType: "uint8", - name: "bits", - type: "uint8" - }, - { - internalType: "uint256", - name: "value", - type: "uint256" - } - ], - name: "SafeCastOverflowedUintDowncast", - type: "error" - }, - { - inputs: [ - { - internalType: "address", - name: "token", - type: "address" - } - ], - name: "SafeERC20FailedOperation", - type: "error" - }, - { - inputs: [ - { - internalType: "string", - name: "str", - type: "string" - } - ], - name: "StringTooLong", - type: "error" - }, - { - inputs: [ - { - internalType: "uint256", - name: "expiry", - type: "uint256" - } - ], - name: "VotesExpiredSignature", - type: "error" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "owner", - type: "address" - }, - { - indexed: true, - internalType: "address", - name: "spender", - type: "address" - }, - { - indexed: false, - internalType: "uint256", - name: "value", - type: "uint256" - } - ], - name: "Approval", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "delegator", - type: "address" - }, - { - indexed: true, - internalType: "address", - name: "fromDelegate", - type: "address" - }, - { - indexed: true, - internalType: "address", - name: "toDelegate", - type: "address" - } - ], - name: "DelegateChanged", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "delegate", - type: "address" - }, - { - indexed: false, - internalType: "uint256", - name: "previousVotes", - type: "uint256" - }, - { - indexed: false, - internalType: "uint256", - name: "newVotes", - type: "uint256" - } - ], - name: "DelegateVotesChanged", - type: "event" - }, - { - anonymous: false, - inputs: [], - name: "EIP712DomainChanged", - type: "event" - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: "address", - name: "from", - type: "address" - }, - { - indexed: true, - internalType: "address", - name: "to", - type: "address" - }, - { - indexed: false, - internalType: "uint256", - name: "value", - type: "uint256" - } - ], - name: "Transfer", - type: "event" - }, - { - inputs: [], - name: "CLOCK_MODE", - outputs: [ - { - internalType: "string", - name: "", - type: "string" - } - ], - stateMutability: "pure", - type: "function" - }, - { - inputs: [], - name: "DOMAIN_SEPARATOR", - outputs: [ - { - internalType: "bytes32", - name: "", - type: "bytes32" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "admin", - outputs: [ - { - internalType: "address", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "owner", - type: "address" - }, - { - internalType: "address", - name: "spender", - type: "address" - } - ], - name: "allowance", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "spender", - type: "address" - }, - { - internalType: "uint256", - name: "value", - type: "uint256" - } - ], - name: "approve", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - } - ], - name: "balanceOf", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - }, - { - internalType: "uint32", - name: "pos", - type: "uint32" - } - ], - name: "checkpoints", - outputs: [ - { - components: [ - { - internalType: "uint48", - name: "_key", - type: "uint48" - }, - { - internalType: "uint208", - name: "_value", - type: "uint208" - } - ], - internalType: "struct Checkpoints.Checkpoint208", - name: "", - type: "tuple" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "clock", - outputs: [ - { - internalType: "uint48", - name: "", - type: "uint48" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "decimals", - outputs: [ - { - internalType: "uint8", - name: "", - type: "uint8" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "delegatee", - type: "address" - } - ], - name: "delegate", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "delegatee", - type: "address" - }, - { - internalType: "uint256", - name: "nonce", - type: "uint256" - }, - { - internalType: "uint256", - name: "expiry", - type: "uint256" - }, - { - internalType: "uint8", - name: "v", - type: "uint8" - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32" - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32" - } - ], - name: "delegateBySig", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - } - ], - name: "delegates", - outputs: [ - { - internalType: "address", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - }, - { - internalType: "uint256", - name: "value", - type: "uint256" - } - ], - name: "depositFor", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [], - name: "eip712Domain", - outputs: [ - { - internalType: "bytes1", - name: "fields", - type: "bytes1" - }, - { - internalType: "string", - name: "name", - type: "string" - }, - { - internalType: "string", - name: "version", - type: "string" - }, - { - internalType: "uint256", - name: "chainId", - type: "uint256" - }, - { - internalType: "address", - name: "verifyingContract", - type: "address" - }, - { - internalType: "bytes32", - name: "salt", - type: "bytes32" - }, - { - internalType: "uint256[]", - name: "extensions", - type: "uint256[]" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "uint256", - name: "timepoint", - type: "uint256" - } - ], - name: "getPastTotalSupply", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - }, - { - internalType: "uint256", - name: "timepoint", - type: "uint256" - } - ], - name: "getPastVotes", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - } - ], - name: "getVotes", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "name", - outputs: [ - { - internalType: "string", - name: "", - type: "string" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "owner_", - type: "address" - } - ], - name: "nonces", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - } - ], - name: "numCheckpoints", - outputs: [ - { - internalType: "uint32", - name: "", - type: "uint32" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "owner", - type: "address" - }, - { - internalType: "address", - name: "spender", - type: "address" - }, - { - internalType: "uint256", - name: "value", - type: "uint256" - }, - { - internalType: "uint256", - name: "deadline", - type: "uint256" - }, - { - internalType: "uint8", - name: "v", - type: "uint8" - }, - { - internalType: "bytes32", - name: "r", - type: "bytes32" - }, - { - internalType: "bytes32", - name: "s", - type: "bytes32" - } - ], - name: "permit", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "newAdmin", - type: "address" - } - ], - name: "setAdmin", - outputs: [], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [], - name: "symbol", - outputs: [ - { - internalType: "string", - name: "", - type: "string" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [], - name: "totalSupply", - outputs: [ - { - internalType: "uint256", - name: "", - type: "uint256" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "to", - type: "address" - }, - { - internalType: "uint256", - name: "value", - type: "uint256" - } - ], - name: "transfer", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "from", - type: "address" - }, - { - internalType: "address", - name: "to", - type: "address" - }, - { - internalType: "uint256", - name: "value", - type: "uint256" - } - ], - name: "transferFrom", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "nonpayable", - type: "function" - }, - { - inputs: [], - name: "underlying", - outputs: [ - { - internalType: "contract IERC20", - name: "", - type: "address" - } - ], - stateMutability: "view", - type: "function" - }, - { - inputs: [ - { - internalType: "address", - name: "account", - type: "address" - }, - { - internalType: "uint256", - name: "value", - type: "uint256" - } - ], - name: "withdrawTo", - outputs: [ - { - internalType: "bool", - name: "", - type: "bool" - } - ], - stateMutability: "nonpayable", - type: "function" - } -] - const WrapContainer = styled(Box)({ background: "#1c2024", borderRadius: "8px", @@ -1075,19 +79,33 @@ export const TokenBridge = () => { setErrorMessage(null) // Approval logic - const erc20Contract = new ethers.Contract(daoSelected.underlyingToken, ERC20_ABI, etherlink.signer) - const tx1 = await erc20Contract.approve(daoSelected.token, wrapAmount) - setApprovalTxHash(tx1.hash) - setTransactionState("approving") - await tx1.wait() + const erc20Contract = new ethers.Contract(daoSelected.underlyingToken, ERC20_ABI.abi, etherlink.signer) + + // Get token decimals + const decimals = await erc20Contract.decimals() + const amountInWei = parseUnits(wrapAmount, decimals) + + // Check existing allowance + const currentAllowance = await erc20Contract.allowance(etherlink.signer.address, daoSelected.token) + + // Only approve if current allowance is insufficient + if (currentAllowance.lt(amountInWei)) { + const tx1 = await erc20Contract.approve(daoSelected.token, amountInWei) + setApprovalTxHash(tx1.hash) + setTransactionState("approving") + await tx1.wait() + } else { + console.log("Sufficient allowance already exists, skipping approval") + setTransactionState("waitingExecution") + } await sleep(1000) // Wrap logic console.log("wrapperContractAbiHumanReadable", etherlink.signer.address, daoSelected.underlyingToken) setTransactionState("waitingExecution") - const wrapperContract = new ethers.Contract(daoSelected.token, wrapperContractAbiJson, etherlink.signer) - const tx2 = await wrapperContract.depositFor(etherlink.signer.address, wrapAmount) + const wrapperContract = new ethers.Contract(daoSelected.token, WRAPPED_TOKEN_ABI.abi, etherlink.signer) + const tx2 = await wrapperContract.depositFor(etherlink.signer.address, amountInWei) setExecutionTxHash(tx2.hash) setTransactionState("executing") await tx2.wait() @@ -1114,8 +132,12 @@ export const TokenBridge = () => { setTransactionState("waitingExecution") setErrorMessage(null) - const wrapperContract = new ethers.Contract(daoSelected.token, wrapperContractAbiJson, etherlink.signer) - const tx = await wrapperContract.withdrawTo(etherlink.signer.address, wrapAmount) + // Get token decimals from wrapped token + const wrapperContract = new ethers.Contract(daoSelected.token, WRAPPED_TOKEN_ABI.abi, etherlink.signer) + const decimals = await wrapperContract.decimals() + const amountInWei = parseUnits(wrapAmount, decimals) + + const tx = await wrapperContract.withdrawTo(etherlink.signer.address, amountInWei) setExecutionTxHash(tx.hash) setTransactionState("executing") await tx.wait() @@ -1139,12 +161,12 @@ export const TokenBridge = () => { return ( - + - Convert {daoSelected?.symbol} <> underlying + Deposit underlying tokens - Use this to wrap your underlying tokens into governance tokens, or unwrap them back. + Wrap your underlying tokens into {daoSelected?.symbol}. You may unwrap them at any future time { setWrapAmount(e.target.value)} style={{ width: "100%", marginBottom: "16px" }} @@ -1235,10 +255,12 @@ export const TokenBridge = () => { {transactionState === "waitingExecution" && ( <> - - - Approval complete - + {wrapTabValue === 0 && ( + + + {approvalTxHash ? "Approval complete" : "Sufficient allowance available"} + + )} Waiting for {wrapTabValue === 0 ? "wrap" : "unwrap"} transaction... diff --git a/src/modules/etherlink/constants.ts b/src/modules/etherlink/constants.ts new file mode 100644 index 000000000..9da253f3e --- /dev/null +++ b/src/modules/etherlink/constants.ts @@ -0,0 +1,9 @@ +// Etherlink-specific constants + +// Contract addresses +export const ETHERLINK_CONSTANTS = { + // Fallback wrapper address for wrapped tokens + WRAPPER_W_FALLBACK_ADDRESS: "0xf4B3022b0fb4e8A73082ba9081722d6a276195c2" +} as const + +export type EtherlinkConstants = typeof ETHERLINK_CONSTANTS diff --git a/src/services/contracts/etherlinkDAO/hooks/useEvmDaoCreateStore.tsx b/src/services/contracts/etherlinkDAO/hooks/useEvmDaoCreateStore.tsx index de9255d28..db02b2904 100644 --- a/src/services/contracts/etherlinkDAO/hooks/useEvmDaoCreateStore.tsx +++ b/src/services/contracts/etherlinkDAO/hooks/useEvmDaoCreateStore.tsx @@ -13,6 +13,7 @@ import { useCallback, useContext, useState } from "react" import { useTezos } from "services/beacon/hooks/useTezos" import { EtherlinkContext } from "services/wagmi/context" import { useNotification } from "modules/common/hooks/useNotification" +import { ETHERLINK_CONSTANTS } from "modules/etherlink/constants" interface EvmDaoCreateStore { currentStep: number @@ -165,7 +166,7 @@ const useEvmDaoCreateStore = () => { const history = useHistory() const { contractData } = useContext(EtherlinkContext) const wrapperAddress = contractData?.wrapper_t - const wrapperAddressForWrapped = contractData?.wrapper_w || "0xf4B3022b0fb4e8A73082ba9081722d6a276195c2" // Fallback to known address + const wrapperAddressForWrapped = contractData?.wrapper_w || ETHERLINK_CONSTANTS.WRAPPER_W_FALLBACK_ADDRESS // Fallback to known address const { etherlink } = useTezos() const notify = useNotification() @@ -272,7 +273,7 @@ const useEvmDaoCreateStore = () => { } // Use legacy ABI if using the fallback address - const isUsingFallbackAddress = selectedWrapperAddress === "0xf4B3022b0fb4e8A73082ba9081722d6a276195c2" + const isUsingFallbackAddress = selectedWrapperAddress === ETHERLINK_CONSTANTS.WRAPPER_W_FALLBACK_ADDRESS const selectedAbi = daoData.tokenDeploymentMechanism === "wrapped" ? isUsingFallbackAddress @@ -338,7 +339,6 @@ const useEvmDaoCreateStore = () => { proposalThresholdType: typeof proposalThreshold, proposalThresholdAsBigInt: BigInt(proposalThreshold || 0).toString() }) - const wrappedDaoPayload = isUsingFallbackAddress ? { // Legacy structure without wrappedTokenName @@ -349,7 +349,7 @@ const useEvmDaoCreateStore = () => { underlyingTokenAddress: daoData.underlyingTokenAddress, minsVotingDelay: Math.min(Math.max(votingDelayInMinutes, 0), 2 ** 48 - 1), // uint48 minsVotingPeriod: Math.min(Math.max(votingDurationInMinutes, 0), 2 ** 32 - 1), // uint32 - proposalThreshold: BigInt(proposalThreshold || 0), // uint256 - raw token amount + proposalThreshold: parseUnits(isNaN(proposalThreshold) ? "0" : proposalThreshold.toString(), 18), // uint256 in wei quorumFraction: Math.min(Math.max(Number(quorumThreshold), 0), 100), // uint8 keys: Object.keys(daoData.registry || {}), values: Object.values(daoData.registry || {}).map(v => String(v)) @@ -364,7 +364,7 @@ const useEvmDaoCreateStore = () => { underlyingTokenAddress: daoData.underlyingTokenAddress, minsVotingDelay: Math.min(Math.max(votingDelayInMinutes, 0), 2 ** 48 - 1), // uint48 minsVotingPeriod: Math.min(Math.max(votingDurationInMinutes, 0), 2 ** 32 - 1), // uint32 - proposalThreshold: BigInt(proposalThreshold || 0), // uint256 - raw token amount + proposalThreshold: parseUnits(isNaN(proposalThreshold) ? "0" : proposalThreshold.toString(), 18), // uint256 in wei quorumFraction: Math.min(Math.max(Number(quorumThreshold), 0), 100), // uint8 keys: Object.keys(daoData.registry || {}), values: Object.values(daoData.registry || {}).map(v => String(v))