Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/foundry/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ remappings = [
[rpc_endpoints]
mainnet = "${ETH_RPC_URL}"
polygon = "${POLYGON_RPC_URL}"
optimism = "${OPTIMISM_RPC_URL}"

[profile.ci]
src = 'src'
Expand Down
154 changes: 154 additions & 0 deletions packages/foundry/src/vault/adapter/hop/HopAdapter.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
60 changes: 60 additions & 0 deletions packages/foundry/src/vault/adapter/hop/IHop.sol
Original file line number Diff line number Diff line change
@@ -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;
}
93 changes: 93 additions & 0 deletions packages/foundry/test/vault/integration/hop/HopAdapter.t.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}