diff --git a/script/10_Swap.s.sol b/script/10_Swap.s.sol index 0adb86ac..dace215d 100644 --- a/script/10_Swap.s.sol +++ b/script/10_Swap.s.sol @@ -16,8 +16,10 @@ contract Swap is ScriptUtils { string memory json = getScriptFile(inputScriptFileName); address uniswapRouterV2 = vm.parseJsonAddress(json, ".uniswapRouterV2"); address uniswapRouterV3 = vm.parseJsonAddress(json, ".uniswapRouterV3"); + address evc = vm.parseJsonAddress(json, ".evc"); + address permit2 = vm.parseJsonAddress(json, ".permit2"); - (swapper, swapVerifier) = execute(uniswapRouterV2, uniswapRouterV3); + (swapper, swapVerifier) = execute(evc, permit2, uniswapRouterV2, uniswapRouterV3); string memory object; object = vm.serializeAddress("swap", "swapper", swapper); @@ -25,19 +27,19 @@ contract Swap is ScriptUtils { vm.writeJson(object, string.concat(vm.projectRoot(), "/script/", outputScriptFileName)); } - function deploy(address uniswapRouterV2, address uniswapRouterV3) + function deploy(address evc, address permit2, address uniswapRouterV2, address uniswapRouterV3) public broadcast returns (address swapper, address swapVerifier) { - (swapper, swapVerifier) = execute(uniswapRouterV2, uniswapRouterV3); + (swapper, swapVerifier) = execute(evc, permit2, uniswapRouterV2, uniswapRouterV3); } - function execute(address uniswapRouterV2, address uniswapRouterV3) + function execute(address evc, address permit2, address uniswapRouterV2, address uniswapRouterV3) public returns (address swapper, address swapVerifier) { swapper = address(new Swapper(uniswapRouterV2, uniswapRouterV3)); - swapVerifier = address(new SwapVerifier()); + swapVerifier = address(new SwapVerifier(evc, permit2)); } } diff --git a/script/50_CoreAndPeriphery.s.sol b/script/50_CoreAndPeriphery.s.sol index d0c45e2d..3b3c8e14 100644 --- a/script/50_CoreAndPeriphery.s.sol +++ b/script/50_CoreAndPeriphery.s.sol @@ -811,7 +811,7 @@ contract CoreAndPeriphery is BatchBuilder, SafeMultisendBuilder { console.log("+ Deploying Swapper..."); Swap deployer = new Swap(); (peripheryAddresses.swapper, peripheryAddresses.swapVerifier) = - deployer.deploy(input.uniswapV2Router, input.uniswapV3Router); + deployer.deploy(coreAddresses.evc, input.permit2, input.uniswapV2Router, input.uniswapV3Router); } else { console.log("- At least one of the Swapper contracts already deployed. Skipping..."); } diff --git a/src/Swaps/SwapVerifier.sol b/src/Swaps/SwapVerifier.sol index e4972629..8454c4ee 100644 --- a/src/Swaps/SwapVerifier.sol +++ b/src/Swaps/SwapVerifier.sol @@ -3,17 +3,23 @@ pragma solidity ^0.8.0; import {IEVault, IERC20} from "evk/EVault/IEVault.sol"; +import {TransferFromSender} from "./TransferFromSender.sol"; /// @title SwapVerifier /// @custom:security-contact security@euler.xyz /// @author Euler Labs (https://www.eulerlabs.com/) /// @notice Simple contract used to verify post swap conditions /// @dev This contract is the only trusted code in the EVK swap periphery -contract SwapVerifier { +contract SwapVerifier is TransferFromSender { error SwapVerifier_skimMin(); error SwapVerifier_debtMax(); error SwapVerifier_pastDeadline(); + /// @notice Contract constructor + /// @param evc Address of the EthereumVaultConnector contract + /// @param permit2 Address of the Permit2 contract + constructor(address evc, address permit2) TransferFromSender(evc, permit2) {} + /// @notice Verify results of a regular swap, when bought tokens are sent to the vault and skim for the buyer /// @param vault The EVault to query /// @param receiver Account to skim to diff --git a/src/Swaps/TransferFromSender.sol b/src/Swaps/TransferFromSender.sol new file mode 100644 index 00000000..1c1c0247 --- /dev/null +++ b/src/Swaps/TransferFromSender.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.8.0; + +import {IERC20} from "evk/EVault/IEVault.sol"; +import {SafeERC20Lib} from "evk/EVault/shared/lib/SafeERC20Lib.sol"; +import {EVCUtil} from "ethereum-vault-connector/utils/EVCUtil.sol"; + +/// @title TransferFromSender +/// @custom:security-contact security@euler.xyz +/// @author Euler Labs (https://www.eulerlabs.com/) +/// @notice Simple contract used to pull tokens from the sender. +/// @dev This contract is trusted and can safely receive allowance on token transfers from users. +contract TransferFromSender is EVCUtil { + using SafeERC20Lib for IERC20; + + error TransferFromSender_InvalidAddress(); + + /// @notice Address of Permit2 contract + address public immutable permit2; + + /// @notice Contract constructor + /// @param _permit2 Address of the Permit2 contract + constructor(address _evc, address _permit2) EVCUtil(_evc) { + if (_permit2 == address(0)) revert TransferFromSender_InvalidAddress(); + permit2 = _permit2; + } + + /// @notice Pull tokens from sender to the designated receiver + /// @param token ERC20 token address + /// @param amount Amount of the token to transfer + /// @param to Receiver of the token + function transferFromSender(address token, uint256 amount, address to) public { + IERC20(token).safeTransferFrom(_msgSender(), to, amount, permit2); + } +} diff --git a/test/Swaps/Swaps1Inch.sol b/test/Swaps/Swaps1Inch.sol index 3dfbb03f..1fbb359e 100644 --- a/test/Swaps/Swaps1Inch.sol +++ b/test/Swaps/Swaps1Inch.sol @@ -10,6 +10,9 @@ import {ISwapper} from "../../src/Swaps/ISwapper.sol"; import {Swapper} from "../../src/Swaps/Swapper.sol"; import {SwapVerifier} from "../../src/Swaps/SwapVerifier.sol"; +import {Permit2ECDSASigner} from "evk-test/mocks/Permit2ECDSASigner.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; + import "./Payloads.sol"; import "forge-std/Test.sol"; @@ -25,6 +28,7 @@ contract Swaps1Inch is EVaultTestBase { address constant uniswapRouterV2 = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; address constant uniswapRouterV3 = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address constant uniswapRouter02 = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45; + address constant permit2Address = 0x000000000022D473030F116dDEE9F6B43aC78BA3; address constant GRT = 0xc944E90C64B2c07662A292be6244BDf05Cda44a7; address constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; @@ -53,7 +57,7 @@ contract Swaps1Inch is EVaultTestBase { user2 = makeAddr("user2"); swapper = new Swapper(uniswapRouterV2, uniswapRouterV3); - swapVerifier = new SwapVerifier(); + swapVerifier = new SwapVerifier(address(evc), permit2Address); if (bytes(FORK_RPC_URL).length != 0) { mainnetFork = vm.createSelectFork(FORK_RPC_URL); @@ -1453,4 +1457,75 @@ contract Swaps1Inch is EVaultTestBase { assertEq(eTST.debtOf(user2), 0); assertEq(eTST.balanceOf(user2), 2e18); } + + function test_transferFromSender_allowance() external { + assetTST.mint(user, 1e18); + assertEq(assetTST.balanceOf(address(swapper)), 0); + + startHoax(user); + vm.expectRevert(); // no approvals + swapVerifier.transferFromSender(address(assetTST), 1e18, address(swapper)); + + assetTST.approve(address(swapVerifier), 2e18); + + startHoax(user2); + vm.expectRevert(); // other users can't pull + swapVerifier.transferFromSender(address(assetTST), 1e18, address(swapper)); + + startHoax(user); + swapVerifier.transferFromSender(address(assetTST), 1e18, address(swapper)); + + assertEq(assetTST.balanceOf(address(swapper)), 1e18); + } + + function test_transferFromSender_permit2() external { + assertEq(assetTST.balanceOf(address(swapper)), 0); + + Permit2ECDSASigner permit2Signer = new Permit2ECDSASigner(address(permit2)); + + uint256 userPK = 0x123400; + address signer = vm.addr(userPK); + + assetTST.mint(signer, 1e18); + + startHoax(signer); + vm.expectRevert(); // no approvals + swapVerifier.transferFromSender(address(assetTST), 1e18, address(swapper)); + + + // approve permit2 contract to spend the tokens + assetTST.approve(permit2, type(uint160).max); + + // build permit2 object + IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({ + details: IAllowanceTransfer.PermitDetails({ + token: address(assetTST), + amount: type(uint160).max, + expiration: type(uint48).max, + nonce: 0 + }), + spender: address(swapVerifier), + sigDeadline: type(uint256).max + }); + + IEVC.BatchItem[] memory items = new IEVC.BatchItem[](2); + items[0].onBehalfOfAccount = signer; + items[0].targetContract = permit2; + items[0].value = 0; + items[0].data = abi.encodeWithSignature( + "permit(address,((address,uint160,uint48,uint48),address,uint256),bytes)", + signer, + permitSingle, + permit2Signer.signPermitSingle(userPK, permitSingle) + ); + + items[1].onBehalfOfAccount = signer; + items[1].targetContract = address(swapVerifier); + items[1].value = 0; + items[1].data = abi.encodeCall(swapVerifier.transferFromSender, (address(assetTST), 1e18, address(swapper))); + + evc.batch(items); + + assertEq(assetTST.balanceOf(address(swapper)), 1e18); + } }