diff --git a/contracts/intent-adapters/liquid-staking/Swell/Interfaces.sol b/contracts/intent-adapters/liquid-staking/Swell/Interfaces.sol new file mode 100644 index 0000000..a3b4751 --- /dev/null +++ b/contracts/intent-adapters/liquid-staking/Swell/Interfaces.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +interface ISwellPool { + function deposit() external payable; +} diff --git a/contracts/intent-adapters/liquid-staking/Swell/SwellStakeEth.sol b/contracts/intent-adapters/liquid-staking/Swell/SwellStakeEth.sol new file mode 100644 index 0000000..6b99744 --- /dev/null +++ b/contracts/intent-adapters/liquid-staking/Swell/SwellStakeEth.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import {ISwellPool} from "./Interfaces.sol"; +import {RouterIntentAdapter, Errors} from "router-intents/contracts/RouterIntentAdapter.sol"; +import {NitroMessageHandler} from "router-intents/contracts/NitroMessageHandler.sol"; +import {IERC20, SafeERC20} from "../../../utils/SafeERC20.sol"; + +/** + * @title SwellStakeEth + * @author Yashika Goyal + * @notice Staking ETH to receive swETH on Swell. + * @notice This contract is only for Ethereum chain. + */ +contract SwellStakeEth is RouterIntentAdapter, NitroMessageHandler { + using SafeERC20 for IERC20; + + address private immutable _swEth; + + event SwellStakeEthDest( + address _recipient, + uint256 _amount, + uint256 _receivedSwEth + ); + + constructor( + address __native, + address __wnative, + address __owner, + address __assetForwarder, + address __dexspan, + address __swEth + ) + RouterIntentAdapter(__native, __wnative, __owner) + NitroMessageHandler(__assetForwarder, __dexspan) + { + _swEth = __swEth; + } + + function swEth() public view returns (address) { + return _swEth; + } + + function name() public pure override returns (string memory) { + return "SwellStakeEth"; + } + + /** + * @inheritdoc RouterIntentAdapter + */ + function execute( + address, + address, + bytes calldata data + ) external payable override returns (address[] memory tokens) { + (address _recipient, uint256 _amount) = parseInputs(data); + + // If the adapter is called using `call` and not `delegatecall` + if (address(this) == self()) { + require( + msg.value == _amount, + Errors.INSUFFICIENT_NATIVE_FUNDS_PASSED + ); + } + + bytes memory logData; + + (tokens, logData) = _stake(_recipient, _amount); + + emit ExecutionEvent(name(), logData); + return tokens; + } + + /** + * @inheritdoc NitroMessageHandler + */ + function handleMessage( + address tokenSent, + uint256 amount, + bytes memory instruction + ) external override onlyNitro nonReentrant { + address recipient = abi.decode(instruction, (address)); + + if (tokenSent != native()) { + withdrawTokens(tokenSent, recipient, amount); + emit OperationFailedRefundEvent(tokenSent, recipient, amount); + return; + } + + try ISwellPool(_swEth).deposit{value: amount}() { + uint256 receivedSwEth = withdrawTokens( + _swEth, + recipient, + type(uint256).max + ); + + emit SwellStakeEthDest(recipient, amount, receivedSwEth); + } catch { + withdrawTokens(tokenSent, recipient, amount); + emit OperationFailedRefundEvent(tokenSent, recipient, amount); + } + } + + //////////////////////////// ACTION LOGIC //////////////////////////// + + function _stake( + address _recipient, + uint256 _amount + ) internal returns (address[] memory tokens, bytes memory logData) { + ISwellPool(_swEth).deposit{value: _amount}(); + uint256 receivedSwEth = withdrawTokens( + _swEth, + _recipient, + type(uint256).max + ); + + tokens = new address[](2); + tokens[0] = native(); + tokens[1] = swEth(); + + logData = abi.encode(_recipient, _amount, receivedSwEth); + } + + /** + * @dev function to parse input data. + * @param data input data. + */ + function parseInputs( + bytes memory data + ) public pure returns (address, uint256) { + return abi.decode(data, (address, uint256)); + } + + // solhint-disable-next-line no-empty-blocks + receive() external payable {} +} diff --git a/tasks/index.ts b/tasks/index.ts index 58bc548..8ee9e75 100644 --- a/tasks/index.ts +++ b/tasks/index.ts @@ -4,6 +4,7 @@ import "./ankr"; import "./origin"; import "./benqi"; import "./rocketPool"; +import "./swell"; import "./dexspan"; import "./erc20"; import "./uniswapV3"; diff --git a/tasks/swell/SwellStakeEth.deploy.ts b/tasks/swell/SwellStakeEth.deploy.ts new file mode 100644 index 0000000..2b7f7b8 --- /dev/null +++ b/tasks/swell/SwellStakeEth.deploy.ts @@ -0,0 +1,102 @@ +import { HardhatRuntimeEnvironment, TaskArguments } from "hardhat/types"; +import { + ASSET_FORWARDER, + CONTRACT_NAME, + DEFAULT_ENV, + DEFAULT_OWNER, + DEFAULT_REFUND_ADDRESS, + DEPLOY_SWELL_STAKE_ETH_ADAPTER, + DEXSPAN, + NATIVE, + VERIFY_SWELL_STAKE_ETH_ADAPTER, + WNATIVE, +} from "../constants"; +import { task } from "hardhat/config"; +import { + IDeployment, + getDeployments, + recordAllDeployments, + saveDeployments, +} from "../utils"; +import { SWELL_SW_ETH } from "./constants"; + +const contractName: string = CONTRACT_NAME.SwellStakeEth; + +task(DEPLOY_SWELL_STAKE_ETH_ADAPTER) + .addFlag("verify", "pass true to verify the contract") + .setAction(async function ( + _taskArguments: TaskArguments, + _hre: HardhatRuntimeEnvironment + ) { + let env = process.env.ENV; + if (!env) env = DEFAULT_ENV; + + let defaultRefundAddress = process.env.DEFAULT_REFUND_ADDRESS; + if (!defaultRefundAddress) defaultRefundAddress = DEFAULT_REFUND_ADDRESS; + + let owner = process.env.OWNER; + if (!owner) owner = DEFAULT_OWNER; + + const network = await _hre.getChainId(); + + console.log(`Deploying ${contractName} Contract on chainId ${network}....`); + const factory = await _hre.ethers.getContractFactory(contractName); + const instance = await factory.deploy( + NATIVE, + WNATIVE[env][network], + owner, + ASSET_FORWARDER[env][network], + DEXSPAN[env][network], + SWELL_SW_ETH[network], + ); + await instance.deployed(); + + const deployment: IDeployment = await recordAllDeployments( + env, + network, + contractName, + instance.address + ); + + await saveDeployments(deployment); + + console.log(`${contractName} contract deployed at`, instance.address); + + if (_taskArguments.verify === true) { + await _hre.run(VERIFY_SWELL_STAKE_ETH_ADAPTER); + } + }); + +task(VERIFY_SWELL_STAKE_ETH_ADAPTER).setAction(async function ( + _taskArguments: TaskArguments, + _hre: HardhatRuntimeEnvironment +) { + let env = process.env.ENV; + if (!env) env = DEFAULT_ENV; + + let defaultRefundAddress = process.env.DEFAULT_REFUND_ADDRESS; + if (!defaultRefundAddress) defaultRefundAddress = DEFAULT_REFUND_ADDRESS; + + let owner = process.env.OWNER; + if (!owner) owner = DEFAULT_OWNER; + + const network = await _hre.getChainId(); + + const deployments: IDeployment = getDeployments(); + const address = deployments[env][network][contractName]; + + console.log(`Verifying ${contractName} Contract....`); + await _hre.run("verify:verify", { + address, + constructorArguments: [ + NATIVE, + WNATIVE[env][network], + owner, + ASSET_FORWARDER[env][network], + DEXSPAN[env][network], + SWELL_SW_ETH[network], + ], + }); + + console.log(`Verified ${contractName} contract address `, address); +}); diff --git a/tasks/swell/constants.ts b/tasks/swell/constants.ts new file mode 100644 index 0000000..5be4a6e --- /dev/null +++ b/tasks/swell/constants.ts @@ -0,0 +1,4 @@ +export const SWELL_SW_ETH: { [chainId: string]: string } = { + "1": "0xf951E335afb289353dc249e82926178EaC7DEd78", + "5": "0x8bb383A752Ff3c1d510625C6F536E3332327068F", + }; \ No newline at end of file diff --git a/tasks/swell/index.ts b/tasks/swell/index.ts new file mode 100644 index 0000000..0679558 --- /dev/null +++ b/tasks/swell/index.ts @@ -0,0 +1 @@ +import "./SwellStakeEth.deploy"; diff --git a/test/Swell/SwellStakeEth.specs.ts b/test/Swell/SwellStakeEth.specs.ts new file mode 100644 index 0000000..b9430b1 --- /dev/null +++ b/test/Swell/SwellStakeEth.specs.ts @@ -0,0 +1,234 @@ +import hardhat, { ethers, waffle } from "hardhat"; +import { expect } from "chai"; +import { RPC } from "../constants"; +import { + DEXSPAN, + DEFAULT_ENV, + NATIVE, + WNATIVE, + DEFAULT_REFUND_ADDRESS, +} from "../../tasks/constants"; +import { SwellStakeEth__factory } from "../../typechain/factories/SwellStakeEth__factory"; +import { TokenInterface__factory } from "../../typechain/factories/TokenInterface__factory"; +import { MockAssetForwarder__factory } from "../../typechain/factories/MockAssetForwarder__factory"; +import { BatchTransaction__factory } from "../../typechain/factories/BatchTransaction__factory"; +import { BigNumber, Contract, Wallet } from "ethers"; +import { getPathfinderData } from "../utils"; +import { defaultAbiCoder } from "ethers/lib/utils"; +import { DexSpanAdapter__factory } from "../../typechain/factories/DexSpanAdapter__factory"; + +const CHAIN_ID = "1"; +const SW_TOKEN = "0xf951E335afb289353dc249e82926178EaC7DEd78"; +const NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; +const USDT = "0xdac17f958d2ee523a2206206994597c13d831ec7"; + +describe("SwellStakeEth Adapter: ", async () => { + const [deployer] = waffle.provider.getWallets(); + + const setupTests = async () => { + let env = process.env.ENV; + if (!env) env = DEFAULT_ENV; + const MockAssetForwarder = await ethers.getContractFactory( + "MockAssetForwarder" + ); + const mockAssetForwarder = await MockAssetForwarder.deploy(); + + const BatchTransaction = await ethers.getContractFactory( + "BatchTransaction" + ); + + const batchTransaction = await BatchTransaction.deploy( + NATIVE, + WNATIVE[env][CHAIN_ID], + mockAssetForwarder.address, + DEXSPAN[env][CHAIN_ID] + ); + + const DexSpanAdapter = await ethers.getContractFactory("DexSpanAdapter"); + const dexSpanAdapter = await DexSpanAdapter.deploy( + NATIVE, + WNATIVE[env][CHAIN_ID], + deployer.address, + mockAssetForwarder.address, + DEXSPAN[env][CHAIN_ID], + DEFAULT_REFUND_ADDRESS + ); + + const SwellStakeEth = await ethers.getContractFactory("SwellStakeEth"); + const swellStakeEthAdapter = await SwellStakeEth.deploy( + NATIVE, + WNATIVE[env][CHAIN_ID], + deployer.address, + mockAssetForwarder.address, + DEXSPAN[env][CHAIN_ID], + SW_TOKEN, + ); + + return { + batchTransaction: BatchTransaction__factory.connect( + batchTransaction.address, + deployer + ), + swellStakeEthAdapter: SwellStakeEth__factory.connect( + swellStakeEthAdapter.address, + deployer + ), + dexSpanAdapter: DexSpanAdapter__factory.connect( + dexSpanAdapter.address, + deployer + ), + mockAssetForwarder: MockAssetForwarder__factory.connect( + mockAssetForwarder.address, + deployer + ), + usdt: TokenInterface__factory.connect(USDT, deployer), + swEth: TokenInterface__factory.connect(SW_TOKEN, deployer), + }; + }; + + beforeEach(async function () { + await hardhat.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: RPC[CHAIN_ID], + }, + }, + ], + }); + }); + + const toBytes32 = (bn: BigNumber) => { + return ethers.utils.hexlify(ethers.utils.zeroPad(bn.toHexString(), 32)); + }; + + // This works for token when it has balance mapping at slot 0. + const setUserTokenBalance = async ( + contract: Contract, + user: Wallet, + balance: BigNumber + ) => { + const index = ethers.utils.solidityKeccak256( + ["uint256", "uint256"], + [user.address, 0] // key, slot + ); + + await hardhat.network.provider.request({ + method: "hardhat_setStorageAt", + params: [contract.address, index, toBytes32(balance).toString()], + }); + + await hardhat.network.provider.request({ + method: "evm_mine", + params: [], + }); + }; + + it("Can stake on swell on same chain", async () => { + const { batchTransaction, swellStakeEthAdapter, swEth } = + await setupTests(); + + const amount = ethers.utils.parseEther("1"); + + const swellData = defaultAbiCoder.encode( + ["address", "uint256"], + [deployer.address, amount] + ); + + const tokens = [NATIVE_TOKEN]; + const amounts = [amount]; + const targets = [swellStakeEthAdapter.address]; + const data = [swellData]; + const value = [0]; + const callType = [2]; + + const balBefore = await ethers.provider.getBalance(deployer.address); + const swEthBalBefore = await swEth.balanceOf(deployer.address); + + await batchTransaction.executeBatchCallsSameChain( + tokens, + amounts, + targets, + value, + callType, + data, + { value: amount } + ); + + const balAfter = await ethers.provider.getBalance(deployer.address); + const swEthBalAfter = await swEth.balanceOf(deployer.address); + + expect(balBefore).gt(balAfter); + expect(swEthBalAfter).gt(swEthBalBefore); + }); + + it("Can stake ETH on Swell on dest chain when instruction is received from BatchTransaction contract", async () => { + const { + batchTransaction, + swellStakeEthAdapter, + swEth, + mockAssetForwarder, + } = await setupTests(); + + const amount = "100000000000000000"; + + const targets = [swellStakeEthAdapter.address]; + const data = [ + defaultAbiCoder.encode( + ["address", "uint256"], + [deployer.address, amount] + ), + ]; + const value = [0]; + const callType = [2]; + + const assetForwarderData = defaultAbiCoder.encode( + ["address", "address[]", "uint256[]", "uint256[]", "bytes[]"], + [deployer.address, targets, value, callType, data] + ); + + const balBefore = await ethers.provider.getBalance(deployer.address); + const swEthBalBefore = await swEth.balanceOf(deployer.address); + + await mockAssetForwarder.handleMessage( + NATIVE_TOKEN, + amount, + assetForwarderData, + batchTransaction.address, + { value: amount } + ); + + const balAfter = await ethers.provider.getBalance(deployer.address); + const swEthBalAfter = await swEth.balanceOf(deployer.address); + + expect(balAfter).lt(balBefore); + expect(swEthBalAfter).gt(swEthBalBefore); + }); + + it("Can stake ETH on Swell on dest chain when instruction is received directly on SwellStakeEth adapter", async () => { + const { swellStakeEthAdapter, swEth, mockAssetForwarder } = + await setupTests(); + + const amount = "100000000000000000"; + + const data = defaultAbiCoder.encode(["address"], [deployer.address]); + + const balBefore = await ethers.provider.getBalance(deployer.address); + const swEthBalBefore = await swEth.balanceOf(deployer.address); + + await mockAssetForwarder.handleMessage( + NATIVE_TOKEN, + amount, + data, + swellStakeEthAdapter.address, + { value: amount } + ); + + const balAfter = await ethers.provider.getBalance(deployer.address); + const swEthBalAfter = await swEth.balanceOf(deployer.address); + + expect(balAfter).lt(balBefore); + expect(swEthBalAfter).gt(swEthBalBefore); + }); +});