From bb8914afde0eaf61879ed6accf06caa05d27d67c Mon Sep 17 00:00:00 2001 From: q1q0 Date: Thu, 22 Jun 2023 12:30:36 -0400 Subject: [PATCH] update logic --- .../connectors/morpho-aave-v3/interface.sol | 2 + .../connectors/morpho-aave-v3/main.sol | 62 ++- test/mainnet/morpho/morpho-aave-v3.test.ts | 371 +++++++++--------- 3 files changed, 238 insertions(+), 197 deletions(-) diff --git a/contracts/mainnet/connectors/morpho-aave-v3/interface.sol b/contracts/mainnet/connectors/morpho-aave-v3/interface.sol index 9535f033..d1f79f64 100644 --- a/contracts/mainnet/connectors/morpho-aave-v3/interface.sol +++ b/contracts/mainnet/connectors/morpho-aave-v3/interface.sol @@ -25,4 +25,6 @@ interface IMorphoCore { function withdrawCollateral(address underlying, uint256 amount, address onBehalf, address receiver) external returns (uint256 withdrawn); + + function approveManager(address manager, bool isAllowed) external; } diff --git a/contracts/mainnet/connectors/morpho-aave-v3/main.sol b/contracts/mainnet/connectors/morpho-aave-v3/main.sol index d7583753..0ebde07a 100644 --- a/contracts/mainnet/connectors/morpho-aave-v3/main.sol +++ b/contracts/mainnet/connectors/morpho-aave-v3/main.sol @@ -271,7 +271,11 @@ abstract contract MorphoAaveV3 is Helpers, Events { address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.borrow(_token, _amt, address(this), _receiver, max_iteration); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } setUint(_setId, _amt); @@ -310,7 +314,11 @@ abstract contract MorphoAaveV3 is Helpers, Events { address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.borrow(_token, _amt, _onBehalf, _receiver, max_iteration); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } setUint(_setId, _amt); @@ -349,7 +357,11 @@ abstract contract MorphoAaveV3 is Helpers, Events { address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.borrow(_token, _amt, address(this), _receiver, _maxIteration); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } setUint(_setId, _amt); @@ -389,7 +401,11 @@ abstract contract MorphoAaveV3 is Helpers, Events { address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.borrow(_token, _amt, _onBehalf, _receiver, _maxIteration); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } setUint(_setId, _amt); @@ -428,7 +444,12 @@ abstract contract MorphoAaveV3 is Helpers, Events { address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.withdraw(_token, _amt, address(this), _receiver, max_iteration); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } + setUint(_setId, _amt); _eventName = "LogWithdraw(address,uint256,address,uint256,uint256)"; @@ -464,7 +485,11 @@ abstract contract MorphoAaveV3 is Helpers, Events { uint256 _amt = getUint(_getId, _amount); address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.withdraw(_token, _amt, _onBehalf, _receiver, max_iteration); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } setUint(_setId, _amt); @@ -504,7 +529,11 @@ abstract contract MorphoAaveV3 is Helpers, Events { address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.withdraw(_token, _amt, _onBehalf, _receiver, _maxIteration); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } setUint(_setId, _amt); @@ -534,7 +563,11 @@ abstract contract MorphoAaveV3 is Helpers, Events { address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.withdrawCollateral(_token, _amt, address(this), _receiver); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } setUint(_setId, _amt); @@ -563,7 +596,11 @@ abstract contract MorphoAaveV3 is Helpers, Events { uint256 _amt = getUint(_getId, _amount); address _token = _tokenAddress == ethAddr ? wethAddr : _tokenAddress; MORPHO_AAVE_V3.withdrawCollateral(_token, _amt, _onBehalf, _receiver); - // convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + if(_tokenAddress == ethAddr) { + if(_receiver == address(this)) + convertWethToEth(_tokenAddress == ethAddr, TokenInterface(wethAddr), _amt); + else revert("cannot convert"); + } setUint(_setId, _amt); @@ -676,6 +713,13 @@ abstract contract MorphoAaveV3 is Helpers, Events { _setId ); } + + /// @notice Approves a `manager` to borrow/withdraw on behalf of the sender. + /// @param _manager The address of the manager. + /// @param _isAllowed Whether `manager` is allowed to manage `msg.sender`'s position or not. + function approveManager(address _manager, bool _isAllowed) external { + MORPHO_AAVE_V3.approveManager(_manager, _isAllowed); + } } contract ConnectV3MorphoAaveV3 is MorphoAaveV3 { diff --git a/test/mainnet/morpho/morpho-aave-v3.test.ts b/test/mainnet/morpho/morpho-aave-v3.test.ts index 56699cee..7c446158 100644 --- a/test/mainnet/morpho/morpho-aave-v3.test.ts +++ b/test/mainnet/morpho/morpho-aave-v3.test.ts @@ -123,27 +123,27 @@ describe("Morpho-Aave-v3", function () { ); }); - // it("Deposit 1 DAI into DSA wallet", async function () { + it("Deposit 1 DAI into DSA wallet", async function () { - // await hre.network.provider.request({ - // method: 'hardhat_impersonateAccount', - // params: [ACC_DAI], - // }) + await hre.network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [ACC_DAI], + }) - // const signer_dai = await ethers.getSigner(ACC_DAI) - // await token_dai.connect(signer_dai).transfer(wallet0.getAddress(), Dai) + const signer_dai = await ethers.getSigner(ACC_DAI) + await token_dai.connect(signer_dai).transfer(wallet0.getAddress(), Dai) - // await hre.network.provider.request({ - // method: 'hardhat_stopImpersonatingAccount', - // params: [ACC_DAI], - // }) + await hre.network.provider.request({ + method: 'hardhat_stopImpersonatingAccount', + params: [ACC_DAI], + }) - // await token_dai.connect(wallet0).transfer(dsaWallet0.address, Dai); + await token_dai.connect(wallet0).transfer(dsaWallet0.address, Dai); - // expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)).to.be.gte( - // parseUnits('1', 18) - // ); - // }); + expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)).to.be.gte( + parseUnits('1', 18) + ); + }); }); describe("Main", function () { @@ -281,7 +281,7 @@ describe("Morpho-Aave-v3", function () { ); }) - it("Should withdraw ETH max on behalf", async function () { + it("Should revert because behalf is different with dsa address", async function () { const spells = [ { connector: connectorName, @@ -290,13 +290,10 @@ describe("Morpho-Aave-v3", function () { }, ]; - const tx = await dsaWallet0 + await expect(dsaWallet0 .connect(wallet0) - .cast(...encodeSpells(spells), wallet1.getAddress()); + .cast(...encodeSpells(spells), wallet1.getAddress())).to.be.revertedWith("cannot convert"); - await tx.wait(); - console.log("------------user balance----------",(await token_weth.balanceOf(user)).toString()) - expect(await token_weth.balanceOf(user)).to.be.gte(parseUnits('1', 18)) }) it("Should borrow WETH into DSA", async function () { @@ -314,7 +311,6 @@ describe("Morpho-Aave-v3", function () { .cast(...encodeSpells(spells), wallet1.getAddress()); await tx.wait(); - console.log("====================", balance.toString(), (await token_weth.balanceOf(dsaWallet0.address)).toString()) expect((await token_weth.balanceOf(dsaWallet0.address)).sub(balance)) .to.be.eq(parseUnits('5', 17)); }) @@ -334,7 +330,6 @@ describe("Morpho-Aave-v3", function () { .cast(...encodeSpells(spells), wallet1.getAddress()); await tx.wait(); - console.log("====================", balance.toString(), (await token_weth.balanceOf(user)).toString()) expect((await token_weth.balanceOf(user)).sub(balance)) .to.be.eq(parseUnits('2', 17)); }) @@ -345,7 +340,7 @@ describe("Morpho-Aave-v3", function () { { connector: connectorName, method: "borrowWithMaxIterations", - args: [tokens.weth.address, "200000000000000000", dsaWallet0.address, 10, "0", "0"], // 0.7 WETH + args: [tokens.weth.address, "20000000000000000", dsaWallet0.address, 10, "0", "0"], // 0.7 WETH }, ]; @@ -356,199 +351,199 @@ describe("Morpho-Aave-v3", function () { await tx.wait(); console.log("====================", balance.toString(), (await token_weth.balanceOf(dsaWallet0.address)).toString()) expect((await token_weth.balanceOf(dsaWallet0.address)).sub(balance)) - .to.be.eq(parseUnits('2', 17)); + .to.be.eq(parseUnits('2', 16)); }) - // it("Should borrow DAI into DSA on behalf", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "borrowOnBehalf", - // args: [tokens.dai.address, "10000000000000000000", user, dsaWallet0.address, "0", "0"], // 10 DAI - // }, - // ]; + it("Should borrow DAI into DSA on behalf", async function () { + const spells = [ + { + connector: connectorName, + method: "borrowOnBehalf", + args: [tokens.dai.address, "10000000000000000000", user, dsaWallet0.address, "0", "0"], // 10 DAI + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)) - // .to.be.gte(parseUnits('21', 18)); - // }) + await tx.wait(); + expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)) + .to.be.gte(parseUnits('21', 18)); + }) - // it("Should borrow DAI into DSA with MaxIteration", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "borrowWithMaxIterations", - // args: [tokens.dai.address, "10000000000000000000", dsaWallet0.address, 5, "0", "0"], // 10 DAI - // }, - // ]; + it("Should borrow DAI into DSA with MaxIteration", async function () { + const spells = [ + { + connector: connectorName, + method: "borrowWithMaxIterations", + args: [tokens.dai.address, "10000000000000000000", dsaWallet0.address, 5, "0", "0"], // 10 DAI + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)) - // .to.be.gte(parseUnits('31', 18)); - // }) + await tx.wait(); + expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)) + .to.be.gte(parseUnits('31', 18)); + }) - // it("Should borrow DAI into DSA on behalf with MaxIteration", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "borrowOnBehalfWithMaxIterations", - // args: [tokens.dai.address, "10000000000000000000", user, dsaWallet0.address, 5, "0", "0"], // 10 DAI - // }, - // ]; + it("Should borrow DAI into DSA on behalf with MaxIteration", async function () { + const spells = [ + { + connector: connectorName, + method: "borrowOnBehalfWithMaxIterations", + args: [tokens.dai.address, "10000000000000000000", user, dsaWallet0.address, 5, "0", "0"], // 10 DAI + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)) - // .to.be.gte(parseUnits('41', 18)); - // }) + await tx.wait(); + expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)) + .to.be.gte(parseUnits('41', 18)); + }) - // it("Should payback DAI MAX", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "payback", - // args: [tokens.dai.address, dsaMaxValue, "0", "0"], // Max DAI - // }, - // ]; + it("Should payback DAI MAX", async function () { + const spells = [ + { + connector: connectorName, + method: "payback", + args: [tokens.dai.address, dsaMaxValue, "0", "0"], // Max DAI + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)).to.be.lte( - // parseUnits('1', 18) - // ); - // }) + await tx.wait(); + expect(await token_dai.connect(masterSigner).balanceOf(dsaWallet0.address)).to.be.lte( + parseUnits('1', 18) + ); + }) - // it("Should payback ETH on behalf", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "paybackOnBehalf", - // args: [tokens.eth.address, user, dsaMaxValue, "0", "0"], // Max ETH - // }, - // ]; + it("Should payback ETH on behalf", async function () { + const spells = [ + { + connector: connectorName, + method: "paybackOnBehalf", + args: [tokens.eth.address, user, dsaMaxValue, "0", "0"], // Max ETH + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.lte( - // parseUnits('125', 18) - // ); - // }) + await tx.wait(); + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.lte( + parseUnits('125', 18) + ); + }) - // it("Should withdraw 8 USDC on behalf", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "withdrawOnBehalf", - // args: [tokens.usdc.address, "8000000", user, dsaWallet0.address, "0", "0"], // 8 USDC - // }, - // ]; + it("Should withdraw 8 USDC on behalf", async function () { + const spells = [ + { + connector: connectorName, + method: "withdrawOnBehalf", + args: [tokens.usdc.address, "8000000", user, dsaWallet0.address, "0", "0"], // 8 USDC + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) - // // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( - // // parseUnits('398', 6)) - // // ); - // }) + await tx.wait(); + console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) + // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( + // parseUnits('398', 6)) + // ); + }) - // it("Should withdraw 8 USDC on behalf", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "withdrawOnBehalf", - // args: [tokens.usdc.address, "8000000", user, dsaWallet0.address, "0", "0"], // 8 USDC - // }, - // ]; + it("Should withdraw 8 USDC on behalf", async function () { + const spells = [ + { + connector: connectorName, + method: "withdrawOnBehalf", + args: [tokens.usdc.address, "8000000", user, dsaWallet0.address, "0", "0"], // 8 USDC + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) - // // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( - // // parseUnits('398', 6)) - // // ); - // }) + await tx.wait(); + console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) + // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( + // parseUnits('398', 6)) + // ); + }) - // it("Should withdraw 8 USDC on behalf with MaxIteration", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "withdrawWithMaxIterations", - // args: [tokens.usdc.address, "8000000", user, dsaWallet0.address, 5, "0", "0"], // 8 USDC - // }, - // ]; + it("Should withdraw 8 USDC on behalf with MaxIteration", async function () { + const spells = [ + { + connector: connectorName, + method: "withdrawWithMaxIterations", + args: [tokens.usdc.address, "8000000", user, dsaWallet0.address, 5, "0", "0"], // 8 USDC + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) - // // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( - // // parseUnits('398', 6)) - // // ); - // }) + await tx.wait(); + console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) + // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( + // parseUnits('398', 6)) + // ); + }) - // it("Should withdraw 8 USDC as collateral", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "withdrawCollateral", - // args: [tokens.usdc.address, "8000000", dsaWallet0.address, 5, "0", "0"], // 8 USDC - // }, - // ]; + it("Should withdraw 8 USDC as collateral", async function () { + const spells = [ + { + connector: connectorName, + method: "withdrawCollateral", + args: [tokens.usdc.address, "8000000", dsaWallet0.address, 5, "0", "0"], // 8 USDC + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) - // // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( - // // parseUnits('398', 6)) - // // ); - // }) + await tx.wait(); + console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) + // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( + // parseUnits('398', 6)) + // ); + }) - // it("Should withdraw 8 USDC as collateral on behalf", async function () { - // const spells = [ - // { - // connector: connectorName, - // method: "withdrawCollateralOnBehalf", - // args: [tokens.usdc.address, "8000000",user, dsaWallet0.address, 5, "0", "0"], // 8 USDC - // }, - // ]; + it("Should withdraw 8 USDC as collateral on behalf", async function () { + const spells = [ + { + connector: connectorName, + method: "withdrawCollateralOnBehalf", + args: [tokens.usdc.address, "8000000",user, dsaWallet0.address, 5, "0", "0"], // 8 USDC + }, + ]; - // const tx = await dsaWallet0 - // .connect(wallet0) - // .cast(...encodeSpells(spells), wallet1.getAddress()); + const tx = await dsaWallet0 + .connect(wallet0) + .cast(...encodeSpells(spells), wallet1.getAddress()); - // await tx.wait(); - // console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) - // // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( - // // parseUnits('398', 6)) - // // ); - // }) + await tx.wait(); + console.log("----balance of USDC----", (await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).toString()) + // expect(expect(await token_usdc.connect(wallet0).balanceOf(dsaWallet0.address)).to.be.gte( + // parseUnits('398', 6)) + // ); + }) }); });