ethers#ContractReceipt TypeScript Examples

The following examples show how to use ethers#ContractReceipt. 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: utils.ts    From anchor-web-app with Apache License 2.0 7 votes vote down vote up
txResult = (
  event: CrossChainEvent<ContractReceipt>,
  connnectType: ConnectType,
  chainId: EvmChainId,
  txKind: TxKind,
) => {
  return {
    value: null,
    message: txResultMessage(event.kind, connnectType, chainId, txKind),
    phase: TxStreamPhase.BROADCAST,
    receipts: [
      //{ name: "Status", value: txResultMessage(event, connnectType, chainId, action) }
    ],
  };
}
Example #2
Source File: misc.ts    From balancer-v2-monorepo with GNU General Public License v3.0 6 votes vote down vote up
async function deployPoolFromFactory(
  vault: Vault,
  poolName: PoolName,
  args: { from: SignerWithAddress; parameters: Array<unknown> }
): Promise<Contract> {
  const fullName = `${poolName == 'StablePool' ? 'v2-pool-stable' : 'v2-pool-weighted'}/${poolName}`;
  const libraries =
    poolName == 'OracleWeightedPool'
      ? { QueryProcessor: await (await deploy('v2-pool-utils/QueryProcessor')).address }
      : undefined;
  let factory: Contract;
  if (poolName == 'ManagedPool') {
    const baseFactory = await deploy('v2-pool-weighted/BaseManagedPoolFactory', { args: [vault.address] });
    factory = await deploy(`${fullName}Factory`, { args: [baseFactory.address] });
  } else {
    factory = await deploy(`${fullName}Factory`, { args: [vault.address], libraries });
  }

  // We could reuse this factory if we saved it across pool deployments

  let receipt: ContractReceipt;
  let event;

  if (poolName == 'ManagedPool') {
    receipt = await (await factory.connect(args.from).create(...args.parameters)).wait();
    event = receipt.events?.find((e) => e.event == 'ManagedPoolCreated');
  } else {
    receipt = await (await factory.connect(args.from).create(name, symbol, ...args.parameters, ZERO_ADDRESS)).wait();
    event = receipt.events?.find((e) => e.event == 'PoolCreated');
  }

  if (event == undefined) {
    throw new Error('Could not find PoolCreated event');
  }

  return deployedAt(fullName, event.args?.pool);
}
Example #3
Source File: StablePhantomPool.ts    From balancer-v2-monorepo with GNU General Public License v3.0 6 votes vote down vote up
async swap(params: GeneralSwap): Promise<{ amountIn: BigNumber; amountOut: BigNumber; receipt: ContractReceipt }> {
    const tx = await this.vault.generalSwap(params);
    const receipt = await tx.wait();
    const args = expectEvent.inReceipt(receipt, 'Swap').args;
    return {
      amountIn: args.amountIn,
      amountOut: args.amountOut,
      receipt,
    };
  }
Example #4
Source File: expectEvent.ts    From balancer-v2-monorepo with GNU General Public License v3.0 6 votes vote down vote up
// Ported from @openzeppelin/test-helpers to use with Ethers. The Test Helpers don't
// yet have Typescript typings, so we're being lax about them here.
// See https://github.com/OpenZeppelin/openzeppelin-test-helpers/issues/122

/* eslint-disable @typescript-eslint/no-explicit-any */

export function inReceipt(receipt: ContractReceipt, eventName: string, eventArgs = {}): any {
  if (receipt.events == undefined) {
    throw new Error('No events found in receipt');
  }

  const events = receipt.events.filter((e) => e.event === eventName);
  expect(events.length > 0).to.equal(true, `No '${eventName}' events found`);

  const exceptions: Array<string> = [];
  const event = events.find(function (e) {
    for (const [k, v] of Object.entries(eventArgs)) {
      try {
        if (e.args == undefined) {
          throw new Error('Event has no arguments');
        }

        contains(e.args, k, v);
      } catch (error) {
        exceptions.push(error);
        return false;
      }
    }
    return true;
  });

  if (event === undefined) {
    // Each event entry may have failed to match for different reasons,
    // throw the first one
    throw exceptions[0];
  }

  return event;
}
Example #5
Source File: web3utils.ts    From lyra-protocol with ISC License 6 votes vote down vote up
export function getEventArgs(receipt: ContractReceipt, eventName: string): Result {
  const value = receipt.events!.find(e => e.event === eventName);
  if (value == undefined || value.args == undefined) {
    throw new Error(`Could not find event ${eventName}`);
  }
  return value.args;
}
Example #6
Source File: index.ts    From nova with GNU Affero General Public License v3.0 6 votes vote down vote up
/**
 * Waits for a cross domain message originating on L1 to be relayed on L2.
 */
export async function waitForL1ToL2Relay(l1Tx: Promise<ContractTransaction>, watcher: any) {
  console.log();

  const loader = ora({
    text: chalk.grey(`waiting for L1 -> L2 cross domain message to be relayed\n`),
    color: "yellow",
    indent: 6,
  }).start();

  const res = await l1Tx;
  await res.wait();

  const [l1ToL2XDomainMsgHash] = await watcher.getMessageHashesFromL1Tx(res.hash);

  const receipt: ContractReceipt = await watcher.getL2TransactionReceipt(l1ToL2XDomainMsgHash);

  loader.stopAndPersist({
    symbol: chalk.yellow("✓"),
    text: chalk.gray(
      `relay completed on L2 for cross domain message: ${chalk.yellow(receipt.transactionHash)}\n`
    ),
  });

  loader.indent = 0;
  loader.stop();
}
Example #7
Source File: index.ts    From nova with GNU Affero General Public License v3.0 6 votes vote down vote up
/**
 * Records the gas usage of a transaction, and checks against the most recent saved Jest snapshot.
 * If not in CI mode it won't stop tests (just show a console log).
 * To update the Jest snapshot run `npm run gas-changed`
 */
export async function snapshotGasCost(tx: Promise<ContractTransaction>) {
  // Only check gas estimates if we're not in coverage mode, as gas estimates are messed up in coverage mode.
  if (!process.env.HARDHAT_COVERAGE_MODE_ENABLED) {
    let receipt: ContractReceipt = await (await tx).wait();
    try {
      receipt.gasUsed.toNumber().should.toMatchSnapshot();
    } catch (e) {
      console.log(
        chalk.red(
          "(CHANGE) " +
            e.message
              .replace("expected", "used")
              .replace("to equal", "gas, but the snapshot expected it to use") +
            " gas"
        )
      );

      if (process.env.CI) {
        return Promise.reject("reverted: Gas consumption changed from expected.");
      }
    }
  }

  return tx;
}
Example #8
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
private mergeEventKind(
    source: TxReceiptLike[],
    map: Map<CrossChainEventKind, string>,
    event?: CrossChainEvent<ContractReceipt>,
  ): TxReceiptLike[] {
    const receipts = [...source];

    const index = receipts.findIndex(
      (receipt) => receipt && 'name' in receipt && receipt.name === 'Status',
    );

    receipts[index < 0 ? receipts.length : index] = {
      name: 'Status',
      value: event?.kind ? map.get(event.kind) ?? 'Pending' : 'Pending',
    };

    return receipts;
  }
Example #9
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
public withdrawCollateral(
    symbol: string,
    event?: CrossChainEvent<ContractReceipt>,
  ) {
    const map = new Map<CrossChainEventKind, string>([
      ...DEFAULT_STATUS,
      [CrossChainEventKind.OutgoingTxSubmitted, `Withdrawing ${symbol}`],
      [CrossChainEventKind.OutgoingTxRequested, `Withdrawing ${symbol}`],
    ]);
    this.write((current) => {
      return {
        ...current,
        message: `Withdrawing your ${symbol}`,
        description: this._description,
        receipts: this.mergeEventKind(current.receipts, map, event),
      };
    });
  }
Example #10
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
public restoreTx(event?: CrossChainEvent<ContractReceipt>) {
    const map = new Map<CrossChainEventKind, string>([
      ...DEFAULT_STATUS,
      [CrossChainEventKind.OutgoingTxSubmitted, 'Restoring'],
      [CrossChainEventKind.OutgoingTxRequested, 'Restoring'],
    ]);
    this.write((current) => {
      return {
        ...current,
        message: 'Restoring transaction',
        description: this._description,
        receipts: this.mergeEventKind(current.receipts, map, event),
      };
    });
  }
Example #11
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
public claimRewards(event?: CrossChainEvent<ContractReceipt>) {
    const map = new Map<CrossChainEventKind, string>([
      ...DEFAULT_STATUS,
      [CrossChainEventKind.IncomingTxSubmitted, `Claiming`],
      [CrossChainEventKind.IncomingTxExecuted, `Claiming`],
    ]);
    this.write((current) => {
      return {
        ...current,
        message: `Claiming rewards`,
        description: this._description,
        receipts: this.mergeEventKind(current.receipts, map, event),
      };
    });
  }
Example #12
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
public provideCollateral(
    symbol: string,
    event?: CrossChainEvent<ContractReceipt>,
  ) {
    const map = new Map<CrossChainEventKind, string>([
      ...DEFAULT_STATUS,
      [CrossChainEventKind.IncomingTxSubmitted, `Providing ${symbol}`],
      [CrossChainEventKind.IncomingTxExecuted, `Providing ${symbol}`],
    ]);
    this.write((current) => {
      return {
        ...current,
        message: `Providing your ${symbol}`,
        description: this._description,
        receipts: this.mergeEventKind(current.receipts, map, event),
      };
    });
  }
Example #13
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
public repayUST(event?: CrossChainEvent<ContractReceipt>) {
    const map = new Map<CrossChainEventKind, string>([
      ...DEFAULT_STATUS,
      [CrossChainEventKind.IncomingTxSubmitted, 'Repaying'],
      [CrossChainEventKind.IncomingTxExecuted, 'Repaying'],
    ]);
    this.write((current) => {
      return {
        ...current,
        message: 'Repaying your loan',
        description: this._description,
        receipts: this.mergeEventKind(current.receipts, map, event),
      };
    });
  }
Example #14
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
public borrowUST(
    event?: CrossChainEvent<ContractReceipt>,
    collateral?: string,
  ) {
    const map = new Map<CrossChainEventKind, string>([
      ...DEFAULT_STATUS,
      [CrossChainEventKind.OutgoingTxSubmitted, 'Borrowing'],
      [CrossChainEventKind.OutgoingTxRequested, 'Borrowing'],
    ]);

    // TODO: output the collateral information to the TxReceipt in the form of
    // sAVAX Collateral  ...  34.5

    this.write((current) => {
      return {
        ...current,
        message: 'Borrowing UST',
        description: this._description,
        receipts: this.mergeEventKind(current.receipts, map, event),
      };
    });
  }
Example #15
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
public depositUST(event?: CrossChainEvent<ContractReceipt>) {
    const map = new Map<CrossChainEventKind, string>([
      ...DEFAULT_STATUS,
      [CrossChainEventKind.IncomingTxSubmitted, 'Depositing'],
      [CrossChainEventKind.IncomingTxExecuted, 'Depositing'],
    ]);
    this.write((current) => {
      return {
        ...current,
        message: 'Depositing your UST',
        description: this._description,
        receipts: this.mergeEventKind(current.receipts, map, event),
      };
    });
  }
Example #16
Source File: EvmTxProgressWriter.ts    From anchor-web-app with Apache License 2.0 6 votes vote down vote up
public withdrawUST(event?: CrossChainEvent<ContractReceipt>) {
    const map = new Map<CrossChainEventKind, string>([
      ...DEFAULT_STATUS,
      [CrossChainEventKind.OutgoingTxSubmitted, 'Withdrawing'],
      [CrossChainEventKind.OutgoingTxRequested, 'Withdrawing'],
    ]);
    this.write((current) => {
      return {
        ...current,
        message: 'Withdrawing your UST',
        description: this._description,
        receipts: this.mergeEventKind(current.receipts, map, event),
      };
    });
  }
Example #17
Source File: useBackgroundTx.tsx    From anchor-web-app with Apache License 2.0 5 votes vote down vote up
useBackgroundTx = <TxParams, TxResult>(
  sendTx: (
    txParams: TxParams,
    renderTxResults: Subject<TxRender<TxResult>>,
    txEvents: Subject<TxEvent<TxParams>>,
  ) => Promise<TxResult>,
  parseTx: (txResult: NonNullable<TxResult>) => ContractReceipt,
  emptyTxResult: TxResult,
  displayTx: (txParams: TxParams) => TransactionDisplay,
  tx?: Transaction,
): BackgroundTxResult<TxParams, TxResult> | undefined => {
  // assume it is running by default, clear the flag if registered
  const [alreadyRunning, setAlreadyRunning] = useState<boolean>(
    !tx || Boolean(tx.backgroundTransactionTabId),
  );
  const backgroundTxId = useMemo(() => uuid(), []);
  const registerAfter = useMemo(() => Math.random() * 500, []);
  const txHash = tx?.txHash;
  const requestInput = Boolean(txHash)
    ? { txHash: txHash! }
    : { id: backgroundTxId };
  const { register, getRequest, updateRequest } = useBackgroundTxRequest();
  const request = getRequest(requestInput);

  useTimeout(() => {
    if (!request) {
      register({
        id: backgroundTxId,
        txHash,
        parseTx,
        displayTx,
        emptyTxResult,
        sendTx,
        minimized: Boolean(tx),
      });
      setAlreadyRunning(false);
    }
  }, registerAfter);

  return useMemo(() => {
    if (!request) {
      return undefined;
    }

    return {
      ...request.persistedTxResult,
      utils: {
        ...request.persistedTxResult?.utils,
        alreadyRunning,
        minimize: () => updateRequest(backgroundTxId, { minimized: true }),
      },
    };
  }, [
    request,
    alreadyRunning,
    updateRequest,
    backgroundTxId,
  ]) as BackgroundTxResult<TxParams, TxResult>;
}
Example #18
Source File: StablePhantomPool.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
async swapGivenIn(params: SwapPhantomPool): Promise<{ amountOut: BigNumber; receipt: ContractReceipt }> {
    const { amountOut, receipt } = await this.swap(await this._buildSwapParams(SwapKind.GivenIn, params));
    return { amountOut, receipt };
  }
Example #19
Source File: tokenTransfer.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
export function expectTransferEvent(
  receipt: ContractReceipt,
  args: { from?: string; to?: string; value?: BigNumberish },
  token: Token
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
): any {
  return expectEvent.inIndirectReceipt(receipt, token.instance.interface, 'Transfer', args, token.address);
}
Example #20
Source File: StablePhantomPool.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
async swapGivenOut(params: SwapPhantomPool): Promise<{ amountIn: BigNumber; receipt: ContractReceipt }> {
    const { amountIn, receipt } = await this.swap(await this._buildSwapParams(SwapKind.GivenOut, params));
    return { amountIn, receipt };
  }
Example #21
Source File: expectEvent.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
export function inIndirectReceipt(
  receipt: ContractReceipt,
  emitter: Interface,
  eventName: string,
  eventArgs = {},
  address?: string
): any {
  const decodedEvents = receipt.logs
    .filter((log) => (address ? log.address.toLowerCase() === address.toLowerCase() : true))
    .map((log) => {
      try {
        return emitter.parseLog(log);
      } catch {
        return undefined;
      }
    })
    .filter((e): e is LogDescription => e !== undefined);

  const expectedEvents = decodedEvents.filter((event) => event.name === eventName);
  expect(expectedEvents.length > 0).to.equal(true, `No '${eventName}' events found`);

  const exceptions: Array<string> = [];
  const event = expectedEvents.find(function (e) {
    for (const [k, v] of Object.entries(eventArgs)) {
      try {
        if (e.args == undefined) {
          throw new Error('Event has no arguments');
        }

        contains(e.args, k, v);
      } catch (error) {
        exceptions.push(error);
        return false;
      }
    }
    return true;
  });

  if (event === undefined) {
    // Each event entry may have failed to match for different reasons,
    // throw the first one
    throw exceptions[0];
  }

  return event;
}
Example #22
Source File: FeeDistributor.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
async function getReceiptTimestamp(receipt: ContractReceipt | Promise<ContractReceipt>): Promise<number> {
  const blockHash = (await receipt).blockHash;
  const block = await ethers.provider.getBlock(blockHash);
  return block.timestamp;
}
Example #23
Source File: expectEvent.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
export function notEmitted(receipt: ContractReceipt, eventName: string): void {
  if (receipt.events != undefined) {
    const events = receipt.events.filter((e) => e.event === eventName);
    expect(events.length > 0).to.equal(false, `'${eventName}' event found`);
  }
}
Example #24
Source File: ChildChainStreamer.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 5 votes vote down vote up
async function getReceiptTimestamp(receipt: ContractReceipt | Promise<ContractReceipt>): Promise<number> {
  const blockHash = (await receipt).blockHash;
  const block = await ethers.provider.getBlock(blockHash);
  return block.timestamp;
}
Example #25
Source File: useTx.ts    From anchor-web-app with Apache License 2.0 5 votes vote down vote up
txReceipt = (tx: ContractReceipt): TxReceipt => {
  return {
    name: 'Tx hash',
    value: truncateEvm(tx.transactionHash),
  };
}
Example #26
Source File: useTx.ts    From anchor-web-app with Apache License 2.0 5 votes vote down vote up
useTx = <TxParams, TxResult>(
  sendTx: (
    txParams: TxParams,
    renderTxResults: Subject<TxResultRendering<TxResult>>,
    txEvents: Subject<TxEvent<TxParams>>,
  ) => Promise<TxResult | null>,
  parseTx: (txResult: NonNullable<TxResult>) => ContractReceipt,
  emptyTxResult: TxResult,
): StreamReturn<TxParams, TxResultRendering<TxResult>> => {
  const { txErrorReporter } = useAnchorWebapp();

  // TODO: represent renderingEvents stream as txEvents.map(render) and remove the need for two subjects
  const txEvents = useMemo(() => new Subject<TxEvent<TxParams>>(), []);
  const renderingEvents = useMemo(
    () =>
      new BehaviorSubject<TxResultRendering<TxResult>>({
        value: emptyTxResult,
        message: 'Processing transaction...',
        phase: TxStreamPhase.BROADCAST,
        receipts: [],
      }),
    [emptyTxResult],
  );

  const txCallback = useCallback(
    (txParams: TxParams) => {
      return merge(
        from(sendTx(txParams, renderingEvents, txEvents))
          .pipe(
            map((txResult) => {
              renderingEvents.complete();
              txEvents.complete();

              return {
                value: txResult,
                phase: TxStreamPhase.SUCCEED,
                receipts: Boolean(txResult)
                  ? [txReceipt(parseTx(txResult!))]
                  : [],
              };
            }),
          )
          .pipe(catchTxError<TxResult | null>({ txErrorReporter })),
        renderingEvents,
      );
    },
    [sendTx, parseTx, txErrorReporter, renderingEvents, txEvents],
  );

  const [fetch, result] = useStream(txCallback);
  const txStreamResult = useMemo(
    () =>
      [fetch, result] as StreamReturn<TxParams, TxResultRendering<TxResult>>,
    [fetch, result],
  );

  return txStreamResult;
}
Example #27
Source File: burnAuction.test.ts    From hubble-contracts with MIT License 4 votes vote down vote up
describe("BurnAuction", function() {
    let burnAuction: TestBurnAuction;
    let rollup: MockRollup;
    let signer1: Signer;
    let signer2: Signer;
    let signer3: Signer;
    let gasPrice: BigNumber;

    before(async () => {
        let signer: Signer;
        [signer, signer1, signer2, signer3] = await ethers.getSigners();

        burnAuction = await new TestBurnAuction__factory(signer).deploy(
            donationAddress,
            DONATION_NUMERATOR
        );
        rollup = await new MockRollup__factory(signer).deploy(
            burnAuction.address
        );
        gasPrice = await ethers.provider.getGasPrice();
        // mine blocks till the first slot begins
        await mineBlocksTillInitialSlot();
    });

    describe("Rollup Burn Auction: functional test - One entire auction process step by step, then multiple auctions with lots of bids", () => {
        it("slot 0 - Fails forging an uninitialized auction", async () => {
            await failForge(signer1, uninitializedAuctionMessage);
        });
        it("slot 0 - Fails bidding on empty auction (slot 2, account 0, amount ==  0)", async () => {
            await failBid(signer1, "0", "0", badBidLessThanCurrentMessage);
        });
        it("slot 0 - Fails bidding on empty auction (slot 2, account 0, amount ==  0)", async () => {
            await failBid(signer1, "1", "0", badBidInsufficienttMessage);
        });
        it("slot 0 - Successfully bids on empty auction (slot 2, account 0, amount > 0)", async () => {
            const bidAmount = "2";
            const value = "5";
            await successBid(signer1, bidAmount, value);
        });
        it("slot 0 - Fails bidding on initialized auction (slot 2, account 1, less than current)", async () => {
            const bidAmount = "1.999999999";
            const value = "1.999999999";
            await failBid(
                signer2,
                bidAmount,
                value,
                badBidLessThanCurrentMessage
            );
        });
        it("slot 0 - Fails bidding on initialized auction (slot 2, account 1, equal to current)", async () => {
            const bidAmount = "2";
            const value = "2";
            await failBid(
                signer2,
                bidAmount,
                value,
                badBidLessThanCurrentMessage
            );
        });
        it("slot 0 - Successfully bids on empty auction (slot 2, account 0, raise bid)", async () => {
            const bidAmount = "3";
            const value = "0";
            await successBid(signer1, bidAmount, value);
        });
        it("slot 0 - Fails bidding on initialized auction (slot 2, account 1, insufficient)", async () => {
            const bidAmount = "4";
            const value = "0";
            await failBid(
                signer2,
                bidAmount,
                value,
                badBidInsufficienttMessage
            );
        });
        it("slot 0 - Successfully bids on empty auction (slot 2, account 0, take over)", async () => {
            const bidAmount = "4";
            const value = "4";
            await successBid(signer2, bidAmount, value);
            const slot = await getAuction(2);
            expect(slot).to.eql({
                coordinator: await signer2.getAddress(),
                amount: toWei(bidAmount),
                initialized: true
            });
        });

        it("slot 1 - Fails bidding on next auction (slot 3, account 0, amount ==  0)", async () => {
            await mineBlocksTillNextSlot();
            assert.equal(1, await getSlot());
            await failBid(signer1, "0", "0", badBidLessThanCurrentMessage);
        });

        it("slot 1 - Successfully bids on next auction (slot 3, account 0, amount > 0).", async () => {
            await successBid(signer1, "1", "0");
            let slot = await getAuction(2);
            expect(slot).to.eql({
                coordinator: await signer2.getAddress(),
                amount: toWei("4"),
                initialized: true
            });
            slot = await getAuction(3);
            expect(slot).to.eql({
                coordinator: await signer1.getAddress(),
                amount: toWei("1"),
                initialized: true
            });
        });

        it("slot 2 - Fails Forging batch (unauthorized coordinator)", async () => {
            await mineBlocksTillNextSlot();
            assert.equal(2, await getSlot());
            await failForge(signer1, badForgeMessage);
        });

        it("slot 2 - Fails Forging batch (unauthorized coordinator)", async () =>
            await failForge(signer1, badForgeMessage));

        it("slot 2 - Successfully forges batch", async () => {
            await successForge(signer2);
            // Winner can forge as many batches as they like in the slot
            await successForge(signer2);
        });
        it("slot 2 - withdraw fail", async () => {
            let amount = await depositAmount(await signer1.getAddress());
            amount = amount.add("1");
            await withdrawFail(signer1, amount, withdrawInsufficientmessage);
        });
        it("slot 2 - withdraw success", async () => {
            let amount = await depositAmount(await signer1.getAddress());
            await withdrawSuccess(signer1, amount);
        });
        it("slot 2 - withdraw donation", async () => {
            await withdrawDonation(signer1);
        });
        it("slot 2 - deposit", async () => {
            const amount = toWei("5566");
            const address3 = await signer3.getAddress();
            const before = await burnAuction.deposits(address3);
            assert.equal(before.toString(), "0");
            // Anyone can deposit for arbitrary beneficiary
            await burnAuction
                .connect(signer1)
                .deposit(address3, { value: amount });
            const after = await burnAuction.deposits(address3);
            assert.equal(after.toString(), amount.toString());
        });
    });

    async function mineBlocksTillInitialSlot() {
        // Get current slot and block
        const genesisSlot = await getSlot();
        const currentBlock = Number(await burnAuction.getBlockNumber());
        // Forward enought blocks to get to the inital slot
        await burnAuction.setBlockNumber(
            currentBlock + DELTA_BLOCKS_INITIAL_SLOT
        );
        // Check results
        const currentSlot = await getSlot();
        expect(genesisSlot + currentSlot).to.equal(0);
    }

    async function mineBlocksTillNextSlot() {
        // Get current slot and block
        const currentSlot = await getSlot();
        const currentBlock = Number(await burnAuction.getBlockNumber());
        // Forward enought blocks to get to next slot
        await burnAuction.setBlockNumber(currentBlock + BLOCKS_PER_SLOT);
        // Check results
        const nextSlot = await getSlot();
        expect(nextSlot - currentSlot).to.equal(1);
    }

    async function successBid(
        signer: Signer,
        bidAmount: string,
        value: string
    ) {
        const bidAmountWei = toWei(bidAmount);
        const valueWei = toWei(value);

        const _burnAuction = burnAuction.connect(signer);
        const coordinator = await signer.getAddress();
        const prevBid = await getCurrentAuction();
        const prevBidAmountWei = prevBid.amount;
        const prevCoordinator = prevBid.coordinator;

        const burnAuctionPrevBalance = await getEtherBalance(
            _burnAuction.address
        );
        const newBidderPrevDeposit = await _burnAuction.deposits(coordinator);
        const oldBidderPrevDeposit = await _burnAuction.deposits(
            prevCoordinator
        );
        const donationPrevDeposit = await _burnAuction.deposits(
            donationAddress
        );
        let correctDonationPrevDeposit = donationPrevDeposit;

        if (prevBid.initialized) {
            correctDonationPrevDeposit = BigNumber.from(prevBid.amount)
                .mul(DONATION_NUMERATOR)
                .div(DONATION_DENOMINATOR);
            correctDonationPrevDeposit = donationPrevDeposit.sub(
                correctDonationPrevDeposit
            );
        }

        const tx = await _burnAuction.bid(bidAmountWei, { value: valueWei });
        const receipt = await tx.wait();
        console.log("bid cost", receipt.gasUsed.toNumber());

        const [event] = await burnAuction.queryFilter(
            burnAuction.filters.NewBestBid(null, null, null),
            tx.blockHash
        );

        const burnAuctionNextBalance = await getEtherBalance(
            _burnAuction.address
        );
        const newBidderNextDeposit = await _burnAuction.deposits(coordinator);
        const oldBidderNextDeposit = await _burnAuction.deposits(
            prevCoordinator
        );

        const donationNextDeposit = await _burnAuction.deposits(
            donationAddress
        );

        expect(event.args?.amount.toString()).to.be.equal(bidAmountWei);
        expect(event.args?.coordinator).to.be.equal(coordinator);

        const bestBid = await getCurrentAuction();
        expect(bestBid.initialized).to.be.equal(true);
        expect(bestBid.coordinator).to.be.equal(coordinator);

        expect(bestBid.amount.toString()).to.be.equal(bidAmountWei);

        expect(burnAuctionPrevBalance.add(valueWei).toString()).to.be.equal(
            burnAuctionNextBalance.toString()
        );

        const donationAmt = ethers.BigNumber.from(bidAmountWei)
            .mul(DONATION_NUMERATOR)
            .div(DONATION_DENOMINATOR);

        expect(
            correctDonationPrevDeposit?.add(donationAmt).toString()
        ).to.be.equal(donationNextDeposit.toString());

        if (
            prevCoordinator !== coordinator &&
            prevCoordinator !== zeroAddress
        ) {
            expect(
                oldBidderPrevDeposit.add(prevBidAmountWei).toString()
            ).to.be.equal(oldBidderNextDeposit.toString());
            expect(
                newBidderPrevDeposit
                    .add(valueWei)
                    .sub(bidAmountWei)
                    .toString()
            ).to.be.equal(newBidderNextDeposit.toString());
        } else {
            expect(
                newBidderPrevDeposit
                    .add(prevBidAmountWei)
                    .add(valueWei)
                    .sub(bidAmountWei)
                    .toString()
            ).to.be.equal(newBidderNextDeposit.toString());
        }
    }

    async function withdrawDonation(signer: Signer) {
        const _burnAuction = burnAuction.connect(signer);
        const oldBalance = await ethers.provider.getBalance(donationAddress);
        const deposit = await depositAmount(donationAddress);
        await _burnAuction.withdrawDonation();
        const newBalance = await ethers.provider.getBalance(donationAddress);
        const mustBeZeroDepoist = await depositAmount(donationAddress);

        expect(oldBalance.add(deposit).toString()).to.be.equal(
            newBalance.toString()
        );
        expect(mustBeZeroDepoist.isZero()).to.be.true;
    }

    async function withdrawSuccess(signer: Signer, amount: BigNumber) {
        const _burnAuction = burnAuction.connect(signer);
        const coordinator = await signer.getAddress();
        const oldDeposit = await _burnAuction.deposits(coordinator);
        const oldBalance = await ethers.provider.getBalance(coordinator);
        const tx = await _burnAuction.withdraw(amount);
        const receipt = await tx.wait();
        const fee = calculateFee(receipt);
        const newDeposit = await _burnAuction.deposits(coordinator);
        const newBalance = await signer.getBalance();
        expect(oldDeposit.sub(amount).toString()).to.be.equal(
            newDeposit.toString()
        );
        expect(
            oldBalance
                .sub(fee)
                .add(amount)
                .toString()
        ).to.be.equal(newBalance.toString());
    }

    async function withdrawFail(
        signer: Signer,
        amount: BigNumber,
        withMessage: string
    ) {
        const _burnAuction = burnAuction.connect(signer);
        const coordinator = await signer.getAddress();
        const oldDeposit = await _burnAuction.deposits(coordinator);
        await expectRevert(_burnAuction.withdraw(amount), withMessage);
        const newDeposit = await _burnAuction.deposits(coordinator);
        expect(oldDeposit.toString()).to.be.equal(newDeposit.toString());
    }

    function calculateFee(receipt: ContractReceipt): BigNumber {
        return gasPrice.mul(receipt.gasUsed);
    }

    async function depositAmount(addr: string): Promise<BigNumber> {
        let _burnAuction = await burnAuction.deployed();
        return await _burnAuction.deposits(addr);
    }

    async function failBid(
        signer: Signer,
        bidAmount: string,
        value: string,
        withMessage: string
    ) {
        const _burnAuction = burnAuction.connect(signer);
        const oldBalance = await signer.getBalance();
        const oldDeposit = await _burnAuction.deposits(
            await signer.getAddress()
        );

        await expectRevert(
            _burnAuction.bid(toWei(bidAmount), { value: toWei(value) }),
            withMessage
        );

        const newBalance = await signer.getBalance();
        const newDeposit = await _burnAuction.deposits(
            await signer.getAddress()
        );

        expect(oldDeposit.toString()).to.equal(newDeposit.toString());
        expect(oldBalance.toString()).to.equal(newBalance.toString());
    }

    async function successForge(signer: Signer) {
        await rollup.connect(signer).submitBatch();
    }

    async function failForge(signer: Signer, failMessage: string) {
        await expectRevert(rollup.connect(signer).submitBatch(), failMessage);
    }

    async function getEtherBalance(address: string): Promise<BigNumber> {
        return await ethers.provider.getBalance(address);
    }

    async function getSlot() {
        return Number(await burnAuction.currentSlot());
    }

    async function getAuction(slot: number) {
        const auction = await burnAuction.auction(slot);
        return {
            amount: auction.amount.toString(),
            initialized: auction.initialized,
            coordinator: auction.coordinator
        };
    }

    async function getCurrentAuction() {
        const currentAuctionSlot = (await getSlot()) + 2;
        const currentAucttion = await getAuction(currentAuctionSlot);
        return {
            slot: currentAuctionSlot,
            ...currentAucttion
        };
    }
});
Example #28
Source File: Swaps.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('Swaps', () => {
  let vault: Contract, authorizer: Contract, funds: FundManagement;
  let tokens: TokenList;
  let mainPoolId: string, secondaryPoolId: string;
  let lp: SignerWithAddress, trader: SignerWithAddress, other: SignerWithAddress, admin: SignerWithAddress;

  const poolInitialBalance = bn(50e18);

  before('setup', async () => {
    [, lp, trader, other, admin] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy vault and tokens', async () => {
    tokens = await TokenList.create(['DAI', 'MKR', 'SNX', 'WETH']);

    authorizer = await deploy('TimelockAuthorizer', { args: [admin.address, ZERO_ADDRESS, MONTH] });
    vault = await deploy('Vault', { args: [authorizer.address, tokens.WETH.address, 0, 0] });

    await tokens.mint({ to: [lp, trader], amount: bn(200e18) });
    await tokens.approve({ to: vault, from: [lp, trader], amount: MAX_UINT112 });
  });

  beforeEach('set up default sender', async () => {
    funds = {
      sender: trader.address,
      recipient: trader.address,
      fromInternalBalance: false,
      toInternalBalance: false,
    };
  });

  context('with two tokens', () => {
    const symbols = ['DAI', 'MKR'];

    context('with a general pool', () => {
      itHandlesSwapsProperly(PoolSpecialization.GeneralPool, symbols);
    });

    context('with a minimal swap info pool', () => {
      itHandlesSwapsProperly(PoolSpecialization.MinimalSwapInfoPool, symbols);
    });

    context('with a two token pool', () => {
      itHandlesSwapsProperly(PoolSpecialization.TwoTokenPool, symbols);
    });
  });

  context('with three tokens', () => {
    const symbols = ['DAI', 'MKR', 'SNX'];

    context('with a general pool', () => {
      itHandlesSwapsProperly(PoolSpecialization.GeneralPool, symbols);
    });

    context('with a minimal swap info pool', () => {
      itHandlesSwapsProperly(PoolSpecialization.MinimalSwapInfoPool, symbols);
    });
  });

  context('when one of the assets is ETH', () => {
    // We only do givenIn tests, as givenIn and givenOut are presumed to be identical as they relate to this feature

    const symbols = ['DAI', 'WETH'];
    let tokenAddresses: string[];

    const limits = Array(symbols.length).fill(MAX_INT256);
    const deadline = MAX_UINT256;

    beforeEach(() => {
      tokenAddresses = [ETH_TOKEN_ADDRESS, tokens.DAI.address];
    });

    context('with minimal swap info pool', () => {
      sharedBeforeEach('setup pool', async () => {
        mainPoolId = await deployPool(PoolSpecialization.GeneralPool, symbols);
      });

      itSwapsWithETHCorrectly();
    });

    context('with general pool', () => {
      sharedBeforeEach('setup pool', async () => {
        mainPoolId = await deployPool(PoolSpecialization.MinimalSwapInfoPool, symbols);
      });

      itSwapsWithETHCorrectly();
    });

    function itSwapsWithETHCorrectly() {
      let sender: SignerWithAddress;

      context('when the sender is the trader', () => {
        beforeEach(() => {
          sender = trader;
        });

        it('received ETH is wrapped into WETH', async () => {
          const swaps = [
            {
              poolId: mainPoolId,
              assetInIndex: 0, // ETH
              assetOutIndex: 1,
              amount: bn(1e18),
              userData: '0x',
            },
          ];

          await expectBalanceChange(
            () =>
              vault
                .connect(sender)
                .batchSwap(SwapKind.GivenIn, swaps, tokenAddresses, funds, limits, deadline, { value: bn(1e18) }),
            tokens,
            [
              { account: vault, changes: { WETH: 1e18, DAI: -2e18 } },
              { account: trader, changes: { DAI: 2e18 } },
            ]
          );
        });

        it('sent WETH is unwrapped into ETH', async () => {
          const swaps = [
            {
              poolId: mainPoolId,
              assetInIndex: 1,
              assetOutIndex: 0, // ETH
              amount: bn(1e18),
              userData: '0x',
            },
          ];

          const traderBalanceBefore = await ethers.provider.getBalance(trader.address);

          const gasPrice = await ethers.provider.getGasPrice();
          const receipt: ContractReceipt = await (
            await expectBalanceChange(
              () =>
                vault
                  .connect(sender)
                  .batchSwap(SwapKind.GivenIn, swaps, tokenAddresses, funds, limits, deadline, { gasPrice }),
              tokens,
              [
                { account: vault, changes: { WETH: -2e18, DAI: 1e18 } },
                { account: trader, changes: { DAI: -1e18 } },
              ]
            )
          ).wait();
          const ethSpent = receipt.gasUsed.mul(gasPrice);

          const traderBalanceAfter = await ethers.provider.getBalance(trader.address);

          expect(traderBalanceAfter.sub(traderBalanceBefore)).to.equal(bn(2e18).sub(ethSpent));
        });

        it('emits an event with WETH as the token address', async () => {
          const swaps = [
            {
              poolId: mainPoolId,
              assetInIndex: 0, // ETH
              assetOutIndex: 1,
              amount: bn(1e18),
              userData: '0x',
            },

            {
              poolId: mainPoolId,
              assetInIndex: 1,
              assetOutIndex: 0, // ETH
              amount: bn(1e18),
              userData: '0x',
            },
          ];

          const receipt = await (
            await vault.connect(sender).batchSwap(SwapKind.GivenIn, swaps, tokenAddresses, funds, limits, deadline)
          ).wait();

          expectEvent.inReceipt(receipt, 'Swap', {
            poolId: mainPoolId,
            tokenIn: tokens.WETH.address,
            tokenOut: tokens.DAI.address,
            amountIn: bn(1e18),
            amountOut: bn(2e18),
          });

          expectEvent.inReceipt(receipt, 'Swap', {
            poolId: mainPoolId,
            tokenIn: tokens.DAI.address,
            tokenOut: tokens.WETH.address,
            amountIn: bn(1e18),
            amountOut: bn(2e18),
          });
        });

        it('reverts if less ETH than required was supplied', async () => {
          const swaps = [
            {
              poolId: mainPoolId,
              assetInIndex: 0, // ETH
              assetOutIndex: 1,
              amount: bn(1e18),
              userData: '0x',
            },
          ];

          await expect(
            vault
              .connect(sender)
              .batchSwap(SwapKind.GivenIn, swaps, tokenAddresses, funds, limits, deadline, { value: bn(1e18).sub(1) })
          ).to.be.revertedWith('INSUFFICIENT_ETH');
        });

        it('returns excess ETH if more ETH than required was supplied', async () => {
          const swaps = [
            {
              poolId: mainPoolId,
              assetInIndex: 0, // ETH
              assetOutIndex: 1,
              amount: bn(1e18),
              userData: '0x',
            },
          ];

          const previousBalance = await ethers.provider.getBalance(sender.address);

          const gasPrice = await ethers.provider.getGasPrice();
          const receipt: ContractReceipt = await (
            await vault.connect(sender).batchSwap(SwapKind.GivenIn, swaps, tokenAddresses, funds, limits, deadline, {
              value: bn(1e18).add(42), // Only 1e18 is required
              gasPrice,
            })
          ).wait();

          const ethSpent = receipt.gasUsed.mul(gasPrice);

          const currentBalance = await ethers.provider.getBalance(sender.address);
          expect(previousBalance.sub(currentBalance)).to.equal(ethSpent.add(bn(1e18)));
        });
      });

      context('when the sender is an approved relayer', () => {
        sharedBeforeEach(async () => {
          const action = await actionId(vault, 'batchSwap');
          await authorizer.connect(admin).grantPermissions([action], other.address, [ANY_ADDRESS]);

          await vault.connect(trader).setRelayerApproval(trader.address, other.address, true);
        });

        it('returns excess sent ETH to the relayer', async () => {
          const swaps = [
            {
              poolId: mainPoolId,
              assetInIndex: 0, // ETH
              assetOutIndex: 1,
              amount: bn(1e18),
              userData: '0x',
            },
          ];

          const relayerBalanceBefore = await ethers.provider.getBalance(other.address);

          const gasPrice = await ethers.provider.getGasPrice();
          const receipt: ContractReceipt = await (
            await vault.connect(other).batchSwap(SwapKind.GivenIn, swaps, tokenAddresses, funds, limits, deadline, {
              value: bn(1e18).add(42), // Only 1e18 is required
              gasPrice,
            })
          ).wait();
          const ethSpent = receipt.gasUsed.mul(gasPrice);

          const relayerBalanceAfter = await ethers.provider.getBalance(other.address);

          expect(relayerBalanceBefore.sub(relayerBalanceAfter)).to.equal(ethSpent.add(bn(1e18)));
        });

        it('returns unreceived ETH to the relayer', async () => {
          const swaps = [
            {
              poolId: mainPoolId,
              assetInIndex: 1,
              assetOutIndex: 0, // ETH
              amount: bn(1e18),
              userData: '0x',
            },
          ];

          const relayerBalanceBefore = await ethers.provider.getBalance(other.address);

          const gasPrice = await ethers.provider.getGasPrice();
          const receipt: ContractReceipt = await (
            await vault.connect(other).batchSwap(SwapKind.GivenIn, swaps, tokenAddresses, funds, limits, deadline, {
              value: 42,
              gasPrice,
            })
          ).wait();
          const ethSpent = receipt.gasUsed.mul(gasPrice);

          const relayerBalanceAfter = await ethers.provider.getBalance(other.address);

          expect(relayerBalanceBefore.sub(relayerBalanceAfter)).to.equal(ethSpent);
        });
      });
    }
  });

  function toBatchSwap(input: SwapInput): BatchSwapStep[] {
    return input.swaps.map((data) => ({
      poolId: ((data.pool ?? 0) == 0 ? mainPoolId : secondaryPoolId) || ZERO_BYTES32,
      amount: data.amount.toString(),
      assetInIndex: data.in,
      assetOutIndex: data.out,
      userData: data.data ?? '0x',
    }));
  }

  function toSingleSwap(kind: SwapKind, input: SwapInput): SingleSwap {
    const data = toBatchSwap(input)[0];
    return {
      kind,
      poolId: data.poolId,
      amount: data.amount,
      assetIn: tokens.addresses[data.assetInIndex] || ZERO_ADDRESS,
      assetOut: tokens.addresses[data.assetOutIndex] || ZERO_ADDRESS,
      userData: data.userData,
    };
  }

  async function deployPool(specialization: PoolSpecialization, tokenSymbols: string[]): Promise<string> {
    const pool = await deploy('MockPool', { args: [vault.address, specialization] });
    await pool.setMultiplier(fp(2));

    // Register tokens
    const sortedTokenAddresses = tokenSymbols
      .map((symbol) => tokens.findBySymbol(symbol))
      .sort((tokenA, tokenB) => tokenA.compare(tokenB))
      .map((token) => token.address);

    const assetManagers = sortedTokenAddresses.map(() => ZERO_ADDRESS);

    await pool.connect(lp).registerTokens(sortedTokenAddresses, assetManagers);

    // Join the pool - the actual amount is not relevant since the MockPool relies on the multiplier to calculate prices
    const tokenAmounts = sortedTokenAddresses.map(() => poolInitialBalance);

    const poolId = pool.getPoolId();
    await vault.connect(lp).joinPool(poolId, lp.address, other.address, {
      assets: sortedTokenAddresses,
      maxAmountsIn: tokenAmounts,
      fromInternalBalance: false,
      userData: encodeJoin(tokenAmounts, Array(sortedTokenAddresses.length).fill(0)),
    });

    return poolId;
  }

  function deployMainPool(specialization: PoolSpecialization, tokenSymbols: string[]) {
    sharedBeforeEach('deploy main pool', async () => {
      mainPoolId = await deployPool(specialization, tokenSymbols);
    });
  }

  function deployAnotherPool(specialization: PoolSpecialization, tokenSymbols: string[]) {
    sharedBeforeEach('deploy secondary pool', async () => {
      secondaryPoolId = await deployPool(specialization, tokenSymbols);
    });
  }

  function itHandlesSwapsProperly(specialization: PoolSpecialization, tokenSymbols: string[]) {
    deployMainPool(specialization, tokenSymbols);

    describe('swap given in', () => {
      const assertSwapGivenIn = (
        input: SwapInput,
        changes?: Dictionary<BigNumberish | Comparison>,
        expectedInternalBalance?: Dictionary<BigNumberish>
      ) => {
        const isSingleSwap = input.swaps.length === 1;

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const assertSwap = async (data: string, sender: SignerWithAddress, expectedChanges: any[]): Promise<void> => {
          // Hardcoding a gas limit prevents (slow) gas estimation
          await expectBalanceChange(
            () => sender.sendTransaction({ to: vault.address, data, gasLimit: MAX_GAS_LIMIT }),
            tokens,
            expectedChanges
          );

          if (expectedInternalBalance) {
            for (const symbol in expectedInternalBalance) {
              const token = tokens.findBySymbol(symbol);
              const internalBalance = await vault.getInternalBalance(sender.address, [token.address]);
              expect(internalBalance[0]).to.be.equal(bn(expectedInternalBalance[symbol]));
            }
          }
        };

        if (isSingleSwap) {
          it('trades the expected amount (single)', async () => {
            const sender = input.fromOther ? other : trader;
            const recipient = input.toOther ? other : trader;
            const swap = toSingleSwap(SwapKind.GivenIn, input);

            let calldata = vault.interface.encodeFunctionData('swap', [swap, funds, 0, MAX_UINT256]);

            if (input.signature) {
              const nonce = await vault.getNextNonce(trader.address);
              const authorization = await RelayerAuthorization.signSwapAuthorization(
                vault,
                trader,
                sender.address,
                calldata,
                MAX_UINT256,
                nonce
              );
              const signature = typeof input.signature === 'string' ? input.signature : authorization;
              calldata = RelayerAuthorization.encodeCalldataAuthorization(calldata, MAX_UINT256, signature);
            }

            await assertSwap(calldata, sender, [{ account: recipient, changes }]);
          });
        }

        it(`trades the expected amount ${isSingleSwap ? '(batch)' : ''}`, async () => {
          const sender = input.fromOther ? other : trader;
          const recipient = input.toOther ? other : trader;
          const swaps = toBatchSwap(input);
          const limits = Array(tokens.length).fill(MAX_INT256);

          const args = [SwapKind.GivenIn, swaps, tokens.addresses, funds, limits, MAX_UINT256];
          let calldata = vault.interface.encodeFunctionData('batchSwap', args);

          if (input.signature) {
            const nonce = await vault.getNextNonce(trader.address);
            const authorization = await RelayerAuthorization.signBatchSwapAuthorization(
              vault,
              trader,
              sender.address,
              calldata,
              MAX_UINT256,
              nonce
            );
            const signature = typeof input.signature === 'string' ? input.signature : authorization;
            calldata = RelayerAuthorization.encodeCalldataAuthorization(calldata, MAX_UINT256, signature);
          }

          await assertSwap(calldata, sender, [{ account: recipient, changes }]);
        });
      };

      const assertSwapGivenInReverts = (input: SwapInput, defaultReason?: string, singleSwapReason = defaultReason) => {
        const isSingleSwap = input.swaps.length === 1;

        if (isSingleSwap) {
          it(`reverts ${isSingleSwap ? '(single)' : ''}`, async () => {
            const sender = input.fromOther ? other : trader;
            const swap = toSingleSwap(SwapKind.GivenIn, input);
            const call = vault.connect(sender).swap(swap, funds, MAX_UINT256, MAX_UINT256);

            singleSwapReason
              ? await expect(call).to.be.revertedWith(singleSwapReason)
              : await expect(call).to.be.reverted;
          });
        }

        it(`reverts ${isSingleSwap ? '(batch)' : ''}`, async () => {
          const sender = input.fromOther ? other : trader;
          const swaps = toBatchSwap(input);

          const limits = Array(tokens.length).fill(MAX_INT256);
          const deadline = MAX_UINT256;

          const call = vault
            .connect(sender)
            .batchSwap(SwapKind.GivenIn, swaps, tokens.addresses, funds, limits, deadline);
          defaultReason ? await expect(call).to.be.revertedWith(defaultReason) : await expect(call).to.be.reverted;
        });
      };

      context('for a single swap', () => {
        context('when the pool is registered', () => {
          context('when an amount is specified', () => {
            context('when the given indexes are valid', () => {
              context('when the given token is in the pool', () => {
                context('when the requested token is in the pool', () => {
                  context('when requesting another token', () => {
                    context('when requesting a reasonable amount', () => {
                      // Send 1 MKR, get 2 DAI back
                      const swaps = [{ in: 1, out: 0, amount: 1e18 }];

                      context('when using managed balance', () => {
                        context('when the sender is the user', () => {
                          const fromOther = false;

                          assertSwapGivenIn({ swaps, fromOther }, { DAI: 2e18, MKR: -1e18 });
                        });

                        context('when the sender is a relayer', () => {
                          const fromOther = true;

                          context('when the relayer is whitelisted by the authorizer', () => {
                            sharedBeforeEach('grant permission to relayer', async () => {
                              const single = await actionId(vault, 'swap');
                              const batch = await actionId(vault, 'batchSwap');
                              await authorizer
                                .connect(admin)
                                .grantPermissions([single, batch], other.address, [ANY_ADDRESS, ANY_ADDRESS]);
                            });

                            context('when the relayer is allowed by the user', () => {
                              sharedBeforeEach('allow relayer', async () => {
                                await vault.connect(trader).setRelayerApproval(trader.address, other.address, true);
                              });

                              assertSwapGivenIn({ swaps, fromOther }, { DAI: 2e18, MKR: -1e18 });
                            });

                            context('when the relayer is not allowed by the user', () => {
                              sharedBeforeEach('disallow relayer', async () => {
                                await vault.connect(trader).setRelayerApproval(trader.address, other.address, false);
                              });

                              context('when the relayer has a valid signature from the user', () => {
                                assertSwapGivenIn({ swaps, fromOther, signature: true }, { DAI: 2e18, MKR: -1e18 });
                              });

                              context('when the relayer has an invalid signature from the user', () => {
                                assertSwapGivenInReverts(
                                  { swaps, fromOther, signature: ZERO_BYTES32 },
                                  'USER_DOESNT_ALLOW_RELAYER'
                                );
                              });

                              context('when there is no signature', () => {
                                assertSwapGivenInReverts({ swaps, fromOther }, 'USER_DOESNT_ALLOW_RELAYER');
                              });
                            });
                          });

                          context('when the relayer is not whitelisted by the authorizer', () => {
                            sharedBeforeEach('revoke permission from relayer', async () => {
                              const single = await actionId(vault, 'swap');
                              const batch = await actionId(vault, 'batchSwap');
                              await authorizer
                                .connect(admin)
                                .revokePermissions([single, batch], other.address, [ANY_ADDRESS, ANY_ADDRESS]);
                            });

                            context('when the relayer is allowed by the user', () => {
                              sharedBeforeEach('allow relayer', async () => {
                                await vault.connect(trader).setRelayerApproval(trader.address, other.address, true);
                              });

                              assertSwapGivenInReverts({ swaps, fromOther }, 'SENDER_NOT_ALLOWED');
                            });

                            context('when the relayer is not allowed by the user', () => {
                              sharedBeforeEach('disallow relayer', async () => {
                                await vault.connect(trader).setRelayerApproval(trader.address, other.address, false);
                              });

                              assertSwapGivenInReverts({ swaps, fromOther }, 'SENDER_NOT_ALLOWED');
                            });
                          });
                        });
                      });

                      context('when withdrawing from internal balance', () => {
                        beforeEach(() => {
                          funds.fromInternalBalance = true;
                        });

                        context('when using less than available as internal balance', () => {
                          sharedBeforeEach('deposit to internal balance', async () => {
                            await vault.connect(trader).manageUserBalance([
                              {
                                kind: 0, // deposit
                                asset: tokens.DAI.address,
                                amount: bn(1e18),
                                sender: trader.address,
                                recipient: trader.address,
                              },
                              {
                                kind: 0, // deposit
                                asset: tokens.MKR.address,
                                amount: bn(1e18),
                                sender: trader.address,
                                recipient: trader.address,
                              },
                            ]);
                          });

                          assertSwapGivenIn({ swaps }, { DAI: 2e18 }, { MKR: 0, DAI: 1e18 });
                        });

                        context('when using more than available as internal balance', () => {
                          sharedBeforeEach('deposit to internal balance', async () => {
                            await vault.connect(trader).manageUserBalance([
                              {
                                kind: 0, // deposit
                                asset: tokens.MKR.address,
                                amount: bn(0.3e18),
                                sender: trader.address,
                                recipient: trader.address,
                              },
                            ]);
                          });

                          assertSwapGivenIn({ swaps }, { DAI: 2e18, MKR: -0.7e18 }, { MKR: 0 });
                        });
                      });

                      context('when depositing from internal balance', () => {
                        beforeEach(() => {
                          funds.toInternalBalance = true;
                        });

                        assertSwapGivenIn({ swaps }, { MKR: -1e18 });
                      });
                    });

                    context('when draining the pool', () => {
                      const swaps = [{ in: 1, out: 0, amount: poolInitialBalance.div(2) }];

                      assertSwapGivenIn({ swaps }, { DAI: poolInitialBalance, MKR: poolInitialBalance.div(2).mul(-1) });
                    });

                    context('when requesting more than the available balance', () => {
                      const swaps = [{ in: 1, out: 0, amount: poolInitialBalance.div(2).add(1) }];

                      assertSwapGivenInReverts({ swaps }, 'SUB_OVERFLOW');
                    });
                  });

                  context('when the requesting the same token', () => {
                    const swaps = [{ in: 1, out: 1, amount: 1e18 }];

                    assertSwapGivenInReverts({ swaps }, 'CANNOT_SWAP_SAME_TOKEN');
                  });
                });

                context('when the requested token is not in the pool', () => {
                  const swaps = [{ in: 1, out: 3, amount: 1e18 }];

                  assertSwapGivenInReverts({ swaps });
                });
              });

              context('when the given token is not in the pool', () => {
                const swaps = [{ in: 3, out: 1, amount: 1e18 }];

                assertSwapGivenInReverts({ swaps });
              });
            });

            context('when the given indexes are not valid', () => {
              context('when the token index in is not valid', () => {
                const swaps = [{ in: 30, out: 1, amount: 1e18 }];

                assertSwapGivenInReverts({ swaps }, 'OUT_OF_BOUNDS', 'TOKEN_NOT_REGISTERED');
              });

              context('when the token index out is not valid', () => {
                const swaps = [{ in: 0, out: 10, amount: 1e18 }];

                assertSwapGivenInReverts({ swaps }, 'OUT_OF_BOUNDS', 'TOKEN_NOT_REGISTERED');
              });
            });
          });

          context('when no amount is specified', () => {
            const swaps = [{ in: 1, out: 0, amount: 0 }];

            assertSwapGivenInReverts({ swaps }, 'UNKNOWN_AMOUNT_IN_FIRST_SWAP');
          });
        });

        context('when the pool is not registered', () => {
          const swaps = [{ pool: 1000, in: 1, out: 0, amount: 1e18 }];

          assertSwapGivenInReverts({ swaps }, 'INVALID_POOL_ID');
        });
      });

      context('for a multi swap', () => {
        context('without hops', () => {
          context('with the same pool', () => {
            const swaps = [
              // Send 1 MKR, get 2 DAI back
              { in: 1, out: 0, amount: 1e18 },
              // Send 2 DAI, get 4 MKR back
              { in: 0, out: 1, amount: 2e18 },
            ];

            assertSwapGivenIn({ swaps }, { MKR: 3e18 });
          });

          context('with another pool', () => {
            context('with two tokens', () => {
              const anotherPoolSymbols = ['DAI', 'MKR'];

              const itHandleMultiSwapsWithoutHopsProperly = (anotherPoolSpecialization: PoolSpecialization) => {
                deployAnotherPool(anotherPoolSpecialization, anotherPoolSymbols);

                context('for a single pair', () => {
                  const swaps = [
                    // In each pool, send 1e18 MKR, get 2e18 DAI back
                    { pool: 0, in: 1, out: 0, amount: 1e18 },
                    { pool: 1, in: 1, out: 0, amount: 1e18 },
                  ];

                  assertSwapGivenIn({ swaps }, { DAI: 4e18, MKR: -2e18 });
                });

                context('for a multi pair', () => {
                  context('when pools offer same price', () => {
                    const swaps = [
                      // Send 1 MKR, get 2 DAI back
                      { pool: 0, in: 1, out: 0, amount: 1e18 },
                      // Send 2 DAI, get 4 MKR back
                      { pool: 1, in: 0, out: 1, amount: 2e18 },
                    ];

                    assertSwapGivenIn({ swaps }, { MKR: 3e18 });
                  });

                  context('when pools do not offer same price', () => {
                    sharedBeforeEach('tweak the main pool to give back as much as it receives', async () => {
                      const [poolAddress] = (await vault.getPool(mainPoolId)) as [string, unknown];
                      const pool = await deployedAt('MockPool', poolAddress);
                      await pool.setMultiplier(fp(1));
                    });

                    beforeEach('tweak sender and recipient to be other address', async () => {
                      // The caller will receive profit in MKR, since it sold DAI for more MKR than it bought it for.
                      // The caller receives tokens and doesn't send any.
                      // Note the caller didn't even have any tokens to begin with.
                      funds.sender = other.address;
                      funds.recipient = other.address;
                    });

                    // Sell DAI in the pool where it is valuable, buy it in the one where it has a regular price
                    const swaps = [
                      // Sell 1e18 DAI for 2e18 MKR
                      { pool: 1, in: 0, out: 1, amount: 1e18 },
                      // Buy 2e18 DAI with 2e18 MKR
                      { pool: 0, in: 1, out: 0, amount: 1e18 },
                    ];

                    assertSwapGivenIn({ swaps, fromOther: true, toOther: true }, { MKR: 1e18 });
                  });
                });
              };
              context('with a general pool', () => {
                itHandleMultiSwapsWithoutHopsProperly(PoolSpecialization.GeneralPool);
              });

              context('with a minimal swap info pool', () => {
                itHandleMultiSwapsWithoutHopsProperly(PoolSpecialization.MinimalSwapInfoPool);
              });

              context('with a two token pool', () => {
                itHandleMultiSwapsWithoutHopsProperly(PoolSpecialization.TwoTokenPool);
              });
            });

            context('with three tokens', () => {
              const anotherPoolSymbols = ['DAI', 'MKR', 'SNX'];

              const itHandleMultiSwapsWithoutHopsProperly = (anotherPoolSpecialization: PoolSpecialization) => {
                deployAnotherPool(anotherPoolSpecialization, anotherPoolSymbols);

                context('for a single pair', () => {
                  // In each pool, send 1e18 MKR, get 2e18 DAI back
                  const swaps = [
                    { pool: 0, in: 1, out: 0, amount: 1e18 },
                    { pool: 1, in: 1, out: 0, amount: 1e18 },
                  ];

                  assertSwapGivenIn({ swaps }, { DAI: 4e18, MKR: -2e18 });
                });

                context('for a multi pair', () => {
                  const swaps = [
                    // Send 1 MKR, get 2 DAI back
                    { pool: 0, in: 1, out: 0, amount: 1e18 },
                    // Send 2 DAI, get 4 SNX back
                    { pool: 1, in: 0, out: 2, amount: 2e18 },
                  ];

                  assertSwapGivenIn({ swaps }, { SNX: 4e18, MKR: -1e18 });
                });
              };

              context('with a general pool', () => {
                const anotherPoolSpecialization = PoolSpecialization.GeneralPool;
                itHandleMultiSwapsWithoutHopsProperly(anotherPoolSpecialization);
              });

              context('with a minimal swap info pool', () => {
                const anotherPoolSpecialization = PoolSpecialization.MinimalSwapInfoPool;
                itHandleMultiSwapsWithoutHopsProperly(anotherPoolSpecialization);
              });
            });
          });
        });

        context('with hops', () => {
          context('with the same pool', () => {
            context('when token in and out match', () => {
              const swaps = [
                // Send 1 MKR, get 2 DAI back
                { in: 1, out: 0, amount: 1e18 },
                // Send the previously acquired 2 DAI, get 4 MKR back
                { in: 0, out: 1, amount: 0 },
              ];

              assertSwapGivenIn({ swaps }, { MKR: 3e18 });
            });

            context('when token in and out mismatch', () => {
              const swaps = [
                // Send 1 MKR, get 2 DAI back
                { in: 1, out: 0, amount: 1e18 },
                // Send the previously acquired 2 DAI, get 4 MKR back
                { in: 1, out: 0, amount: 0 },
              ];

              assertSwapGivenInReverts({ swaps }, 'MALCONSTRUCTED_MULTIHOP_SWAP');
            });
          });

          context('with another pool', () => {
            context('with two tokens', () => {
              const anotherPoolSymbols = ['DAI', 'MKR'];

              const itHandleMultiSwapsWithHopsProperly = (anotherPoolSpecialization: PoolSpecialization) => {
                deployAnotherPool(anotherPoolSpecialization, anotherPoolSymbols);

                const swaps = [
                  // Send 1 MKR, get 2 DAI back
                  { pool: 0, in: 1, out: 0, amount: 1e18 },
                  // Send the previously acquired 2 DAI, get 4 MKR back
                  { pool: 1, in: 0, out: 1, amount: 0 },
                ];

                assertSwapGivenIn({ swaps }, { MKR: 3e18 });
              };

              context('with a general pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.GeneralPool);
              });

              context('with a minimal swap info pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.MinimalSwapInfoPool);
              });

              context('with a two token pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.TwoTokenPool);
              });
            });

            context('with three tokens', () => {
              const anotherPoolSymbols = ['DAI', 'MKR', 'SNX'];

              const itHandleMultiSwapsWithHopsProperly = (anotherPoolSpecialization: PoolSpecialization) => {
                deployAnotherPool(anotherPoolSpecialization, anotherPoolSymbols);

                const swaps = [
                  // Send 1 MKR, get 2 DAI back
                  { pool: 0, in: 1, out: 0, amount: 1e18 },
                  // Send the previously acquired 2 DAI, get 4 SNX back
                  { pool: 1, in: 0, out: 2, amount: 0 },
                ];

                assertSwapGivenIn({ swaps }, { SNX: 4e18, MKR: -1e18 });
              };

              context('with a general pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.GeneralPool);
              });

              context('with a minimal swap info pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.MinimalSwapInfoPool);
              });
            });
          });
        });
      });
    });

    describe('swap given out', () => {
      const assertSwapGivenOut = (
        input: SwapInput,
        changes?: Dictionary<BigNumberish | Comparison>,
        expectedInternalBalance?: Dictionary<BigNumberish>
      ) => {
        const isSingleSwap = input.swaps.length === 1;

        if (isSingleSwap) {
          it('trades the expected amount (single)', async () => {
            const sender = input.fromOther ? other : trader;
            const recipient = input.toOther ? other : trader;
            const swap = toSingleSwap(SwapKind.GivenOut, input);

            await expectBalanceChange(() => vault.connect(sender).swap(swap, funds, MAX_UINT256, MAX_UINT256), tokens, [
              { account: recipient, changes },
            ]);

            if (expectedInternalBalance) {
              for (const symbol in expectedInternalBalance) {
                const token = tokens.findBySymbol(symbol);
                const internalBalance = await vault.getInternalBalance(sender.address, [token.address]);
                expect(internalBalance[0]).to.be.equal(bn(expectedInternalBalance[symbol]));
              }
            }
          });
        }

        it(`trades the expected amount ${isSingleSwap ? '(batch)' : ''}`, async () => {
          const sender = input.fromOther ? other : trader;
          const recipient = input.toOther ? other : trader;
          const swaps = toBatchSwap(input);

          const limits = Array(tokens.length).fill(MAX_INT256);
          const deadline = MAX_UINT256;

          await expectBalanceChange(
            () => vault.connect(sender).batchSwap(SwapKind.GivenOut, swaps, tokens.addresses, funds, limits, deadline),
            tokens,
            [{ account: recipient, changes }]
          );

          if (expectedInternalBalance) {
            for (const symbol in expectedInternalBalance) {
              const token = tokens.findBySymbol(symbol);
              const internalBalance = await vault.getInternalBalance(sender.address, [token.address]);
              expect(internalBalance[0]).to.be.equal(bn(expectedInternalBalance[symbol]));
            }
          }
        });
      };

      const assertSwapGivenOutReverts = (
        input: SwapInput,
        defaultReason?: string,
        singleSwapReason = defaultReason
      ) => {
        const isSingleSwap = input.swaps.length === 1;

        if (isSingleSwap) {
          it(`reverts ${isSingleSwap ? '(single)' : ''}`, async () => {
            const sender = input.fromOther ? other : trader;
            const swap = toSingleSwap(SwapKind.GivenOut, input);
            const call = vault.connect(sender).swap(swap, funds, MAX_UINT256, MAX_UINT256);

            singleSwapReason
              ? await expect(call).to.be.revertedWith(singleSwapReason)
              : await expect(call).to.be.reverted;
          });
        }

        it(`reverts ${isSingleSwap ? '(batch)' : ''}`, async () => {
          const sender = input.fromOther ? other : trader;
          const swaps = toBatchSwap(input);

          const limits = Array(tokens.length).fill(MAX_INT256);
          const deadline = MAX_UINT256;

          const call = vault
            .connect(sender)
            .batchSwap(SwapKind.GivenOut, swaps, tokens.addresses, funds, limits, deadline);

          defaultReason ? await expect(call).to.be.revertedWith(defaultReason) : await expect(call).to.be.reverted;
        });
      };

      context('for a single swap', () => {
        context('when the pool is registered', () => {
          context('when an amount is specified', () => {
            context('when the given indexes are valid', () => {
              context('when the given token is in the pool', () => {
                context('when the requested token is in the pool', () => {
                  context('when the requesting another token', () => {
                    context('when requesting a reasonable amount', () => {
                      // Get 1e18 DAI by sending 0.5e18 MKR
                      const swaps = [{ in: 1, out: 0, amount: 1e18 }];

                      context('when using managed balance', () => {
                        context('when the sender is the user', () => {
                          const fromOther = false;

                          assertSwapGivenOut({ swaps, fromOther }, { DAI: 1e18, MKR: -0.5e18 });
                        });

                        context('when the sender is a relayer', () => {
                          const fromOther = true;

                          context('when the relayer is whitelisted by the authorizer', () => {
                            sharedBeforeEach('grant permission to relayer', async () => {
                              const single = await actionId(vault, 'swap');
                              const batch = await actionId(vault, 'batchSwap');
                              await authorizer
                                .connect(admin)
                                .grantPermissions([single, batch], other.address, [ANY_ADDRESS, ANY_ADDRESS]);
                            });

                            context('when the relayer is allowed by the user', () => {
                              sharedBeforeEach('allow relayer', async () => {
                                await vault.connect(trader).setRelayerApproval(trader.address, other.address, true);
                              });

                              assertSwapGivenOut({ swaps, fromOther }, { DAI: 1e18, MKR: -0.5e18 });
                            });

                            context('when the relayer is not allowed by the user', () => {
                              sharedBeforeEach('disallow relayer', async () => {
                                await vault.connect(trader).setRelayerApproval(trader.address, other.address, false);
                              });

                              assertSwapGivenOutReverts({ swaps, fromOther }, 'USER_DOESNT_ALLOW_RELAYER');
                            });
                          });

                          context('when the relayer is not whitelisted by the authorizer', () => {
                            sharedBeforeEach('revoke permission from relayer', async () => {
                              const single = await actionId(vault, 'swap');
                              const batch = await actionId(vault, 'batchSwap');
                              await authorizer
                                .connect(admin)
                                .revokePermissions([single, batch], other.address, [ANY_ADDRESS, ANY_ADDRESS]);
                            });

                            context('when the relayer is allowed by the user', () => {
                              sharedBeforeEach('allow relayer', async () => {
                                await vault.connect(trader).setRelayerApproval(trader.address, other.address, true);
                              });

                              assertSwapGivenOutReverts({ swaps, fromOther }, 'SENDER_NOT_ALLOWED');
                            });

                            context('when the relayer is not allowed by the user', () => {
                              sharedBeforeEach('disallow relayer', async () => {
                                await vault.connect(trader).setRelayerApproval(trader.address, other.address, false);
                              });

                              assertSwapGivenOutReverts({ swaps, fromOther }, 'SENDER_NOT_ALLOWED');
                            });
                          });
                        });
                      });

                      context('when withdrawing from internal balance', () => {
                        beforeEach(() => {
                          funds.fromInternalBalance = true;
                        });

                        context('when using less than available as internal balance', () => {
                          sharedBeforeEach('deposit to internal balance', async () => {
                            await vault.connect(trader).manageUserBalance([
                              {
                                kind: 0, // deposit
                                asset: tokens.DAI.address,
                                amount: bn(1e18),
                                sender: trader.address,
                                recipient: trader.address,
                              },
                              {
                                kind: 0, // deposit
                                asset: tokens.MKR.address,
                                amount: bn(0.5e18),
                                sender: trader.address,
                                recipient: trader.address,
                              },
                            ]);
                          });

                          assertSwapGivenOut({ swaps }, { DAI: 1e18 }, { MKR: 0, DAI: 1e18 });
                        });

                        context('when using more than available as internal balance', () => {
                          sharedBeforeEach('deposit to internal balance', async () => {
                            await vault.connect(trader).manageUserBalance([
                              {
                                kind: 0, // deposit
                                asset: tokens.MKR.address,
                                amount: bn(0.3e18),
                                sender: trader.address,
                                recipient: trader.address,
                              },
                            ]);
                          });

                          assertSwapGivenOut({ swaps }, { DAI: 1e18, MKR: -0.2e18 });
                        });
                      });

                      context('when depositing from internal balance', () => {
                        beforeEach(() => {
                          funds.toInternalBalance = true;
                        });

                        assertSwapGivenOut({ swaps }, { MKR: -0.5e18 });
                      });
                    });

                    context('when draining the pool', () => {
                      const swaps = [{ in: 1, out: 0, amount: poolInitialBalance }];

                      assertSwapGivenOut(
                        { swaps },
                        { DAI: poolInitialBalance, MKR: poolInitialBalance.div(2).mul(-1) }
                      );
                    });

                    context('when requesting more than the available balance', () => {
                      const swaps = [{ in: 1, out: 0, amount: poolInitialBalance.add(1) }];

                      assertSwapGivenOutReverts({ swaps }, 'SUB_OVERFLOW');
                    });
                  });

                  context('when the requesting the same token', () => {
                    const swaps = [{ in: 1, out: 1, amount: 1e18 }];

                    assertSwapGivenOutReverts({ swaps }, 'CANNOT_SWAP_SAME_TOKEN');
                  });
                });

                context('when the requested token is not in the pool', () => {
                  const swaps = [{ in: 1, out: 3, amount: 1e18 }];

                  assertSwapGivenOutReverts({ swaps });
                });
              });

              context('when the given token is not in the pool', () => {
                const swaps = [{ in: 3, out: 1, amount: 1e18 }];

                assertSwapGivenOutReverts({ swaps });
              });
            });

            context('when the given indexes are not valid', () => {
              context('when the token index in is not valid', () => {
                const swaps = [{ in: 30, out: 1, amount: 1e18 }];

                assertSwapGivenOutReverts({ swaps }, 'OUT_OF_BOUNDS', 'TOKEN_NOT_REGISTERED');
              });

              context('when the token index out is not valid', () => {
                const swaps = [{ in: 0, out: 10, amount: 1e18 }];

                assertSwapGivenOutReverts({ swaps }, 'OUT_OF_BOUNDS', 'TOKEN_NOT_REGISTERED');
              });
            });
          });

          context('when no amount is specified', () => {
            const swaps = [{ in: 1, out: 0, amount: 0 }];

            assertSwapGivenOutReverts({ swaps }, 'UNKNOWN_AMOUNT_IN_FIRST_SWAP');
          });
        });

        context('when the pool is not registered', () => {
          const swaps = [{ pool: 1000, in: 1, out: 0, amount: 1e18 }];

          assertSwapGivenOutReverts({ swaps }, 'INVALID_POOL_ID');
        });
      });

      context('for a multi swap', () => {
        context('without hops', () => {
          context('with the same pool', () => {
            const swaps = [
              // Get 1 DAI by sending 0.5 MKR
              { in: 1, out: 0, amount: 1e18 },
              // Get 2 MKR by sending 1 DAI
              { in: 0, out: 1, amount: 2e18 },
            ];

            assertSwapGivenOut({ swaps }, { MKR: 1.5e18 });
          });

          context('with another pool', () => {
            context('with two tokens', () => {
              const anotherPoolSymbols = ['DAI', 'MKR'];

              const itHandleMultiSwapsWithoutHopsProperly = (anotherPoolSpecialization: PoolSpecialization) => {
                deployAnotherPool(anotherPoolSpecialization, anotherPoolSymbols);

                context('for a single pair', () => {
                  // In each pool, get 1e18 DAI by sending 0.5e18 MKR
                  const swaps = [
                    { pool: 0, in: 1, out: 0, amount: 1e18 },
                    { pool: 1, in: 1, out: 0, amount: 1e18 },
                  ];

                  assertSwapGivenOut({ swaps }, { DAI: 2e18, MKR: -1e18 });
                });

                context('for a multi pair', () => {
                  context('when pools offer same price', () => {
                    const swaps = [
                      // Get 1 DAI by sending 0.5 MKR
                      { pool: 0, in: 1, out: 0, amount: 1e18 },
                      // Get 2 MKR by sending 1 DAI
                      { pool: 1, in: 0, out: 1, amount: 2e18 },
                    ];

                    assertSwapGivenOut({ swaps }, { MKR: 1.5e18 });
                  });

                  context('when pools do not offer same price', () => {
                    beforeEach('tweak the main pool to give back as much as it receives', async () => {
                      const [poolAddress] = (await vault.getPool(mainPoolId)) as [string, unknown];
                      const pool = await deployedAt('MockPool', poolAddress);
                      await pool.setMultiplier(fp(1));
                    });

                    beforeEach('tweak sender and recipient to be other address', async () => {
                      // The caller will receive profit in MKR, since it sold DAI for more MKR than it bought it for.
                      // The caller receives tokens and doesn't send any.
                      // Note the caller didn't even have any tokens to begin with.
                      funds.sender = other.address;
                      funds.recipient = other.address;
                    });

                    // Sell DAI in the pool where it is valuable, buy it in the one where it has a regular price
                    const swaps = [
                      // Sell 1 DAI for 2 MKR
                      { pool: 1, in: 0, out: 1, amount: 2e18 },
                      // Buy 1 DAI with 1 MKR
                      { pool: 0, in: 1, out: 0, amount: 1e18 },
                    ];

                    assertSwapGivenOut({ swaps, fromOther: true, toOther: true }, { MKR: 1e18 });
                  });
                });
              };

              context('with a general pool', () => {
                itHandleMultiSwapsWithoutHopsProperly(PoolSpecialization.GeneralPool);
              });

              context('with a minimal swap info pool', () => {
                itHandleMultiSwapsWithoutHopsProperly(PoolSpecialization.MinimalSwapInfoPool);
              });
              context('with a two token pool', () => {
                itHandleMultiSwapsWithoutHopsProperly(PoolSpecialization.TwoTokenPool);
              });
            });

            context('with three tokens', () => {
              const anotherPoolSymbols = ['DAI', 'MKR', 'SNX'];

              const itHandleMultiSwapsWithoutHopsProperly = (anotherPoolSpecialization: PoolSpecialization) => {
                deployAnotherPool(anotherPoolSpecialization, anotherPoolSymbols);

                context('for a single pair', () => {
                  // In each pool, get 1e18 DAI by sending 0.5e18 MKR
                  const swaps = [
                    { pool: 0, in: 1, out: 0, amount: 1e18 },
                    { pool: 1, in: 1, out: 0, amount: 1e18 },
                  ];

                  assertSwapGivenOut({ swaps }, { DAI: 2e18, MKR: -1e18 });
                });

                context('for a multi pair', () => {
                  const swaps = [
                    // Get 1 DAI by sending 0.5 MKR
                    { pool: 0, in: 1, out: 0, amount: 1e18 },
                    // Get 1 SNX by sending 0.5 MKR
                    { pool: 1, in: 1, out: 2, amount: 1e18 },
                  ];

                  assertSwapGivenOut({ swaps }, { DAI: 1e18, SNX: 1e18, MKR: -1e18 });
                });
              };

              context('with a general pool', () => {
                itHandleMultiSwapsWithoutHopsProperly(PoolSpecialization.GeneralPool);
              });

              context('with a minimal swap info pool', () => {
                itHandleMultiSwapsWithoutHopsProperly(PoolSpecialization.MinimalSwapInfoPool);
              });
            });
          });
        });

        context('with hops', () => {
          context('with the same pool', () => {
            context('when token in and out match', () => {
              const swaps = [
                // Get 1 MKR by sending 0.5 DAI
                { in: 0, out: 1, amount: 1e18 },
                // Get the previously required amount of 0.5 DAI by sending 0.25 MKR
                { in: 1, out: 0, amount: 0 },
              ];

              assertSwapGivenOut({ swaps }, { MKR: 0.75e18 });
            });

            context('when token in and out mismatch', () => {
              const swaps = [
                { in: 1, out: 0, amount: 1e18 },
                { in: 1, out: 0, amount: 0 },
              ];

              assertSwapGivenOutReverts({ swaps }, 'MALCONSTRUCTED_MULTIHOP_SWAP');
            });
          });

          context('with another pool', () => {
            context('with two tokens', () => {
              const anotherPoolSymbols = ['DAI', 'MKR'];

              const itHandleMultiSwapsWithHopsProperly = (anotherPoolSpecialization: PoolSpecialization) => {
                deployAnotherPool(anotherPoolSpecialization, anotherPoolSymbols);

                const swaps = [
                  // Get 1 MKR by sending 0.5 DAI
                  { pool: 0, in: 0, out: 1, amount: 1e18 },
                  // Get the previously required amount of 0.5 DAI by sending 0.25 MKR
                  { pool: 1, in: 1, out: 0, amount: 0 },
                ];

                assertSwapGivenOut({ swaps }, { MKR: 0.75e18 });
              };

              context('with a general pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.GeneralPool);
              });

              context('with a minimal swap info pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.MinimalSwapInfoPool);
              });

              context('with a two token pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.TwoTokenPool);
              });
            });

            context('with three tokens', () => {
              const anotherPoolSymbols = ['DAI', 'MKR', 'SNX'];

              const itHandleMultiSwapsWithHopsProperly = (anotherPoolSpecialization: PoolSpecialization) => {
                deployAnotherPool(anotherPoolSpecialization, anotherPoolSymbols);

                const swaps = [
                  // Get 1 MKR by sending 0.5 DAI
                  { pool: 0, in: 0, out: 1, amount: 1e18 },
                  // Get the previously required amount of 0.5 DAI by sending 0.25 SNX
                  { pool: 1, in: 2, out: 0, amount: 0 },
                ];

                assertSwapGivenOut({ swaps }, { MKR: 1e18, SNX: -0.25e18 });
              };

              context('with a general pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.GeneralPool);
              });

              context('with a minimal swap info pool', () => {
                itHandleMultiSwapsWithHopsProperly(PoolSpecialization.MinimalSwapInfoPool);
              });
            });
          });
        });
      });
    });
  }
});
Example #29
Source File: InternalBalance.test.ts    From balancer-v2-monorepo with GNU General Public License v3.0 4 votes vote down vote up
describe('Internal Balance', () => {
  let admin: SignerWithAddress, sender: SignerWithAddress, recipient: SignerWithAddress;
  let relayer: SignerWithAddress, otherRecipient: SignerWithAddress;
  let authorizer: Contract, vault: Contract;
  let tokens: TokenList, weth: Token;

  before('setup signers', async () => {
    [, admin, sender, recipient, otherRecipient, relayer] = await ethers.getSigners();
  });

  sharedBeforeEach('deploy vault & tokens', async () => {
    tokens = await TokenList.create(['DAI', 'MKR'], { sorted: true });
    weth = await TokensDeployer.deployToken({ symbol: 'WETH' });

    authorizer = await deploy('TimelockAuthorizer', { args: [admin.address, ZERO_ADDRESS, MONTH] });
    vault = await deploy('Vault', { args: [authorizer.address, weth.address, MONTH, MONTH] });
  });

  describe('deposit internal balance', () => {
    const kind = OP_KIND.DEPOSIT_INTERNAL;
    const initialBalance = bn(10);

    const itHandlesDepositsProperly = (amount: BigNumber, relayed = false) => {
      it('transfers the tokens from the sender to the vault', async () => {
        await expectBalanceChange(
          () =>
            vault.manageUserBalance([
              { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
            ]),
          tokens,
          [
            { account: sender.address, changes: { DAI: -amount } },
            { account: vault.address, changes: { DAI: amount } },
          ]
        );
      });

      it('deposits the internal balance into the recipient account', async () => {
        const previousSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
        const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);

        await vault.manageUserBalance([
          { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
        ]);

        const currentSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
        expect(currentSenderBalance[0]).to.be.equal(previousSenderBalance[0]);

        const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);
        expect(currentRecipientBalance[0]).to.be.equal(previousRecipientBalance[0].add(amount));
      });

      it('returns ETH if any is sent', async () => {
        const senderAddress = relayed ? relayer.address : sender.address;
        const previousBalance = await ethers.provider.getBalance(senderAddress);

        const gasPrice = await ethers.provider.getGasPrice();
        const receipt: ContractReceipt = await (
          await vault.manageUserBalance(
            [{ kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address }],
            { value: 100, gasPrice }
          )
        ).wait();

        const ethSpent = receipt.gasUsed.mul(gasPrice);

        const currentBalance = await ethers.provider.getBalance(senderAddress);
        expect(previousBalance.sub(currentBalance)).to.equal(ethSpent);
      });

      it('emits an event', async () => {
        const receipt = await (
          await vault.manageUserBalance([
            { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
          ])
        ).wait();

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: recipient.address,
          token: tokens.DAI.address,
          delta: amount,
        });
      });
    };

    context('when the sender is the user', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(sender);
      });

      context('when the asset is a token', () => {
        context('when the sender does hold enough balance', () => {
          sharedBeforeEach('mint tokens', async () => {
            await tokens.DAI.mint(sender, initialBalance);
          });

          context('when the given amount is approved by the sender', () => {
            sharedBeforeEach('approve tokens', async () => {
              await tokens.DAI.approve(vault, initialBalance, { from: sender });
            });

            context('when tokens and balances match', () => {
              context('when depositing zero balance', () => {
                const depositAmount = bn(0);

                itHandlesDepositsProperly(depositAmount);
              });

              context('when depositing some balance', () => {
                const depositAmount = initialBalance;

                itHandlesDepositsProperly(depositAmount);
              });
            });
          });

          context('when the given amount is not approved by the sender', () => {
            it('reverts', async () => {
              await expect(
                vault.manageUserBalance([
                  {
                    kind,
                    asset: tokens.DAI.address,
                    amount: initialBalance,
                    sender: sender.address,
                    recipient: recipient.address,
                  },
                ])
              ).to.be.revertedWith('ERC20_TRANSFER_EXCEEDS_ALLOWANCE');
            });
          });
        });

        context('when the sender does not hold enough balance', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: initialBalance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('ERC20_TRANSFER_EXCEEDS_BALANCE');
          });
        });
      });

      context('when the asset is ETH', () => {
        const amount = bn(100e18);

        sharedBeforeEach('mint tokens', async () => {
          await weth.mint(sender.address, amount);
          await weth.approve(vault, amount, { from: sender });
        });

        it('does not take WETH from the sender', async () => {
          await expectBalanceChange(
            () =>
              vault.manageUserBalance(
                [{ kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address }],
                { value: amount }
              ),
            tokens,
            { account: sender }
          );
        });

        it('increases the WETH internal balance for the recipient', async () => {
          const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          await vault.manageUserBalance(
            [{ kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address }],
            { value: amount }
          );

          const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          expect(currentRecipientBalance[0].sub(previousRecipientBalance[0])).to.equal(amount);
        });

        it('emits an event with WETH as the token address', async () => {
          const receipt: ContractReceipt = await (
            await vault.manageUserBalance(
              [{ kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address }],
              { value: amount }
            )
          ).wait();

          expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
            user: recipient.address,
            token: weth.address,
            delta: amount,
          });
        });

        it('accepts deposits of both ETH and WETH', async () => {
          const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          await vault.manageUserBalance(
            [
              { kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address },
              { kind, asset: weth.address, amount, sender: sender.address, recipient: recipient.address },
            ],
            { value: amount }
          );

          const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          expect(currentRecipientBalance[0].sub(previousRecipientBalance[0])).to.equal(amount.mul(2));
        });

        it('accepts multiple ETH deposits', async () => {
          const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          await vault.manageUserBalance(
            [
              {
                kind,
                asset: ETH_TOKEN_ADDRESS,
                amount: amount.div(2),
                sender: sender.address,
                recipient: recipient.address,
              },
              {
                kind,
                asset: ETH_TOKEN_ADDRESS,
                amount: amount.div(2),
                sender: sender.address,
                recipient: recipient.address,
              },
            ],
            { value: amount }
          );

          const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [weth.address]);

          expect(currentRecipientBalance[0].sub(previousRecipientBalance[0])).to.equal(amount);
        });

        it('reverts if not enough ETH was supplied', async () => {
          // Send ETH to the Vault to make sure that the test fails because of the supplied ETH, even if the Vault holds
          // enough to mint the WETH using its own.
          await forceSendEth(vault, amount);

          await expect(
            vault.manageUserBalance(
              [
                {
                  kind,
                  asset: ETH_TOKEN_ADDRESS,
                  amount: amount.div(2),
                  sender: sender.address,
                  recipient: recipient.address,
                },
                {
                  kind,
                  asset: ETH_TOKEN_ADDRESS,
                  amount: amount.div(2),
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ],
              { value: amount.sub(1) }
            )
          ).to.be.revertedWith('INSUFFICIENT_ETH');
        });
      });
    });

    context('when the sender is a relayer', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(relayer);
      });

      sharedBeforeEach('mint tokens for sender', async () => {
        await tokens.DAI.mint(sender, initialBalance);
        await tokens.DAI.approve(vault, initialBalance, { from: sender });
      });

      context('when the relayer is whitelisted by the authorizer', () => {
        sharedBeforeEach('grant permission to relayer', async () => {
          const action = await actionId(vault, 'manageUserBalance');
          await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
        });

        context('when the relayer is allowed to deposit by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          itHandlesDepositsProperly(initialBalance, true);

          context('when the asset is ETH', () => {
            it('returns excess ETH to the relayer', async () => {
              const amount = bn(100e18);

              const relayerBalanceBefore = await ethers.provider.getBalance(relayer.address);

              const gasPrice = await ethers.provider.getGasPrice();
              const receipt: ContractReceipt = await (
                await vault.manageUserBalance(
                  [
                    {
                      kind,
                      asset: ETH_TOKEN_ADDRESS,
                      amount: amount.sub(42),
                      sender: sender.address,
                      recipient: recipient.address,
                    },
                  ],
                  { value: amount, gasPrice }
                )
              ).wait();
              const txETH = receipt.gasUsed.mul(gasPrice);

              const relayerBalanceAfter = await ethers.provider.getBalance(relayer.address);

              const ethSpent = txETH.add(amount).sub(42);
              expect(relayerBalanceBefore.sub(relayerBalanceAfter)).to.equal(ethSpent);
            });
          });
        });

        context('when the relayer is not allowed by the user', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: initialBalance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
          });
        });
      });

      context('when the relayer is not whitelisted by the authorizer', () => {
        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: initialBalance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });

        context('when the relayer is not allowed by the user', () => {
          sharedBeforeEach('disallow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, false);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: initialBalance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });
      });
    });
  });

  describe('withdraw internal balance', () => {
    const kind = OP_KIND.WITHDRAW_INTERNAL;

    const itHandlesWithdrawalsProperly = (depositedAmount: BigNumber, amount: BigNumber) => {
      context('when tokens and balances match', () => {
        it('transfers the tokens from the vault to recipient', async () => {
          await expectBalanceChange(
            () =>
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: amount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ]),
            tokens,
            { account: recipient, changes: { DAI: amount } }
          );
        });

        it('withdraws the internal balance from the sender account', async () => {
          const previousSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
          const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);

          await vault.manageUserBalance([
            { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
          ]);

          const currentSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
          expect(currentSenderBalance[0]).to.be.equal(previousSenderBalance[0].sub(amount));

          const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);
          expect(currentRecipientBalance[0]).to.be.equal(previousRecipientBalance[0]);
        });

        it('emits an event', async () => {
          const receipt = await (
            await vault.manageUserBalance([
              { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
            ])
          ).wait();

          expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
            user: sender.address,
            token: tokens.DAI.address,
            delta: amount.mul(-1),
          });
        });
      });
    };

    context('when the sender is a user', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(sender);
      });

      context('when the asset is a token', () => {
        context('when the sender has enough internal balance', () => {
          const depositedAmount = bn(10e18);

          sharedBeforeEach('deposit internal balance', async () => {
            await tokens.DAI.mint(sender, depositedAmount);
            await tokens.DAI.approve(vault, depositedAmount, { from: sender });
            await vault.manageUserBalance([
              {
                kind: OP_KIND.DEPOSIT_INTERNAL,
                asset: tokens.DAI.address,
                amount: depositedAmount,
                sender: sender.address,
                recipient: sender.address,
              },
            ]);
          });

          context('when requesting all the available balance', () => {
            const amount = depositedAmount;

            itHandlesWithdrawalsProperly(depositedAmount, amount);
          });

          context('when requesting part of the balance', () => {
            const amount = depositedAmount.div(2);

            itHandlesWithdrawalsProperly(depositedAmount, amount);
          });

          context('when requesting no balance', () => {
            const amount = bn(0);

            itHandlesWithdrawalsProperly(depositedAmount, amount);
          });

          context('with requesting more balance than available', () => {
            const amount = depositedAmount.add(1);

            it('reverts', async () => {
              await expect(
                vault.manageUserBalance([
                  {
                    kind,
                    asset: tokens.DAI.address,
                    amount: amount,
                    sender: sender.address,
                    recipient: recipient.address,
                  },
                ])
              ).to.be.revertedWith('INSUFFICIENT_INTERNAL_BALANCE');
            });
          });
        });

        context('when the sender does not have any internal balance', () => {
          const amount = 1;

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: amount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('INSUFFICIENT_INTERNAL_BALANCE');
          });
        });
      });

      context('when the asset is ETH', () => {
        const amount = bn(100e18);

        context('when the sender has enough internal balance', () => {
          sharedBeforeEach('deposit internal balance', async () => {
            await weth.mint(sender, amount, { from: sender });
            await weth.approve(vault, amount, { from: sender });
            await vault.manageUserBalance([
              {
                kind: OP_KIND.DEPOSIT_INTERNAL,
                asset: weth.address,
                amount: amount,
                sender: sender.address,
                recipient: sender.address,
              },
            ]);
          });

          it('does not send WETH to the recipient', async () => {
            await expectBalanceChange(
              () =>
                vault.manageUserBalance([
                  { kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address },
                ]),
              tokens,
              { account: recipient }
            );
          });

          it('decreases the WETH internal balance for the sender', async () => {
            const previousSenderBalance = await vault.getInternalBalance(sender.address, [weth.address]);

            await vault.manageUserBalance([
              { kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address },
            ]);

            const currentSenderBalance = await vault.getInternalBalance(sender.address, [weth.address]);

            expect(previousSenderBalance[0].sub(currentSenderBalance[0])).to.equal(amount);
          });

          it('emits an event with WETH as the token address', async () => {
            const receipt: ContractReceipt = await (
              await vault.manageUserBalance([
                { kind, asset: ETH_TOKEN_ADDRESS, amount, sender: sender.address, recipient: recipient.address },
              ])
            ).wait();

            expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
              user: sender.address,
              token: weth.address,
              delta: amount.mul(-1),
            });
          });

          it('accepts withdrawals of both ETH and WETH', async () => {
            const previousSenderBalance = await vault.getInternalBalance(sender.address, [weth.address]);

            await vault.manageUserBalance([
              {
                kind,
                asset: ETH_TOKEN_ADDRESS,
                amount: amount.div(2),
                sender: sender.address,
                recipient: recipient.address,
              },
              {
                kind,
                asset: weth.address,
                amount: amount.div(2),
                sender: sender.address,
                recipient: recipient.address,
              },
            ]);

            const currentSenderBalance = await vault.getInternalBalance(sender.address, [weth.address]);

            expect(previousSenderBalance[0].sub(currentSenderBalance[0])).to.equal(amount);
          });
        });
      });
    });

    context('when the sender is a relayer', () => {
      const depositedAmount = bn(10e18);

      beforeEach('set sender', () => {
        vault = vault.connect(relayer);
      });

      sharedBeforeEach('mint tokens and deposit to internal balance', async () => {
        await tokens.DAI.mint(sender, depositedAmount);
        await tokens.DAI.approve(vault, depositedAmount, { from: sender });
        await vault.connect(sender).manageUserBalance([
          {
            kind: OP_KIND.DEPOSIT_INTERNAL,
            asset: tokens.DAI.address,
            amount: depositedAmount,
            sender: sender.address,
            recipient: sender.address,
          },
        ]);
      });

      context('when the relayer is whitelisted by the authorizer', () => {
        sharedBeforeEach('grant permission to relayer', async () => {
          const action = await actionId(vault, 'manageUserBalance');
          await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
        });

        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          itHandlesWithdrawalsProperly(depositedAmount, depositedAmount);
        });

        context('when the relayer is not allowed by the user', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: depositedAmount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
          });
        });
      });

      context('when the relayer is not whitelisted by the authorizer', () => {
        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: depositedAmount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });

        context('when the relayer is not allowed by the user', () => {
          sharedBeforeEach('disallow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, false);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: depositedAmount,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });
      });
    });
  });

  describe('transfer internal balance', () => {
    const kind = OP_KIND.TRANSFER_INTERNAL;

    function itHandlesTransfersProperly(
      initialBalances: Dictionary<BigNumber>,
      transferredAmounts: Dictionary<BigNumber>
    ) {
      const amounts = Object.values(transferredAmounts);

      it('transfers the tokens from the sender to a single recipient', async () => {
        const previousSenderBalances = await vault.getInternalBalance(sender.address, tokens.addresses);
        const previousRecipientBalances = await vault.getInternalBalance(recipient.address, tokens.addresses);

        await vault.manageUserBalance(
          tokens.map((token, i) => ({
            kind,
            asset: token.address,
            amount: amounts[i],
            sender: sender.address,
            recipient: recipient.address,
          }))
        );

        const senderBalances = await vault.getInternalBalance(sender.address, tokens.addresses);
        const recipientBalances = await vault.getInternalBalance(recipient.address, tokens.addresses);

        for (let i = 0; i < tokens.addresses.length; i++) {
          expect(senderBalances[i]).to.equal(previousSenderBalances[i].sub(amounts[i]));
          expect(recipientBalances[i]).to.equal(previousRecipientBalances[i].add(amounts[i]));
        }
      });

      it('transfers the tokens from the sender to multiple recipients', async () => {
        const previousSenderBalances = await vault.getInternalBalance(sender.address, tokens.addresses);
        const previousRecipientBalances = await vault.getInternalBalance(recipient.address, tokens.addresses);
        const previousOtherRecipientBalances = await vault.getInternalBalance(otherRecipient.address, tokens.addresses);

        await vault.manageUserBalance([
          {
            kind,
            asset: tokens.first.address,
            amount: amounts[0],
            sender: sender.address,
            recipient: recipient.address,
          },
          {
            kind,
            asset: tokens.second.address,
            amount: amounts[1],
            sender: sender.address,
            recipient: otherRecipient.address,
          },
        ]);

        const senderBalances = await vault.getInternalBalance(sender.address, tokens.addresses);
        const recipientBalances = await vault.getInternalBalance(recipient.address, tokens.addresses);
        const otherRecipientBalances = await vault.getInternalBalance(otherRecipient.address, tokens.addresses);

        for (let i = 0; i < tokens.addresses.length; i++) {
          expect(senderBalances[i]).to.equal(previousSenderBalances[i].sub(amounts[i]));
        }

        expect(recipientBalances[0]).to.equal(previousRecipientBalances[0].add(amounts[0]));
        expect(recipientBalances[1]).to.equal(previousRecipientBalances[1]);

        expect(otherRecipientBalances[0]).to.equal(previousOtherRecipientBalances[0]);
        expect(otherRecipientBalances[1]).to.equal(previousOtherRecipientBalances[1].add(amounts[1]));
      });

      it('does not affect the token balances of the sender nor the recipient', async () => {
        const previousBalances: Dictionary<Dictionary<BigNumber>> = {};

        await tokens.asyncEach(async (token: Token) => {
          const senderBalance = await token.balanceOf(sender.address);
          const recipientBalance = await token.balanceOf(recipient.address);
          previousBalances[token.symbol] = { sender: senderBalance, recipient: recipientBalance };
        });

        await vault.manageUserBalance(
          tokens.map((token, i) => ({
            kind,
            asset: token.address,
            amount: amounts[i],
            sender: sender.address,
            recipient: recipient.address,
          }))
        );

        await tokens.asyncEach(async (token: Token) => {
          const senderBalance = await token.balanceOf(sender.address);
          expect(senderBalance).to.equal(previousBalances[token.symbol].sender);

          const recipientBalance = await token.balanceOf(recipient.address);
          expect(recipientBalance).to.equal(previousBalances[token.symbol].recipient);
        });
      });

      it('emits an event for each transfer', async () => {
        const receipt = await (
          await vault.manageUserBalance(
            tokens.map((token, i) => ({
              kind,
              asset: token.address,
              amount: amounts[i],
              sender: sender.address,
              recipient: recipient.address,
            }))
          )
        ).wait();

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: sender.address,
          token: tokens.DAI.address,
          delta: transferredAmounts.DAI.mul(-1),
        });

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: sender.address,
          token: tokens.MKR.address,
          delta: transferredAmounts.MKR.mul(-1),
        });

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: recipient.address,
          token: tokens.DAI.address,
          delta: transferredAmounts.DAI,
        });

        expectEvent.inReceipt(receipt, 'InternalBalanceChanged', {
          user: recipient.address,
          token: tokens.MKR.address,
          delta: transferredAmounts.MKR,
        });
      });
    }

    function depositInitialBalances(initialBalances: Dictionary<BigNumber>) {
      sharedBeforeEach('deposit initial balances', async () => {
        const balances = await tokens.asyncMap(async (token: Token) => {
          const amount = initialBalances[token.symbol];
          await token.mint(sender, amount);
          await token.approve(vault, amount, { from: sender });
          return amount;
        });

        await vault.connect(sender).manageUserBalance(
          tokens.map((token, i) => ({
            kind: OP_KIND.DEPOSIT_INTERNAL,
            asset: token.address,
            amount: balances[i],
            sender: sender.address,
            recipient: sender.address,
          }))
        );
      });
    }

    context('when the sender is a user', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(sender);
      });

      function itReverts(transferredAmounts: Dictionary<BigNumber>, errorReason = 'INSUFFICIENT_INTERNAL_BALANCE') {
        it('reverts', async () => {
          const amounts = Object.values(transferredAmounts);
          await expect(
            vault.manageUserBalance(
              tokens.map((token, i) => ({
                kind,
                asset: token.address,
                amount: amounts[i],
                sender: sender.address,
                recipient: recipient.address,
              }))
            )
          ).to.be.revertedWith(errorReason);
        });
      }

      context('when the sender specifies some balance', () => {
        const transferredAmounts = { DAI: bn(1e16), MKR: bn(2e16) };

        context('when the sender holds enough balance', () => {
          const initialBalances = { DAI: bn(1e18), MKR: bn(5e19) };

          depositInitialBalances(initialBalances);
          itHandlesTransfersProperly(initialBalances, transferredAmounts);
        });

        context('when the sender does not hold said balance', () => {
          context('when the sender does not hold enough balance of one token', () => {
            depositInitialBalances({ DAI: bn(10), MKR: bn(5e19) });

            itReverts(transferredAmounts);
          });

          context('when the sender does not hold enough balance of the other token', () => {
            depositInitialBalances({ DAI: bn(1e18), MKR: bn(5) });

            itReverts(transferredAmounts);
          });

          context('when the sender does not hold enough balance of both tokens', () => {
            depositInitialBalances({ DAI: bn(10), MKR: bn(5) });

            itReverts(transferredAmounts);
          });
        });
      });

      context('when the sender does not specify any balance', () => {
        const transferredAmounts = { DAI: bn(0), MKR: bn(0) };

        context('when the sender holds some balance', () => {
          const initialBalances: Dictionary<BigNumber> = { DAI: bn(1e18), MKR: bn(5e19) };

          depositInitialBalances(initialBalances);
          itHandlesTransfersProperly(initialBalances, transferredAmounts);
        });

        context('when the sender does not have any balance', () => {
          const initialBalances = { DAI: bn(0), MKR: bn(0) };

          itHandlesTransfersProperly(initialBalances, transferredAmounts);
        });
      });
    });

    context('when the sender is a relayer', () => {
      const transferredAmounts = { DAI: bn(1e16), MKR: bn(2e16) };
      const amounts = Object.values(transferredAmounts);

      beforeEach('set sender', () => {
        vault = vault.connect(relayer);
      });

      depositInitialBalances(transferredAmounts);

      context('when the relayer is whitelisted by the authorizer', () => {
        sharedBeforeEach('grant permission to relayer', async () => {
          const action = await actionId(vault, 'manageUserBalance');
          await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
        });

        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          itHandlesTransfersProperly(transferredAmounts, transferredAmounts);
        });

        context('when the relayer is not allowed by the user', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance(
                tokens.map((token, i) => ({
                  kind,
                  asset: token.address,
                  amount: amounts[i],
                  sender: sender.address,
                  recipient: recipient.address,
                }))
              )
            ).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
          });
        });
      });

      context('when the relayer is not whitelisted by the authorizer', () => {
        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance(
                tokens.map((token, i) => ({
                  kind,
                  asset: token.address,
                  amount: amounts[i],
                  sender: sender.address,
                  recipient: recipient.address,
                }))
              )
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });

        context('when the relayer is not allowed by the user', () => {
          sharedBeforeEach('disallow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, false);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance(
                tokens.map((token, i) => ({
                  kind,
                  asset: token.address,
                  amount: amounts[i],
                  sender: sender.address,
                  recipient: recipient.address,
                }))
              )
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });
      });
    });
  });

  describe('transfer external balance', () => {
    const balance = bn(10);
    const kind = OP_KIND.TRANSFER_EXTERNAL;

    const itHandlesExternalTransfersProperly = (amount: BigNumber) => {
      it('transfers the tokens from the sender to the recipient, using the vault allowance of the sender', async () => {
        await expectBalanceChange(
          () =>
            vault.manageUserBalance([
              { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
            ]),
          tokens,
          [
            { account: sender.address, changes: { DAI: -amount } },
            { account: vault.address, changes: { DAI: 0 } },
            { account: recipient.address, changes: { DAI: amount } },
          ]
        );
      });

      it('does not change the internal balances of the accounts', async () => {
        const previousSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
        const previousRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);

        await vault.manageUserBalance([
          { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
        ]);

        const currentSenderBalance = await vault.getInternalBalance(sender.address, [tokens.DAI.address]);
        expect(currentSenderBalance[0]).to.be.equal(previousSenderBalance[0]);

        const currentRecipientBalance = await vault.getInternalBalance(recipient.address, [tokens.DAI.address]);
        expect(currentRecipientBalance[0]).to.be.equal(previousRecipientBalance[0]);
      });

      it(`${amount.gt(0) ? 'emits' : 'does not emit'} an event`, async () => {
        const receipt = await (
          await vault.manageUserBalance([
            { kind, asset: tokens.DAI.address, amount: amount, sender: sender.address, recipient: recipient.address },
          ])
        ).wait();

        expectEvent.notEmitted(receipt, 'InternalBalanceChanged');

        if (amount.gt(0)) {
          expectEvent.inReceipt(receipt, 'ExternalBalanceTransfer', {
            sender: sender.address,
            recipient: recipient.address,
            token: tokens.DAI.address,
            amount,
          });
        } else {
          expectEvent.notEmitted(receipt, 'ExternalBalanceTransfer');
        }
      });
    };

    context('when the sender is the user', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(sender);
      });

      context('when the token is not the zero address', () => {
        context('when the sender does hold enough balance', () => {
          sharedBeforeEach('mint tokens', async () => {
            await tokens.DAI.mint(sender, balance);
          });

          context('when the given amount is approved by the sender', () => {
            sharedBeforeEach('approve tokens', async () => {
              await tokens.DAI.approve(vault, balance, { from: sender });
            });

            context('when tokens and balances match', () => {
              context('when depositing zero balance', () => {
                const transferAmount = bn(0);

                itHandlesExternalTransfersProperly(transferAmount);
              });

              context('when depositing some balance', () => {
                const transferAmount = balance;

                itHandlesExternalTransfersProperly(transferAmount);
              });
            });
          });

          context('when the given amount is not approved by the sender', () => {
            it('reverts', async () => {
              await expect(
                vault.manageUserBalance([
                  {
                    kind,
                    asset: tokens.DAI.address,
                    amount: balance,
                    sender: sender.address,
                    recipient: recipient.address,
                  },
                ])
              ).to.be.revertedWith('ERC20_TRANSFER_EXCEEDS_ALLOWANCE');
            });
          });
        });

        context('when the sender does not hold enough balance', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: balance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('ERC20_TRANSFER_EXCEEDS_BALANCE');
          });
        });
      });
    });

    context('when the sender is a relayer', () => {
      beforeEach('set sender', () => {
        vault = vault.connect(relayer);
      });

      sharedBeforeEach('mint tokens for sender', async () => {
        await tokens.DAI.mint(sender, balance);
        await tokens.DAI.approve(vault, balance, { from: sender });
      });

      context('when the relayer is whitelisted by the authorizer', () => {
        sharedBeforeEach('grant permission to relayer', async () => {
          const action = await actionId(vault, 'manageUserBalance');
          await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
        });

        context('when the relayer is allowed to transfer by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          itHandlesExternalTransfersProperly(balance);
        });

        context('when the relayer is not allowed by the user', () => {
          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: balance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
          });
        });
      });

      context('when the relayer is not whitelisted by the authorizer', () => {
        context('when the relayer is allowed by the user', () => {
          sharedBeforeEach('allow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: balance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });

        context('when the relayer is not allowed by the user', () => {
          sharedBeforeEach('disallow relayer', async () => {
            await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, false);
          });

          it('reverts', async () => {
            await expect(
              vault.manageUserBalance([
                {
                  kind,
                  asset: tokens.DAI.address,
                  amount: balance,
                  sender: sender.address,
                  recipient: recipient.address,
                },
              ])
            ).to.be.revertedWith('SENDER_NOT_ALLOWED');
          });
        });
      });
    });
  });

  describe('batch', () => {
    type UserBalanceOp = {
      kind: number;
      amount: number;
      asset: string;
      sender: string;
      recipient: string;
    };

    const op = (
      kind: number,
      token: Token,
      amount: number,
      from: SignerWithAddress,
      to?: SignerWithAddress
    ): UserBalanceOp => {
      return { kind, asset: token.address, amount, sender: from.address, recipient: (to || from).address };
    };

    sharedBeforeEach('mint and approve tokens', async () => {
      await tokens.mint({ to: sender, amount: bn(1000e18) });
      await tokens.approve({ from: sender, to: vault });
    });

    sharedBeforeEach('allow relayer', async () => {
      const action = await actionId(vault, 'manageUserBalance');
      await authorizer.connect(admin).grantPermissions([action], relayer.address, [ANY_ADDRESS]);
      await vault.connect(sender).setRelayerApproval(sender.address, relayer.address, true);
      await vault.connect(recipient).setRelayerApproval(recipient.address, relayer.address, true);
    });

    context('when unpaused', () => {
      context('when all the senders allowed the relayer', () => {
        context('when all ops add up', () => {
          it('succeeds', async () => {
            const ops = [
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
              op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient),
              op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 8, recipient, otherRecipient),
              op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 3, recipient, sender),
              op(OP_KIND.TRANSFER_EXTERNAL, tokens.MKR, 200, sender, otherRecipient),
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 100, sender, sender),
            ];

            await vault.connect(relayer).manageUserBalance(ops);

            expect((await vault.getInternalBalance(sender.address, [tokens.DAI.address]))[0]).to.equal(0);
            expect((await vault.getInternalBalance(sender.address, [tokens.MKR.address]))[0]).to.equal(103);

            expect((await vault.getInternalBalance(recipient.address, [tokens.DAI.address]))[0]).to.equal(5);
            expect((await vault.getInternalBalance(recipient.address, [tokens.MKR.address]))[0]).to.equal(9);

            expect((await vault.getInternalBalance(otherRecipient.address, [tokens.DAI.address]))[0]).to.equal(0);
            expect((await vault.getInternalBalance(otherRecipient.address, [tokens.MKR.address]))[0]).to.equal(8);

            expect(await tokens.MKR.balanceOf(otherRecipient)).to.be.equal(200);
          });
        });

        context('when all ops do not add up', () => {
          it('reverts', async () => {
            const ops = [
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
              op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient),
              op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 8, recipient, otherRecipient),
              op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 3, recipient, sender),
              op(OP_KIND.TRANSFER_EXTERNAL, tokens.MKR, 200, sender, otherRecipient),
              op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 100, sender, sender),
              op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 10, recipient),
            ];

            await expect(vault.connect(relayer).manageUserBalance(ops)).to.be.revertedWith(
              'INSUFFICIENT_INTERNAL_BALANCE'
            );
          });
        });
      });

      context('when one of the senders did not allow the relayer', () => {
        it('reverts', async () => {
          const ops = [
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient),
            op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 8, recipient, otherRecipient),
            op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 3, recipient, sender),
            op(OP_KIND.TRANSFER_EXTERNAL, tokens.MKR, 200, sender, otherRecipient),
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 100, sender, sender),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 1, otherRecipient),
          ];

          await expect(vault.connect(relayer).manageUserBalance(ops)).to.be.revertedWith('USER_DOESNT_ALLOW_RELAYER');
        });
      });
    });

    context('when paused', () => {
      sharedBeforeEach('deposit some internal balances', async () => {
        const ops = [
          op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 1, sender, sender),
          op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
          op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
          op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 50, sender, otherRecipient),
        ];

        await vault.connect(relayer).manageUserBalance(ops);

        await vault.connect(otherRecipient).setRelayerApproval(otherRecipient.address, relayer.address, true);
      });

      sharedBeforeEach('pause', async () => {
        const action = await actionId(vault, 'setPaused');
        await authorizer.connect(admin).grantPermissions([action], admin.address, [ANY_ADDRESS]);
        await vault.connect(admin).setPaused(true);
      });

      context('when only withdrawing internal balance', () => {
        it('succeeds', async () => {
          const ops = [
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 10, otherRecipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 1, sender),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 8, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 3, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 35, otherRecipient),
          ];

          await vault.connect(relayer).manageUserBalance(ops);

          expect((await vault.getInternalBalance(sender.address, [tokens.DAI.address]))[0]).to.equal(0);
          expect((await vault.getInternalBalance(sender.address, [tokens.MKR.address]))[0]).to.equal(0);

          expect((await vault.getInternalBalance(recipient.address, [tokens.DAI.address]))[0]).to.equal(5);
          expect((await vault.getInternalBalance(recipient.address, [tokens.MKR.address]))[0]).to.equal(9);

          expect((await vault.getInternalBalance(otherRecipient.address, [tokens.DAI.address]))[0]).to.equal(5);
          expect((await vault.getInternalBalance(otherRecipient.address, [tokens.MKR.address]))[0]).to.equal(0);
        });
      });

      context('when trying to perform multiple ops', () => {
        it('reverts', async () => {
          const ops = [
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.DAI, 10, sender, recipient),
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 20, sender, recipient),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.DAI, 5, recipient, recipient),
            op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 8, recipient, otherRecipient),
            op(OP_KIND.TRANSFER_INTERNAL, tokens.MKR, 3, recipient, sender),
            op(OP_KIND.TRANSFER_EXTERNAL, tokens.MKR, 200, sender, otherRecipient),
            op(OP_KIND.DEPOSIT_INTERNAL, tokens.MKR, 100, sender, sender),
            op(OP_KIND.WITHDRAW_INTERNAL, tokens.MKR, 1, otherRecipient, recipient),
          ];

          await expect(vault.connect(relayer).manageUserBalance(ops)).to.be.revertedWith('PAUSED');
        });
      });
    });
  });
});