diff --git a/test/bridgeAdmin.ts b/test/bridgeAdmin.ts index 47c5c73..842e73d 100644 --- a/test/bridgeAdmin.ts +++ b/test/bridgeAdmin.ts @@ -21,6 +21,29 @@ describe("Bridge Admin: ", function () { const singleTransactionLimit = convertToUnit(10, 18); const maxDailyTransactionLimit = convertToUnit(100, 18); + const functionregistry = [ + "setOracle(address)", + "setMaxSingleTransactionLimit(uint16,uint256)", + "setMaxDailyLimit(uint16,uint256)", + "setMaxSingleReceiveTransactionLimit(uint16,uint256)", + "setMaxDailyReceiveLimit(uint16,uint256)", + "pause()", + "unpause()", + "setWhitelist(address,bool)", + "setConfig(uint16,uint16,uint256,bytes)", + "setSendVersion(uint16)", + "setReceiveVersion(uint16)", + "forceResumeReceive(uint16,bytes)", + "setTrustedRemoteAddress(uint16,bytes)", + "setPrecrime(address)", + "setMinDstGas(uint16,uint16,uint256)", + "setPayloadSizeLimit(uint16,uint256)", + "removeTrustedRemote(uint16)", + "updateSendAndCallEnabled(bool)", + "sweepToken(address,address,uint256)", + "dropFailedMessage(uint16,bytes,uint64)", + ]; + let LZEndpointMock: LZEndpointMock__factory, ProxyOFTV2Dest: XVSProxyOFTDest__factory, RemoteTokenFactory: XVS__factory, @@ -84,31 +107,38 @@ describe("Bridge Admin: ", function () { await remoteOFT.transferOwnership(bridgeAdmin.address); remotePath = ethers.utils.solidityPack(["address", "address"], [AddressOne, remoteOFT.address]); - const functionregistry = [ - "setOracle(address)", - "setMaxSingleTransactionLimit(uint16,uint256)", - "setMaxDailyLimit(uint16,uint256)", - "setMaxSingleReceiveTransactionLimit(uint16,uint256)", - "setMaxDailyReceiveLimit(uint16,uint256)", - "pause()", - "unpause()", - "setWhitelist(address,bool)", - "setConfig(uint16,uint16,uint256,bytes)", - "setSendVersion(uint16)", - "setReceiveVersion(uint16)", - "forceResumeReceive(uint16,bytes)", - "setTrustedRemote(uint16,bytes)", - "setTrustedRemoteAddress(uint16,bytes)", - "setPrecrime(address)", - "setMinDstGas(uint16,uint16,uint256)", - "setPayloadSizeLimit(uint16,uint256)", - "setUseCustomAdapterParams(bool)", - ]; + const activeArray = new Array(functionregistry.length).fill(true); await bridgeAdmin.upsertSignature(functionregistry, activeArray); await loadFixture(grantPermissionsFixture); }); + it("Revert when inputs length mismatch in function registry", async function () { + const activeArray = new Array(functionregistry.length - 1).fill(true); + + await expect(bridgeAdmin.upsertSignature(functionregistry, activeArray)).to.be.revertedWith( + "Input arrays must have the same length", + ); + }); + + it("Deletes from function registry", async function () { + const activeArray = new Array(functionregistry.length).fill(true); + await bridgeAdmin.upsertSignature(functionregistry, activeArray); + await bridgeAdmin.upsertSignature(["fakeFunction(uint256)"], [true]); + await expect(bridgeAdmin.upsertSignature(["fakeFunction(uint256)"], [false])).to.emit( + bridgeAdmin, + "FunctionRegistryChanged", + ); + }); + + it("Reverts when non owner calls upsert signature", async function () { + const activeArray = new Array(functionregistry.length - 1).fill(true); + + await expect(bridgeAdmin.connect(acc2).upsertSignature(functionregistry, activeArray)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + it("Revert if EOA called owner function of bridge", async function () { await expect(remoteOFT.connect(acc1).setTrustedRemote(localChainId, remotePath)).to.be.revertedWith( "Ownable: caller is not the owner", @@ -116,7 +146,7 @@ describe("Bridge Admin: ", function () { }); it("Revert if permissions are not granted to call owner functions of bridge", async function () { - let data = remoteOFT.interface.encodeFunctionData("setTrustedRemote", [localChainId, remotePath]); + let data = remoteOFT.interface.encodeFunctionData("setTrustedRemoteAddress", [localChainId, remotePath]); await expect( acc1.sendTransaction({ to: bridgeAdmin.address, @@ -162,7 +192,6 @@ describe("Bridge Admin: ", function () { }), ).to.revertedWithCustomError(bridgeAdmin, "Unauthorized"); }); - it("Success if permissions are granted to call owner functions of bridge", async function () { let data = remoteOFT.interface.encodeFunctionData("setMaxDailyLimit", [localChainId, maxDailyTransactionLimit]); await acc2.sendTransaction({ diff --git a/test/proxyOFT.ts b/test/proxyOFT.ts index a91b777..cfd9adc 100644 --- a/test/proxyOFT.ts +++ b/test/proxyOFT.ts @@ -44,12 +44,42 @@ describe("Proxy OFTV2: ", function () { bridgeAdminLocal: XVSBridgeAdmin, remoteToken: XVS, localPath: string, + remotePath: string, acc2: SignerWithAddress, acc3: SignerWithAddress, acc1: SignerWithAddress, accessControlManager: FakeContract, oracle: FakeContract, defaultAdapterParams: any; + + async function sendTokensFromLocalToRemote() { + const amount = ethers.utils.parseEther("1", 18); + await localToken.connect(acc2).faucet(amount); + expect(await localToken.balanceOf(acc2.address)).to.be.equal(amount); + expect(await remoteToken.balanceOf(acc3.address)).to.be.equal(0); + await localToken.connect(acc2).approve(localOFT.address, amount); + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + const nativeFee = ( + await localOFT.estimateSendFee(remoteChainId, acc3AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + + await localOFT + .connect(acc2) + .sendFrom( + acc2.address, + remoteChainId, + acc3AddressBytes32, + amount, + [acc2.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ); + + expect(await localToken.balanceOf(localOFT.address)).to.equal(amount); + expect(await localOFT.circulatingSupply()).to.equal(0); + expect(await localToken.balanceOf(acc2.address)).to.equal(0); + expect(await remoteOFT.circulatingSupply()).to.equal(amount); + expect(await remoteToken.balanceOf(acc3.address)).to.be.equal(amount); + } const bridgeFixture = async () => { LZEndpointMock = await ethers.getContractFactory("LZEndpointMock"); ProxyOFTV2Src = await ethers.getContractFactory("XVSProxyOFTSrc"); @@ -100,9 +130,10 @@ describe("Proxy OFTV2: ", function () { // set each contracts source address so it can send to each other localPath = ethers.utils.solidityPack(["address", "address"], [localOFT.address, remoteOFT.address]); + remotePath = ethers.utils.solidityPack(["address", "address"], [remoteOFT.address, localOFT.address]); // Should revert admin of remoteOFT is BridgeAdmin contract - await expect(remoteOFT.setTrustedRemote(localChainId, localPath)).to.be.revertedWith( + await expect(remoteOFT.setTrustedRemoteAddress(localChainId, remoteOFT.address)).to.be.revertedWith( "Ownable: caller is not the owner", ); @@ -130,9 +161,12 @@ describe("Proxy OFTV2: ", function () { "setPrecrime(address)", "setMinDstGas(uint16,uint16,uint256)", "setPayloadSizeLimit(uint16,uint256)", - "setUseCustomAdapterParams(bool)", "removeTrustedRemote(uint16)", "updateSendAndCallEnabled(bool)", + "fallbackWithdraw(address,uint256)", + "fallbackDeposit(address,uint256)", + "sweepToken(address,address,uint256)", + "dropFailedMessage(uint16,bytes,uint64)", ]; const activeArray = new Array(functionregistry.length).fill(true); await bridgeAdminRemote.upsertSignature(functionregistry, activeArray); @@ -217,7 +251,124 @@ describe("Proxy OFTV2: ", function () { beforeEach(async function () { await loadFixture(bridgeFixture); }); + it("Reverts when incorrect argument passed to the constructor", async function () { + const ProxyOFTV2Src = await ethers.getContractFactory("XVSProxyOFTSrc"); + const LocalTokenFactory = await ethers.getContractFactory("MockToken"); + const oracle = await smock.fake("ResilientOracleInterface"); + const localEndpoint = await LZEndpointMock.deploy(localChainId); + + // create two OmnichainFungibleToken instances + const localToken = await LocalTokenFactory.deploy(name, symbol, 18); + const fakeToken = await smock.fake("MockToken"); + fakeToken.decimals.returns(2); + await expect( + ProxyOFTV2Src.deploy(ethers.constants.AddressZero, sharedDecimals, localEndpoint.address, oracle.address), + ).to.be.revertedWithCustomError(localOFT, "ZeroAddressNotAllowed"); + await expect( + ProxyOFTV2Src.deploy(localToken.address, sharedDecimals, ethers.constants.AddressZero, oracle.address), + ).to.be.revertedWithCustomError(localOFT, "ZeroAddressNotAllowed"); + await expect( + ProxyOFTV2Src.deploy(localToken.address, sharedDecimals, localEndpoint.address, ethers.constants.AddressZero), + ).to.be.revertedWithCustomError(localOFT, "ZeroAddressNotAllowed"); + await expect( + ProxyOFTV2Src.deploy(fakeToken.address, sharedDecimals, localEndpoint.address, oracle.address), + ).to.be.revertedWith("ProxyOFT: sharedDecimals must be <= decimals"); + }); + + it("Reverts when non admin access admin functions on remote", async function () { + await expect(remoteOFT.connect(acc2).setMinDstGas(localChainId, 0, 200000)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + + await expect(remoteOFT.connect(acc2).setMaxDailyLimit(localChainId, maxDailyTransactionLimit)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + await expect( + remoteOFT.connect(acc2).setMaxSingleTransactionLimit(localChainId, singleTransactionLimit), + ).to.be.revertedWith("Ownable: caller is not the owner"); + await expect( + remoteOFT.connect(acc2).setMaxDailyReceiveLimit(localChainId, maxDailyTransactionLimit), + ).to.be.revertedWith("Ownable: caller is not the owner"); + await expect( + remoteOFT.connect(acc2).setMaxSingleReceiveTransactionLimit(localChainId, singleTransactionLimit), + ).to.be.revertedWith("Ownable: caller is not the owner"); + await expect(remoteOFT.connect(acc2).pause()).to.be.revertedWith("Ownable: caller is not the owner"); + await expect(remoteOFT.connect(acc2).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); + await expect(remoteOFT.connect(acc2).setOracle(oracle.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + await expect(remoteOFT.connect(acc2).dropFailedMessage(localChainId, localPath, 1)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + it("Reverts when non admin access admin functionalities on local", async function () { + const amount = ethers.utils.parseEther("1", 18); + await expect(localOFT.connect(acc2).fallbackWithdraw(acc1.address, amount)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + await expect(localOFT.connect(acc2).fallbackDeposit(acc1.address, amount)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + await expect(localOFT.connect(acc2).sweepToken(localToken.address, acc1.address, amount)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + await expect(localOFT.connect(acc2).removeTrustedRemote(remoteChainId)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + await expect(localOFT.connect(acc2).updateSendAndCallEnabled(true)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + await expect(localOFT.connect(acc2).setOracle(oracle.address)).to.be.revertedWith( + "Ownable: caller is not the owner", + ); + }); + + it("Reverts when single transaction limit exceeds daily limit", async function () { + const data = remoteOFT.interface.encodeFunctionData("setMaxSingleTransactionLimit", [ + localChainId, + convertToUnit(110, 18), + ]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminRemote.address, + data: data, + }), + ).to.be.reverted; + }); + it("Reverts when daily limit is less than single transaction limit", async function () { + const data = remoteOFT.interface.encodeFunctionData("setMaxDailyLimit", [localChainId, convertToUnit(9, 18)]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminRemote.address, + data: data, + }), + ).to.be.reverted; + }); + it("Reverts when single limit exceeds daily limit on remote", async () => { + const data = remoteOFT.interface.encodeFunctionData("setMaxDailyReceiveLimit", [ + localChainId, + convertToUnit(9, 18), + ]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminRemote.address, + data: data, + }), + ).to.be.reverted; + }); + it("Reverts when single receive transaction limit exceeds daily limit", async () => { + const data = remoteOFT.interface.encodeFunctionData("setMaxSingleReceiveTransactionLimit", [ + localChainId, + convertToUnit(110, 18), + ]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminRemote.address, + data: data, + }), + ).to.be.reverted; + }); it("send tokens from proxy oft and receive them back", async function () { const initialAmount = ethers.utils.parseEther("1.0000000001", 18); // 1 ether const amount = ethers.utils.parseEther("1", 18); @@ -277,7 +428,41 @@ describe("Proxy OFTV2: ", function () { expect(await localToken.balanceOf(localOFT.address)).to.be.equal(halfAmount); expect(await localToken.balanceOf(acc2.address)).to.be.equal(halfAmount.add(dust)); }); + it("Reverts when caller and sender are different on remote", async function () { + await sendTokensFromLocalToRemote(); + // acc3 send tokens back to acc2 from remote chain + const acc2AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc2.address]); + const amount = ethers.utils.parseEther("1", 18); + const nativeFee = ( + await remoteOFT.estimateSendFee(localChainId, acc2AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + + await expect( + remoteOFT + .connect(acc3) + .sendFrom( + acc2.address, + localChainId, + acc2AddressBytes32, + amount, + [acc3.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ), + ).to.be.revertedWith("ProxyOFT: owner is not send caller"); + }); + it("Reverts when single limit exceeds daily limit on local", async () => { + const data = localOFT.interface.encodeFunctionData("setMaxDailyReceiveLimit", [ + remoteChainId, + convertToUnit(9, 18), + ]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.be.reverted; + }); it("Reverts if single transaction limit exceed", async function () { const amount = ethers.utils.parseEther("11", 18); await localToken.connect(acc2).faucet(amount); @@ -647,7 +832,81 @@ describe("Proxy OFTV2: ", function () { expect(await remoteOFT.circulatingSupply()).to.equal(amount); expect(await remoteToken.balanceOf(acc3.address)).to.be.equal(amount); }); + it("Drops failed message on remote", async function () { + const initialAmount = ethers.utils.parseEther("20", 18); + const amount = ethers.utils.parseEther("10", 18); + await localToken.connect(acc2).faucet(initialAmount); + await localToken.connect(acc2).approve(localOFT.address, initialAmount); + + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + const defaultAdapterParams = ethers.utils.solidityPack(["uint16", "uint256"], [1, 200000]); + const nativeFee = ( + await localOFT.estimateSendFee(remoteChainId, acc3AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + + let data = remoteOFT.interface.encodeFunctionData("pause"); + await acc1.sendTransaction({ + to: bridgeAdminRemote.address, + data: data, + }); + await localOFT + .connect(acc2) + .sendFrom( + acc2.address, + remoteChainId, + acc3AddressBytes32, + amount, + [acc2.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ); + expect(await remoteOFT.failedMessages(localChainId, localPath, 1)).to.not.equals(ethers.constants.HashZero); + + data = remoteOFT.interface.encodeFunctionData("dropFailedMessage", [localChainId, localPath, 1]); + await acc1.sendTransaction({ + to: bridgeAdminRemote.address, + data: data, + }); + expect(await remoteOFT.failedMessages(localChainId, localPath, 1)).to.equals(ethers.constants.HashZero); + }); + + it("Drops failed message on local", async function () { + await sendTokensFromLocalToRemote(); + const acc2AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc2.address]); + const amount = ethers.utils.parseEther("1", 18); + const nativeFee = ( + await remoteOFT.estimateSendFee(localChainId, acc2AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + let data = localOFT.interface.encodeFunctionData("pause"); + await acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }); + await remoteOFT + .connect(acc3) + .sendFrom( + acc3.address, + localChainId, + acc2AddressBytes32, + amount, + [acc3.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ); + + expect(await localOFT.failedMessages(remoteChainId, remotePath, 1)).to.not.equals(ethers.constants.HashZero); + data = localOFT.interface.encodeFunctionData("dropFailedMessage", [remoteChainId, remotePath, 1]); + await acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }); + + expect(await localOFT.failedMessages(remoteChainId, remotePath, 1)).to.equals(ethers.constants.HashZero); + }); + it("Reverts when zero chain id provided in set trusted remote", async function () { + await expect(bridgeAdminRemote.setTrustedRemoteAddress(0, remoteOFT.address)).to.be.revertedWith( + "ChainId must not be zero", + ); + }); it("Reverts initialy and should fail on retry if trusted remote changed", async function () { const initialAmount = ethers.utils.parseEther("20", 18); const amount = ethers.utils.parseEther("10", 18); @@ -767,6 +1026,40 @@ describe("Proxy OFTV2: ", function () { ), ).to.be.revertedWith("OFTCore: amount too small"); }); + + it("Reverts when caller and sender are different on local", async function () { + const amount = ethers.utils.parseEther("10", 18); + await localToken.connect(acc2).faucet(amount); + await localToken.connect(acc2).approve(localOFT.address, amount); + + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + const nativeFee = ( + await localOFT.estimateSendFee(remoteChainId, acc3AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + + await expect( + localOFT + .connect(acc1) + .sendFrom( + acc2.address, + remoteChainId, + acc3AddressBytes32, + amount, + [acc2.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ), + ).to.be.revertedWith("ProxyOFT: owner is not send caller"); + }); + it("Reverts when amount is too large", async function () { + const amount = ethers.utils.parseEther(Number.MAX_SAFE_INTEGER.toString(), 18); + await localToken.connect(acc2).faucet(amount); + await localToken.connect(acc2).approve(localOFT.address, amount); + + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + await expect( + localOFT.estimateSendFee(remoteChainId, acc3AddressBytes32, amount, false, defaultAdapterParams), + ).to.be.revertedWith("OFTCore: amountSD overflow"); + }); it("Successs on removal of trusted remote", async function () { const data = localOFT.interface.encodeFunctionData("removeTrustedRemote", [remoteChainId]); await acc1.sendTransaction({ @@ -858,9 +1151,7 @@ describe("Proxy OFTV2: ", function () { it("Reverts when sendAndCall is disabled", async function () { const amount = ethers.utils.parseEther("2", 18); const dstGasForCall_ = 0; - const uint160Value = BigInt("0x" + acc3.address.slice(2)); - const bytes32Value = uint160Value << BigInt(96); - const acc3AddressBytes32 = "0x" + bytes32Value.toString(16).padStart(32, "0"); + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); await expect( localOFT @@ -874,9 +1165,8 @@ describe("Proxy OFTV2: ", function () { }); it("Successfully call sendAndCall", async function () { - const uint160Value = BigInt("0x" + acc3.address.slice(2)); - const bytes32Value = uint160Value << BigInt(96); - const acc3AddressBytes32 = "0x" + bytes32Value.toString(16).padStart(32, "0"); + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + let data = localOFT.interface.encodeFunctionData("setMinDstGas", [remoteChainId, 1, 300000]); const amount = ethers.utils.parseEther("2", 18); const dstGasForCall_ = 0; @@ -916,4 +1206,307 @@ describe("Proxy OFTV2: ", function () { { value: nativeFee }, ); }); + it("Reverts when withdraw amount is greater than outbound amount", async function () { + const data = localOFT.interface.encodeFunctionData("fallbackWithdraw", [ + acc1.address, + ethers.utils.parseEther("20", 18), + ]); + + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.be.reverted; + }); + it("Recover tokens", async function () { + const amount = ethers.utils.parseEther("10", 18); + await localToken.connect(acc2).faucet(amount); + await localToken.connect(acc2).approve(localOFT.address, amount); + + const acc3AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc3.address]); + const nativeFee = ( + await localOFT.estimateSendFee(remoteChainId, acc3AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + + await localOFT + .connect(acc2) + .sendFrom( + acc2.address, + remoteChainId, + acc3AddressBytes32, + amount, + [acc2.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ); + expect(await localOFT.outboundAmount()).to.equals(amount); + const data = localOFT.interface.encodeFunctionData("fallbackWithdraw", [acc1.address, amount]); + + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.emit(localOFT, "FallbackWithdraw"); + }); + it("Locks tokens in contract", async function () { + const amount = ethers.utils.parseEther("10", 18); + await localToken.connect(acc1).faucet(amount); + + await localToken.connect(acc1).approve(localOFT.address, amount); + const data = localOFT.interface.encodeFunctionData("fallbackDeposit", [acc1.address, amount]); + + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.emit(localOFT, "FallbackDeposit"); + }); + it("Reverts when cap is reached in fallbackDeposit", async function () { + const amount = Number.MAX_SAFE_INTEGER; + const data = localOFT.interface.encodeFunctionData("fallbackDeposit", [ + acc1.address, + ethers.utils.parseEther(amount.toString(), 18), + ]); + + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.be.reverted; + }); + + it("Sweeps token back to the user", async function () { + const amount = ethers.utils.parseEther("10", 18); + await localToken.connect(acc1).faucet(amount); + await localToken.connect(acc1).transfer(localOFT.address, amount); + const data = localOFT.interface.encodeFunctionData("sweepToken", [localToken.address, acc1.address, amount]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.emit(localOFT, "SweepToken"); + }); + it("Reverts when amount exceeds balance", async function () { + const amount = ethers.utils.parseEther("10", 18); + const sweepAmount = ethers.utils.parseEther("11", 18); + await localToken.connect(acc1).faucet(amount); + await localToken.connect(acc1).transfer(localOFT.address, amount); + const data = localOFT.interface.encodeFunctionData("sweepToken", [localToken.address, acc1.address, sweepAmount]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.be.reverted; + }); + + it("Sets whitelisted user", async function () { + const data = localOFT.interface.encodeFunctionData("setWhitelist", [acc1.address, true]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.emit(localOFT, "SetWhitelist"); + }); + it("Reverts on zero address otherwise sets oracle", async function () { + let data = remoteOFT.interface.encodeFunctionData("setOracle", [ethers.constants.AddressZero]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.be.reverted; + const oracleNew = await smock.fake("ResilientOracleInterface"); + data = remoteOFT.interface.encodeFunctionData("setOracle", [oracleNew.address]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.emit(localOFT, "OracleChanged"); + }); + it("Replace bridge with new bridge", async function () { + await sendTokensFromLocalToRemote(); + const amount = ethers.utils.parseEther("1", 18); + const localOFT2 = await ProxyOFTV2Src.deploy( + localToken.address, + sharedDecimals, + localEndpoint.address, + oracle.address, + ); + const remoteOFT2 = await ProxyOFTV2Dest.deploy( + remoteToken.address, + sharedDecimals, + remoteEndpoint.address, + oracle.address, + ); + + const bridgeAdminFactory = await ethers.getContractFactory("XVSBridgeAdmin"); + + const bridgeAdminRemote2 = await upgrades.deployProxy(bridgeAdminFactory, [accessControlManager.address], { + constructorArgs: [remoteOFT2.address], + initializer: "initialize", + }); + + const bridgeAdminLocal2 = await upgrades.deployProxy(bridgeAdminFactory, [accessControlManager.address], { + constructorArgs: [localOFT2.address], + initializer: "initialize", + }); + + await bridgeAdminLocal.deployed(); + + await remoteOFT2.transferOwnership(bridgeAdminRemote2.address); + await localOFT2.transferOwnership(bridgeAdminLocal2.address); + + await localEndpoint.setDestLzEndpoint(remoteOFT2.address, remoteEndpoint.address); + await remoteEndpoint.setDestLzEndpoint(localOFT2.address, localEndpoint.address); + + const functionregistry = [ + "setMaxSingleTransactionLimit(uint16,uint256)", + "setMaxDailyLimit(uint16,uint256)", + "setMaxSingleReceiveTransactionLimit(uint16,uint256)", + "setMaxDailyReceiveLimit(uint16,uint256)", + "setTrustedRemoteAddress(uint16,bytes)", + "setMinDstGas(uint16,uint16,uint256)", + "fallbackWithdraw(address,uint256)", + "fallbackDeposit(address,uint256)", + ]; + const activeArray = new Array(functionregistry.length).fill(true); + await bridgeAdminRemote2.upsertSignature(functionregistry, activeArray); + await bridgeAdminLocal2.upsertSignature(functionregistry, activeArray); + await bridgeAdminLocal2.setTrustedRemoteAddress(remoteChainId, remoteOFT2.address); + let data = localOFT2.interface.encodeFunctionData("setMinDstGas", [remoteChainId, 0, 200000]); + await acc1.sendTransaction({ + to: bridgeAdminLocal2.address, + data: data, + }); + data = localOFT2.interface.encodeFunctionData("setMaxDailyLimit", [remoteChainId, maxDailyTransactionLimit]); + await acc1.sendTransaction({ + to: bridgeAdminLocal2.address, + data: data, + }); + + data = localOFT2.interface.encodeFunctionData("setMaxSingleTransactionLimit", [ + remoteChainId, + singleTransactionLimit, + ]); + await acc1.sendTransaction({ + to: bridgeAdminLocal2.address, + data: data, + }); + + data = localOFT2.interface.encodeFunctionData("setMaxDailyReceiveLimit", [remoteChainId, maxDailyTransactionLimit]); + await acc1.sendTransaction({ + to: bridgeAdminLocal2.address, + data: data, + }); + data = localOFT2.interface.encodeFunctionData("setMaxSingleReceiveTransactionLimit", [ + remoteChainId, + singleTransactionLimit, + ]); + await acc1.sendTransaction({ + to: bridgeAdminLocal2.address, + data: data, + }); + + await bridgeAdminRemote2.setTrustedRemoteAddress(localChainId, localOFT2.address); + + data = remoteOFT2.interface.encodeFunctionData("setMinDstGas", [localChainId, 0, 200000]); + await acc1.sendTransaction({ + to: bridgeAdminRemote2.address, + data: data, + }); + + data = remoteOFT2.interface.encodeFunctionData("setMaxDailyLimit", [localChainId, maxDailyTransactionLimit]); + await acc1.sendTransaction({ + to: bridgeAdminRemote2.address, + data: data, + }); + data = remoteOFT2.interface.encodeFunctionData("setMaxSingleTransactionLimit", [ + localChainId, + singleTransactionLimit, + ]); + await acc1.sendTransaction({ + to: bridgeAdminRemote2.address, + data: data, + }); + + data = remoteOFT2.interface.encodeFunctionData("setMaxDailyReceiveLimit", [localChainId, maxDailyTransactionLimit]); + await acc1.sendTransaction({ + to: bridgeAdminRemote2.address, + data: data, + }); + data = remoteOFT2.interface.encodeFunctionData("setMaxSingleReceiveTransactionLimit", [ + localChainId, + singleTransactionLimit, + ]); + await acc1.sendTransaction({ + to: bridgeAdminRemote2.address, + data: data, + }); + + const mintCap = ethers.utils.parseEther("2", 18); + await remoteToken.setMintCap(remoteOFT.address, mintCap); + expect(await remoteToken.minterToCap(remoteOFT.address)).to.equals(mintCap); + + await remoteToken.setMintCap(remoteOFT2.address, mintCap); + expect(await remoteToken.minterToCap(remoteOFT2.address)).to.equals(mintCap); + + await expect(remoteToken.migrateMinterTokens(remoteOFT.address, remoteOFT2.address)).to.emit( + remoteToken, + "MintedTokensMigrated", + ); + data = localOFT.interface.encodeFunctionData("fallbackWithdraw", [acc1.address, amount]); + + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal.address, + data: data, + }), + ).to.emit(localOFT, "FallbackWithdraw"); + + await localToken.approve(localOFT2.address, amount); + + data = localOFT2.interface.encodeFunctionData("fallbackDeposit", [acc1.address, amount]); + await expect( + acc1.sendTransaction({ + to: bridgeAdminLocal2.address, + data: data, + }), + ).to.emit(localOFT2, "FallbackDeposit"); + + const acc2AddressBytes32 = ethers.utils.defaultAbiCoder.encode(["address"], [acc2.address]); + const nativeFee = ( + await remoteOFT2.estimateSendFee(localChainId, acc2AddressBytes32, amount, false, defaultAdapterParams) + ).nativeFee; + + expect(await remoteOFT2.circulatingSupply()).to.equal(amount); + expect(await remoteToken.balanceOf(acc3.address)).to.be.equal(amount); + expect(await localToken.balanceOf(localOFT2.address)).to.be.equal(amount); + + await expect( + remoteOFT2 + .connect(acc3) + .sendFrom( + acc3.address, + localChainId, + acc2AddressBytes32, + amount, + [acc3.address, ethers.constants.AddressZero, defaultAdapterParams], + { value: nativeFee }, + ), + ).to.emit(remoteOFT2, "SendToChain"); + + expect(await remoteOFT2.circulatingSupply()).to.equal(0); + expect(await remoteToken.balanceOf(acc3.address)).to.be.equal(0); + + expect(await localToken.balanceOf(localOFT2.address)).to.be.equal(0); + expect(await localOFT2.outboundAmount()).to.be.equal(0); + expect(await localToken.balanceOf(acc2.address)).to.be.equal(amount); + }); }); diff --git a/test/tokenController.ts b/test/tokenController.ts new file mode 100644 index 0000000..a17c51c --- /dev/null +++ b/test/tokenController.ts @@ -0,0 +1,120 @@ +import { FakeContract, smock } from "@defi-wonderland/smock"; +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { SignerWithAddress } from "hardhat-deploy-ethers/signers"; + +import { AccessControlManager, XVS, XVS__factory } from "../typechain"; + +describe("Token Controller: ", function () { + let tokenFactory: XVS__factory, + token: XVS, + acc2: SignerWithAddress, + acc1: SignerWithAddress, + accessControlManager: FakeContract; + + const tokenFixture = async () => { + accessControlManager = await smock.fake("AccessControlManager"); + accessControlManager.isAllowedToCall.returns(true); + tokenFactory = await ethers.getContractFactory("XVS"); + token = await tokenFactory.deploy(accessControlManager.address); + acc1 = (await ethers.getSigners())[0]; + acc2 = (await ethers.getSigners())[1]; + }; + beforeEach(async function () { + await loadFixture(tokenFixture); + }); + it("Reverts when token is paused", async () => { + await token.pause(); + await expect(token.mint(acc1.address, ethers.utils.parseEther("2", 18))).to.be.revertedWith("Pausable: paused"); + await token.unpause(); + }); + it("Reverts when minting cap reached", async () => { + await token.connect(acc1).setMintCap(acc1.address, ethers.utils.parseEther("1", 18)); + await expect( + token.connect(acc1).mint(acc1.address, ethers.utils.parseEther("2", 18)), + ).to.be.revertedWithCustomError(token, "MintLimitExceed"); + }); + it("Mint succesfully", async () => { + const mintAmount = ethers.utils.parseEther("2", 18); + await token.connect(acc1).setMintCap(acc1.address, mintAmount); + await expect(token.connect(acc1).mint(acc1.address, mintAmount)).to.emit(token, "MintLimitDecreased"); + expect(await token.minterToMintedAmount(acc1.address)).to.equals(mintAmount); + }); + + it("Reverts when token is paused", async () => { + await token.pause(); + await expect(token.burn(acc1.address, ethers.utils.parseEther("2", 18))).to.be.revertedWith("Pausable: paused"); + }); + it("Reverts when burned token is greater than minted token", async () => { + await token.connect(acc1).setMintCap(acc1.address, ethers.utils.parseEther("2", 18)); + await expect(token.connect(acc1).mint(acc1.address, ethers.utils.parseEther("2", 18))).to.emit( + token, + "MintLimitDecreased", + ); + await expect(token.connect(acc1).burn(acc1.address, ethers.utils.parseEther("3", 18))).to.be.revertedWith( + "ERC20: burn amount exceeds balance", + ); + }); + it("Burn succesfully", async () => { + await token.connect(acc1).setMintCap(acc1.address, ethers.utils.parseEther("2", 18)); + await expect(token.connect(acc1).mint(acc1.address, ethers.utils.parseEther("2", 18))).to.emit( + token, + "MintLimitDecreased", + ); + await expect(token.connect(acc1).burn(acc1.address, ethers.utils.parseEther("2", 18))).to.emit( + token, + "MintLimitIncreased", + ); + }); + it("Update blackList", async () => { + await expect(token.connect(acc1).updateBlacklist(acc2.address, true)).to.emit(token, "BlacklistUpdated"); + expect(await token.isBlackListed(acc2.address)).to.be.true; + }); + it("Sets access control manager", async () => { + const accessControlManagerNew = await smock.fake("AccessControlManager"); + await expect(token.setAccessControlManager(accessControlManagerNew.address)).to.emit( + token, + "NewAccessControlManager", + ); + + await expect(token.setAccessControlManager(ethers.constants.AddressZero)).to.be.revertedWithCustomError( + token, + "ZeroAddressNotAllowed", + ); + }); + it("Reverts when source and destination address is same in migration", async () => { + await expect(token.migrateMinterTokens(acc1.address, acc1.address)).to.be.revertedWithCustomError( + token, + "AddressesMustDiffer", + ); + }); + it("Reverts when destination cap is less than source and destination cap", async () => { + const mintCap = ethers.utils.parseEther("2", 18); + await token.setMintCap(acc1.address, mintCap); + expect(await token.minterToCap(acc1.address)).to.equals(mintCap); + await token.setMintCap(acc2.address, mintCap); + expect(await token.minterToCap(acc2.address)).to.equals(mintCap); + const amount = ethers.utils.parseEther("1", 18); + await token.mint(acc1.address, amount); + await token.mint(acc2.address, amount); + await token.setMintCap(acc2.address, amount); + await expect(token.migrateMinterTokens(acc1.address, acc2.address)).to.be.revertedWithCustomError( + token, + "MintLimitExceed", + ); + }); + it("Migrates token from source minter to destination minter", async () => { + const mintCap = ethers.utils.parseEther("2", 18); + await token.setMintCap(acc1.address, mintCap); + expect(await token.minterToCap(acc1.address)).to.equals(mintCap); + await token.setMintCap(acc2.address, mintCap); + expect(await token.minterToCap(acc2.address)).to.equals(mintCap); + const amount = ethers.utils.parseEther("1", 18); + await token.mint(acc1.address, amount); + await token.mint(acc2.address, amount); + await expect(token.migrateMinterTokens(acc1.address, acc2.address)).to.emit(token, "MintLimitDecreased"); + await expect(token.migrateMinterTokens(acc1.address, acc2.address)).to.emit(token, "MintLimitIncreased"); + await expect(token.migrateMinterTokens(acc1.address, acc2.address)).to.emit(token, "MintedTokensMigrated"); + }); +});