From 72715e03318e15325771ae0403d6fd659cb7798e Mon Sep 17 00:00:00 2001 From: Xykota Date: Thu, 23 Nov 2023 13:39:49 +0700 Subject: [PATCH] wip(hardhat): warplink energyshield --- .../hardhat/contracts/facets/WarpLink.sol | 235 +++++++++++++++++ .../foundry/WarpLink/Mainnet-17853419.t.sol | 244 ++++++++++++++++++ 2 files changed, 479 insertions(+) diff --git a/packages/hardhat/contracts/facets/WarpLink.sol b/packages/hardhat/contracts/facets/WarpLink.sol index 1ffeb48c..bb56294b 100644 --- a/packages/hardhat/contracts/facets/WarpLink.sol +++ b/packages/hardhat/contracts/facets/WarpLink.sol @@ -19,6 +19,7 @@ import {PermitParams} from '../libraries/PermitParams.sol'; import {IStargateRouter} from '../interfaces/external/IStargateRouter.sol'; import {IStargateReceiver} from '../interfaces/external/IStargateReceiver.sol'; import {IStargateComposer} from '../interfaces/external/IStargateComposer.sol'; +import {IEnergyShield} from '../interfaces/IEnergyShield.sol'; abstract contract WarpLinkCommandTypes { uint256 internal constant COMMAND_TYPE_WRAP = 1; @@ -30,6 +31,8 @@ abstract contract WarpLinkCommandTypes { uint256 internal constant COMMAND_TYPE_WARP_UNI_V3_LIKE_EXACT_INPUT = 7; uint256 internal constant COMMAND_TYPE_WARP_CURVE_EXACT_INPUT_SINGLE = 8; uint256 internal constant COMMAND_TYPE_JUMP_STARGATE = 9; + uint256 internal constant COMMAND_TYPE_WARP_STATELESS_SINGLE = 10; + uint256 internal constant COMMAND_TYPE_WARP_STATELESS_MULTI = 11; } contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes { @@ -74,6 +77,27 @@ contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes { bytes payload; } + struct WarpStatelessSingleParams { + address tokenOut; + address target; + bytes data; + bool push; + bool delivers; + } + + struct WarpStatelessMultiParams { + address tokenOut; + address[] targets; + bytes data; + uint256[] offsets; + /** + * The current token amount will be written as uint256 to these offsets in the data. + */ + uint256[] amountOffsets; + bool push; + bool delivers; + } + struct TransientState { address paramPartner; uint16 paramFeeBps; @@ -904,6 +928,213 @@ contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes { return t; } + /** + * Warp using the Energ yhield (single call) + * + * While the target call is not trusted, the Energy Shield is trusted to report + * the correct output amount. + * + * The payer can be the sender or this contract. The token may be ETH (0) + * + * After this operation, the token will be `params.tokenOut` and the amount will + * be the output of the swap. The next payer will be this contract. + * + * Params are read from the stream as: + * - tokenOut (address) + * - target (address) + * - data (bytes) + * - amountOffset (uint16), when > 0 the amount is written this many bytes into `data` + * - push (uint8, 0 or 1) + * - delivers (uint8, 0 or 1) + */ + function processWarpStatelessSingle( + uint256 stream, + TransientState memory t + ) internal returns (TransientState memory) { + WarpStatelessSingleParams memory params; + + params.tokenOut = stream.readAddress(); + params.target = stream.readAddress(); + + bytes memory data = stream.readBytes(); + + uint256 amountOffset = stream.readUint16(); + + if (amountOffset > 0) { + uint256 amount = t.amount; + + // Write amount at the offset in data + assembly { + mstore(add(add(data, 32), amountOffset), amount) + } + } + + params.data = data; + params.push = stream.readUint8() == 1; + params.delivers = stream.readUint8() == 1; + + IEnergyShield energyShield = LibWarp.state().energyShield; + + // Transfer tokens from the sender to this contract when `params.push` is true, else to the target + // Native tokens are never pushed to the target, but included as value in the call + if (t.payer == address(this)) { + if (t.token != address(0)) { + IERC20(t.token).safeTransfer(params.push ? params.target : address(energyShield), t.amount); + } + } else { + // Transfer tokens from the sender to the energy shield + if (t.usePermit == 1) { + // NOTE: `t.usePermit` is left as 1 + LibWarp.state().permit2.transferFrom( + t.payer, + params.push ? params.target : address(energyShield), + uint160(t.amount), + t.token + ); + } else { + IERC20(t.token).safeTransferFrom( + t.payer, + params.push ? params.target : address(energyShield), + t.amount + ); + } + + // Update the payer to this contract + t.payer = address(this); + } + + // NOTE: The EnergyShield is trusted to report the correct output amount + t.amount = LibWarp.state().energyShield.single{value: t.token == address(0) ? t.amount : 0}( + IEnergyShield.SingleParams({ + tokenOut: params.tokenOut, + target: params.target, + data: params.data, + delivers: params.delivers + }) + ); + + t.token = params.tokenOut; + + return t; + } + + /** + * Warp using the Energy Shield (multi call) + * + * While the target call is not trusted, the Energy Shield is trusted to report + * the correct output amount. + * + * The payer can be the sender or this contract. The token may be ETH (0) + * + * After this operation, the token will be `params.tokenOut` and the amount will + * be the output of the swap. The next payer will be this contract. + * + * Params are read from the stream as: + * - tokenOut (address) + * - target count (uint8) + * - targets (target count of address) + * - data (bytes) + * - offsets (target count - 1 of uint16) + * - push (uint8, 0 or 1) + * - delivers (uint8, 0 or 1) + */ + function processWarpStatelessMulti( + uint256 stream, + TransientState memory t + ) internal returns (TransientState memory) { + WarpStatelessMultiParams memory params; + + params.tokenOut = stream.readAddress(); + + uint256 targetCount = stream.readUint8(); + params.targets = new address[](targetCount); + + unchecked { + for (uint256 index; index < targetCount; index++) { + params.targets[index] = stream.readAddress(); + } + } + + bytes memory data = stream.readBytes(); + + unchecked { + params.offsets = new uint256[](targetCount - 1); + } + + unchecked { + for (uint256 index = 0; index < targetCount - 1; index++) { + params.offsets[index] = stream.readUint16(); + } + } + + uint256 amountOffsetsLength = stream.readUint8(); + + unchecked { + for (uint256 index; index < amountOffsetsLength; index++) { + uint256 amountOffset = stream.readUint16(); + + uint256 amount = t.amount; + + // Write amount at the offset in data + assembly { + mstore(add(add(data, 32), amountOffset), amount) + } + } + } + + params.data = data; + params.push = stream.readUint8() == 1; + params.delivers = stream.readUint8() == 1; + + IEnergyShield energyShield = LibWarp.state().energyShield; + + // Transfer tokens from the sender to this contract when `params.push` is true, else to the target + // Native tokens are never pushed to the target, but included as value in the call + if (t.payer == address(this)) { + if (t.token != address(0)) { + IERC20(t.token).safeTransfer( + params.push ? params.targets[0] : address(energyShield), + t.amount + ); + } + } else { + // Transfer tokens from the sender to the energy shield + if (t.usePermit == 1) { + // NOTE: `t.usePermit` is left as 1 + LibWarp.state().permit2.transferFrom( + t.payer, + params.push ? params.targets[0] : address(energyShield), + uint160(t.amount), + t.token + ); + } else { + IERC20(t.token).safeTransferFrom( + t.payer, + params.push ? params.targets[0] : address(energyShield), + t.amount + ); + } + + // Update the payer to this contract + t.payer = address(this); + } + + // NOTE: The EnergyShield is trusted to report the correct output amount + t.amount = LibWarp.state().energyShield.multi{value: t.token == address(0) ? t.amount : 0}( + IEnergyShield.MultiParams({ + tokenOut: params.tokenOut, + targets: params.targets, + data: params.data, + offsets: params.offsets, + delivers: params.delivers + }) + ); + + t.token = params.tokenOut; + + return t; + } + function engageInternal( uint256 stream, TransientState memory t @@ -937,6 +1168,10 @@ contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes { } t = processJumpStargate(stream, t); + } else if (commandType == COMMAND_TYPE_WARP_STATELESS_SINGLE) { + t = processWarpStatelessSingle(stream, t); + } else if (commandType == COMMAND_TYPE_WARP_STATELESS_MULTI) { + t = processWarpStatelessMulti(stream, t); } else { revert UnhandledCommand(); } diff --git a/packages/hardhat/test/foundry/WarpLink/Mainnet-17853419.t.sol b/packages/hardhat/test/foundry/WarpLink/Mainnet-17853419.t.sol index 1c3d861e..c3e529c5 100644 --- a/packages/hardhat/test/foundry/WarpLink/Mainnet-17853419.t.sol +++ b/packages/hardhat/test/foundry/WarpLink/Mainnet-17853419.t.sol @@ -1,8 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import {IUniswapV2Router02} from '@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol'; +import {IWETH} from '@uniswap/v2-periphery/contracts/interfaces/IWETH.sol'; import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol'; import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import {IUniswapV2Factory} from 'contracts/interfaces/external/IUniswapV2Factory.sol'; import {IWarpLink} from 'contracts/interfaces/IWarpLink.sol'; import {IAllowanceTransfer} from 'contracts/interfaces/external/IAllowanceTransfer.sol'; import {PermitParams} from 'contracts/libraries/PermitParams.sol'; @@ -1388,4 +1391,245 @@ contract WarpLinkMainnet17853419Test is WarpLinkTestBase, CurveHelpers { assertEq(Mainnet.WETH.balanceOf(user), 6315168, 'balance'); } + + function testFork_warpLinkEngage_warpStatelessSingle_wrap() public { + bytes memory data = abi.encodeWithSelector(IWETH.deposit.selector); + + bytes memory commands = abi.encodePacked( + uint8(1), // Command count + uint8(COMMAND_TYPE_WARP_STATELESS_SINGLE), + address(Mainnet.WETH), // tokenOut + address(Mainnet.WETH), // target + uint256(data.length), // data.length + data, // data + uint16(0), // amountOffset + uint8(0), // push + uint8(0) // delivers + ); + + vm.deal(user, 1 ether); + vm.prank(user); + + facet.warpLinkEngage{value: 1 ether}( + IWarpLink.Params({ + tokenIn: address(0), + tokenOut: address(Mainnet.WETH), + commands: commands, + amountIn: 1 ether, + amountOut: 1 ether, + recipient: user, + partner: partner, + feeBps: 10, + slippageBps: 0, + deadline: deadline + }) + ); + + assertEq(Mainnet.WETH.balanceOf(user), 0.999 ether, 'balance'); + } + + function testFork_warpLinkEngage_warpStatelessSingle_ethToWbtc() public { + address tokenIn = address(0); + address tokenOut = address(Mainnet.WBTC); + + IUniswapV2Router02 router = IUniswapV2Router02(Mainnet.UNISWAP_V2_ROUTER_02_ADDR); + + address[] memory path = new address[](2); + path[0] = address(Mainnet.WETH); + path[1] = tokenOut; + + bytes memory data = abi.encodeWithSelector( + router.swapExactETHForTokens.selector, + 1, + path, + address(diamond), + deadline + ); + + bytes memory commands = abi.encodePacked( + uint8(1), // Command count + uint8(COMMAND_TYPE_WARP_STATELESS_SINGLE), + address(tokenOut), // tokenOut + address(router), // target + uint256(data.length), // data.length + data, // data + uint16(0), // amountOffset + uint8(0), // push + uint8(1) // delivers + ); + + vm.deal(user, 1 ether); + vm.prank(user); + + facet.warpLinkEngage{value: 1 ether}( + IWarpLink.Params({ + tokenIn: tokenIn, + tokenOut: tokenOut, + commands: commands, + amountIn: 1 ether, + amountOut: 6316483, + recipient: user, + partner: partner, + feeBps: 10, + slippageBps: 0, + deadline: deadline + }) + ); + + assertEq(Mainnet.WBTC.balanceOf(user), 6310167, 'balance'); + } + + function testFork_warpLinkEngage_warpStatelessMulti_usdcToWbtc() public { + uint256 amountIn = 2000 * (10 ** 6); + address tokenIn = address(Mainnet.USDC); + address tokenOut = address(Mainnet.WBTC); + + IUniswapV2Router02 router = IUniswapV2Router02(Mainnet.UNISWAP_V2_ROUTER_02_ADDR); + + bytes memory dataApprove = abi.encodeWithSelector( + IERC20.approve.selector, + address(router), + 0 // This will be overwritten + ); + + address[] memory path = new address[](2); + path[0] = tokenIn; + path[1] = tokenOut; + + bytes memory dataSwap = abi.encodeWithSelector( + router.swapExactTokensForTokens.selector, + 0, // This will be overwritten + 6572604, + path, + address(diamond), + deadline + ); + + bytes memory data = bytes.concat(dataApprove, dataSwap); + + bytes memory commands = abi.encodePacked( + uint8(1), // Command count + uint8(COMMAND_TYPE_WARP_STATELESS_MULTI), + address(tokenOut), // tokenOut + uint8(2), // targets.length + address(Mainnet.USDC), // targets[0] + address(router), // targets[1] + uint256(data.length), // data.length + data, // data + uint16(dataApprove.length), // offsets[0] + uint8(2), // amountOffsets.length + uint16(4 + 32), // amountOffsets[0], after selector and approve address (all params are 32 bytes) + uint16(dataApprove.length + 4), // amountOffsets[1], after approve data and swap function selector + uint8(0), // push + uint8(1) // delivers + ); + + deal(address(Mainnet.USDC), user, amountIn); + + vm.prank(user); + Mainnet.USDC.approve(address(diamond), amountIn); + + vm.deal(user, 1 ether); + vm.prank(user); + + facet.warpLinkEngage( + IWarpLink.Params({ + tokenIn: tokenIn, + tokenOut: tokenOut, + commands: commands, + amountIn: amountIn, + amountOut: 6572604, + recipient: user, + partner: partner, + feeBps: 10, + slippageBps: 10, + deadline: deadline + }) + ); + + assertEq(Mainnet.WBTC.balanceOf(user), 6566032, 'balance'); + } + + function testFork_warpLinkEngagePermit_warpStatelessMulti_usdcToWbtc() public { + uint256 amountIn = 2000 * (10 ** 6); + address tokenIn = address(Mainnet.USDC); + address tokenOut = address(Mainnet.WBTC); + + IUniswapV2Router02 router = IUniswapV2Router02(Mainnet.UNISWAP_V2_ROUTER_02_ADDR); + + bytes memory dataApprove = abi.encodeWithSelector( + IERC20.approve.selector, + address(router), + 0 // This will be overwritten + ); + + address[] memory path = new address[](2); + path[0] = tokenIn; + path[1] = tokenOut; + + bytes memory dataSwap = abi.encodeWithSelector( + router.swapExactTokensForTokens.selector, + 0, // This will be overwritten + 6572604, + path, + address(diamond), + deadline + ); + + bytes memory data = bytes.concat(dataApprove, dataSwap); + + bytes memory commands = abi.encodePacked( + uint8(1), // Command count + uint8(COMMAND_TYPE_WARP_STATELESS_MULTI), + address(tokenOut), // tokenOut + uint8(2), // targets.length + address(Mainnet.USDC), // targets[0] + address(router), // targets[1] + uint256(data.length), // data.length + data, // data + uint16(dataApprove.length), // offsets[0] + uint8(2), // amountOffsets.length + uint16(4 + 32), // amountOffsets[0], after selector and approve address (all params are 32 bytes) + uint16(dataApprove.length + 4), // amountOffsets[1], after approve data and swap function selector + uint8(0), // push + uint8(1) // delivers + ); + + deal(address(Mainnet.USDC), user, amountIn); + + vm.prank(user); + Mainnet.USDC.approve(address(Addresses.PERMIT2), amountIn); + + IAllowanceTransfer.PermitDetails memory details = IAllowanceTransfer.PermitDetails({ + token: address(Mainnet.USDC), + amount: (uint160)(amountIn), + expiration: deadline, + nonce: 0 + }); + + bytes memory sig = getPermitSignature( + IAllowanceTransfer.PermitSingle(details, address(diamond), deadline), + privateKey, + permit2.DOMAIN_SEPARATOR() + ); + + vm.prank(user); + facet.warpLinkEngagePermit( + IWarpLink.Params({ + tokenIn: tokenIn, + tokenOut: tokenOut, + commands: commands, + amountIn: amountIn, + amountOut: 6572604, + recipient: user, + partner: partner, + feeBps: 10, + slippageBps: 10, + deadline: deadline + }), + PermitParams({nonce: details.nonce, signature: sig}) + ); + + assertEq(Mainnet.WBTC.balanceOf(user), 6566032, 'balance'); + } }