From 13136d477ddc28902f137e286f5c4df40433dbbe Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Fri, 5 Jan 2024 10:26:56 +0800 Subject: [PATCH 1/4] build: compile --- foundry.toml | 2 +- script/deploy-forwarder.s.sol | 24 +++---- script/deploy-paymaster.s.sol | 5 +- src/gelato/LyraPermitForwarder.sol | 101 +++++++++++++++++++++++++++++ src/mocks/USDC.sol | 14 ---- 5 files changed, 115 insertions(+), 31 deletions(-) create mode 100644 src/gelato/LyraPermitForwarder.sol diff --git a/foundry.toml b/foundry.toml index 5d0b478..42fe13c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,5 +9,5 @@ optimizer_runs = 2000 [rpc_endpoints] mainnet = "https://mainnet.infura.io/v3/${INFURA_PROJECT_ID}" -op_goerli = "https://optimism-goerli.blockpi.network/v1/rpc/public" +op_goerli = "https://optimism-goerli.infura.io/v3/ea21b2313cdf43c28f25b849d7a75274" lyra = "https://rpc.lyra.finance" \ No newline at end of file diff --git a/script/deploy-forwarder.s.sol b/script/deploy-forwarder.s.sol index 0804de9..05dd42c 100644 --- a/script/deploy-forwarder.s.sol +++ b/script/deploy-forwarder.s.sol @@ -25,19 +25,17 @@ contract Deploy is Script { DeploymentConfig memory config = _getConfig(); // deploy LyraSponsoredForwarder - LyraSponsoredForwarder sponsoredForwarder = new LyraSponsoredForwarder{value: config.fundingAmount}( - config.usdcLocal, - config.socketVault - ); + LyraSponsoredForwarder sponsoredForwarder = + new LyraSponsoredForwarder{value: config.fundingAmount}(config.usdcLocal, config.socketVault); - LyraSelfPayingForwarder selfPayingForwarder = new LyraSelfPayingForwarder{value: config.fundingAmount}( - config.usdcLocal, - config.socketVault - ); + // LyraSelfPayingForwarder selfPayingForwarder = new LyraSelfPayingForwarder{value: config.fundingAmount}( + // config.usdcLocal, + // config.socketVault + // ); console2.log("LyraSponsoredForwarder deployed at: ", address(sponsoredForwarder)); - console2.log("LyraSelfPayingForwarder deployed at: ", address(selfPayingForwarder)); + // console2.log("LyraSelfPayingForwarder deployed at: ", address(selfPayingForwarder)); vm.stopBroadcast(); } @@ -46,23 +44,21 @@ contract Deploy is Script { if (block.chainid == 420) { // OP-Goerli return DeploymentConfig({ - fundingAmount: 0.15 ether, + fundingAmount: 0.07 ether, usdcLocal: 0x0f8BEaf58d4A237C88c9ed99D82003ab5c252c26, // our clone of USDC on op-goerli // Socket configs // See: https://github.com/SocketDotTech/app-chain-token/blob/lyra-tesnet-to-prod/deployments/prod_lyra_addresses.json socketVault: 0x3d74c019E9caCBc968cF31B0810044a030B3E903 - }) + }); // socketConnector: 0xfBf496B6DBda9d5e778e2563493BCb32F5A52B51 - ; } else if (block.chainid == 1) { // Mainnet return DeploymentConfig({ fundingAmount: 0.15 ether, usdcLocal: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // mainnet USDC socketVault: 0x6D303CEE7959f814042D31E0624fB88Ec6fbcC1d - }) + }); // socketConnector: 0x0000000000000000000000000000000000000001 // todo: add l1 address - ; } revert("No config for this network! Please set config in script/Deploy.s.sol"); diff --git a/script/deploy-paymaster.s.sol b/script/deploy-paymaster.s.sol index 7510e16..7678ec5 100644 --- a/script/deploy-paymaster.s.sol +++ b/script/deploy-paymaster.s.sol @@ -8,8 +8,9 @@ contract DeployPaymaster is Script { function run() public { uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); vm.startBroadcast(deployerPrivateKey); - VerifyingPaymasterFix paymaster = - new VerifyingPaymasterFix(IEntryPoint(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789), vm.addr(deployerPrivateKey)); + VerifyingPaymasterFix paymaster = new VerifyingPaymasterFix( + IEntryPoint(0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789), vm.addr(deployerPrivateKey) + ); console2.log("VerifyingPaymasterFix deployed at: ", address(paymaster)); vm.stopBroadcast(); } diff --git a/src/gelato/LyraPermitForwarder.sol b/src/gelato/LyraPermitForwarder.sol new file mode 100644 index 0000000..524e6ba --- /dev/null +++ b/src/gelato/LyraPermitForwarder.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {GelatoRelayContextERC2771} from "../../lib/relay-context-contracts/contracts/GelatoRelayContextERC2771.sol"; + +import {ISocketVault} from "../interfaces/ISocketVault.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {ILightAccountFactory} from "../interfaces/ILightAccountFactory.sol"; + +/** + * @title LyraPermitBridgeForwarder + * @notice Use this contract to allow gasless transactions, users pay gelato relayers in USDC + * + * @dev All functions are guarded with onlyGelatoRelayERC2771. They should only be called by GELATO_RELAY_ERC2771 or GELATO_RELAY_CONCURRENT_ERC2771 + * @dev Someone need to fund this contract with ETH to use Socket Bridge + */ +contract LyraPermitBridgeForwarder is Ownable, GelatoRelayContextERC2771 { + ///@dev SocketVault address. + address public immutable socketVault; + + ///@dev local token address. This token must support permit + address public immutable usdcLocal; + + ///@dev Light Account factory address. + /// See this script for more info https://github.com/alchemyplatform/light-account/blob/main/script/Deploy_LightAccountFactory.s.sol + address public constant lightAccountFactory = 0x000000893A26168158fbeaDD9335Be5bC96592E2; + + struct PermitData { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + constructor(address _usdcLocal, address _socketVault) payable GelatoRelayContextERC2771() { + usdcLocal = _usdcLocal; + socketVault = _socketVault; + } + + /** + * @notice Deposit USDC to L2 through socket bridge. Gas is paid in token + * @dev Users never have to approve USDC to this contract. + * @param maxFeeToken Maximum fee in that user is willing to pay + * @param isScwWallet True if user wants to deposit to default LightAccount on L2. False if the user wants to deposit to its own L2 address + * @param minGasLimit Minimum gas limit for the L2 execution + * @param connector Socket Connector + * @param permitData Data and signatures for permit + */ + function depositGasless( + uint256 maxFeeToken, + bool isScwWallet, + uint32 minGasLimit, + address connector, + PermitData calldata permitData + ) external payable onlyGelatoRelayERC2771 { + address msgSender = _getMsgSender(); + + try IERC20Permit(usdcLocal).permit( + msgSender, address(this), permitData.value, permitData.deadline, permitData.v, permitData.r, permitData.s + ) {} catch {} + + IERC20(usdcLocal).transferFrom(msgSender, address(this), permitData.value); + + // Pay gelato fee, reverts if exceeded max fee + _transferRelayFeeCapped(maxFeeToken); + + uint256 remaining = permitData.value - _getFee(); + + uint256 socketFee = ISocketVault(socketVault).getMinFees(connector, minGasLimit); + + // Pay socket fee and deposit to Lyra Chain + ISocketVault(socketVault).depositToAppChain{value: socketFee}( + _getL2Receiver(msgSender, isScwWallet), remaining, minGasLimit, connector + ); + } + + /** + * @notice Return the receiver address on L2 + */ + function _getL2Receiver(address msgSender, bool isScwWallet) internal view returns (address) { + if (isScwWallet) { + return ILightAccountFactory(lightAccountFactory).getAddress(msgSender, 0); + } else { + return msgSender; + } + } + + /** + * @dev Owner can withdraw ETH deposited to cover socket protocol fee + */ + function withdrawETH() external onlyOwner { + payable(msg.sender).transfer(address(this).balance); + } + + receive() external payable {} +} diff --git a/src/mocks/USDC.sol b/src/mocks/USDC.sol index 475df8a..9335065 100644 --- a/src/mocks/USDC.sol +++ b/src/mocks/USDC.sol @@ -265,7 +265,6 @@ interface IERC20 { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; abstract contract AbstractFiatTokenV1 is IERC20 { @@ -385,7 +384,6 @@ contract Ownable { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -474,7 +472,6 @@ contract Pausable is Ownable { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -563,7 +560,6 @@ contract Blacklistable is Ownable { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -1100,7 +1096,6 @@ library SafeERC20 { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; contract Rescuable is Ownable { @@ -1170,7 +1165,6 @@ contract Rescuable is Ownable { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -1202,7 +1196,6 @@ contract FiatTokenV1_1 is FiatTokenV1, Rescuable {} * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; abstract contract AbstractFiatTokenV2 is AbstractFiatTokenV1 { @@ -1235,7 +1228,6 @@ abstract contract AbstractFiatTokenV2 is AbstractFiatTokenV1 { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -1302,7 +1294,6 @@ library ECRecover { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -1375,7 +1366,6 @@ library EIP712 { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -1411,7 +1401,6 @@ contract EIP712Domain { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -1599,7 +1588,6 @@ abstract contract EIP3009 is AbstractFiatTokenV2, EIP712Domain { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -1666,7 +1654,6 @@ abstract contract EIP2612 is AbstractFiatTokenV2, EIP712Domain { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; /** @@ -1855,7 +1842,6 @@ contract FiatTokenV2 is FiatTokenV1_1, EIP3009, EIP2612 { * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - pragma solidity ^0.8.0; // solhint-disable func-name-mixedcase From b44c9ae622095b0bdfc79a2305074f448df4a2df Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Fri, 5 Jan 2024 11:35:29 +0800 Subject: [PATCH 2/4] chore: cleanup --- src/gelato/LyraPermitForwarder.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/gelato/LyraPermitForwarder.sol b/src/gelato/LyraPermitForwarder.sol index 524e6ba..906051d 100644 --- a/src/gelato/LyraPermitForwarder.sol +++ b/src/gelato/LyraPermitForwarder.sol @@ -13,7 +13,7 @@ import {ILightAccountFactory} from "../interfaces/ILightAccountFactory.sol"; /** * @title LyraPermitBridgeForwarder - * @notice Use this contract to allow gasless transactions, users pay gelato relayers in USDC + * @notice Use this contract to allow gasless transactions, users pay gelato relayers in tokens like (USDC.e) * * @dev All functions are guarded with onlyGelatoRelayERC2771. They should only be called by GELATO_RELAY_ERC2771 or GELATO_RELAY_CONCURRENT_ERC2771 * @dev Someone need to fund this contract with ETH to use Socket Bridge @@ -23,7 +23,7 @@ contract LyraPermitBridgeForwarder is Ownable, GelatoRelayContextERC2771 { address public immutable socketVault; ///@dev local token address. This token must support permit - address public immutable usdcLocal; + address public immutable token; ///@dev Light Account factory address. /// See this script for more info https://github.com/alchemyplatform/light-account/blob/main/script/Deploy_LightAccountFactory.s.sol @@ -37,8 +37,8 @@ contract LyraPermitBridgeForwarder is Ownable, GelatoRelayContextERC2771 { bytes32 s; } - constructor(address _usdcLocal, address _socketVault) payable GelatoRelayContextERC2771() { - usdcLocal = _usdcLocal; + constructor(address _token, address _socketVault) payable GelatoRelayContextERC2771() { + token = _token; socketVault = _socketVault; } @@ -60,11 +60,12 @@ contract LyraPermitBridgeForwarder is Ownable, GelatoRelayContextERC2771 { ) external payable onlyGelatoRelayERC2771 { address msgSender = _getMsgSender(); - try IERC20Permit(usdcLocal).permit( + // use try catch so that others cannot grief by submitting the same permit data before this tx + try IERC20Permit(token).permit( msgSender, address(this), permitData.value, permitData.deadline, permitData.v, permitData.r, permitData.s ) {} catch {} - IERC20(usdcLocal).transferFrom(msgSender, address(this), permitData.value); + IERC20(token).transferFrom(msgSender, address(this), permitData.value); // Pay gelato fee, reverts if exceeded max fee _transferRelayFeeCapped(maxFeeToken); From fab5a58d0cf857d5193db67c1808b4cbd288460d Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Fri, 5 Jan 2024 15:06:35 +0800 Subject: [PATCH 3/4] build: add sponsored one --- foundry.toml | 3 +- ....sol => LyraPermitSelfPayingForwarder.sol} | 4 +- src/gelato/LyraPermitSponsoredForwarder.sol | 100 ++++++++++++++++++ 3 files changed, 104 insertions(+), 3 deletions(-) rename src/gelato/{LyraPermitForwarder.sol => LyraPermitSelfPayingForwarder.sol} (97%) create mode 100644 src/gelato/LyraPermitSponsoredForwarder.sol diff --git a/foundry.toml b/foundry.toml index 42fe13c..16eb0b7 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,4 +10,5 @@ optimizer_runs = 2000 [rpc_endpoints] mainnet = "https://mainnet.infura.io/v3/${INFURA_PROJECT_ID}" op_goerli = "https://optimism-goerli.infura.io/v3/ea21b2313cdf43c28f25b849d7a75274" -lyra = "https://rpc.lyra.finance" \ No newline at end of file +lyra = "https://rpc.lyra.finance" +lyra_testnet = "https://l2-prod-testnet-0eakp60405.t.conduit.xyz" \ No newline at end of file diff --git a/src/gelato/LyraPermitForwarder.sol b/src/gelato/LyraPermitSelfPayingForwarder.sol similarity index 97% rename from src/gelato/LyraPermitForwarder.sol rename to src/gelato/LyraPermitSelfPayingForwarder.sol index 906051d..e4682e0 100644 --- a/src/gelato/LyraPermitForwarder.sol +++ b/src/gelato/LyraPermitSelfPayingForwarder.sol @@ -12,13 +12,13 @@ import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC2 import {ILightAccountFactory} from "../interfaces/ILightAccountFactory.sol"; /** - * @title LyraPermitBridgeForwarder + * @title LyraPermitSelfPayingForwarder * @notice Use this contract to allow gasless transactions, users pay gelato relayers in tokens like (USDC.e) * * @dev All functions are guarded with onlyGelatoRelayERC2771. They should only be called by GELATO_RELAY_ERC2771 or GELATO_RELAY_CONCURRENT_ERC2771 * @dev Someone need to fund this contract with ETH to use Socket Bridge */ -contract LyraPermitBridgeForwarder is Ownable, GelatoRelayContextERC2771 { +contract LyraPermitSelfPayingForwarder is Ownable, GelatoRelayContextERC2771 { ///@dev SocketVault address. address public immutable socketVault; diff --git a/src/gelato/LyraPermitSponsoredForwarder.sol b/src/gelato/LyraPermitSponsoredForwarder.sol new file mode 100644 index 0000000..0a2d8f1 --- /dev/null +++ b/src/gelato/LyraPermitSponsoredForwarder.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +import {Ownable, Context} from "@openzeppelin/contracts/access/Ownable.sol"; + +import {ERC2771Context} from "../../lib/relay-context-contracts/contracts/vendor/ERC2771Context.sol"; + +import {ISocketVault} from "../interfaces/ISocketVault.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {ILightAccountFactory} from "../interfaces/ILightAccountFactory.sol"; + +/** + * @title LyraPermitSponsoredForwarder + * @notice Use this contract to allow gasless transactions, we sponsor the gas for users + * + */ +contract LyraPermitSponsoredForwarder is Ownable, ERC2771Context { + ///@dev SocketVault address. + address public immutable socketVault; + + ///@dev local token address. This token must support permit + address public immutable token; + + ///@dev Light Account factory address. + address public constant lightAccountFactory = 0x000000893A26168158fbeaDD9335Be5bC96592E2; + + struct PermitData { + uint256 value; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + } + + constructor(address _token, address _socketVault) payable ERC2771Context(0xd8253782c45a12053594b9deB72d8e8aB2Fca54c) { + token = _token; + socketVault = _socketVault; + } + + /** + * @notice Deposit USDC to L2 through socket bridge. Gas is paid in token + * @dev Users never have to approve USDC to this contract. + * @param isScwWallet True if user wants to deposit to default LightAccount on L2. False if the user wants to deposit to its own L2 address + * @param minGasLimit Minimum gas limit for the L2 execution + * @param connector Socket Connector + * @param permitData Data and signatures for permit + */ + function depositGasless( + bool isScwWallet, + uint32 minGasLimit, + address connector, + PermitData calldata permitData + ) external payable { + address msgSender = _msgSender(); + + // use try catch so that others cannot grief by submitting the same permit data before this tx + try IERC20Permit(token).permit( + msgSender, address(this), permitData.value, permitData.deadline, permitData.v, permitData.r, permitData.s + ) {} catch {} + + IERC20(token).transferFrom(msgSender, address(this), permitData.value); + + uint256 socketFee = ISocketVault(socketVault).getMinFees(connector, minGasLimit); + + // Pay socket fee and deposit to Lyra Chain + ISocketVault(socketVault).depositToAppChain{value: socketFee}( + _getL2Receiver(msgSender, isScwWallet), permitData.value, minGasLimit, connector + ); + } + + /** + * @notice Return the receiver address on L2 + */ + function _getL2Receiver(address msgSender, bool isScwWallet) internal view returns (address) { + if (isScwWallet) { + return ILightAccountFactory(lightAccountFactory).getAddress(msgSender, 0); + } else { + return msgSender; + } + } + + /** + * @dev Owner can withdraw ETH deposited to cover socket protocol fee + */ + function withdrawETH() external onlyOwner { + payable(msg.sender).transfer(address(this).balance); + } + + function _msgSender() internal view override(Context, ERC2771Context) returns (address sender) { + return ERC2771Context._msgSender(); + } + + function _msgData() internal view override(Context, ERC2771Context) returns (bytes calldata) { + return ERC2771Context._msgData(); + } + + receive() external payable {} +} From dbfef8bb76def11f85cc47e17c7922130f7b1766 Mon Sep 17 00:00:00 2001 From: Anton Cheng Date: Mon, 8 Jan 2024 06:59:40 +0800 Subject: [PATCH 4/4] fix: approve --- src/gelato/LyraPermitSelfPayingForwarder.sol | 2 ++ src/gelato/LyraPermitSponsoredForwarder.sol | 17 ++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/gelato/LyraPermitSelfPayingForwarder.sol b/src/gelato/LyraPermitSelfPayingForwarder.sol index e4682e0..0ee57e6 100644 --- a/src/gelato/LyraPermitSelfPayingForwarder.sol +++ b/src/gelato/LyraPermitSelfPayingForwarder.sol @@ -40,6 +40,8 @@ contract LyraPermitSelfPayingForwarder is Ownable, GelatoRelayContextERC2771 { constructor(address _token, address _socketVault) payable GelatoRelayContextERC2771() { token = _token; socketVault = _socketVault; + + IERC20(_token).approve(_socketVault, type(uint256).max); } /** diff --git a/src/gelato/LyraPermitSponsoredForwarder.sol b/src/gelato/LyraPermitSponsoredForwarder.sol index 0a2d8f1..8892546 100644 --- a/src/gelato/LyraPermitSponsoredForwarder.sol +++ b/src/gelato/LyraPermitSponsoredForwarder.sol @@ -34,9 +34,14 @@ contract LyraPermitSponsoredForwarder is Ownable, ERC2771Context { bytes32 s; } - constructor(address _token, address _socketVault) payable ERC2771Context(0xd8253782c45a12053594b9deB72d8e8aB2Fca54c) { + constructor(address _token, address _socketVault) + payable + ERC2771Context(0xd8253782c45a12053594b9deB72d8e8aB2Fca54c) + { token = _token; socketVault = _socketVault; + + IERC20(_token).approve(_socketVault, type(uint256).max); } /** @@ -47,12 +52,10 @@ contract LyraPermitSponsoredForwarder is Ownable, ERC2771Context { * @param connector Socket Connector * @param permitData Data and signatures for permit */ - function depositGasless( - bool isScwWallet, - uint32 minGasLimit, - address connector, - PermitData calldata permitData - ) external payable { + function depositGasless(bool isScwWallet, uint32 minGasLimit, address connector, PermitData calldata permitData) + external + payable + { address msgSender = _msgSender(); // use try catch so that others cannot grief by submitting the same permit data before this tx