diff --git a/packages/foundry/foundry.toml b/packages/foundry/foundry.toml index e6320750..6ef1c7d7 100644 --- a/packages/foundry/foundry.toml +++ b/packages/foundry/foundry.toml @@ -19,6 +19,7 @@ remappings = [ [rpc_endpoints] mainnet = "${ETH_RPC_URL}" polygon = "${POLYGON_RPC_URL}" +optimism = "${OPTIMISM_RPC_URL}" [profile.ci] src = 'src' diff --git a/packages/foundry/src/vault/adapter/hop/HopAdapter.sol b/packages/foundry/src/vault/adapter/hop/HopAdapter.sol new file mode 100644 index 00000000..f994b6a6 --- /dev/null +++ b/packages/foundry/src/vault/adapter/hop/HopAdapter.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-3.0 +// Docgen-SOLC: 0.8.15 + +pragma solidity ^0.8.15; + +import "hardhat/console.sol"; + +import { AdapterBase, IERC20, IERC20Metadata, SafeERC20, ERC20, Math, IStrategy, IAdapter } from "../abstracts/AdapterBase.sol"; +import { WithRewards, IWithRewards } from "../abstracts/WithRewards.sol"; +import { ILiquidityPool, IStakingRewards } from "./IHop.sol"; + +/** + * @title Hop Protocol Adapter + * @notice ERC4626 wrapper for Hop Vaults. + * + * An ERC4626 compliant Wrapper for https://github.com/hop-exchange/contracts/blob/28ec0f1a8df497a102c0a3e779a68a81bf69b9ad/contracts/saddle/Swap.sol. + * Allows wrapping Hop Vaults. + */ +contract HopAdapter is AdapterBase, WithRewards { + using SafeERC20 for IERC20; + using Math for uint256; + + string internal _name; + string internal _symbol; + + uint256[] amounts; + uint256[] minAmounts; + uint256 deadline; + uint256 minToMint; + + // @notice The Reward interface + IStakingRewards public stakingRewards; + + /// @notice The Hop protocol swap contract + ILiquidityPool public liquidityPool; + + // @notice The address of the LP token //need to stake this + address LPToken; + + /** + * @notice Initialize a new Hop Adapter. + * @param adapterInitData Encoded data for the base adapter initialization. + * @dev This function is called by the factory contract when deploying a new vault. + */ + + function initialize( + bytes memory adapterInitData, + address registry, + bytes memory hopInitData + ) external initializer { + __AdapterBase_init(adapterInitData); + + (address _liquidityPool, address _stakingRewards) = abi.decode(hopInitData, (address, address)); + + liquidityPool = ILiquidityPool(registry); + stakingRewards = IStakingRewards(_stakingRewards); + + require(asset() == liquidityPool.getToken(0), "Asset tokens do not match"); + + ILiquidityPool.Swap memory swapStorage = liquidityPool.swapStorage(); + + LPToken = swapStorage.lpToken; + + deadline = block.timestamp; + + minToMint = 0; + + _name = string.concat("Popcorn Hop", IERC20Metadata(asset()).name(), " Adapter"); + _symbol = string.concat("popB-", IERC20Metadata(asset()).symbol()); + + IERC20(asset()).approve(address(liquidityPool), type(uint256).max); + IERC20(LPToken).approve(address(liquidityPool), type(uint256).max); + IERC20(LPToken).approve(address(stakingRewards), type(uint256).max); + IERC20(LPToken).approve(address(this), type(uint256).max); + } + + function name() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _name; + } + + function symbol() public view override(IERC20Metadata, ERC20) returns (string memory) { + return _symbol; + } + + /*////////////////////////////////////////////////////////////// + ACCOUNTING LOGIC + //////////////////////////////////////////////////////////////*/ + + /// @notice Calculates the total amount of underlying tokens the Vault holds. + /// @return The total amount of underlying tokens the Vault holds. + + function _totalAssets() internal view override returns (uint256) { + return paused() ? IERC20(asset()).balanceOf(address(this)) : stakingRewards.balanceOf(address(this)); + } + + /// @notice The amount of hop shares to withdraw given an amount of adapter shares + function convertToUnderlyingShares(uint256 assets, uint256 shares) public view override returns (uint256) { + uint256 supply = totalSupply(); + return supply == 0 ? shares : shares.mulDiv(IERC20(LPToken).balanceOf(address(this)), supply, Math.Rounding.Up); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL HOOKS LOGIC + //////////////////////////////////////////////////////////////*/ + + function _protocolDeposit(uint256 amount, uint256) internal virtual override { + amounts = [amount, 0]; + liquidityPool.addLiquidity(amounts, minToMint, deadline); + console.log("this is staking rewards", address(stakingRewards)); //0xf587B9309c603feEdf0445aF4D3B21300989e93a + uint256 adapBal = stakingRewards.balanceOf(address(this)); + console.log("This is the staking rewards bal of adapter", adapBal); + console.log("this is staking token on staking contact", stakingRewards.stakingToken()); + uint256 balance = IERC20(LPToken).balanceOf(address(this)); + uint256 stakingBalance = IERC20(stakingRewards.stakingToken()).balanceOf(address(this)); + + console.log("This is the lp token balance of adap", balance); + console.log("This is the staking token balance of adap", stakingBalance); + stakingRewards.stake(10); + } + + /// @notice Withdraw from the hop vault + function _protocolWithdraw(uint256, uint256 shares) internal virtual override { + require(shares > 0, "shares cant be 0"); + uint256 hopShares = convertToUnderlyingShares(0, shares); + minAmounts = [hopShares, 0]; + liquidityPool.removeLiquidity(hopShares, minAmounts, deadline); + stakingRewards.withdraw(hopShares); + + //( convertToUnderlyingShares / totalSupply ) * usershares + // also need to factor in slippage + } + + /*////////////////////////////////////////////////////////////// + STRATEGY LOGIC + //////////////////////////////////////////////////////////////*/ + /// @notice Claim rewards from Hop + function claim() public override onlyStrategy { + stakingRewards.getReward(); + } + + /// @notice The token rewarded + function rewardTokens() external view override returns (address[] memory) { + address[] memory _rewardTokens = new address[](1); + _rewardTokens[0] = LPToken; + } + + /*////////////////////////////////////////////////////////////// + EIP-165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) public pure override(WithRewards, AdapterBase) returns (bool) { + return interfaceId == type(IWithRewards).interfaceId || interfaceId == type(IAdapter).interfaceId; + } +} diff --git a/packages/foundry/src/vault/adapter/hop/IHop.sol b/packages/foundry/src/vault/adapter/hop/IHop.sol new file mode 100644 index 00000000..c10cefa6 --- /dev/null +++ b/packages/foundry/src/vault/adapter/hop/IHop.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.10; + +interface ILiquidityPool { + struct Swap { + uint256 initialA; + uint256 futureA; + uint256 initialATime; + uint256 futureATime; + uint256 swapFee; + uint256 adminFee; + uint256 defaultWithdrawFee; + address lpToken; + } + + function addLiquidity( + uint256[] calldata amounts, + uint256 minToMint, + uint256 deadline + ) external returns (uint256); + + function removeLiquidity( + uint256 amount, + uint256[] calldata minAmounts, + uint256 deadline + ) external returns (uint256[] memory); + + function getVirtualPrice() external view returns (uint256); + + function getToken(uint8) external view returns (address); + + function swapStorage() external view returns (ILiquidityPool.Swap memory); +} + +interface IStakingRewards { + // Views + function lastTimeRewardApplicable() external view returns (uint256); + + function rewardPerToken() external view returns (uint256); + + function earned(address account) external view returns (uint256); + + function getRewardForDuration() external view returns (uint256); + + function totalSupply() external view returns (uint256); + + function balanceOf(address account) external view returns (uint256); + + function stakingToken() external view returns (address); + + // Mutative + + function stake(uint256 amount) external; + + function withdraw(uint256 amount) external; + + function getReward() external; + + function exit() external; +} diff --git a/packages/foundry/test/vault/integration/hop/HopAdapter.t.sol b/packages/foundry/test/vault/integration/hop/HopAdapter.t.sol new file mode 100644 index 00000000..d6cfa145 --- /dev/null +++ b/packages/foundry/test/vault/integration/hop/HopAdapter.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.15; + +import { Test } from "forge-std/Test.sol"; + +import { HopAdapter, SafeERC20, IERC20, IERC20Metadata, Math, ILiquidityPool, IStakingRewards } from "../../../../src/vault/adapter/hop/HopAdapter.sol"; +import { HopTestConfigStorage, HopTestConfig } from "./HopTestConfigStorage.sol"; +import { AbstractAdapterTest, ITestConfigStorage, IAdapter } from "../abstract/AbstractAdapterTest.sol"; + +contract HopAdapterTest is AbstractAdapterTest { + using Math for uint256; + + // Note: using the hop liquidity pool contract + // https://optimistic.etherscan.io/address/0xaa30d6bba6285d0585722e2440ff89e23ef68864#writeContract + ILiquidityPool public liquidityPool = ILiquidityPool(0xaa30D6bba6285d0585722e2440Ff89E23EF68864); + //IStakingRewards public stakingRewards = IStakingRewards(0xfD49C7EE330fE060ca66feE33d49206eB96F146D); + IStakingRewards public stakingRewards = IStakingRewards(0xf587B9309c603feEdf0445aF4D3B21300989e93a); + address public LPToken; + + function setUp() public { + uint256 forkId = vm.createSelectFork(vm.rpcUrl("optimism")); + vm.selectFork(forkId); + + testConfigStorage = ITestConfigStorage(address(new HopTestConfigStorage())); + + _setUpTest(testConfigStorage.getTestConfig(0)); + } + + function overrideSetup(bytes memory testConfig) public override { + _setUpTest(testConfig); + } + + function _setUpTest(bytes memory testConfig) internal { + (address _liquidityPool, address _stakingRewards) = abi.decode(testConfig, (address, address)); + liquidityPool = ILiquidityPool(_liquidityPool); + + stakingRewards = IStakingRewards(_stakingRewards); + address asset = liquidityPool.getToken(0); + + ILiquidityPool.Swap memory swapStorage = liquidityPool.swapStorage(); + + LPToken = swapStorage.lpToken; + + setUpBaseTest(IERC20(asset), address(new HopAdapter()), address(liquidityPool), 10, "Hop", true); + + vm.label(address(liquidityPool), "hopLiquidityPool"); + vm.label(address(stakingRewards), "hopStakingRewards"); + vm.label(address(LPToken), "hopLPToken"); + vm.label(address(asset), "asset"); + vm.label(address(this), "test"); + + adapter.initialize(abi.encode(asset, address(this), strategy, 0, sigs, ""), externalRegistry, testConfig); + } + + /*////////////////////////////////////////////////////////////// + HELPER + //////////////////////////////////////////////////////////////*/ + + // Verify that totalAssets returns the expected amount + function verify_totalAssets() public override { + deal(address(asset), bob, defaultAmount); + vm.startPrank(bob); + asset.approve(address(adapter), defaultAmount); + adapter.deposit(defaultAmount, bob); + vm.stopPrank(); + + assertEq( + adapter.totalAssets(), + adapter.convertToAssets(adapter.totalSupply()), + string.concat("totalSupply converted != totalAssets", baseTestId) + ); + } + + /*////////////////////////////////////////////////////////////// + INITIALIZATION + //////////////////////////////////////////////////////////////*/ + + function verify_adapterInit() public override { + assertEq(adapter.asset(), address(asset), "asset"); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popB-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + assertEq( + IERC20Metadata(address(adapter)).symbol(), + string.concat("popB-", IERC20Metadata(address(asset)).symbol()), + "symbol" + ); + + assertEq(asset.allowance(address(adapter), address(liquidityPool)), type(uint256).max, "allowance"); + } +} diff --git a/packages/foundry/test/vault/integration/hop/HopTestConfigStorage.sol b/packages/foundry/test/vault/integration/hop/HopTestConfigStorage.sol new file mode 100644 index 00000000..bfe0dd56 --- /dev/null +++ b/packages/foundry/test/vault/integration/hop/HopTestConfigStorage.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.8.15; + +import { ITestConfigStorage } from "../abstract/ITestConfigStorage.sol"; + +struct HopTestConfig { + address liquidityPool; + address stakingRewards; +} + +contract HopTestConfigStorage is ITestConfigStorage { + HopTestConfig[] internal testConfigs; + + constructor() { + testConfigs.push( + //HopTestConfig(0xaa30D6bba6285d0585722e2440Ff89E23EF68864, 0xfD49C7EE330fE060ca66feE33d49206eB96F146D) + HopTestConfig(0xaa30D6bba6285d0585722e2440Ff89E23EF68864, 0xf587B9309c603feEdf0445aF4D3B21300989e93a) + ); + } + + function getTestConfig(uint256 i) public view returns (bytes memory) { + return abi.encode(testConfigs[i].liquidityPool, testConfigs[i].stakingRewards); + } + + function getTestConfigLength() public view returns (uint256) { + return testConfigs.length; + } +}