Skip to content

Commit

Permalink
feat(hardhat): support Stargate ETH in WarpLink
Browse files Browse the repository at this point in the history
  • Loading branch information
xykota committed Oct 5, 2023
1 parent 3ba7f45 commit 0659258
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 21 deletions.
35 changes: 22 additions & 13 deletions packages/hardhat/contracts/facets/WarpLink.sol
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,14 @@ contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes {

Params memory params = abi.decode(payload, (Params));

if (params.tokenIn == address(0)) {
// Distinguish between receiving ETH and SGETH. Note that the `params.tokenIn` address is useless
// otherwise because `_token` may be different on this chain
_token = address(0);
}

try
IWarpLink(this).warpLinkEngage(
IWarpLink(this).warpLinkEngage{value: _token == address(0) ? amountLD : 0}(
Params({
partner: params.partner,
feeBps: params.feeBps,
Expand All @@ -705,15 +711,17 @@ contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes {
)
{} catch {
// Refund tokens to the recipient
IERC20(_token).safeTransfer(params.recipient, amountLD);
if (_token == address(0)) {
payable(params.recipient).transfer(amountLD);
} else {
IERC20(_token).safeTransfer(params.recipient, amountLD);
}
}
}

/**
* Jump to another chain using the Stargate bridge
*
* The token must not be ETH (0)
*
* After this operation, the token will be unchanged and `t.amount` will
* be how much was sent. `t.jumped` will be set to `1` to indicate
* that no more commands should be run
Expand Down Expand Up @@ -741,11 +749,6 @@ contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes {
uint256 stream,
TransientState memory t
) internal returns (TransientState memory) {
if (t.token == address(0)) {
// NOTE: There is a WETH pool
revert NativeTokenNotSupported();
}

// TODO: Does this use the same gas than (a, b, c,) = (stream.read, ...)?
JumpStargateParams memory params;
params.dstChainId = stream.readUint16();
Expand All @@ -754,12 +757,15 @@ contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes {
params.dstGasForCall = stream.readUint32();

if (params.dstGasForCall > 0) {
// NOTE: `tokenIn`, `amountIn` are not required
// NOTE: `amountIn` is left as zero
Params memory destParams;
destParams.partner = t.paramPartner;
destParams.feeBps = t.paramFeeBps;
destParams.slippageBps = t.paramSlippageBps;
destParams.recipient = t.paramRecipient;
// NOTE: Used to distinguish ETH vs SGETH. Tokens on the other chain do not not necessarily have
// the same address as on this chain
destParams.tokenIn = t.token;
destParams.tokenOut = stream.readAddress();
destParams.amountOut = stream.readUint256();
destParams.deadline = t.paramDeadline;
Expand Down Expand Up @@ -809,16 +815,19 @@ contract WarpLink is IWarpLink, IStargateReceiver, WarpLinkCommandTypes {
// Swap on the composer if there is a payload, else the router
IStargateRouter(
params.payload.length == 0 ? stargateComposer.stargateRouter() : address(stargateComposer)
).swap{value: t.nativeValueRemaining}({
).swap{
value: t.token == address(0) ? t.amount + t.nativeValueRemaining : t.nativeValueRemaining
}({
_dstChainId: params.dstChainId,
_srcPoolId: params.srcPoolId,
_dstPoolId: params.dstPoolId,
// NOTE: There is no guarantee that `msg.sender` can handle receiving tokens/ETH
// TODO: Use `msg.sender` if it's EOA, else use this contract
_refundAddress: payable(address(this)),
_amountLD: t.amount,
// Max 5% slippage
_minAmountLD: (t.amount * 95) / 100,
// NOTE: This is imperfect because the user may already have eaten some slippage and may eat
// more on the other chain. It also assumes the tokens are of nearly equal value
_minAmountLD: LibWarp.applySlippage(t.amount, t.paramSlippageBps),
_lzTxParams: IStargateRouter.lzTxObj({
dstGasForCall: params.dstGasForCall,
dstNativeAmount: 0,
Expand Down
113 changes: 113 additions & 0 deletions packages/hardhat/script/jumpAndEngageEthGoerli.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import 'forge-std/console2.sol';
import 'forge-std/Script.sol';
import {IERC20} from '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import {IWarpLink} from 'contracts/interfaces/IWarpLink.sol';
import {WarpLinkCommandTypes} from 'contracts/facets/WarpLink.sol';
import {IStargateComposer} from 'test/foundry/helpers/IStargateComposer.sol';
import {IStargateRouter} from 'contracts/interfaces/external/IStargateRouter.sol';
import {IAllowanceTransfer} from 'contracts/interfaces/external/IAllowanceTransfer.sol';
import {PermitParams} from 'contracts/libraries/PermitParams.sol';
import {Goerli, OptimismGoerli, Addresses} from '../test/foundry/helpers/Networks.sol';
import {PermitSignature} from 'test/foundry/helpers/PermitSignature.sol';

/**
* Bridge mock USDC from Goerli to Optimism-Goerli and invoke WarpLink on the other
* side, but with no commands.
*
* Adding commands would be even more interesting, but
* there are no actions that can be made using Statgate mock USDC.
*
* Invoke this script using:
* forge script script/jumpAndEngageGoerli.sol --rpc-url goerli --no-storage-caching -vvvv --broadcast
*/
contract JumpAndEngageEthGoerli is Script, WarpLinkCommandTypes, PermitSignature {
address goerliDiamondAddr = 0x2A104392321e978495dBC91b68914eDbA3126D9c;

function setUp() public {}

function run() public {
uint256 privateKey = vm.deriveKey(vm.envString('DEV_MNEMONIC'), 1);
address user = vm.rememberKey(privateKey);

console2.log('User: %s', user);

uint48 deadline = uint48(block.timestamp) + 60 * 60 * 24 * 365;

bytes memory destCommands = abi.encodePacked(
(uint8)(0) // Command count
);

// NOTE: Only including vartiable length field
bytes memory destParamsEncoded;

{
IWarpLink.Params memory destParams;
destParams.commands = destCommands;

destParamsEncoded = abi.encode(destParams);
}

uint256 srcAmountIn = 0.0005 ether;
uint256 dstGasForCall = 500_000;
uint16 dstChainId = OptimismGoerli.STARGATE_CHAIN_ID;

bytes memory sourceCommands = bytes.concat(
abi.encodePacked(
(uint8)(1), // Command count
(uint8)(COMMAND_TYPE_JUMP_STARGATE),
(uint16)(dstChainId), // dstChainId
(uint8)(13), // srcPoolId (SGETH)
(uint8)(13), // dstPoolId (SGETH),
uint32(dstGasForCall), // dstGasForCall
address(0), // destParams.tokenOut
// NOTE: There's massive slippage on the testnet
uint256((srcAmountIn * 99) / 100), // destParams.amountOut
uint256(destCommands.length) // destParams.commands.length
),
destCommands // destParams.commands
);

(uint256 nativeWei, ) = IStargateComposer(Goerli.STARGATE_COMPOSER_ADDR).quoteLayerZeroFee({
_dstChainId: dstChainId,
_functionType: 1, // Swap remote
_toAddress: abi.encodePacked(goerliDiamondAddr),
_transferAndCallPayload: destParamsEncoded,
_lzTxParams: IStargateRouter.lzTxObj({
dstGasForCall: dstGasForCall,
dstNativeAmount: 0,
dstNativeAddr: ''
})
});

console2.log('Native fee: %s', nativeWei);

PermitParams memory permitParams;

vm.startBroadcast(user);

console2.log('User balance: %s', user.balance);

IWarpLink(goerliDiamondAddr).warpLinkEngage{value: nativeWei + srcAmountIn}(
IWarpLink.Params({
tokenIn: address(0),
tokenOut: address(0),
commands: sourceCommands,
amountIn: srcAmountIn,
amountOut: srcAmountIn,
recipient: user,
partner: address(0),
feeBps: 0,
// NOTE: There's massive slippage on the testnet
slippageBps: 100 * 20,
deadline: deadline
}),
permitParams
);

vm.stopBroadcast();
}
}
112 changes: 104 additions & 8 deletions packages/hardhat/test/foundry/WarpLink.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,7 @@ contract WarpLinkMainnet18240282Test is WarpLinkTestBase {
recipient: USER,
partner: address(0),
feeBps: 0,
slippageBps: 0,
slippageBps: 100,
deadline: deadline
}),
PermitParams({nonce: permit.details.nonce, signature: sig})
Expand Down Expand Up @@ -1213,7 +1213,7 @@ contract WarpLinkMainnet18240282Test is WarpLinkTestBase {
recipient: USER,
partner: address(0),
feeBps: 0,
slippageBps: 0,
slippageBps: 200,
deadline: deadline
}),
PermitParams({nonce: permit.details.nonce, signature: sig})
Expand All @@ -1234,7 +1234,7 @@ contract WarpLinkMainnet18240282Test is WarpLinkTestBase {
);

IWarpLink.Params memory destParams = IWarpLink.Params({
tokenIn: address(0), // Unused
tokenIn: address(Mainnet.USDT),
tokenOut: address(Mainnet.WETH),
commands: destCommands,
amountIn: 0, // Unused
Expand Down Expand Up @@ -1308,10 +1308,10 @@ contract WarpLinkMainnet18240282Test is WarpLinkTestBase {
commands: sourceCommands,
amountIn: srcAmountIn,
amountOut: 0, // TODO
recipient: USER, // Unused
partner: address(0), // Unused
feeBps: 0, // Unused
slippageBps: 0,
recipient: USER,
partner: address(0),
feeBps: 0,
slippageBps: 100,
deadline: deadline
}),
PermitParams({nonce: permit.details.nonce, signature: sig})
Expand All @@ -1333,6 +1333,101 @@ contract WarpLinkMainnet18240282Test is WarpLinkTestBase {

assertApproxEqRel(Mainnet.USDC.balanceOf(USER), 990 * (10 ** 6), 0.001 ether);
}

function testFork_jumpAndSwapEth() public {
bytes memory destCommands = abi.encodePacked(
(uint8)(2), // Command count
(uint8)(COMMAND_TYPE_WRAP),
encoder.encodeWarpUniV3LikeExactInputSingle({
tokenOut: address(Mainnet.USDT),
pool: 0x4e68Ccd3E89f51C3074ca5072bbAC773960dFa36 // WETH/USDT 0.3%
})
);

IWarpLink.Params memory destParams = IWarpLink.Params({
tokenIn: address(0),
tokenOut: address(Mainnet.USDT),
commands: destCommands,
amountIn: 0, // Unused
amountOut: 1657900441, // USDT
recipient: USER,
partner: address(0),
feeBps: 0,
slippageBps: 100,
deadline: deadline
});

bytes memory destParamsEncoded = abi.encode(destParams);

uint256 srcAmountIn = 1 ether;
address srcTokenIn = address(0);
uint256 dstGasForCall = 500_000;

bytes memory sourceCommands = bytes.concat(
abi.encodePacked(
(uint8)(1), // Command count
(uint8)(COMMAND_TYPE_JUMP_STARGATE),
(uint16)(111), // dstChainId, Optimism
(uint8)(13), // srcPoolId, SGETH
(uint8)(13), // dstPoolId, SGETH
uint32(dstGasForCall), // dstGasForCall, 500K
address(0), // destParams.tokenOut
uint256(0.9 ether), // destParams.amountOut
uint256(destCommands.length), // destParams.commands.length
destCommands // destParams.commands
)
);

(uint256 nativeWei, ) = IStargateComposer(Mainnet.STARGATE_COMPOSER_ADDR).quoteLayerZeroFee({
_dstChainId: 111, // Optimism
_functionType: 1, // Swap remote
_toAddress: abi.encodePacked(address(diamond)),
_transferAndCallPayload: destParamsEncoded,
_lzTxParams: IStargateRouter.lzTxObj({
dstGasForCall: dstGasForCall,
dstNativeAmount: 0,
dstNativeAddr: ''
})
});

vm.deal(USER, nativeWei + 1 ether);

console2.log('Native fee: %s', nativeWei);

PermitParams memory permitParams;

vm.prank(USER);
facet.warpLinkEngage{value: nativeWei + srcAmountIn}(
IWarpLink.Params({
tokenIn: srcTokenIn,
tokenOut: srcTokenIn,
commands: sourceCommands,
amountIn: srcAmountIn,
amountOut: 0, // TODO
recipient: USER,
partner: address(0),
feeBps: 0, // Unused
slippageBps: 100,
deadline: deadline
}),
permitParams
);

vm.deal(address(facet), (srcAmountIn * 99) / 100);

// And calls sgReceive
vm.prank(Mainnet.STARGATE_COMPOSER_ADDR);
facet.sgReceive(
uint16(Mainnet.CHAIN_ID), // _srcChain
abi.encodePacked(address(diamond)), // _srcAddress
0, // _nonce
address(0x72E2F4830b9E45d52F80aC08CB2bEC0FeF72eD9c), // _token, SGETH
(srcAmountIn * 99) / 100, // amountLD
destParamsEncoded // payload
);

assertEq(Mainnet.USDT.balanceOf(USER), 1657900441, 'usdt balance');
}
}

contract WarpLinkBlock18069811Test is WarpLinkTestBase {
Expand Down Expand Up @@ -1745,7 +1840,8 @@ contract WarpLinkGoerliTest is WarpLinkTestBase {
recipient: USER,
partner: address(0),
feeBps: 0,
slippageBps: 0,
// NOTE: There is massive slippage on the testnet
slippageBps: 100 * 20,
deadline: deadline
}),
PermitParams({nonce: permit.details.nonce, signature: sig})
Expand Down

0 comments on commit 0659258

Please sign in to comment.