ethers/lib/utils#parseUnits TypeScript Examples

The following examples show how to use ethers/lib/utils#parseUnits. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: generator.ts    From merkle-airdrop-starter with GNU Affero General Public License v3.0 6 votes vote down vote up
/**
   * Setup generator
   * @param {number} decimals of token
   * @param {Record<string, number>} airdrop address to token claim mapping
   */
  constructor(decimals: number, airdrop: Record<string, number>) {
    // For each airdrop entry
    for (const [address, tokens] of Object.entries(airdrop)) {
      // Push:
      this.recipients.push({
        // Checksum address
        address: getAddress(address),
        // Scaled number of tokens claimable by recipient
        value: parseUnits(tokens.toString(), decimals).toString()
      });
    }
  }
Example #2
Source File: useStringToBigNumber.ts    From dxvote with GNU Affero General Public License v3.0 6 votes vote down vote up
export default function useStringToBigNumber(
  numString: string,
  decimals: number
) {
  const bigNumber = useMemo(() => {
    if (numString) {
      return parseUnits(numString, decimals);
    } else {
      return null;
    }
  }, [numString, decimals]);

  return bigNumber;
}
Example #3
Source File: useUnstakePool.ts    From vvs-ui with GNU General Public License v3.0 6 votes vote down vote up
sousUnstake = async (sousChefContract: any, amount: string, decimals: number) => {
  const gasPrice = getGasPrice()
  const units = parseUnits(amount, decimals)

  const tx = await sousChefContract.withdraw(units.toString(), {
    gasPrice,
  })
  const receipt = await tx.wait()
  return receipt.status
}
Example #4
Source File: tryParseAmount.ts    From skeleton-web3-interface with GNU General Public License v3.0 6 votes vote down vote up
// try to parse a user entered amount for a given token
export function tryParseAmount(value?: string, currency?: Currency): CurrencyAmount | undefined {
  if (!value || !currency) {
    return undefined
  }
  try {
    const typedValueParsed = parseUnits(value, currency.decimals).toString()
    if (typedValueParsed !== '0') {
      return currency instanceof Token
        ? new TokenAmount(currency, JSBI.BigInt(typedValueParsed))
        : CurrencyAmount.ether(JSBI.BigInt(typedValueParsed))
    }
  } catch (error) {
    // should fail if the user specifies too many decimal places of precision (or maybe exceed max uint?)
    console.debug(`Failed to parse input amount: "${value}"`, error)
  }
  // necessary for all paths to return a value
  return undefined
}
Example #5
Source File: bridge.test.ts    From arbitrum-dai-bridge with GNU Affero General Public License v3.0 5 votes vote down vote up
amount = parseUnits('7', 'ether')
Example #6
Source File: SetPositionCard.tsx    From vvs-ui with GNU General Public License v3.0 5 votes vote down vote up
getValueAsEthersBn = (value: string) => {
  const valueAsFloat = parseFloat(value)
  return Number.isNaN(valueAsFloat) ? ethers.BigNumber.from(0) : parseUnits(value)
}
Example #7
Source File: SetPositionCard.tsx    From vvs-ui with GNU General Public License v3.0 5 votes vote down vote up
dust = parseUnits('0.01', 18)
Example #8
Source File: ContributeModal.tsx    From vvs-ui with GNU General Public License v3.0 5 votes vote down vote up
gasPrice = parseUnits('10', 'gwei').toString()
Example #9
Source File: helpers.ts    From vvs-ui with GNU General Public License v3.0 5 votes vote down vote up
GAS_PRICE_GWEI = {
  default: parseUnits(GAS_PRICE.default, 'gwei').toString(),
  fast: parseUnits(GAS_PRICE.fast, 'gwei').toString(),
  instant: parseUnits(GAS_PRICE.instant, 'gwei').toString(),
  testnet: parseUnits(GAS_PRICE.testnet, 'gwei').toString(),
}
Example #10
Source File: ModalInput.tsx    From vvs-ui with GNU General Public License v3.0 5 votes vote down vote up
ModalInput: React.FC<ModalInputProps> = ({
  max,
  symbol,
  onChange,
  onSelectMax,
  value,
  addLiquidityUrl,
  inputTitle,
  decimals = 18,
}) => {
  const { t } = useTranslation()
  const isBalanceZero = max === '0' || !max

  const displayBalance = (balance: string) => {
    if (isBalanceZero) {
      return '0'
    }

    const balanceUnits = parseUnits(balance, decimals)
    return formatBigNumber(balanceUnits, decimals, decimals)
  }

  return (
    <div style={{ position: 'relative' }}>
      <StyledTokenInput isWarning={isBalanceZero}>
        <Flex justifyContent="space-between" pl="16px">
          <Text fontSize="14px">{inputTitle}</Text>
          <Text fontSize="14px">{t('Balance: %balance%', { balance: displayBalance(max) })}</Text>
        </Flex>
        <Flex alignItems="flex-end" justifyContent="space-around">
          <StyledInput
            pattern={`^[0-9]*[.,]?[0-9]{0,${decimals}}$`}
            inputMode="decimal"
            step="any"
            min="0"
            onChange={onChange}
            placeholder="0"
            value={value}
          />
          <Button scale="sm" onClick={onSelectMax} mr="8px">
            {t('Max')}
          </Button>
          <Text fontSize="16px">{symbol}</Text>
        </Flex>
      </StyledTokenInput>
      {isBalanceZero && (
        <StyledErrorMessage fontSize="14px" color="failure">
          {t('No tokens to stake')}:{' '}
          <Link fontSize="14px" bold={false} href={addLiquidityUrl} external color="failure">
            {t('Get %symbol%', { symbol })}
          </Link>
        </StyledErrorMessage>
      )}
    </div>
  )
}
Example #11
Source File: WalletModal.tsx    From vvs-ui with GNU General Public License v3.0 5 votes vote down vote up
LOW_CRO_BALANCE = parseUnits('2', 'gwei')
Example #12
Source File: decimal.ts    From hubble-contracts with MIT License 5 votes vote down vote up
/**
     * @param humanValue could be a fractional number but is string. Like '1.23'
     */
    fromHumanValue(humanValue: string) {
        const l1Value = parseUnits(humanValue, this.l1Decimals);
        return new ERC20Value(this.decimals, l1Value);
    }
Example #13
Source File: bridge.test.ts    From optimism-dai-bridge with GNU Affero General Public License v3.0 5 votes vote down vote up
initialL1DaiNumber = parseUnits('10000', 'ether')
Example #14
Source File: bridge.test.ts    From optimism-dai-bridge with GNU Affero General Public License v3.0 5 votes vote down vote up
depositAmount = parseUnits('500', 'ether')
Example #15
Source File: L1GovernanceRelay.ts    From arbitrum-dai-bridge with GNU Affero General Public License v3.0 5 votes vote down vote up
defaultEthValue = parseUnits('0.1', 'ether')
Example #16
Source File: index.ts    From common-ts with MIT License 5 votes vote down vote up
parseGRT = (grt: string): BigNumber => parseUnits(grt, 18)
Example #17
Source File: SetPositionCard.tsx    From glide-frontend with GNU General Public License v3.0 5 votes vote down vote up
getValueAsEthersBn = (value: string) => {
  const valueAsFloat = parseFloat(value)
  return Number.isNaN(valueAsFloat) ? ethers.BigNumber.from(0) : parseUnits(value)
}
Example #18
Source File: SetPositionCard.tsx    From glide-frontend with GNU General Public License v3.0 5 votes vote down vote up
dust = parseUnits('0.01', 18)
Example #19
Source File: SetPositionCard.tsx    From glide-frontend with GNU General Public License v3.0 5 votes vote down vote up
gasPrice = parseUnits('6', 'gwei')
Example #20
Source File: tx.test.ts    From bodhi.js with Apache License 2.0 4 votes vote down vote up
describe('transaction tests', () => {
  const endpoint = process.env.ENDPOINT_URL || 'ws://127.0.0.1:9944';
  const provider = EvmRpcProvider.from(endpoint);

  const account1 = evmAccounts[0];
  const account2 = evmAccounts[1];
  const account3 = evmAccounts[2];
  const account4 = evmAccounts[3];
  const wallet1 = new Wallet(account1.privateKey).connect(provider as any);
  const wallet2 = new Wallet(account2.privateKey).connect(provider as any);
  const wallet3 = new Wallet(account3.privateKey).connect(provider as any);
  const wallet4 = new Wallet(account4.privateKey).connect(provider as any);

  let chainId: number;
  let storageByteDeposit: bigint;
  let txFeePerGas: bigint;
  let txGasLimit: BigNumber;
  let txGasPrice: BigNumber;

  before('prepare common variables', async () => {
    await provider.isReady();

    chainId = await provider.chainId();
    storageByteDeposit = (provider.api.consts.evm.storageDepositPerByte as UInt).toBigInt();
    txFeePerGas = (provider.api.consts.evm.txFeePerGas as UInt).toBigInt();

    ({ txGasLimit, txGasPrice } = calcEthereumTransactionParams({
      gasLimit: 2100001n,
      validUntil: 360001n,
      storageLimit: 64001n,
      txFeePerGas,
      storageByteDeposit
    }));
  });

  after('clean up', async () => {
    await provider.disconnect();
  });

  describe('test eth gas', () => {
    it('getEthResources', async () => {
      const randomWallet = Wallet.createRandom().connect(provider);

      const amount = '1000000000000000000';
      const resources = await provider.getEthResources({
        type: 0,
        from: wallet3.address,
        to: randomWallet.address,
        value: BigNumber.from(amount)
      });

      await wallet3.sendTransaction({
        type: 0,
        to: randomWallet.address,
        value: BigNumber.from(amount),
        ...resources
      });

      expect((await randomWallet.getBalance()).toString()).eq(amount);
    });

    it('getPrice', async () => {
      const randomWallet = Wallet.createRandom().connect(provider);

      const amount = '1000000000000000000';

      const params = await wallet3.populateTransaction({
        type: 0,
        to: randomWallet.address,
        value: BigNumber.from(amount)
      });

      const data = provider.validSubstrateResources({
        gasLimit: params.gasLimit,
        gasPrice: params.gasPrice
      });

      console.log({
        gasLimit: data.gasLimit.toString(),
        storageLimit: data.storageLimit.toString(),
        validUntil: data.validUntil.toString()
      });

      // expect((await randomWallet.getBalance()).toString()).eq(amount);
    });
  });

  describe('test the error tx', () => {
    it('InvalidDecimals', async () => {
      await expect(
        wallet1.sendTransaction({
          type: 0,
          to: wallet2.address,
          value: 1000001,
          gasLimit: txGasLimit,
          gasPrice: txGasPrice
        })
      ).to.be.rejectedWith('InvalidDecimals');
    });

    it('OutOfFund', async () => {
      await expect(
        wallet1.sendTransaction({
          type: 0,
          to: wallet2.address,
          value: 1000000000n * 10n ** 18n,
          gasLimit: txGasLimit,
          gasPrice: txGasPrice
        })
      ).to.be.rejectedWith('OutOfFund');
    });

    it('ExistentialDeposit', async () => {
      await expect(
        wallet3.sendTransaction({
          type: 0,
          to: Wallet.createRandom().address,
          value: 1000000,
          gasLimit: txGasLimit,
          gasPrice: txGasPrice
        })
      ).to.be.rejectedWith('ExistentialDeposit');
    });
  });

  describe('test deploy contract (hello world)', () => {
    const deployHelloWorldData =
      '0x60806040526040518060400160405280600c81526020017f48656c6c6f20576f726c642100000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610166565b82805461006e90610134565b90600052602060002090601f01602090048101928261009057600085556100d7565b82601f106100a957805160ff19168380011785556100d7565b828001600101855582156100d7579182015b828111156100d65782518255916020019190600101906100bb565b5b5090506100e491906100e8565b5090565b5b808211156101015760008160009055506001016100e9565b5090565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061014c57607f821691505b602082108114156101605761015f610105565b5b50919050565b61022e806101756000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063c605f76c14610030575b600080fd5b61003861004e565b6040516100459190610175565b60405180910390f35b6000805461005b906101c6565b80601f0160208091040260200160405190810160405280929190818152602001828054610087906101c6565b80156100d45780601f106100a9576101008083540402835291602001916100d4565b820191906000526020600020905b8154815290600101906020018083116100b757829003601f168201915b505050505081565b600081519050919050565b600082825260208201905092915050565b60005b838110156101165780820151818401526020810190506100fb565b83811115610125576000848401525b50505050565b6000601f19601f8301169050919050565b6000610147826100dc565b61015181856100e7565b93506101618185602086016100f8565b61016a8161012b565b840191505092915050565b6000602082019050818103600083015261018f818461013c565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806101de57607f821691505b602082108114156101f2576101f1610197565b5b5091905056fea26469706673582212204d363ed34111d1be492d4fd086e9f2df62b3c625e89ade31f30e63201ed1e24f64736f6c63430008090033';

    let partialDeployTx;

    before(() => {
      partialDeployTx = {
        chainId,
        gasLimit: txGasLimit,
        gasPrice: txGasPrice,
        data: deployHelloWorldData,
        value: BigNumber.from(0)
      };
    });

    describe('with wallet', () => {
      it('succeeds', async () => {
        const response = await wallet1.sendTransaction({
          ...partialDeployTx,
          type: 0
        });
        const receipt = await response.wait(0);

        expect(receipt.type).equal(0);
        expect(receipt.status).equal(1);
        expect(receipt.from).equal(wallet1.address);
      });
    });

    describe('with legacy EIP-155 signature', () => {
      it('serialize, parse, and send tx correctly', async () => {
        const unsignedTx: AcalaEvmTX = {
          ...partialDeployTx,
          nonce: await wallet1.getTransactionCount()
        };

        const rawTx = await wallet1.signTransaction(unsignedTx);
        const parsedTx = parseTransaction(rawTx);

        expect(parsedTx.gasPrice.eq(txGasPrice)).equal(true);
        expect(parsedTx.gasLimit.eq(txGasLimit)).equal(true);

        expect(parsedTx.from).equal(wallet1.address);
        expect(parsedTx.data).equal(deployHelloWorldData);
        expect(parsedTx.type).equal(null);
        expect(parsedTx.maxPriorityFeePerGas).equal(undefined);
        expect(parsedTx.maxFeePerGas).equal(undefined);

        const response = await provider.sendTransaction(rawTx);
        const receipt = await response.wait(0);

        expect(receipt.type).equal(0); // TODO: should be null, need to fix getPartialTransactionReceipt
        expect(receipt.status).equal(1);
        expect(receipt.from).equal(wallet1.address);
      });
    });

    describe('with EIP-1559 signature', () => {
      it('serialize, parse, and send tx correctly', async () => {
        const priorityFee = BigNumber.from(2);
        const unsignedTx: AcalaEvmTX = {
          ...partialDeployTx,
          nonce: await wallet1.getTransactionCount(),
          gasPrice: undefined,
          maxPriorityFeePerGas: priorityFee,
          maxFeePerGas: txGasPrice,
          type: 2
        };

        const rawTx = await wallet1.signTransaction(unsignedTx);
        const parsedTx = parseTransaction(rawTx);

        expect(parsedTx.maxFeePerGas.eq(txGasPrice)).equal(true);
        expect(parsedTx.maxPriorityFeePerGas.eq(priorityFee)).equal(true);
        expect(parsedTx.gasLimit.eq(txGasLimit)).equal(true);

        expect(parsedTx.from).equal(wallet1.address);
        expect(parsedTx.data).equal(deployHelloWorldData);
        expect(parsedTx.type).equal(2);
        expect(parsedTx.gasPrice).equal(null);

        const response = await provider.sendTransaction(rawTx);
        const receipt = await response.wait(0);

        expect(receipt.type).equal(0); // TODO: should be 2, need to fix getPartialTransactionReceipt
        expect(receipt.status).equal(1);
        expect(receipt.from).equal(wallet1.address);
      });
    });

    describe('with EIP-712 signature', () => {
      let rawTx1: string;
      let rawTx2: string;

      it('serialize, parse, and send tx correctly', async () => {
        const gasLimit = BigNumber.from('210000');
        const validUntil = 10000;
        const storageLimit = 100000;

        const unsignEip712Tx: AcalaEvmTX = {
          ...partialDeployTx,
          nonce: await wallet1.getTransactionCount(),
          salt: provider.genesisHash,
          gasLimit,
          validUntil,
          storageLimit,
          type: 0x60,
          accessList: []
        };

        const sig = signTransaction(account1.privateKey, unsignEip712Tx);
        rawTx1 = serializeTransaction(unsignEip712Tx, sig);
        const parsedTx = parseTransaction(rawTx1);

        expect(parsedTx.gasLimit.eq(gasLimit)).equal(true);
        expect(parsedTx.validUntil.eq(validUntil)).equal(true);
        expect(parsedTx.storageLimit.eq(storageLimit)).equal(true);

        expect(parsedTx.from).equal(wallet1.address);
        expect(parsedTx.data).equal(deployHelloWorldData);
        expect(parsedTx.type).equal(96);
        expect(parsedTx.maxPriorityFeePerGas).equal(undefined);
        expect(parsedTx.maxFeePerGas).equal(undefined);
      });

      it('eip712 tip', async () => {
        const gasLimit = BigNumber.from('210000');
        const validUntil = 10000;
        const storageLimit = 100000;

        const unsignEip712Tx: AcalaEvmTX = {
          ...partialDeployTx,
          nonce: (await wallet1.getTransactionCount()) + 1,
          salt: provider.genesisHash,
          gasLimit,
          validUntil,
          storageLimit,
          tip: 2,
          type: 0x60,
          accessList: []
        };

        const sig = signTransaction(account1.privateKey, unsignEip712Tx);
        rawTx2 = serializeTransaction(unsignEip712Tx, sig);
        const parsedTx = parseTransaction(rawTx2);

        expect(parsedTx.gasLimit.eq(gasLimit)).equal(true);
        expect(parsedTx.validUntil.eq(validUntil)).equal(true);
        expect(parsedTx.storageLimit.eq(storageLimit)).equal(true);

        expect(parsedTx.tip.eq(2)).equal(true);
        expect(parsedTx.from).equal(wallet1.address);
        expect(parsedTx.data).equal(deployHelloWorldData);
        expect(parsedTx.type).equal(96);
        expect(parsedTx.maxPriorityFeePerGas).equal(undefined);
        expect(parsedTx.maxFeePerGas).equal(undefined);
      });

      it('send eip712 tx', async () => {
        await provider.sendTransaction(rawTx1);
        await provider.sendTransaction(rawTx2);
      });
    });
  });

  describe('test call contract (transfer ACA)', () => {
    const ACADigits = provider.api.registry.chainDecimals[0];
    const acaContract = new Contract(ADDRESS.ACA, ACAABI.abi, wallet1);
    const iface = new Interface(ACAABI.abi);
    const queryBalance = (addr) => acaContract.balanceOf(addr) as BigNumber;
    const transferAmount = parseUnits('100', ACADigits);
    let partialTransferTX: any;

    before(() => {
      partialTransferTX = {
        chainId,
        to: ADDRESS.ACA,
        gasLimit: txGasLimit,
        gasPrice: txGasPrice,
        data: iface.encodeFunctionData('transfer', [account2.evmAddress, transferAmount]),
        value: BigNumber.from(0)
      };
    });

    it('evm address match', () => {
      expect(computeDefaultSubstrateAddress(account1.evmAddress)).to.equal(account1.defaultSubstrateAddress);
      expect(computeDefaultSubstrateAddress(account2.evmAddress)).to.equal(account2.defaultSubstrateAddress);
    });

    describe('with provider', () => {
      it('has correct balance after transfer', async () => {
        const pairs = createTestPairs();
        const alice = pairs.alice;

        const oneAca = 10n ** BigInt(ACADigits);
        const amount = 1000n * oneAca;
        const balance = await queryBalance(account1.evmAddress);

        const extrinsic = provider.api.tx.balances.transfer(account1.defaultSubstrateAddress, amount);
        await extrinsic.signAsync(alice);
        await sendTx(provider.api, extrinsic);

        const _balance = await queryBalance(account1.evmAddress);
        expect(_balance.sub(balance).toBigInt()).equal(amount);
      });
    });

    describe('with legacy EIP-155 signature', () => {
      it('has correct balance after transfer', async () => {
        const balance1 = await queryBalance(account1.evmAddress);
        const balance2 = await queryBalance(account2.evmAddress);

        const transferTX: AcalaEvmTX = {
          ...partialTransferTX,
          nonce: await wallet1.getTransactionCount()
        };

        const rawTx = await wallet1.signTransaction(transferTX);
        const parsedTx = parseTransaction(rawTx);

        const response = await provider.sendTransaction(rawTx);
        const receipt = await response.wait(0);

        const _balance1 = await queryBalance(account1.evmAddress);
        const _balance2 = await queryBalance(account2.evmAddress);

        // TODO: check sender's balance is correct
        // expect(balance1.sub(_balance1).toNumber()).equal(transferAmount.toNumber() + gasUsed);
        expect(_balance2.sub(balance2).toNumber()).equal(transferAmount.toNumber());
      });
    });

    describe('with EIP-1559 signature', () => {
      it('has correct balance after transfer', async () => {
        const balance1 = await queryBalance(account1.evmAddress);
        const balance2 = await queryBalance(account2.evmAddress);

        const priorityFee = BigNumber.from(2);
        const transferTX: AcalaEvmTX = {
          ...partialTransferTX,
          nonce: await wallet1.getTransactionCount(),
          gasPrice: undefined,
          maxPriorityFeePerGas: priorityFee,
          maxFeePerGas: txGasPrice,
          type: 2
        };

        const rawTx = await wallet1.signTransaction(transferTX);
        const parsedTx = parseTransaction(rawTx);

        const response = await provider.sendTransaction(rawTx);
        const receipt = await response.wait(0);

        const _balance1 = await queryBalance(account1.evmAddress);
        const _balance2 = await queryBalance(account2.evmAddress);

        // TODO: check sender's balance is correct
        // expect(balance1.sub(_balance1).toNumber()).equal(transferAmount.toNumber() + gasUsed);
        expect(_balance2.sub(balance2).toNumber()).equal(transferAmount.toNumber());
      });
    });

    describe('with EIP-712 signature', () => {
      it('has correct balance after transfer', async () => {
        const balance1 = await queryBalance(account1.evmAddress);
        const balance2 = await queryBalance(account2.evmAddress);

        const gasLimit = BigNumber.from('210000');
        const validUntil = 10000;
        const storageLimit = 100000;

        const transferTX: AcalaEvmTX = {
          ...partialTransferTX,
          nonce: await wallet1.getTransactionCount(),
          salt: provider.genesisHash,
          gasLimit,
          validUntil,
          storageLimit,
          type: 0x60,
          accessList: []
        };

        const sig = signTransaction(account1.privateKey, transferTX);
        const rawTx = serializeTransaction(transferTX, sig);
        const parsedTx = parseTransaction(rawTx);

        await provider.sendTransaction(rawTx);

        const _balance1 = await queryBalance(account1.evmAddress);
        const _balance2 = await queryBalance(account2.evmAddress);

        // TODO: check sender's balance is correct
        // expect(balance1.sub(_balance1).toNumber()).equal(transferAmount.toNumber() + gasUsed);
        expect(_balance2.sub(balance2).toNumber()).equal(transferAmount.toNumber());
      });
    });
  });
});
Example #21
Source File: 0004-layer2-clearingHouse-amm-openPos.ts    From perpetual-protocol with GNU General Public License v3.0 4 votes vote down vote up
migration: MigrationDefinition = {
    configPath: "hardhat.flatten.clearinghouse.config.ts",

    // deploy the flattened clearingHouse and init it just in case
    getTasks: (context: MigrationContext) => {
        let arbitrageur: string
        let oldClearingHouseImpAddr: string
        let oldAmmImpAddr: string
        let newClearingHouseImplAddr: string
        let newAmmImplAddr: string
        let ammETHAddr: string
        let arbitrageurPosition: any
        let oldQuoteAssetReserve: BigNumber
        let quoteAssetAddr: string
        return [
            async (): Promise<void> => {
                console.log("get state variables for verification later...")

                // flat clearingHouse
                const fileClearingHouse = `${ContractName.ClearingHouse}.sol`
                await flatten(SRC_DIR, hre.config.paths.sources, fileClearingHouse)
                await hre.run(TASK_COMPILE)

                // flat Amm
                const fileAmm = `${ContractName.Amm}.sol`
                await flatten(SRC_DIR, hre.config.paths.sources, fileAmm)
                await hre.run(TASK_COMPILE)

                const clearingHouseContract = await context.factory
                    .create<ClearingHouse>(ContractFullyQualifiedName.FlattenClearingHouse)
                    .instance()
                const ammContract = await context.factory
                    .createAmm(AmmInstanceName.ETHUSDC, ContractFullyQualifiedName.FlattenAmmUnderClearingHouse)
                    .instance()

                oldClearingHouseImpAddr = await getImplementation(clearingHouseContract.address)
                oldAmmImpAddr = await getImplementation(ammContract.address)
                ammETHAddr = ammContract.address

                arbitrageur = context.externalContract.arbitrageur!
                arbitrageurPosition = await clearingHouseContract.getPosition(ammETHAddr, arbitrageur)
                oldQuoteAssetReserve = await ammContract.quoteAssetReserve()
                quoteAssetAddr = await ammContract.quoteAsset()
            },
            async (): Promise<void> => {
                console.log("prepare upgrading...")

                // deploy clearingHouse implementation
                const clearingHouseContract = await context.factory.create<ClearingHouse>(
                    ContractFullyQualifiedName.FlattenClearingHouse,
                )
                newClearingHouseImplAddr = await clearingHouseContract.prepareUpgradeContractLegacy()

                // deploy Amm implementation
                const ammContract = await context.factory.createAmm(
                    AmmInstanceName.ETHUSDC,
                    ContractFullyQualifiedName.FlattenAmmUnderClearingHouse,
                )
                newAmmImplAddr = await ammContract.prepareUpgradeContractLegacy()
            },
            async (): Promise<void> => {
                console.info("upgrading...")

                // create an impersonated signer
                const govSigner = await impersonateAccount(context.externalContract.foundationGovernance!)

                // prepare information for upgrading
                const contractNameClearingHouse = ContractFullyQualifiedName.FlattenClearingHouse
                const proxyClearingHouseAddr = context.factory.create<ClearingHouse>(contractNameClearingHouse).address!

                const proxyAdmin = await upgrades.admin.getInstance()
                await proxyAdmin.connect(govSigner).upgrade(proxyClearingHouseAddr, newClearingHouseImplAddr)
                console.log(
                    `upgrade: contractFullyQualifiedName=${contractNameClearingHouse}, proxy=${proxyClearingHouseAddr}, implementation=${newClearingHouseImplAddr}`,
                )

                await proxyAdmin.connect(govSigner).upgrade(ammETHAddr, newAmmImplAddr)
                console.log(
                    `upgrade: contractFullyQualifiedName=${contractNameClearingHouse}, proxy=${ammETHAddr}, implementation=${newAmmImplAddr}`,
                )
            },
            // verify can openPosition
            async (): Promise<void> => {
                const clearingHouseContract = await context.factory
                    .create<ClearingHouse>(ContractFullyQualifiedName.FlattenClearingHouse)
                    .instance()

                const gov = context.externalContract.foundationGovernance!
                const govSigner = await impersonateAccount(gov)
                const arbitrageurSigner = await impersonateAccount(arbitrageur)

                const usdcAddr = context.externalContract.usdc!
                const USDCArtifact = await artifacts.readArtifact(ContractFullyQualifiedName.FlattenIERC20)
                const usdcInstance = (await ethers.getContractAt(USDCArtifact.abi, usdcAddr)) as ERC20

                // send eth and usdc to gov account
                const txETH = await arbitrageurSigner.sendTransaction({
                    to: gov,
                    value: ethers.utils.parseEther("0.1"),
                })
                await txETH.wait()
                const txUSDC = await usdcInstance.connect(arbitrageurSigner).transfer(gov, parseUnits("1000", 6))
                await txUSDC.wait()
                const txApprove = await usdcInstance
                    .connect(govSigner)
                    .approve(clearingHouseContract.address, parseUnits("1000", 6))
                await txApprove.wait()

                // open position
                const receipt = await clearingHouseContract
                    .connect(govSigner)
                    .openPosition(
                        ammETHAddr,
                        Side.BUY,
                        { d: parseEther("600") },
                        { d: parseEther("1") },
                        { d: parseEther("0") },
                    )

                const ownerPosition = await clearingHouseContract.getPosition(ammETHAddr, gov)
                expect(ownerPosition.margin.d).to.eq(parseEther("600"))
                expect(ownerPosition.blockNumber).to.eq(receipt.blockNumber)
            },
            // verify arbitrageur's position on ETH market and Amm's reserve
            async (): Promise<void> => {
                const clearingHouseContract = await context.factory
                    .create<ClearingHouse>(ContractFullyQualifiedName.FlattenClearingHouse)
                    .instance()
                const ammContract = await context.factory
                    .createAmm(AmmInstanceName.ETHUSDC, ContractFullyQualifiedName.FlattenAmmUnderClearingHouse)
                    .instance()

                // for comparing with the new implementation address
                console.log("old implementation address of ClearingHouse: ", oldClearingHouseImpAddr)
                console.log(
                    "new implementation address of ClearingHouse: ",
                    await getImplementation(clearingHouseContract.address),
                )
                console.log("old implementation address of Amm: ", oldAmmImpAddr)
                console.log("new implementation address of Amm: ", await getImplementation(ammContract.address))

                console.log("arbitrageur position: ")
                const arbitrageurPositionNow = await clearingHouseContract.getPosition(ammETHAddr, arbitrageur)
                console.log("size: ", arbitrageurPositionNow.size.d.toString())
                console.log("margin: ", arbitrageurPositionNow.margin.d.toString())
                console.log("last updated blockNumber: ", arbitrageurPositionNow.blockNumber.toString())
                expect(arbitrageurPosition.size.d).to.eq(arbitrageurPositionNow.size.d)
                expect(arbitrageurPosition.margin.d).to.eq(arbitrageurPositionNow.margin.d)
                expect(arbitrageurPosition.blockNumber).to.eq(arbitrageurPositionNow.blockNumber)

                console.log("amm states: ")
                const newQuoteAssetReserve = await ammContract.quoteAssetReserve()
                console.log("quote asset reserve: ", oldQuoteAssetReserve.toString())
                console.log("USDC addr: ", quoteAssetAddr.toString())
                expect(newQuoteAssetReserve).to.eq(oldQuoteAssetReserve.add(parseEther("600")))
                expect(await ammContract.quoteAsset()).to.eq(quoteAssetAddr)
            },
        ]
    },
}
Example #22
Source File: L1DaiGateway.ts    From arbitrum-dai-bridge with GNU Affero General Public License v3.0 4 votes vote down vote up
describe('L1DaiGateway', () => {
  describe('outboundTransfer()', () => {
    const depositAmount = 100
    const defaultGas = 42
    const maxSubmissionCost = 7
    const emptyCallHookData = '0x'
    const defaultEthValue = parseUnits('0.1', 'ether')
    const defaultData = defaultAbiCoder.encode(['uint256', 'bytes'], [maxSubmissionCost, emptyCallHookData])
    const notEmptyCallHookData = '0x12'
    const defaultDataWithNotEmptyCallHookData = defaultAbiCoder.encode(
      ['uint256', 'bytes'],
      [maxSubmissionCost, notEmptyCallHookData],
    )

    it('escrows funds and sends xdomain message', async () => {
      const [_deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, sender] = await ethers.getSigners()
      const defaultInboxBalance = await inboxImpersonator.getBalance()
      const { l1Dai, inboxMock, l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1: sender,
      })

      await l1Dai.connect(sender).approve(l1DaiGateway.address, depositAmount)
      const depositTx = await l1DaiGateway
        .connect(sender)
        .outboundTransfer(l1Dai.address, sender.address, depositAmount, defaultGas, 0, defaultData, {
          value: defaultEthValue,
        })
      const depositCallToMessengerCall = inboxMock.smocked.createRetryableTicket.calls[0]

      const expectedDepositId = 0
      const l2EncodedData = defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', emptyCallHookData])
      const expectedDepositXDomainCallData = new L2DaiGateway__factory().interface.encodeFunctionData(
        'finalizeInboundTransfer',
        [l1Dai.address, sender.address, sender.address, depositAmount, l2EncodedData],
      )

      expect(await l1Dai.balanceOf(sender.address)).to.be.eq(initialTotalL1Supply - depositAmount)
      expect(await l1Dai.balanceOf(l1DaiGateway.address)).to.be.eq(0)
      expect(await l1Dai.balanceOf(l1EscrowEOA.address)).to.be.eq(depositAmount)

      expect(await inboxImpersonator.getBalance()).to.equal(defaultInboxBalance.add(defaultEthValue))
      expect(depositCallToMessengerCall.destAddr).to.equal(l2DaiGatewayEOA.address)
      expect(depositCallToMessengerCall.l2CallValue).to.equal(0)
      expect(depositCallToMessengerCall.maxSubmissionCost).to.equal(maxSubmissionCost)
      expect(depositCallToMessengerCall.excessFeeRefundAddress).to.equal(sender.address)
      expect(depositCallToMessengerCall.callValueRefundAddress).to.equal(sender.address)
      expect(depositCallToMessengerCall.maxGas).to.equal(defaultGas)
      expect(depositCallToMessengerCall.gasPriceBid).to.equal(0)
      expect(depositCallToMessengerCall.data).to.equal(expectedDepositXDomainCallData)

      await expect(depositTx)
        .to.emit(l1DaiGateway, 'DepositInitiated')
        .withArgs(l1Dai.address, sender.address, sender.address, expectedDepositId, depositAmount)
      await expect(depositTx)
        .to.emit(l1DaiGateway, 'TxToL2')
        .withArgs(sender.address, l2DaiGatewayEOA.address, expectedDepositId, expectedDepositXDomainCallData)
    })

    it('escrows funds and sends xdomain message for 3rd party', async () => {
      const [_deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, sender, receiver] =
        await ethers.getSigners()
      const { l1Dai, inboxMock, l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1: sender,
      })

      await l1Dai.connect(sender).approve(l1DaiGateway.address, depositAmount)
      const depositTx = await l1DaiGateway
        .connect(sender)
        .outboundTransfer(l1Dai.address, receiver.address, depositAmount, defaultGas, 0, defaultData)
      const depositCallToMessengerCall = inboxMock.smocked.createRetryableTicket.calls[0]

      const expectedDepositId = 0
      const l2EncodedData = defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', emptyCallHookData])
      const expectedDepositXDomainCallData = new L2DaiGateway__factory().interface.encodeFunctionData(
        'finalizeInboundTransfer',
        [l1Dai.address, sender.address, receiver.address, depositAmount, l2EncodedData],
      )

      expect(await l1Dai.balanceOf(sender.address)).to.be.eq(initialTotalL1Supply - depositAmount)
      expect(await l1Dai.balanceOf(receiver.address)).to.be.eq(0)
      expect(await l1Dai.balanceOf(l1DaiGateway.address)).to.be.eq(0)
      expect(await l1Dai.balanceOf(l1EscrowEOA.address)).to.be.eq(depositAmount)

      expect(depositCallToMessengerCall.destAddr).to.equal(l2DaiGatewayEOA.address)
      expect(depositCallToMessengerCall.l2CallValue).to.equal(0)
      expect(depositCallToMessengerCall.maxSubmissionCost).to.equal(maxSubmissionCost)
      expect(depositCallToMessengerCall.excessFeeRefundAddress).to.equal(sender.address)
      expect(depositCallToMessengerCall.callValueRefundAddress).to.equal(sender.address)
      expect(depositCallToMessengerCall.maxGas).to.equal(defaultGas)
      expect(depositCallToMessengerCall.gasPriceBid).to.equal(0)
      expect(depositCallToMessengerCall.data).to.equal(expectedDepositXDomainCallData)

      await expect(depositTx)
        .to.emit(l1DaiGateway, 'DepositInitiated')
        .withArgs(l1Dai.address, sender.address, receiver.address, expectedDepositId, depositAmount)
      await expect(depositTx)
        .to.emit(l1DaiGateway, 'TxToL2')
        .withArgs(sender.address, l2DaiGatewayEOA.address, expectedDepositId, expectedDepositXDomainCallData)
    })

    it('decodes data correctly when called via router', async () => {
      const [_deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, sender] = await ethers.getSigners()
      const { l1Dai, inboxMock, l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1: sender,
      })
      const routerEncodedData = defaultAbiCoder.encode(['address', 'bytes'], [sender.address, defaultData])

      await l1Dai.connect(sender).approve(l1DaiGateway.address, depositAmount)
      const depositTx = await l1DaiGateway
        .connect(routerEOA)
        .outboundTransfer(l1Dai.address, sender.address, depositAmount, defaultGas, 0, routerEncodedData)
      const depositCallToMessengerCall = inboxMock.smocked.createRetryableTicket.calls[0]

      const expectedDepositId = 0
      const l2EncodedData = defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', emptyCallHookData])
      const expectedDepositXDomainCallData = new L2DaiGateway__factory().interface.encodeFunctionData(
        'finalizeInboundTransfer',
        [l1Dai.address, sender.address, sender.address, depositAmount, l2EncodedData],
      )

      expect(await l1Dai.balanceOf(sender.address)).to.be.eq(initialTotalL1Supply - depositAmount)
      expect(await l1Dai.balanceOf(l1DaiGateway.address)).to.be.eq(0)
      expect(await l1Dai.balanceOf(l1EscrowEOA.address)).to.be.eq(depositAmount)

      expect(depositCallToMessengerCall.destAddr).to.equal(l2DaiGatewayEOA.address)
      expect(depositCallToMessengerCall.l2CallValue).to.equal(0)
      expect(depositCallToMessengerCall.maxSubmissionCost).to.equal(maxSubmissionCost)
      expect(depositCallToMessengerCall.excessFeeRefundAddress).to.equal(sender.address)
      expect(depositCallToMessengerCall.callValueRefundAddress).to.equal(sender.address)
      expect(depositCallToMessengerCall.maxGas).to.equal(defaultGas)
      expect(depositCallToMessengerCall.gasPriceBid).to.equal(0)
      expect(depositCallToMessengerCall.data).to.equal(expectedDepositXDomainCallData)

      await expect(depositTx)
        .to.emit(l1DaiGateway, 'DepositInitiated')
        .withArgs(l1Dai.address, sender.address, sender.address, expectedDepositId, depositAmount)
      await expect(depositTx)
        .to.emit(l1DaiGateway, 'TxToL2')
        .withArgs(sender.address, l2DaiGatewayEOA.address, expectedDepositId, expectedDepositXDomainCallData)
    })

    it('reverts when called with a different token', async () => {
      const [_deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, sender] = await ethers.getSigners()
      const { l1Dai, l1DaiGateway, l2Dai } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1: sender,
      })

      await l1Dai.connect(sender).approve(l1DaiGateway.address, depositAmount)
      await expect(
        l1DaiGateway
          .connect(sender)
          .outboundTransfer(l2Dai.address, sender.address, depositAmount, defaultGas, 0, defaultData),
      ).to.be.revertedWith(errorMessages.tokenMismatch)
    })

    it('reverts when called with hook calldata', async () => {
      const [_deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, sender] = await ethers.getSigners()
      const { l1Dai, l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1: sender,
      })

      await l1Dai.connect(sender).approve(l1DaiGateway.address, depositAmount)
      await expect(
        l1DaiGateway
          .connect(sender)
          .outboundTransfer(
            l1Dai.address,
            sender.address,
            depositAmount,
            defaultGas,
            0,
            defaultDataWithNotEmptyCallHookData,
          ),
      ).to.be.revertedWith(errorMessages.callHookDataNotAllowed)
    })

    it('reverts when approval is too low', async () => {
      const [_deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, sender, receiver] =
        await ethers.getSigners()
      const { l1Dai, l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1: sender,
      })

      await expect(
        l1DaiGateway
          .connect(sender)
          .outboundTransfer(l1Dai.address, receiver.address, depositAmount, defaultGas, 0, defaultData),
      ).to.be.revertedWith(errorMessages.insufficientAllowance)
    })

    it('reverts when funds too low', async () => {
      const [_deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, user1, sender, receiver] =
        await ethers.getSigners()
      const { l1Dai, l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
      })

      await l1Dai.connect(sender).approve(l1DaiGateway.address, depositAmount)
      await expect(
        l1DaiGateway
          .connect(sender)
          .outboundTransfer(l1Dai.address, receiver.address, depositAmount, defaultGas, 0, defaultData),
      ).to.be.revertedWith(errorMessages.insufficientFunds)
    })

    it('reverts when bridge is closed', async () => {
      const [deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, sender, receiver] =
        await ethers.getSigners()
      const { l1Dai, l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1: sender,
      })

      await l1DaiGateway.connect(deployer).close()

      await l1Dai.connect(sender).approve(l1DaiGateway.address, depositAmount)
      await expect(
        l1DaiGateway
          .connect(sender)
          .outboundTransfer(l1Dai.address, receiver.address, depositAmount, defaultGas, 0, defaultData),
      ).to.revertedWith(errorMessages.closed)
    })
  })

  describe('finalizeInboundTransfer', () => {
    const withdrawAmount = 100
    const expectedTransferId = 1
    const defaultWithdrawData = ethers.utils.defaultAbiCoder.encode(['uint256', 'bytes'], [expectedTransferId, '0x'])

    it('sends funds from the escrow', async () => {
      const [
        _deployer,
        inboxImpersonator,
        l1EscrowEOA,
        l2DaiGatewayEOA,
        routerEOA,
        bridgeImpersonator,
        outboxImpersonator,
        user1,
      ] = await ethers.getSigners()
      const { l1Dai, outboxMock, l1DaiGateway } = await setupWithdrawalTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
        bridgeImpersonator,
        outboxImpersonator,
      })
      outboxMock.smocked.l2ToL1Sender.will.return.with(() => l2DaiGatewayEOA.address)

      const finalizeWithdrawalTx = await l1DaiGateway
        .connect(bridgeImpersonator)
        .finalizeInboundTransfer(l1Dai.address, user1.address, user1.address, withdrawAmount, defaultWithdrawData)

      expect(await l1Dai.balanceOf(user1.address)).to.be.equal(withdrawAmount)
      expect(await l1Dai.balanceOf(l1EscrowEOA.address)).to.be.equal(initialTotalL1Supply - withdrawAmount)
      await expect(finalizeWithdrawalTx)
        .to.emit(l1DaiGateway, 'WithdrawalFinalized')
        .withArgs(l1Dai.address, user1.address, user1.address, expectedTransferId, withdrawAmount)
      //   await expect(finalizeWithdrawalTx).not.to.emit(l1DaiGateway, 'TransferAndCallTriggered')
    })

    it('sends funds from the escrow to the 3rd party', async () => {
      const [
        _deployer,
        inboxImpersonator,
        l1EscrowEOA,
        l2DaiGatewayEOA,
        routerEOA,
        bridgeImpersonator,
        outboxImpersonator,
        sender,
        receiver,
      ] = await ethers.getSigners()
      const { l1Dai, outboxMock, l1DaiGateway } = await setupWithdrawalTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1: sender,
        bridgeImpersonator,
        outboxImpersonator,
      })
      outboxMock.smocked.l2ToL1Sender.will.return.with(() => l2DaiGatewayEOA.address)

      const finalizeWithdrawalTx = await l1DaiGateway
        .connect(bridgeImpersonator)
        .finalizeInboundTransfer(l1Dai.address, sender.address, receiver.address, withdrawAmount, defaultWithdrawData)

      expect(await l1Dai.balanceOf(sender.address)).to.be.equal(0)
      expect(await l1Dai.balanceOf(receiver.address)).to.be.equal(withdrawAmount)
      expect(await l1Dai.balanceOf(l1EscrowEOA.address)).to.be.equal(initialTotalL1Supply - withdrawAmount)
      await expect(finalizeWithdrawalTx)
        .to.emit(l1DaiGateway, 'WithdrawalFinalized')
        .withArgs(l1Dai.address, sender.address, receiver.address, expectedTransferId, withdrawAmount)
      //   await expect(finalizeWithdrawalTx).not.to.emit(l1DaiGateway, 'TransferAndCallTriggered')
    })

    // todo: test revert when calldata !=  0

    // pending withdrawals MUST success even if bridge is closed
    it('completes withdrawals even when closed', async () => {
      const [
        deployer,
        inboxImpersonator,
        l1EscrowEOA,
        l2DaiGatewayEOA,
        routerEOA,
        bridgeImpersonator,
        outboxImpersonator,
        user1,
      ] = await ethers.getSigners()
      const { l1Dai, outboxMock, l1DaiGateway } = await setupWithdrawalTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
        bridgeImpersonator,
        outboxImpersonator,
      })
      outboxMock.smocked.l2ToL1Sender.will.return.with(() => l2DaiGatewayEOA.address)
      await l1DaiGateway.connect(deployer).close()

      const finalizeWithdrawalTx = await l1DaiGateway
        .connect(bridgeImpersonator)
        .finalizeInboundTransfer(l1Dai.address, user1.address, user1.address, withdrawAmount, defaultWithdrawData)

      expect(await l1Dai.balanceOf(user1.address)).to.be.equal(withdrawAmount)
      expect(await l1Dai.balanceOf(l1EscrowEOA.address)).to.be.equal(initialTotalL1Supply - withdrawAmount)
      await expect(finalizeWithdrawalTx)
        .to.emit(l1DaiGateway, 'WithdrawalFinalized')
        .withArgs(l1Dai.address, user1.address, user1.address, expectedTransferId, withdrawAmount)
    })

    it('reverts when called with a different token', async () => {
      const [
        _deployer,
        inboxImpersonator,
        l1EscrowEOA,
        l2DaiGatewayEOA,
        routerEOA,
        bridgeImpersonator,
        outboxImpersonator,
        user1,
      ] = await ethers.getSigners()
      const { l2Dai, outboxMock, l1DaiGateway } = await setupWithdrawalTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
        bridgeImpersonator,
        outboxImpersonator,
      })
      outboxMock.smocked.l2ToL1Sender.will.return.with(() => l2DaiGatewayEOA.address)

      await expect(
        l1DaiGateway
          .connect(bridgeImpersonator)
          .finalizeInboundTransfer(l2Dai.address, user1.address, user1.address, withdrawAmount, defaultWithdrawData),
      ).to.be.revertedWith(errorMessages.tokenMismatch)
    })

    it('reverts when called not by the outbox', async () => {
      const [
        _deployer,
        inboxImpersonator,
        l1EscrowEOA,
        l2DaiGatewayEOA,
        routerEOA,
        bridgeImpersonator,
        outboxImpersonator,
        user1,
      ] = await ethers.getSigners()
      const { l1Dai, outboxMock, l1DaiGateway } = await setupWithdrawalTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
        bridgeImpersonator,
        outboxImpersonator,
      })
      outboxMock.smocked.l2ToL1Sender.will.return.with(() => l2DaiGatewayEOA.address)

      await expect(
        l1DaiGateway.finalizeInboundTransfer(
          l1Dai.address,
          user1.address,
          user1.address,
          withdrawAmount,
          defaultWithdrawData,
        ),
      ).to.be.revertedWith(errorMessages.notFromBridge)
    })

    it('reverts when called by the outbox but not relying message from l2 counterpart', async () => {
      const [
        _deployer,
        inboxImpersonator,
        l1EscrowEOA,
        l2DaiGatewayEOA,
        routerEOA,
        bridgeImpersonator,
        outboxImpersonator,
        user1,
        user2,
      ] = await ethers.getSigners()
      const { l1Dai, outboxMock, l1DaiGateway } = await setupWithdrawalTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
        bridgeImpersonator,
        outboxImpersonator,
      })
      outboxMock.smocked.l2ToL1Sender.will.return.with(() => user2.address)

      await expect(
        l1DaiGateway
          .connect(bridgeImpersonator)
          .finalizeInboundTransfer(l1Dai.address, user1.address, user1.address, withdrawAmount, defaultWithdrawData),
      ).to.be.revertedWith(errorMessages.l2CounterpartMismatch)
    })
  })

  describe('close()', () => {
    it('can be called by owner', async () => {
      const [owner, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, user1] = await ethers.getSigners()
      const { l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
      })

      expect(await l1DaiGateway.isOpen()).to.be.eq(1)
      const closeTx = await l1DaiGateway.connect(owner).close()

      await expect(closeTx).to.emit(l1DaiGateway, 'Closed')

      expect(await l1DaiGateway.isOpen()).to.be.eq(0)
    })

    it('can be called multiple times by the owner but nothing changes', async () => {
      const [owner, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, user1] = await ethers.getSigners()
      const { l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
      })

      await l1DaiGateway.connect(owner).close()
      expect(await l1DaiGateway.isOpen()).to.be.eq(0)

      await l1DaiGateway.connect(owner).close()
      expect(await l1DaiGateway.isOpen()).to.be.eq(0)
    })

    it('reverts when called not by the owner', async () => {
      const [_deployer, inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, user1] = await ethers.getSigners()
      const { l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
      })

      await expect(l1DaiGateway.connect(user1).close()).to.be.revertedWith(errorMessages.notOwner)
    })
  })

  describe('calculateL2TokenAddress', () => {
    it('return l2Dai address when asked about dai', async () => {
      const [inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, user1] = await ethers.getSigners()
      const { l1DaiGateway, l1Dai, l2Dai } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
      })

      expect(await l1DaiGateway.calculateL2TokenAddress(l1Dai.address)).to.eq(l2Dai.address)
    })

    it('returns zero address for unknown tokens', async () => {
      const [inboxImpersonator, l1EscrowEOA, l2DaiGatewayEOA, routerEOA, user1] = await ethers.getSigners()
      const randomToken = await getRandomAddress()
      const { l1DaiGateway } = await setupTest({
        inboxImpersonator,
        l1Escrow: l1EscrowEOA,
        l2DaiGateway: l2DaiGatewayEOA,
        router: routerEOA,
        user1,
      })

      expect(await l1DaiGateway.calculateL2TokenAddress(randomToken)).to.eq(ethers.constants.AddressZero)
    })
  })

  describe('constructor', () => {
    it('assigns all variables properly', async () => {
      const [l2DaiGateway, l1Router, inbox, l1Dai, l2Dai, l1Escrow] = await getRandomAddresses()

      const l1DaiGateway = await simpleDeploy<L1DaiGateway__factory>('L1DaiGateway', [
        l2DaiGateway,
        l1Router,
        inbox,
        l1Dai,
        l2Dai,
        l1Escrow,
      ])

      expect(await l1DaiGateway.l2Counterpart()).to.be.eq(l2DaiGateway)
      expect(await l1DaiGateway.l1Router()).to.be.eq(l1Router)
      expect(await l1DaiGateway.inbox()).to.be.eq(inbox)
      expect(await l1DaiGateway.l1Dai()).to.be.eq(l1Dai)
      expect(await l1DaiGateway.l2Dai()).to.be.eq(l2Dai)
      expect(await l1DaiGateway.l1Escrow()).to.be.eq(l1Escrow)
      expect(await l1DaiGateway.isOpen()).to.be.eq(1)
    })
  })

  it('has correct public interface', async () => {
    await assertPublicMutableMethods('L1DaiGateway', [
      'finalizeInboundTransfer(address,address,address,uint256,bytes)', // withdraw
      'outboundTransfer(address,address,uint256,uint256,uint256,bytes)', // deposit
      'close()',
      'deny(address)',
      'rely(address)',
    ])

    await assertPublicNotMutableMethods('L1DaiGateway', [
      'calculateL2TokenAddress(address)',
      'getOutboundCalldata(address,address,address,uint256,bytes)',

      // storage variables:
      'inbox()',
      'isOpen()',
      'l1Dai()',
      'l1Escrow()',
      'l1Router()',
      'l2Counterpart()',
      'l2Dai()',
      'wards(address)',
      'counterpartGateway()',
    ])
  })

  testAuth({
    name: 'L1DaiGateway',
    getDeployArgs: async () => {
      const [l2Counterpart, l1Router, inbox, l1Dai, l2Dai, l1Escrow] = await getRandomAddresses()

      return [l2Counterpart, l1Router, inbox, l1Dai, l2Dai, l1Escrow]
    },
    authedMethods: [(c) => c.close()],
  })
})
Example #23
Source File: index.tsx    From dxvote with GNU Affero General Public License v3.0 4 votes vote down vote up
Stakes = () => {
  const {
    context: { daoStore, configStore, providerStore, daoService },
  } = useContext();

  const [stakeAmount, setStakeAmount] = useState(0);
  // We should get the ID in another way
  const proposalId = useLocation().pathname.split('/')[3];

  const proposal = daoStore.getProposal(proposalId);
  const proposalEvents = daoStore.getProposalEvents(proposalId);
  const { account } = providerStore.getActiveWeb3React();
  const scheme = daoStore.getScheme(proposal.scheme);

  let stakedAmount = bnum(0);
  let positiveStakesCount = proposalEvents.stakes.filter(
    stake => stake.vote.toString() === '1'
  ).length;
  let negativeStakesCount = proposalEvents.stakes.filter(
    stake => stake.vote.toString() === '2'
  ).length;
  const networkContracts = configStore.getNetworkContracts();
  const votingMachines = networkContracts.votingMachines;

  const redeemsLeft = daoStore.getUserRedeemsLeft(account);

  const votingMachineOfProposal =
    daoStore.getVotingMachineOfProposal(proposalId);

  const votingMachineTokenName =
    votingMachines[votingMachineOfProposal.address].type == 'DXDVotingMachine'
      ? 'DXD'
      : 'GEN';

  const votingMachineTokenAllowed = useAllowance(
    votingMachines[votingMachineOfProposal.address].token,
    account,
    votingMachineOfProposal.address
  );

  const votingMachineTokenApproved = votingMachineTokenAllowed.gt(
    bnum(parseUnits('10000'))
  );

  const votingParameters =
    daoStore.getVotingMachineOfProposal(proposalId).params;

  proposalEvents.stakes.map(stake => {
    if (stake.staker === account && stake.vote.toString() === '1') {
      stakedAmount = stakedAmount.plus(stake.amount);
    } else if (stake.staker === account && stake.vote.toString() === '2') {
      stakedAmount = stakedAmount.minus(stake.amount);
    }
  });

  const { recommendedStakeToBoost, recommendedStakeToUnBoost } =
    calculateStakes(
      votingParameters.thresholdConst,
      scheme.boostedProposals,
      proposal.stateInVotingMachine === 4
        ? daoStore.getAmountOfProposalsPreBoostedInScheme(scheme.address) - 1
        : daoStore.getAmountOfProposalsPreBoostedInScheme(scheme.address),
      proposal.positiveStakes,
      proposal.negativeStakes
    );

  if (Number(votingMachineTokenApproved) > 0 && stakeAmount === 0) {
    setStakeAmount(
      Number(formatBalance(recommendedStakeToBoost, 18, 1, false))
    );
  }

  const { finishTime } = daoStore.getProposalStatus(proposalId);
  const finishTimeReached = finishTime.toNumber() < moment().unix();

  // Event Handlers
  function onStakeAmountChange(event) {
    setStakeAmount(event.target.value);
  }

  const submitStake = function (decision) {
    daoService.stake(
      decision,
      denormalizeBalance(bnum(stakeAmount)).toString(),
      proposalId
    );
  };

  const redeem = function () {
    if (
      scheme.type === 'ContributionReward' &&
      networkContracts.daostack[scheme.address].redeemer
    ) {
      daoService.redeemContributionReward(
        networkContracts.daostack[scheme.address].redeemer,
        scheme.address,
        scheme.votingMachine,
        proposalId,
        account
      );
    } else {
      daoService.redeem(proposalId, account);
    }
  };

  const redeemDaoBounty = function () {
    daoService.redeemDaoBounty(proposalId, account);
  };

  const votingMachineUsed = daoStore.getVotingMachineOfProposal(proposalId);
  const approveVotingMachineToken = function () {
    daoService.approveVotingMachineToken(votingMachineUsed);
  };

  return (
    <>
      <SpaceAroundRow>
        <strong>
          Stakes <Question question="5" />
        </strong>
      </SpaceAroundRow>
      <SpaceAroundRow>
        <PositiveSummary>
          <SummaryTotal>
            <AmountBadge color="green">{positiveStakesCount}</AmountBadge>
            {formatBalance(proposal.positiveStakes).toString()}
            {votingMachineTokenName}
          </SummaryTotal>
          <HorizontalSeparator />
          <SummaryDetails>
            {proposalEvents &&
              proposalEvents.stakes
                .filter(stakeEvent => stakeEvent?.vote?.toString() === '1')
                .map((stakeEvent, i) => (
                  <Vote key={`stakeUp${i}`} style={{ flexDirection: 'column' }}>
                    <BlockchainLink
                      size="short"
                      type="user"
                      text={stakeEvent.staker}
                    />
                    <span>
                      {formatBalance(bnum(stakeEvent.amount)).toString()}
                      {votingMachineTokenName}
                    </span>
                  </Vote>
                ))}
          </SummaryDetails>
        </PositiveSummary>

        <NegativeSummary>
          <SummaryTotal>
            <AmountBadge color="red">{negativeStakesCount}</AmountBadge>
            {formatBalance(proposal.negativeStakes).toString()}
            {votingMachineTokenName}
          </SummaryTotal>
          <HorizontalSeparator />
          <SummaryDetails>
            {proposalEvents &&
              proposalEvents.stakes
                .filter(stakeEvent => stakeEvent?.vote?.toString() === '2')
                .map((stakeEvent, i) => (
                  <Vote
                    key={`stakeDown${i}`}
                    style={{ flexDirection: 'column' }}
                  >
                    <BlockchainLink
                      size="short"
                      type="user"
                      text={stakeEvent.staker}
                    />
                    <span>
                      {formatBalance(bnum(stakeEvent.amount)).toString()}
                      {votingMachineTokenName}
                    </span>
                  </Vote>
                ))}
          </SummaryDetails>
        </NegativeSummary>
      </SpaceAroundRow>

      {stakedAmount.toNumber() > 0 ? (
        <SpaceAroundRow>
          {`Already staked ${
            stakedAmount.toNumber() > 0 ? 'for' : 'against'
          } with `}
          {formatBalance(stakedAmount).toString()} {votingMachineTokenName}
        </SpaceAroundRow>
      ) : (
        <div></div>
      )}

      {account &&
      !finishTimeReached &&
      (proposal.stateInVotingMachine === 3 ||
        proposal.stateInVotingMachine === 4) &&
      !votingMachineTokenApproved ? (
        <SpaceAroundRow>
          <ActionArea>
            <small>Approve {votingMachineTokenName} to stake</small>
            <ActionButton
              color="blue"
              onClick={() => approveVotingMachineToken()}
            >
              Approve {votingMachineTokenName}
            </ActionButton>
          </ActionArea>
        </SpaceAroundRow>
      ) : (
        account &&
        !finishTimeReached &&
        (proposal.stateInVotingMachine === 3 ||
          proposal.stateInVotingMachine === 4) && (
          <div>
            {Number(recommendedStakeToBoost) > 0 && (
              <small>
                Stake ~
                {formatBalance(
                  recommendedStakeToBoost,
                  18,
                  1,
                  false
                ).toString()}
                {votingMachineTokenName} to boost
              </small>
            )}
            {Number(recommendedStakeToUnBoost) > 0 && (
              <small>
                Stake ~
                {formatBalance(
                  recommendedStakeToUnBoost,
                  18,
                  1,
                  false
                ).toString()}
                {votingMachineTokenName} to unboost
              </small>
            )}
            <SpaceAroundRow>
              <AmountInput
                type="number"
                placeholder={votingMachineTokenName}
                name="stakeAmount"
                value={stakeAmount}
                id="stakeAmount"
                step="0.01"
                min="0"
                onChange={onStakeAmountChange}
                style={{ flex: 2 }}
              />
              <ActionButton
                style={{ flex: 1, maxWidth: '20px', textAlign: 'center' }}
                color="green"
                onClick={() => submitStake(1)}
              >
                <FiThumbsUp />
              </ActionButton>
              <ActionButton
                style={{ flex: 1, maxWidth: '20px', textAlign: 'center' }}
                color="red"
                onClick={() => submitStake(2)}
              >
                <FiThumbsDown />
              </ActionButton>
            </SpaceAroundRow>
          </div>
        )
      )}

      {proposal.stateInVotingMachine < 3 &&
        (redeemsLeft.rep.indexOf(proposalId) > -1 ||
          redeemsLeft.stake.indexOf(proposalId) > -1) && (
          <SpaceAroundRow
            style={{
              borderTop: '1px solid gray',
              margin: '0px 10px',
              justifyContent: 'center',
            }}
          >
            <ActionButton color="blue" onClick={() => redeem()}>
              Redeem
            </ActionButton>
          </SpaceAroundRow>
        )}

      {account &&
        proposal.stateInVotingMachine < 3 &&
        redeemsLeft.bounty[proposalId] && (
          <SpaceAroundRow
            style={{
              borderTop: '1px solid gray',
              margin: '0px 10px',
              justifyContent: 'center',
            }}
          >
            <ActionButton color="blue" onClick={() => redeemDaoBounty()}>
              Redeem Stake Bounty
            </ActionButton>
          </SpaceAroundRow>
        )}
    </>
  );
}
Example #24
Source File: endpoint.test.ts    From bodhi.js with Apache License 2.0 4 votes vote down vote up
describe('eth_sendRawTransaction', () => {
  const eth_sendRawTransaction = rpcGet('eth_sendRawTransaction');
  const eth_getTransactionCount = rpcGet('eth_getTransactionCount');
  const eth_getBalance = rpcGet('eth_getBalance');
  const eth_chainId = rpcGet('eth_chainId');
  const eth_gasPrice = rpcGet('eth_gasPrice');
  const eth_estimateGas = rpcGet('eth_estimateGas');

  const account1 = evmAccounts[0];
  const account2 = evmAccounts[1];
  const wallet1 = new Wallet(account1.privateKey);

  let chainId: number;
  let txGasLimit: BigNumber;
  let txGasPrice: BigNumber;
  let genesisHash: string;

  let api: ApiPromise;

  before('prepare common variables', async () => {
    chainId = BigNumber.from((await eth_chainId()).data.result).toNumber();

    txGasLimit = BigNumber.from(34132001);
    txGasPrice = BigNumber.from(200786445289);

    const endpoint = process.env.ENDPOINT_URL || 'ws://127.0.0.1:9944';
    const wsProvider = new WsProvider(endpoint);
    api = await ApiPromise.create({ provider: wsProvider });

    genesisHash = api.genesisHash.toHex(); // TODO: why EIP-712 salt has to be genesis hash?
  });

  after(async () => {
    await api.disconnect();
  });

  describe('deploy contract (hello world)', () => {
    const deployHelloWorldData =
      '0x60806040526040518060400160405280600c81526020017f48656c6c6f20576f726c642100000000000000000000000000000000000000008152506000908051906020019061004f929190610062565b5034801561005c57600080fd5b50610166565b82805461006e90610134565b90600052602060002090601f01602090048101928261009057600085556100d7565b82601f106100a957805160ff19168380011785556100d7565b828001600101855582156100d7579182015b828111156100d65782518255916020019190600101906100bb565b5b5090506100e491906100e8565b5090565b5b808211156101015760008160009055506001016100e9565b5090565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061014c57607f821691505b602082108114156101605761015f610105565b5b50919050565b61022e806101756000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063c605f76c14610030575b600080fd5b61003861004e565b6040516100459190610175565b60405180910390f35b6000805461005b906101c6565b80601f0160208091040260200160405190810160405280929190818152602001828054610087906101c6565b80156100d45780601f106100a9576101008083540402835291602001916100d4565b820191906000526020600020905b8154815290600101906020018083116100b757829003601f168201915b505050505081565b600081519050919050565b600082825260208201905092915050565b60005b838110156101165780820151818401526020810190506100fb565b83811115610125576000848401525b50505050565b6000601f19601f8301169050919050565b6000610147826100dc565b61015181856100e7565b93506101618185602086016100f8565b61016a8161012b565b840191505092915050565b6000602082019050818103600083015261018f818461013c565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806101de57607f821691505b602082108114156101f2576101f1610197565b5b5091905056fea26469706673582212204d363ed34111d1be492d4fd086e9f2df62b3c625e89ade31f30e63201ed1e24f64736f6c63430008090033';

    let partialDeployTx;

    before(() => {
      partialDeployTx = {
        chainId,
        gasLimit: txGasLimit,
        gasPrice: txGasPrice,
        data: deployHelloWorldData,
        value: BigNumber.from(0)
      };
    });

    describe('with legacy EIP-155 signature', () => {
      it('serialize, parse, and send tx correctly', async () => {
        const unsignedTx: AcalaEvmTX = {
          ...partialDeployTx,
          nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result
        };

        const rawTx = await wallet1.signTransaction(unsignedTx);
        const parsedTx = parseTransaction(rawTx);

        expect(parsedTx.gasPrice.eq(txGasPrice)).equal(true);
        expect(parsedTx.gasLimit.eq(txGasLimit)).equal(true);

        expect(parsedTx.from).equal(wallet1.address);
        expect(parsedTx.data).equal(deployHelloWorldData);
        expect(parsedTx.type).equal(null);
        expect(parsedTx.maxPriorityFeePerGas).equal(undefined);
        expect(parsedTx.maxFeePerGas).equal(undefined);

        const res = await eth_sendRawTransaction([rawTx]);
        expect(res.status).to.equal(200);
        expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200
      });
    });

    describe('with EIP-1559 signature', () => {
      it('serialize, parse, and send tx correctly', async () => {
        const priorityFee = BigNumber.from(2);
        const unsignedTx: AcalaEvmTX = {
          ...partialDeployTx,
          nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result,
          gasPrice: undefined,
          maxPriorityFeePerGas: priorityFee,
          maxFeePerGas: txGasPrice,
          type: 2
        };

        const rawTx = await wallet1.signTransaction(unsignedTx);
        const parsedTx = parseTransaction(rawTx);

        expect(parsedTx.maxFeePerGas.eq(txGasPrice)).equal(true);
        expect(parsedTx.maxPriorityFeePerGas.eq(priorityFee)).equal(true);
        expect(parsedTx.gasLimit.eq(txGasLimit)).equal(true);

        expect(parsedTx.from).equal(wallet1.address);
        expect(parsedTx.data).equal(deployHelloWorldData);
        expect(parsedTx.type).equal(2);
        expect(parsedTx.gasPrice).equal(null);

        const res = await eth_sendRawTransaction([rawTx]);
        expect(res.status).to.equal(200);
        expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200
      });
    });

    describe('with EIP-712 signature', () => {
      it('serialize, parse, and send tx correctly', async () => {
        const gasLimit = BigNumber.from('210000');
        const validUntil = 10000;
        const storageLimit = 100000;

        const unsignEip712Tx: AcalaEvmTX = {
          ...partialDeployTx,
          nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result,
          salt: genesisHash,
          gasLimit,
          validUntil,
          storageLimit,
          type: 0x60
        };

        const sig = signTransaction(account1.privateKey, unsignEip712Tx);
        const rawTx = serializeTransaction(unsignEip712Tx, sig);
        const parsedTx = parseTransaction(rawTx);

        expect(parsedTx.gasLimit.eq(gasLimit)).equal(true);
        expect(parsedTx.validUntil.eq(validUntil)).equal(true);
        expect(parsedTx.storageLimit.eq(storageLimit)).equal(true);

        expect(parsedTx.from).equal(wallet1.address);
        expect(parsedTx.data).equal(deployHelloWorldData);
        expect(parsedTx.type).equal(96);
        expect(parsedTx.maxPriorityFeePerGas).equal(undefined);
        expect(parsedTx.maxFeePerGas).equal(undefined);

        const res = await eth_sendRawTransaction([rawTx]);
        expect(res.status).to.equal(200);
        expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200
      });
    });
  });

  describe('call contract (transfer ACA)', () => {
    const ETHDigits = 18;
    const ACADigits = 12;
    const acaContract = new Contract(ADDRESS.ACA, TokenABI.abi, wallet1);
    const iface = new Interface(TokenABI.abi);
    const queryBalance = async (addr) =>
      BigNumber.from((await eth_getBalance([addr, 'latest'])).data.result).div(10 ** (ETHDigits - ACADigits));
    const transferAmount = parseUnits('100', ACADigits);
    let partialTransferTX: Partial<AcalaEvmTX>;

    before(() => {
      partialTransferTX = {
        chainId,
        to: ADDRESS.ACA,
        gasLimit: txGasLimit,
        gasPrice: txGasPrice,
        data: iface.encodeFunctionData('transfer', [account2.evmAddress, transferAmount]),
        value: BigNumber.from(0)
      };
    });

    describe('with legacy EIP-155 signature', () => {
      it('has correct balance after transfer', async () => {
        const balance1 = await queryBalance(account1.evmAddress);
        const balance2 = await queryBalance(account2.evmAddress);

        const transferTX: AcalaEvmTX = {
          ...partialTransferTX,
          nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result
        };

        const rawTx = await wallet1.signTransaction(transferTX);

        const res = await eth_sendRawTransaction([rawTx]);
        expect(res.status).to.equal(200);
        expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200

        const _balance1 = await queryBalance(account1.evmAddress);
        const _balance2 = await queryBalance(account2.evmAddress);

        // TODO: check gasUsed is correct
        // const gasUsed = balance1.sub(_balance1).sub(transferAmount).toBigInt();
        expect(_balance2.sub(balance2).toBigInt()).equal(transferAmount.toBigInt());
      });
    });

    describe('with EIP-1559 signature', () => {
      it('has correct balance after transfer', async () => {
        const balance1 = await queryBalance(account1.evmAddress);
        const balance2 = await queryBalance(account2.evmAddress);

        const priorityFee = BigNumber.from(2);
        const transferTX: AcalaEvmTX = {
          ...partialTransferTX,
          nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result,
          gasPrice: undefined,
          maxPriorityFeePerGas: priorityFee,
          maxFeePerGas: txGasPrice,
          type: 2
        };

        const rawTx = await wallet1.signTransaction(transferTX);
        const parsedTx = parseTransaction(rawTx);

        const res = await eth_sendRawTransaction([rawTx]);
        expect(res.status).to.equal(200);
        expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200

        const _balance1 = await queryBalance(account1.evmAddress);
        const _balance2 = await queryBalance(account2.evmAddress);

        // TODO: check gasUsed is correct
        // const gasUsed = balance1.sub(_balance1).sub(transferAmount).toBigInt();
        expect(_balance2.sub(balance2).toBigInt()).equal(transferAmount.toBigInt());
      });
    });

    describe('with EIP-712 signature', () => {
      it('has correct balance after transfer', async () => {
        const balance1 = await queryBalance(account1.evmAddress);
        const balance2 = await queryBalance(account2.evmAddress);

        const gasLimit = BigNumber.from('210000');
        const validUntil = 10000;
        const storageLimit = 100000;

        const transferTX: AcalaEvmTX = {
          ...partialTransferTX,
          nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result,
          salt: genesisHash,
          gasLimit,
          validUntil,
          storageLimit,
          type: 0x60
        };

        const sig = signTransaction(account1.privateKey, transferTX);
        const rawTx = serializeTransaction(transferTX, sig);
        const parsedTx = parseTransaction(rawTx);

        const res = await eth_sendRawTransaction([rawTx]);
        expect(res.status).to.equal(200);
        expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200

        const _balance1 = await queryBalance(account1.evmAddress);
        const _balance2 = await queryBalance(account2.evmAddress);

        // TODO: check gasUsed is correct
        // const gasUsed = balance1.sub(_balance1).sub(transferAmount).toBigInt();
        expect(_balance2.sub(balance2).toBigInt()).equal(transferAmount.toBigInt());
      });
    });
  });

  describe('MetaMask send native ACA token', () => {
    const ETHDigits = 18;
    const ACADigits = 12;
    const queryBalance = async (addr) => BigNumber.from((await eth_getBalance([addr, 'latest'])).data.result);
    const transferAmount = parseUnits('16.8668', ETHDigits);
    let partialNativeTransferTX: Partial<AcalaEvmTX>;

    const estimateGas = async (): Promise<{
      gasPrice: string;
      gasLimit: string;
    }> => {
      const gasPrice = (await eth_gasPrice([])).data.result;
      const gasLimit = (
        await eth_estimateGas([
          {
            from: account1.evmAddress,
            to: account2.evmAddress,
            value: transferAmount,
            data: null,
            gasPrice
          }
        ])
      ).data.result;

      return {
        gasPrice,
        gasLimit
      };
    };

    before(() => {
      partialNativeTransferTX = {
        chainId,
        to: account2.evmAddress,
        data: '0x',
        value: transferAmount
      };
    });

    describe('with legacy EIP-155 signature', () => {
      it('has correct balance after transfer', async () => {
        const balance1 = await queryBalance(account1.evmAddress);
        const balance2 = await queryBalance(account2.evmAddress);

        const transferTX: AcalaEvmTX = {
          ...partialNativeTransferTX,
          ...(await estimateGas()),
          nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result
        };

        const rawTx = await wallet1.signTransaction(transferTX);

        const res = await eth_sendRawTransaction([rawTx]);
        expect(res.status).to.equal(200);
        expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200

        const _balance1 = await queryBalance(account1.evmAddress);
        const _balance2 = await queryBalance(account2.evmAddress);

        // TODO: check gasUsed is correct
        // const gasUsed = balance1.sub(_balance1).sub(transferAmount).toBigInt();
        expect(_balance2.sub(balance2).toBigInt()).equal(transferAmount.toBigInt());
      });
    });

    describe('with EIP-1559 signature', () => {
      it('has correct balance after transfer', async () => {
        const balance1 = await queryBalance(account1.evmAddress);
        const balance2 = await queryBalance(account2.evmAddress);

        const priorityFee = BigNumber.from(2);
        const { gasPrice, gasLimit } = await estimateGas();
        const transferTX: AcalaEvmTX = {
          ...partialNativeTransferTX,
          gasLimit,
          nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result,
          gasPrice: undefined,
          maxPriorityFeePerGas: priorityFee,
          maxFeePerGas: gasPrice,
          type: 2
        };

        const rawTx = await wallet1.signTransaction(transferTX);
        const parsedTx = parseTransaction(rawTx);

        const res = await eth_sendRawTransaction([rawTx]);
        expect(res.status).to.equal(200);
        expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200

        const _balance1 = await queryBalance(account1.evmAddress);
        const _balance2 = await queryBalance(account2.evmAddress);

        // TODO: check gasUsed is correct
        // const gasUsed = balance1.sub(_balance1).sub(transferAmount).toBigInt();
        expect(_balance2.sub(balance2).toBigInt()).equal(transferAmount.toBigInt());
      });
    });

    describe('with EIP-712 signature', () => {
      // TODO: EIP-712 doesn't use ETH gasLimit and gasPrice, do we need to support it?
      it.skip('has correct balance after transfer', async () => {
        // const balance1 = await queryBalance(account1.evmAddress);
        // const balance2 = await queryBalance(account2.evmAddress);
        // const gasLimit = BigNumber.from('210000');
        // const validUntil = 10000;
        // const storageLimit = 100000;
        // const transferTX: AcalaEvmTX = {
        //   ...partialNativeTransferTX,
        //   ...(await estimateGas()),
        //   nonce: (await eth_getTransactionCount([wallet1.address, 'pending'])).data.result,
        //   salt: genesisHash,
        //   gasLimit,
        //   validUntil,
        //   storageLimit,
        //   type: 0x60
        // };
        // const sig = signTransaction(account1.privateKey, transferTX);
        // const rawTx = serializeTransaction(transferTX, sig);
        // const parsedTx = parseTransaction(rawTx);
        // const res = await eth_sendRawTransaction([rawTx]);
        // expect(res.status).to.equal(200);
        // expect(res.data.error?.message).to.equal(undefined); // for TX error RPC will still return 200
        // const _balance1 = await queryBalance(account1.evmAddress);
        // const _balance2 = await queryBalance(account2.evmAddress);
        // // TODO: check gasUsed is correct
        // const gasUsed = balance1.sub(_balance1).sub(transferAmount).toBigInt();
        // expect(_balance2.sub(balance2).toBigInt()).equal(transferAmount.toBigInt());
      });
    });
  });
});