"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils/utils");
const SpokePool_Fixture_1 = require("./fixtures/SpokePool.Fixture");
const constants_1 = require("./constants");
const { AddressZero: ZERO_ADDRESS } = utils_1.ethers.constants;
describe("SpokePool Depositor Logic", async function () {
    let spokePool, weth, erc20, unwhitelistedErc20;
    let depositor, recipient;
    let quoteTimestamp;
    let amount = constants_1.amountToDeposit;
    const relayerFeePct = (0, utils_1.toBN)(constants_1.depositRelayerFeePct).add(constants_1.realizedLpFeePct);
    beforeEach(async function () {
        [depositor, recipient] = await utils_1.ethers.getSigners();
        ({ weth, erc20, spokePool, unwhitelistedErc20 } = await (0, SpokePool_Fixture_1.spokePoolFixture)());
        // mint some fresh tokens and deposit ETH for weth for the depositor.
        await (0, utils_1.seedWallet)(depositor, [erc20], weth, constants_1.amountToSeedWallets);
        // Approve spokepool to spend tokens
        await erc20.connect(depositor).approve(spokePool.address, constants_1.amountToDeposit);
        await weth.connect(depositor).approve(spokePool.address, constants_1.amountToDeposit);
        // Whitelist origin token => destination chain ID routes:
        await (0, SpokePool_Fixture_1.enableRoutes)(spokePool, [{ originToken: erc20.address }, { originToken: weth.address }]);
        quoteTimestamp = (await spokePool.getCurrentTime()).toNumber();
    });
    it("Depositing ERC20 tokens correctly pulls tokens and changes contract state", async function () {
        const revertReason = "DepositsArePaused";
        // Can't deposit when paused:
        await spokePool.connect(depositor).pauseDeposits(true);
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount: constants_1.amountToDeposit,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }))).to.be.revertedWith(revertReason);
        await spokePool.connect(depositor).pauseDeposits(false);
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            recipient: recipient.address,
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        })))
            .to.emit(spokePool, "V3FundsDeposited")
            .withArgs(erc20.address, ZERO_ADDRESS, constants_1.amountToDeposit, constants_1.amountReceived, constants_1.destinationChainId, 0, quoteTimestamp, constants_1.MAX_UINT32, 0, depositor.address, recipient.address, ZERO_ADDRESS, "0x");
        // The collateral should have transferred from depositor to contract.
        (0, utils_1.expect)(await erc20.balanceOf(depositor.address)).to.equal(constants_1.amountToSeedWallets.sub(constants_1.amountToDeposit));
        (0, utils_1.expect)(await erc20.balanceOf(spokePool.address)).to.equal(constants_1.amountToDeposit);
        // Deposit nonce should increment.
        (0, utils_1.expect)(await spokePool.numberOfDeposits()).to.equal(1);
    });
    it("DepositFor overrrides the depositor", async function () {
        const newDepositor = (0, utils_1.randomAddress)();
        await (0, utils_1.expect)(spokePool.connect(depositor).depositFor(newDepositor, ...(0, SpokePool_Fixture_1.getDepositParams)({
            recipient: recipient.address,
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        })))
            .to.emit(spokePool, "V3FundsDeposited")
            .withArgs(erc20.address, ZERO_ADDRESS, constants_1.amountToDeposit, constants_1.amountReceived, constants_1.destinationChainId, 0, quoteTimestamp, utils_1.BigNumber.from("0xFFFFFFFF"), 0, newDepositor, // Depositor is overridden.
        recipient.address, ZERO_ADDRESS, "0x");
    });
    it("Depositing ETH correctly wraps into WETH", async function () {
        const revertReason = "MsgValueDoesNotMatchInputAmount";
        // Fails if msg.value > 0 but doesn't match amount to deposit.
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: weth.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }), { value: 1 })).to.be.revertedWith(revertReason);
        await (0, utils_1.expect)(() => spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            recipient: recipient.address,
            originToken: weth.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }), { value: constants_1.amountToDeposit })).to.changeEtherBalances([depositor, weth], [constants_1.amountToDeposit.mul((0, utils_1.toBN)("-1")), constants_1.amountToDeposit]); // ETH should transfer from depositor to WETH contract.
        // WETH balance for user should be same as start, but WETH balancein pool should increase.
        (0, utils_1.expect)(await weth.balanceOf(depositor.address)).to.equal(constants_1.amountToSeedWallets);
        (0, utils_1.expect)(await weth.balanceOf(spokePool.address)).to.equal(constants_1.amountToDeposit);
    });
    it("Depositing ETH with msg.value = 0 pulls WETH from depositor", async function () {
        await (0, utils_1.expect)(() => spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: weth.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }), { value: 0 })).to.changeTokenBalances(weth, [depositor, spokePool], [constants_1.amountToDeposit.mul((0, utils_1.toBN)("-1")), constants_1.amountToDeposit]);
    });
    it("SpokePool is not approved to spend originToken", async function () {
        const insufficientAllowance = "ERC20: insufficient allowance";
        await erc20.connect(depositor).approve(spokePool.address, 0);
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }))).to.be.revertedWith(insufficientAllowance);
        await erc20.connect(depositor).approve(spokePool.address, constants_1.amountToDeposit);
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }))).to.emit(spokePool, "V3FundsDeposited");
    });
    it("Deposit route is disabled", async function () {
        const revertReason = "DisabledRoute";
        // Verify that routes are disabled by default.
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: unwhitelistedErc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }))).to.be.revertedWith(revertReason);
        // Verify that the route is enabled.
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }))).to.emit(spokePool, "V3FundsDeposited");
        // Disable the route.
        await spokePool.connect(depositor).setEnableRoute(erc20.address, constants_1.destinationChainId, false);
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }))).to.be.revertedWith(revertReason);
        // Re-enable the route and verify that it works again.
        await spokePool.connect(depositor).setEnableRoute(erc20.address, constants_1.destinationChainId, true);
        await erc20.connect(depositor).approve(spokePool.address, constants_1.amountToDeposit);
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp,
        }))).to.emit(spokePool, "V3FundsDeposited");
    });
    it("Relayer fee is invalid", async function () {
        const revertReason = "InvalidRelayerFee";
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct: (0, utils_1.toWei)("1"),
            quoteTimestamp,
        }))).to.be.revertedWith(revertReason);
    });
    it("quoteTimestamp is out of range", async function () {
        const revertReason = "InvalidQuoteTimestamp";
        const quoteTimeBuffer = await spokePool.depositQuoteTimeBuffer();
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp: quoteTimestamp + 1,
        }))).to.be.revertedWith("underflowed");
        await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
            originToken: erc20.address,
            amount,
            destinationChainId: constants_1.destinationChainId,
            relayerFeePct,
            quoteTimestamp: quoteTimestamp - (quoteTimeBuffer + 1),
        }))).to.be.revertedWith(revertReason);
        // quoteTimestamp at the exact margins should succeed.
        for (const offset of [0, quoteTimeBuffer]) {
            await erc20.connect(depositor).approve(spokePool.address, constants_1.amountToDeposit);
            await (0, utils_1.expect)(spokePool.connect(depositor).deposit(...(0, SpokePool_Fixture_1.getDepositParams)({
                originToken: erc20.address,
                amount,
                destinationChainId: constants_1.destinationChainId,
                relayerFeePct,
                quoteTimestamp: quoteTimestamp - offset,
            }))).to.emit(spokePool, "V3FundsDeposited");
        }
    });
    describe("deposit V3", function () {
        let relayData, depositArgs;
        function getDepositArgsFromRelayData(_relayData, _destinationChainId = constants_1.destinationChainId, _quoteTimestamp = quoteTimestamp) {
            return [
                _relayData.depositor,
                _relayData.recipient,
                _relayData.inputToken,
                _relayData.outputToken,
                _relayData.inputAmount,
                _relayData.outputAmount,
                _destinationChainId,
                _relayData.exclusiveRelayer,
                _quoteTimestamp,
                _relayData.fillDeadline,
                _relayData.exclusivityDeadline,
                _relayData.message,
            ];
        }
        beforeEach(async function () {
            relayData = {
                depositor: depositor.address,
                recipient: recipient.address,
                exclusiveRelayer: ZERO_ADDRESS,
                inputToken: erc20.address,
                outputToken: (0, utils_1.randomAddress)(),
                inputAmount: constants_1.amountToDeposit,
                outputAmount: constants_1.amountToDeposit.sub(19),
                originChainId: constants_1.originChainId,
                depositId: 0,
                fillDeadline: quoteTimestamp + 1000,
                exclusivityDeadline: 0,
                message: "0x",
            };
            depositArgs = getDepositArgsFromRelayData(relayData);
        });
        it("placeholder: gas test", async function () {
            await spokePool.connect(depositor).depositV3(...depositArgs);
        });
        it("route disabled", async function () {
            // Verify that routes are disabled by default for a new route
            const _depositArgs = getDepositArgsFromRelayData(relayData, 999);
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(..._depositArgs)).to.be.revertedWith("DisabledRoute");
            // Enable the route:
            await spokePool.connect(depositor).setEnableRoute(erc20.address, 999, true);
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(..._depositArgs)).to.not.be.reverted;
        });
        it("invalid quoteTimestamp", async function () {
            const quoteTimeBuffer = await spokePool.depositQuoteTimeBuffer();
            const currentTime = await spokePool.getCurrentTime();
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(
            // quoteTimestamp too far into past (i.e. beyond the buffer)
            ...getDepositArgsFromRelayData(relayData, constants_1.destinationChainId, currentTime.sub(quoteTimeBuffer).sub(1)))).to.be.revertedWith("InvalidQuoteTimestamp");
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(
            // quoteTimestamp right at the buffer is OK
            ...getDepositArgsFromRelayData(relayData, constants_1.destinationChainId, currentTime.sub(quoteTimeBuffer)))).to.not.be.reverted;
        });
        it("invalid fillDeadline", async function () {
            const fillDeadlineBuffer = await spokePool.fillDeadlineBuffer();
            const currentTime = await spokePool.getCurrentTime();
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(
            // fillDeadline too far into future (i.e. beyond the buffer)
            ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.add(fillDeadlineBuffer).add(1) }))).to.be.revertedWith("InvalidFillDeadline");
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(
            // fillDeadline in past
            ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.sub(1) }))).to.be.revertedWith("InvalidFillDeadline");
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(
            // fillDeadline right at the buffer is OK
            ...getDepositArgsFromRelayData({ ...relayData, fillDeadline: currentTime.add(fillDeadlineBuffer) }))).to.not.be.reverted;
        });
        it("if input token is WETH and msg.value > 0, msg.value must match inputAmount", async function () {
            await (0, utils_1.expect)(spokePool
                .connect(depositor)
                .depositV3(...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: 1 })).to.be.revertedWith("MsgValueDoesNotMatchInputAmount");
            // Pulls ETH from depositor and deposits it into WETH via the wrapped contract.
            await (0, utils_1.expect)(() => spokePool
                .connect(depositor)
                .depositV3(...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), {
                value: constants_1.amountToDeposit,
            })).to.changeEtherBalances([depositor, weth], [constants_1.amountToDeposit.mul((0, utils_1.toBN)("-1")), constants_1.amountToDeposit]); // ETH should transfer from depositor to WETH contract.
            // WETH balance for user should be same as start, but WETH balance in pool should increase.
            (0, utils_1.expect)(await weth.balanceOf(spokePool.address)).to.equal(constants_1.amountToDeposit);
        });
        it("if input token is not WETH then msg.value must be 0", async function () {
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(...getDepositArgsFromRelayData(relayData), { value: 1 })).to.be.revertedWith("MsgValueDoesNotMatchInputAmount");
        });
        it("if input token is WETH and msg.value = 0, pulls ERC20 from depositor", async function () {
            await (0, utils_1.expect)(() => spokePool
                .connect(depositor)
                .depositV3(...getDepositArgsFromRelayData({ ...relayData, inputToken: weth.address }), { value: 0 })).to.changeTokenBalances(weth, [depositor, spokePool], [constants_1.amountToDeposit.mul((0, utils_1.toBN)("-1")), constants_1.amountToDeposit]);
        });
        it("pulls input token from caller", async function () {
            await (0, utils_1.expect)(() => spokePool.connect(depositor).depositV3(...depositArgs)).to.changeTokenBalances(erc20, [depositor, spokePool], [constants_1.amountToDeposit.mul((0, utils_1.toBN)("-1")), constants_1.amountToDeposit]);
        });
        it("depositV3Now uses current time as quote time", async function () {
            const currentTime = (await spokePool.getCurrentTime()).toNumber();
            const fillDeadlineOffset = 1000;
            const exclusivityDeadline = 0;
            await (0, utils_1.expect)(spokePool
                .connect(depositor)
                .depositV3Now(relayData.depositor, relayData.recipient, relayData.inputToken, relayData.outputToken, relayData.inputAmount, relayData.outputAmount, constants_1.destinationChainId, relayData.exclusiveRelayer, fillDeadlineOffset, exclusivityDeadline, relayData.message))
                .to.emit(spokePool, "V3FundsDeposited")
                .withArgs(relayData.inputToken, relayData.outputToken, relayData.inputAmount, relayData.outputAmount, constants_1.destinationChainId, 
            // deposit ID is 0 for first deposit
            0, currentTime, // quoteTimestamp should be current time
            currentTime + fillDeadlineOffset, // fillDeadline should be current time + offset
            currentTime, relayData.depositor, relayData.recipient, relayData.exclusiveRelayer, relayData.message);
        });
        it("emits V3FundsDeposited event with correct deposit ID", async function () {
            const currentTime = (await spokePool.getCurrentTime()).toNumber();
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(...depositArgs))
                .to.emit(spokePool, "V3FundsDeposited")
                .withArgs(relayData.inputToken, relayData.outputToken, relayData.inputAmount, relayData.outputAmount, constants_1.destinationChainId, 
            // deposit ID is 0 for first deposit
            0, quoteTimestamp, relayData.fillDeadline, currentTime, relayData.depositor, relayData.recipient, relayData.exclusiveRelayer, relayData.message);
        });
        it("deposit ID state variable incremented", async function () {
            await spokePool.connect(depositor).depositV3(...depositArgs);
            (0, utils_1.expect)(await spokePool.numberOfDeposits()).to.equal(1);
        });
        it("tokens are always pulled from caller, even if different from specified depositor", async function () {
            const currentTime = (await spokePool.getCurrentTime()).toNumber();
            const balanceBefore = await erc20.balanceOf(depositor.address);
            const newDepositor = (0, utils_1.randomAddress)();
            await (0, utils_1.expect)(spokePool
                .connect(depositor)
                .depositV3(...getDepositArgsFromRelayData({ ...relayData, depositor: newDepositor })))
                .to.emit(spokePool, "V3FundsDeposited")
                .withArgs(relayData.inputToken, relayData.outputToken, relayData.inputAmount, relayData.outputAmount, constants_1.destinationChainId, 0, quoteTimestamp, relayData.fillDeadline, currentTime, 
            // New depositor
            newDepositor, relayData.recipient, relayData.exclusiveRelayer, relayData.message);
            (0, utils_1.expect)(await erc20.balanceOf(depositor.address)).to.equal(balanceBefore.sub(constants_1.amountToDeposit));
        });
        it("deposits are not paused", async function () {
            await spokePool.pauseDeposits(true);
            await (0, utils_1.expect)(spokePool.connect(depositor).depositV3(...depositArgs)).to.be.revertedWith("DepositsArePaused");
        });
        it("reentrancy protected", async function () {
            const functionCalldata = spokePool.interface.encodeFunctionData("depositV3", [...depositArgs]);
            await (0, utils_1.expect)(spokePool.connect(depositor).callback(functionCalldata)).to.be.revertedWith("ReentrancyGuard: reentrant call");
        });
    });
    describe("speed up V3 deposit", function () {
        const updatedOutputAmount = constants_1.amountToDeposit.add(1);
        const updatedRecipient = (0, utils_1.randomAddress)();
        const updatedMessage = "0x1234";
        const depositId = 100;
        it("_verifyUpdateV3DepositMessage", async function () {
            const signature = await (0, SpokePool_Fixture_1.getUpdatedV3DepositSignature)(depositor, depositId, constants_1.originChainId, updatedOutputAmount, updatedRecipient, updatedMessage);
            await spokePool.verifyUpdateV3DepositMessage(depositor.address, depositId, constants_1.originChainId, updatedOutputAmount, updatedRecipient, updatedMessage, signature);
            // Reverts if passed in depositor is the signer or if signature is incorrect
            await (0, utils_1.expect)(spokePool.verifyUpdateV3DepositMessage(updatedRecipient, depositId, constants_1.originChainId, updatedOutputAmount, updatedRecipient, updatedMessage, signature)).to.be.revertedWith("InvalidDepositorSignature");
            // @dev Creates an invalid signature using different params
            const invalidSignature = await (0, SpokePool_Fixture_1.getUpdatedV3DepositSignature)(depositor, depositId + 1, constants_1.originChainId, updatedOutputAmount, updatedRecipient, updatedMessage);
            await (0, utils_1.expect)(spokePool.verifyUpdateV3DepositMessage(depositor.address, depositId, constants_1.originChainId, updatedOutputAmount, updatedRecipient, updatedMessage, invalidSignature)).to.be.revertedWith("InvalidDepositorSignature");
        });
        it("passes spoke pool's chainId() as origin chainId", async function () {
            const spokePoolChainId = await spokePool.chainId();
            const expectedSignature = await (0, SpokePool_Fixture_1.getUpdatedV3DepositSignature)(depositor, depositId, spokePoolChainId, updatedOutputAmount, updatedRecipient, updatedMessage);
            await (0, utils_1.expect)(spokePool.speedUpV3Deposit(depositor.address, depositId, updatedOutputAmount, updatedRecipient, updatedMessage, expectedSignature))
                .to.emit(spokePool, "RequestedSpeedUpV3Deposit")
                .withArgs(updatedOutputAmount, depositId, depositor.address, updatedRecipient, updatedMessage, expectedSignature);
            // Can't use a signature for a different chain ID, even if the signature is valid otherwise for the depositor.
            const otherChainId = spokePoolChainId.add(1);
            const invalidSignatureForChain = await (0, SpokePool_Fixture_1.getUpdatedV3DepositSignature)(depositor, depositId, otherChainId, updatedOutputAmount, updatedRecipient, updatedMessage);
            await (0, utils_1.expect)(spokePool.verifyUpdateV3DepositMessage(depositor.address, depositId, otherChainId, updatedOutputAmount, updatedRecipient, updatedMessage, invalidSignatureForChain)).to.not.be.reverted;
            await (0, utils_1.expect)(spokePool.speedUpV3Deposit(depositor.address, depositId, updatedOutputAmount, updatedRecipient, updatedMessage, invalidSignatureForChain)).to.be.revertedWith("InvalidDepositorSignature");
        });
    });
});
