"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const constants_1 = require("../constants");
const utils_1 = require("../../../../utils/utils");
const utils_hre_1 = require("../../../../utils/utils.hre");
const HubPool_Fixture_1 = require("../fixtures/HubPool.Fixture");
const MerkleLib_utils_1 = require("../MerkleLib.utils");
const abis_1 = require("../../../../utils/abis");
const IOFT__factory_1 = require("../../../../typechain/factories/contracts/interfaces/IOFT.sol/IOFT__factory");
const constants_2 = require("@across-protocol/constants");
let hubPool, arbitrumSpokePool, dai, weth, l2UsdtContract;
let l2Weth, l2Dai, l2Usdc, crossDomainAliasAddress;
let owner, relayer, rando, crossDomainAlias;
let l2GatewayRouter, l2CctpTokenMessenger, cctpTokenMinter, l2OftMessenger;
const oftHubEid = (0, utils_1.getOftEid)(constants_2.CHAIN_IDs.MAINNET);
describe("Arbitrum Spoke Pool", function () {
    beforeEach(async function () {
        [owner, relayer, rando] = await utils_1.ethers.getSigners();
        ({ weth, l2Weth, dai, l2Dai, hubPool, l2Usdc, l2UsdtContract } = await (0, HubPool_Fixture_1.hubPoolFixture)());
        // Create an alias for the Owner. Impersonate the account. Crate a signer for it and send it ETH.
        crossDomainAliasAddress = (0, utils_1.avmL1ToL2Alias)(owner.address);
        await utils_hre_1.hre.network.provider.request({ method: "hardhat_impersonateAccount", params: [crossDomainAliasAddress] });
        crossDomainAlias = await utils_1.ethers.getSigner(crossDomainAliasAddress);
        await owner.sendTransaction({ to: crossDomainAliasAddress, value: (0, utils_1.toWei)("1") });
        l2GatewayRouter = await (0, utils_1.createFake)("L2GatewayRouter");
        l2CctpTokenMessenger = await (0, utils_1.createFakeFromABI)(abis_1.CCTPTokenMessengerInterface);
        cctpTokenMinter = await (0, utils_1.createFakeFromABI)(abis_1.CCTPTokenMinterInterface);
        l2CctpTokenMessenger.localMinter.returns(cctpTokenMinter.address);
        cctpTokenMinter.burnLimitsPerMessage.returns((0, utils_1.toWei)("1000000"));
        l2OftMessenger = await (0, utils_1.createTypedFakeFromABI)([...IOFT__factory_1.IOFT__factory.abi]);
        arbitrumSpokePool = await utils_hre_1.hre.upgrades.deployProxy(await (0, utils_1.getContractFactory)("Arbitrum_SpokePool", owner), [0, l2GatewayRouter.address, owner.address, hubPool.address], {
            kind: "uups",
            unsafeAllow: ["delegatecall"],
            constructorArgs: [l2Weth, 60 * 60, 9 * 60 * 60, l2Usdc, l2CctpTokenMessenger.address, oftHubEid, (0, utils_1.toWei)("1")],
        });
        await (0, utils_1.seedContract)(arbitrumSpokePool, relayer, [dai], weth, constants_1.amountHeldByPool);
        await arbitrumSpokePool.connect(crossDomainAlias).whitelistToken(l2Dai, dai.address);
    });
    it("Only cross domain owner upgrade logic contract", async function () {
        // TODO: Could also use upgrades.prepareUpgrade but I'm unclear of differences
        const implementation = await utils_hre_1.hre.upgrades.deployImplementation(await (0, utils_1.getContractFactory)("Arbitrum_SpokePool", owner), {
            kind: "uups",
            unsafeAllow: ["delegatecall"],
            constructorArgs: [l2Weth, 60 * 60, 9 * 60 * 60, l2Usdc, l2CctpTokenMessenger.address, oftHubEid, (0, utils_1.toWei)("1")],
        });
        // upgradeTo fails unless called by cross domain admin
        await (0, utils_1.expect)(arbitrumSpokePool.upgradeTo(implementation)).to.be.revertedWith("ONLY_COUNTERPART_GATEWAY");
        await arbitrumSpokePool.connect(crossDomainAlias).upgradeTo(implementation);
    });
    it("Only cross domain owner can set L2GatewayRouter", async function () {
        await (0, utils_1.expect)(arbitrumSpokePool.setL2GatewayRouter(rando.address)).to.be.reverted;
        await arbitrumSpokePool.connect(crossDomainAlias).setL2GatewayRouter(rando.address);
        (0, utils_1.expect)(await arbitrumSpokePool.l2GatewayRouter()).to.equal(rando.address);
    });
    it("Only cross domain owner can whitelist a token pair", async function () {
        await (0, utils_1.expect)(arbitrumSpokePool.whitelistToken(l2Dai, dai.address)).to.be.reverted;
        await arbitrumSpokePool.connect(crossDomainAlias).whitelistToken(l2Dai, dai.address);
        (0, utils_1.expect)(await arbitrumSpokePool.whitelistedTokens(l2Dai)).to.equal(dai.address);
    });
    it("Only cross domain owner can set the cross domain admin", async function () {
        await (0, utils_1.expect)(arbitrumSpokePool.setCrossDomainAdmin(rando.address)).to.be.reverted;
        await arbitrumSpokePool.connect(crossDomainAlias).setCrossDomainAdmin(rando.address);
        (0, utils_1.expect)(await arbitrumSpokePool.crossDomainAdmin()).to.equal(rando.address);
    });
    it("Only cross domain owner can set the hub pool address", async function () {
        await (0, utils_1.expect)(arbitrumSpokePool.setWithdrawalRecipient(rando.address)).to.be.reverted;
        await arbitrumSpokePool.connect(crossDomainAlias).setWithdrawalRecipient(rando.address);
        (0, utils_1.expect)(await arbitrumSpokePool.withdrawalRecipient()).to.equal(rando.address);
    });
    it("Only cross domain owner can initialize a relayer refund", async function () {
        await (0, utils_1.expect)(arbitrumSpokePool.relayRootBundle(constants_1.mockTreeRoot, constants_1.mockTreeRoot)).to.be.reverted;
        await arbitrumSpokePool.connect(crossDomainAlias).relayRootBundle(constants_1.mockTreeRoot, constants_1.mockTreeRoot);
        (0, utils_1.expect)((await arbitrumSpokePool.rootBundles(0)).slowRelayRoot).to.equal(constants_1.mockTreeRoot);
        (0, utils_1.expect)((await arbitrumSpokePool.rootBundles(0)).relayerRefundRoot).to.equal(constants_1.mockTreeRoot);
    });
    it("Only cross domain owner can delete a relayer refund", async function () {
        await arbitrumSpokePool.connect(crossDomainAlias).relayRootBundle(constants_1.mockTreeRoot, constants_1.mockTreeRoot);
        await (0, utils_1.expect)(arbitrumSpokePool.emergencyDeleteRootBundle(0)).to.be.reverted;
        await (0, utils_1.expect)(arbitrumSpokePool.connect(crossDomainAlias).emergencyDeleteRootBundle(0)).to.not.be.reverted;
        (0, utils_1.expect)((await arbitrumSpokePool.rootBundles(0)).slowRelayRoot).to.equal(utils_1.ethers.utils.hexZeroPad("0x0", 32));
        (0, utils_1.expect)((await arbitrumSpokePool.rootBundles(0)).relayerRefundRoot).to.equal(utils_1.ethers.utils.hexZeroPad("0x0", 32));
    });
    it("Bridge tokens to hub pool correctly calls the Standard L2 Gateway router", async function () {
        const { leaves, tree } = await (0, MerkleLib_utils_1.constructSingleRelayerRefundTree)(l2Dai, await arbitrumSpokePool.callStatic.chainId());
        await arbitrumSpokePool.connect(crossDomainAlias).relayRootBundle(tree.getHexRoot(), constants_1.mockTreeRoot);
        // Reverts if route from arbitrum to mainnet for l2Dai isn't whitelisted.
        await arbitrumSpokePool.connect(crossDomainAlias).whitelistToken(l2Dai, constants_1.zeroAddress);
        await (0, utils_1.expect)(arbitrumSpokePool.executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]))).to.be.revertedWith("Uninitialized mainnet token");
        await arbitrumSpokePool.connect(crossDomainAlias).whitelistToken(l2Dai, dai.address);
        await arbitrumSpokePool.connect(relayer).executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]));
        // This should have sent tokens back to L1. Check the correct methods on the gateway are correctly called.
        // outboundTransfer is overloaded in the arbitrum gateway. Define the interface to check the method is called.
        const functionKey = "outboundTransfer(address,address,uint256,bytes)";
        (0, utils_1.expect)(l2GatewayRouter[functionKey]).to.have.been.calledOnce;
        (0, utils_1.expect)(l2GatewayRouter[functionKey]).to.have.been.calledWith(dai.address, hubPool.address, constants_1.amountToReturn, "0x");
    });
    it("Bridge tokens to hub pool correctly using the OFT messaging for L2 USDT token", async function () {
        l2OftMessenger.token.returns(l2UsdtContract.address);
        await arbitrumSpokePool.connect(crossDomainAlias).setOftMessenger(l2UsdtContract.address, l2OftMessenger.address);
        l2OftMessenger.token.returns(l2UsdtContract.address);
        await arbitrumSpokePool.connect(crossDomainAlias).setOftMessenger(l2UsdtContract.address, l2OftMessenger.address);
        const l2UsdtSendAmount = utils_1.BigNumber.from("1234567");
        const { leaves, tree } = await (0, MerkleLib_utils_1.constructSingleRelayerRefundTree)(l2UsdtContract.address, await arbitrumSpokePool.callStatic.chainId(), l2UsdtSendAmount);
        await arbitrumSpokePool.connect(crossDomainAlias).relayRootBundle(tree.getHexRoot(), constants_1.mockTreeRoot);
        const oftNativeFee = (0, utils_1.toWeiWithDecimals)("1", 9).mul(200_000); // 1 GWEI gas price * 200,000 gas cost
        // set up `quoteSend` return val
        const msgFeeStruct = [
            oftNativeFee, // nativeFee
            utils_1.BigNumber.from(0), // lzTokenFee
        ];
        l2OftMessenger.quoteSend.returns(msgFeeStruct);
        // set up `send` return val
        const msgReceipt = [
            (0, utils_1.randomBytes32)(), // guid
            utils_1.BigNumber.from("1"), // nonce
            msgFeeStruct, // fee
        ];
        const oftReceipt = [l2UsdtSendAmount, l2UsdtSendAmount];
        l2OftMessenger.send.returns([msgReceipt, oftReceipt]);
        await arbitrumSpokePool
            .connect(relayer)
            .executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]), { value: oftNativeFee });
        // Adapter should have approved gateway to spend its ERC20.
        (0, utils_1.expect)(await l2UsdtContract.allowance(arbitrumSpokePool.address, l2OftMessenger.address)).to.equal(l2UsdtSendAmount);
        const sendParam = {
            dstEid: oftHubEid,
            to: utils_1.ethers.utils.hexZeroPad(hubPool.address, 32).toLowerCase(),
            amountLD: l2UsdtSendAmount,
            minAmountLD: l2UsdtSendAmount,
            extraOptions: "0x",
            composeMsg: "0x",
            oftCmd: "0x",
        };
        // We should have called send on the l2OftMessenger once with correct params
        (0, utils_1.expect)(l2OftMessenger.send).to.have.been.calledOnce;
        (0, utils_1.expect)(l2OftMessenger.send).to.have.been.calledWith(sendParam, msgFeeStruct, arbitrumSpokePool.address);
    });
    describe("OFT bridging", function () {
        const l2UsdtSendAmount = utils_1.BigNumber.from("1234567");
        let leaves, tree;
        beforeEach(async function () {
            l2OftMessenger.token.returns(l2UsdtContract.address);
            await arbitrumSpokePool.connect(crossDomainAlias).setOftMessenger(l2UsdtContract.address, l2OftMessenger.address);
            const treeResult = await (0, MerkleLib_utils_1.constructSingleRelayerRefundTree)(l2UsdtContract.address, await arbitrumSpokePool.callStatic.chainId(), l2UsdtSendAmount);
            leaves = treeResult.leaves;
            tree = treeResult.tree;
            await arbitrumSpokePool.connect(crossDomainAlias).relayRootBundle(tree.getHexRoot(), constants_1.mockTreeRoot);
        });
        it("reverts with OftLzFeeNotZero if lzTokenFee is not zero", async function () {
            const nativeFee = (0, utils_1.toWei)("0.1");
            const msgFeeStruct = [
                nativeFee,
                utils_1.BigNumber.from(1), // lzTokenFee
            ];
            l2OftMessenger.quoteSend.returns(msgFeeStruct);
            await (0, utils_1.expect)(arbitrumSpokePool.executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]), { value: nativeFee })).to.be.revertedWith("OftLzFeeNotZero");
        });
        it("reverts with OftFeeCapExceeded if nativeFee is too high", async function () {
            const highNativeFee = (0, utils_1.toWei)("2"); // Higher than oftFeeCap (1 ETH)
            const msgFeeStruct = [
                highNativeFee, // nativeFee
                utils_1.BigNumber.from(0), // lzTokenFee
            ];
            l2OftMessenger.quoteSend.returns(msgFeeStruct);
            await (0, utils_1.expect)(arbitrumSpokePool.executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]), { value: highNativeFee })).to.be.revertedWith("OftFeeCapExceeded");
        });
        it("reverts with OFTFeeUnderpaid if caller does not supply the required OFT fee", async function () {
            const nativeFee = (0, utils_1.toWei)("0.1");
            const msgFeeStruct = [
                nativeFee, // nativeFee
                utils_1.BigNumber.from(0), // lzTokenFee
            ];
            l2OftMessenger.quoteSend.returns(msgFeeStruct);
            await (0, utils_1.expect)(arbitrumSpokePool.executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]))).to.be.revertedWith("OFTFeeUnderpaid");
        });
        it("reverts with OftIncorrectAmountReceivedLD if OFT receipt has wrong received amount", async function () {
            const msgFeeStruct = [utils_1.BigNumber.from(0), utils_1.BigNumber.from(0)];
            l2OftMessenger.quoteSend.returns(msgFeeStruct);
            const msgReceipt = [
                (0, utils_1.randomBytes32)(),
                utils_1.BigNumber.from(1),
                msgFeeStruct,
            ];
            const oftReceipt = [
                l2UsdtSendAmount,
                l2UsdtSendAmount.sub(1), // Incorrect received amount
            ];
            l2OftMessenger.send.returns([msgReceipt, oftReceipt]);
            await (0, utils_1.expect)(arbitrumSpokePool.executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]))).to.be.revertedWith("OftIncorrectAmountReceivedLD");
        });
        it("reverts with OftIncorrectAmountSentLD if OFT receipt has wrong sent amount", async function () {
            const msgFeeStruct = [utils_1.BigNumber.from(0), utils_1.BigNumber.from(0)];
            l2OftMessenger.quoteSend.returns(msgFeeStruct);
            const msgReceipt = [
                (0, utils_1.randomBytes32)(),
                utils_1.BigNumber.from(1),
                msgFeeStruct,
            ];
            const oftReceipt = [
                l2UsdtSendAmount.sub(1), // Incorrect sent amount
                l2UsdtSendAmount,
            ];
            l2OftMessenger.send.returns([msgReceipt, oftReceipt]);
            await (0, utils_1.expect)(arbitrumSpokePool.executeRelayerRefundLeaf(0, leaves[0], tree.getHexProof(leaves[0]))).to.be.revertedWith("OftIncorrectAmountSentLD");
        });
    });
    it("Cross domain owner can set and remove an OFT messenger", async function () {
        l2OftMessenger.token.returns(l2UsdtContract.address);
        await (0, utils_1.expect)(arbitrumSpokePool.setOftMessenger(l2UsdtContract.address, l2OftMessenger.address)).to.be.reverted;
        await arbitrumSpokePool.connect(crossDomainAlias).setOftMessenger(l2UsdtContract.address, l2OftMessenger.address);
        (0, utils_1.expect)(await arbitrumSpokePool.oftMessengers(l2UsdtContract.address)).to.equal(l2OftMessenger.address);
        await (0, utils_1.expect)(arbitrumSpokePool.setOftMessenger(l2UsdtContract.address, constants_1.zeroAddress)).to.be.reverted;
        await arbitrumSpokePool.connect(crossDomainAlias).setOftMessenger(l2UsdtContract.address, constants_1.zeroAddress);
        (0, utils_1.expect)(await arbitrumSpokePool.oftMessengers(l2UsdtContract.address)).to.equal(constants_1.zeroAddress);
    });
});
