Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new withdraw helper that accept multiple assets #2

Merged
merged 2 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:
version: nightly

- name: Run tests
run: forge test -vvv
run: forge test -vvv --fork-url https://rpc.lyra.finance
1 change: 0 additions & 1 deletion src/withdraw/LyraWithdrawWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {IFiatController} from "../interfaces/IFiatController.sol";

/**
* @title LyraWithdrawWrapper
* @notice Shared logic for both self-paying and sponsored forwarder
*/
contract LyraWithdrawWrapper is Ownable {
///@dev L2 USDC address.
Expand Down
82 changes: 82 additions & 0 deletions src/withdraw/LyraWithdrawWrapperV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import {IERC20} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Ownable} from "../../lib/openzeppelin-contracts/contracts/access/Ownable.sol";
import {IFiatController} from "../interfaces/IFiatController.sol";

/**
* @title LyraWithdrawWrapperV2
* @notice Helper contract to charge token, pay socket fee and withdraw to another chain
*/
contract LyraWithdrawWrapperV2 is Ownable {
/// @dev price of asset in wei. How many {token wei} is 1 ETH * 1e18.
mapping(address token => uint256) public staticPrice;

constructor() payable {}

/**
* @notice withdraw token from Lyra chain to another chain with Socket bridge
* @dev this function requires paying a fee in token
*
* @param token Token to withdraw, also will be used to pay fee
* @param amount Amount of token to withdraw
* @param recipient Recipient address on the destination chain
* @param socketController Socket Controller address, determine what is the destination chain.
* Lyra USDC can be withdrawn as USDC or USDC.e on Arbitrum & Optimism.
* @param connector Socket Connector address, can be fast connector / native connector ..etc
* @param gasLimit Gas limit on the destination chain.
*/
function withdrawToChain(
address token,
uint256 amount,
address recipient,
address socketController,
address connector,
uint256 gasLimit
) external {
if (staticPrice[token] == 0) revert("Token price not set");

IERC20(token).transferFrom(msg.sender, address(this), amount);

IERC20(token).approve(socketController, amount);

// get fee in wei
uint256 minFee = IFiatController(socketController).getMinFees(connector, gasLimit);

uint256 feeInToken = minFee * staticPrice[token] / 1e36;

if (feeInToken > amount) revert("withdraw amount < fee");

uint256 remaining = amount - feeInToken;

IERC20(token).transfer(owner(), feeInToken);

IFiatController(socketController).withdrawFromAppChain{value: minFee}(recipient, remaining, gasLimit, connector);
}

/**
* @dev get the estimated fee in token for a withdrawal
*/
function getFeeInToken(address token, address controller, address connector, uint256 gasLimit)
public
view
returns (uint256 feeInToken)
{
uint256 minFee = IFiatController(controller).getMinFees(connector, gasLimit);
feeInToken = minFee * staticPrice[token] / 1e36;
}

/**
* Get ETH out of the contract
*/
function rescueEth() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}

function setStaticRate(address token, uint256 newRate) external onlyOwner {
staticPrice[token] = newRate;
}

receive() external payable {}
}
142 changes: 142 additions & 0 deletions test/fork-tests/withdraw/LyraWithdrawWrapperV2.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.18;

import "lib/forge-std/src/Test.sol";

import "src/withdraw/LyraWithdrawWrapperV2.sol";
import {USDC} from "src/mocks/USDC.sol";

/**
* forge test --fork-url https://rpc.lyra.finance -vvv
*/
contract FORK_LyraWithdrawalV2Test is Test {
address public usdc = address(0x6879287835A86F50f784313dBEd5E5cCC5bb8481);

// withdraw as official USDC
address public usdcController = address(0x4C9faD010D8be90Aba505c85eacc483dFf9b8Fa9);
address public usdc_Mainnet_Connector = address(0x1281C1464449DB73bdAa30928BCC63Dc25D8D187);
address public usdc_Arbi_Connector = address(0xBdE9e687F3A23Ebbc972c58D510dfc1f58Fb35EF); // as native USDC

// wbtc asset
address public wBTC = address(0x9b80ab732a6F1030326Af0014f106E12C4Db18EC);
address public wBTCController = address(0xaf33761742beF3B7d0D0726671660CCF260fc5c3);
address public wBTC_OP_Connector = address(0xC50Abb760555f73CCCa8C4D4ff56D4Bd4AAAAfC9);

LyraWithdrawWrapperV2 public wrapper;

uint256 public alicePk = 0xbabebabe;
address public alice = vm.addr(alicePk);

/**
* Only run the test when running with --fork flag, and connected to Lyra mainnet
*/
modifier onlyLyra() {
if (block.chainid != 957) return;
_;
}

function setUp() public onlyLyra {
wrapper = new LyraWithdrawWrapperV2{value: 1 ether}();

_mintLyraUSDC(alice, 1000e6);

wrapper.setStaticRate(usdc, 2500 * 1e18 * 1e6); // 2500 USDC = 1 ETH

wrapper.setStaticRate(wBTC, 0.06 * 1e18 * 1e8); // 0.06 WBTC = 1 ETH
}

function test_fork_Withdraw_USDC() public onlyLyra {
uint256 balanceBefore = IERC20(usdc).balanceOf(alice);
uint256 amount = 100e6;

vm.startPrank(alice);
IERC20(usdc).approve(address(wrapper), type(uint256).max);

wrapper.withdrawToChain(usdc, amount, alice, usdcController, usdc_Mainnet_Connector, 200_000);
vm.stopPrank();

uint256 balanceAfter = IERC20(usdc).balanceOf(alice);
assertEq(balanceBefore - balanceAfter, amount);
}

function test_fork_Withdraw_BridgeUSDC() public onlyLyra {
uint256 balanceBefore = IERC20(usdc).balanceOf(alice);
uint256 amount = 100e6;

vm.startPrank(alice);
IERC20(usdc).approve(address(wrapper), type(uint256).max);

wrapper.withdrawToChain(usdc, amount, alice, usdcController, usdc_Arbi_Connector, 200_000);
vm.stopPrank();

uint256 balanceAfter = IERC20(usdc).balanceOf(alice);
assertEq(balanceBefore - balanceAfter, amount);
}

function test_fork_RevertIf_tokenMismatch() public onlyLyra {
// _mintLyraUSDC(address(wrapper), 1000e6);
uint256 amount = 100e6;

vm.startPrank(alice);
IERC20(usdc).approve(address(wrapper), type(uint256).max);

// send USDC but request withdraw WBTC
vm.expectRevert();
wrapper.withdrawToChain(usdc, amount, alice, wBTCController, wBTC_OP_Connector, 200_000);
vm.stopPrank();
}

function test_fork_Withdraw_WBTC() public onlyLyra {
uint256 amount = 1e8;

_mintLyraWBTC(alice, amount);

uint256 feeInWBTC = wrapper.getFeeInToken(wBTC, wBTCController, wBTC_OP_Connector, 200_000);

uint256 balanceBefore = IERC20(wBTC).balanceOf(alice);

vm.startPrank(alice);
IERC20(wBTC).approve(address(wrapper), type(uint256).max);

wrapper.withdrawToChain(wBTC, amount, alice, wBTCController, wBTC_OP_Connector, 200_000);
vm.stopPrank();

uint256 balanceAfter = IERC20(wBTC).balanceOf(alice);
assertEq(balanceBefore - balanceAfter, amount);

// fee is paid to owner
assertEq(IERC20(wBTC).balanceOf(address(this)), feeInWBTC);
}

function test_fork_RevertIf_AmountToLow() public onlyLyra {
vm.startPrank(alice);
IERC20(usdc).approve(address(wrapper), type(uint256).max);

uint256 amount = 1e6;
vm.expectRevert(bytes("withdraw amount < fee"));
wrapper.withdrawToChain(usdc, amount, alice, usdcController, usdc_Arbi_Connector, 200_000);

vm.stopPrank();
}

function test_fork_getFee() public onlyLyra {
uint256 fee = wrapper.getFeeInToken(usdc, usdcController, usdc_Mainnet_Connector, 200_000);
assertGt(fee, 1e6);
assertLt(fee, 300e6);

fee = wrapper.getFeeInToken(usdc, usdcController, usdc_Arbi_Connector, 200_000);
assertLt(fee, 10e6);
}

function _mintLyraUSDC(address account, uint256 amount) public {
vm.prank(usdc_Mainnet_Connector);
IFiatController(usdcController).receiveInbound(abi.encode(account, amount));
}

function _mintLyraWBTC(address account, uint256 amount) public {
vm.prank(wBTC_OP_Connector);
IFiatController(wBTCController).receiveInbound(abi.encode(account, amount));
}

receive() external payable {}
}