From afb89dbad8c2861fff3f7b79163b3a98a30fd794 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Tue, 16 Jan 2024 12:14:32 +0800 Subject: [PATCH 1/2] feat: new withdraw helper that accept multiple assets --- src/withdraw/LyraWithdrawWrapper.sol | 1 - src/withdraw/LyraWithdrawWrapperV2.sol | 82 ++++++++++ ...wWrapper.sol => LyraWithdrawWrapper.t.sol} | 0 .../withdraw/LyraWithdrawWrapperV2.t.sol | 142 ++++++++++++++++++ 4 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 src/withdraw/LyraWithdrawWrapperV2.sol rename test/fork-tests/withdraw/{LyraWithdrawWrapper.sol => LyraWithdrawWrapper.t.sol} (100%) create mode 100644 test/fork-tests/withdraw/LyraWithdrawWrapperV2.t.sol diff --git a/src/withdraw/LyraWithdrawWrapper.sol b/src/withdraw/LyraWithdrawWrapper.sol index 774e5d1..cb113d9 100644 --- a/src/withdraw/LyraWithdrawWrapper.sol +++ b/src/withdraw/LyraWithdrawWrapper.sol @@ -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. diff --git a/src/withdraw/LyraWithdrawWrapperV2.sol b/src/withdraw/LyraWithdrawWrapperV2.sol new file mode 100644 index 0000000..fa0f45e --- /dev/null +++ b/src/withdraw/LyraWithdrawWrapperV2.sol @@ -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 {} +} diff --git a/test/fork-tests/withdraw/LyraWithdrawWrapper.sol b/test/fork-tests/withdraw/LyraWithdrawWrapper.t.sol similarity index 100% rename from test/fork-tests/withdraw/LyraWithdrawWrapper.sol rename to test/fork-tests/withdraw/LyraWithdrawWrapper.t.sol diff --git a/test/fork-tests/withdraw/LyraWithdrawWrapperV2.t.sol b/test/fork-tests/withdraw/LyraWithdrawWrapperV2.t.sol new file mode 100644 index 0000000..6dc42e9 --- /dev/null +++ b/test/fork-tests/withdraw/LyraWithdrawWrapperV2.t.sol @@ -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 {} +} From 4afb8c15e5750e1613fe6127c3f9c28272e8489c Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Tue, 16 Jan 2024 12:18:38 +0800 Subject: [PATCH 2/2] ci: run test on lyra-fork --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7a23b4..643c6a7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,4 +22,4 @@ jobs: version: nightly - name: Run tests - run: forge test -vvv \ No newline at end of file + run: forge test -vvv --fork-url https://rpc.lyra.finance \ No newline at end of file