diff --git a/.github/workflows/hardhat.yml b/.github/workflows/hardhat.yml index 1b2dc0be..a4f37d85 100644 --- a/.github/workflows/hardhat.yml +++ b/.github/workflows/hardhat.yml @@ -23,3 +23,6 @@ jobs: - name: Build contracts & package with hardhat uses: ./.github/actions/build-hardhat + + - name: Run hardhat tests + run: yarn test:hardhat diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index 2066a2fd..6c00f11b 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -27,7 +27,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { /// @param wrapper The address of the ERC20 wrapper contract. /// @param amount The amount of underlying tokens to deposit. Pass `type(uint256).max` to deposit the bundler's /// balance. - function erc20WrapperDepositFor(address wrapper, uint256 amount) external protected { + function erc20WrapperDepositFor(address wrapper, uint256 amount) external payable protected { ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); amount = Math.min(amount, underlying.balanceOf(address(this))); @@ -44,7 +44,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { /// @param wrapper The address of the ERC20 wrapper contract. /// @param account The address receiving the underlying tokens. /// @param amount The amount of wrapped tokens to burn. Pass `type(uint256).max` to burn the bundler's balance. - function erc20WrapperWithdrawTo(address wrapper, address account, uint256 amount) external protected { + function erc20WrapperWithdrawTo(address wrapper, address account, uint256 amount) external payable protected { require(account != address(0), ErrorsLib.ZERO_ADDRESS); amount = Math.min(amount, ERC20(wrapper).balanceOf(address(this))); diff --git a/test/hardhat/EthereumBundler.spec.ts b/test/hardhat/EthereumBundler.spec.ts index 3055a51a..f72a2a39 100644 --- a/test/hardhat/EthereumBundler.spec.ts +++ b/test/hardhat/EthereumBundler.spec.ts @@ -1,7 +1,16 @@ +import { expect } from "chai"; import { AbiCoder, MaxUint256, Signature, keccak256, toBigInt, TypedDataDomain, TypedDataField } from "ethers"; import hre from "hardhat"; import { BundlerAction } from "pkg"; -import { ERC20Mock, ERC4626Mock, EthereumBundler, MorphoMock, OracleMock, IrmMock } from "types"; +import { + ERC20Mock, + ERC4626Mock, + EthereumBundler, + MorphoMock, + OracleMock, + IrmMock, + EthereumBundler__factory, +} from "types"; import { MarketParamsStruct } from "types/lib/morpho-blue/src/Morpho"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; @@ -25,32 +34,36 @@ const permit2Config: TypedDataConfig = { verifyingContract: permit2Address, }, types: { - PermitTransferFrom: [ + PermitSingle: [ { - name: "permitted", - type: "TokenPermissions", + name: "details", + type: "PermitDetails", }, { name: "spender", type: "address", }, { - name: "nonce", - type: "uint256", - }, - { - name: "deadline", + name: "sigDeadline", type: "uint256", }, ], - TokenPermissions: [ + PermitDetails: [ { name: "token", type: "address", }, { name: "amount", - type: "uint256", + type: "uint160", + }, + { + name: "expiration", + type: "uint48", + }, + { + name: "nonce", + type: "uint48", }, ], }, @@ -250,14 +263,17 @@ describe("EthereumBundler", () => { deadline: MAX_UINT48, }; - const permit2TransferFrom = { - permitted: { - token: await collateral.getAddress(), + const collateralAddress = await collateral.getAddress(); + + const approve2 = { + details: { + token: collateralAddress, amount: assets, + nonce: 0n, + expiration: MAX_UINT48, }, spender: bundlerAddress, - nonce: 0n, - deadline: MAX_UINT48, + sigDeadline: MAX_UINT48, }; await collateral.connect(borrower).approve(permit2Address, MaxUint256); @@ -278,12 +294,12 @@ describe("EthereumBundler", () => { ), false, ), - BundlerAction.permit2TransferFrom( - permit2TransferFrom, - Signature.from( - await borrower.signTypedData(permit2Config.domain, permit2Config.types, permit2TransferFrom), - ), + BundlerAction.approve2( + approve2, + Signature.from(await borrower.signTypedData(permit2Config.domain, permit2Config.types, approve2)), + false, ), + BundlerAction.transferFrom2(collateralAddress, assets), BundlerAction.morphoSupplyCollateral(marketParams, assets, borrower.address, []), BundlerAction.morphoBorrow(marketParams, assets / 2n, 0, MaxUint256, borrower.address), ]); @@ -297,15 +313,17 @@ describe("EthereumBundler", () => { const supplier = suppliers[i]; const assets = BigInt.WAD * toBigInt(1 + Math.floor(random() * 100)); + const collateralAddress = await collateral.getAddress(); - const permit2TransferFrom = { - permitted: { - token: await collateral.getAddress(), + const approve2 = { + details: { + token: collateralAddress, amount: assets, + expiration: MAX_UINT48, + nonce: 0n, }, spender: bundlerAddress, - nonce: 0n, - deadline: MAX_UINT48, + sigDeadline: MAX_UINT48, }; await collateral.connect(supplier).approve(permit2Address, MaxUint256); @@ -315,14 +333,24 @@ describe("EthereumBundler", () => { await bundler .connect(supplier) .multicall([ - BundlerAction.permit2TransferFrom( - permit2TransferFrom, - Signature.from( - await supplier.signTypedData(permit2Config.domain, permit2Config.types, permit2TransferFrom), - ), + BundlerAction.approve2( + approve2, + Signature.from(await supplier.signTypedData(permit2Config.domain, permit2Config.types, approve2)), + false, ), + BundlerAction.transferFrom2(collateralAddress, assets), BundlerAction.erc4626Deposit(erc4626Address, assets, 0, supplier.address), ]); } }); + + it("should have all batched functions payable", async () => { + EthereumBundler__factory.createInterface().forEachFunction((func) => { + if (func.stateMutability === "view" || func.stateMutability === "pure") return; + + const shouldPayable = !func.name.startsWith("onMorpho"); + + expect(func.payable).to.equal(shouldPayable); + }); + }); });