diff --git a/packages/contracts/contracts/core/HackMoneyVault.sol b/packages/contracts/contracts/core/HackMoneyVault.sol index 57e3fcb..fe52df7 100644 --- a/packages/contracts/contracts/core/HackMoneyVault.sol +++ b/packages/contracts/contracts/core/HackMoneyVault.sol @@ -84,6 +84,11 @@ contract HackMoneyVault is Multicall, Ownable, BaseVault { uint premiumExchangeValue ) = strategy.doTrade(size); // update the remaining locked amount + console.log( + "vaultState.lockedAmountLeft:", + vaultState.lockedAmountLeft + ); + console.log("capitalUsed:", capitalUsed); vaultState.lockedAmountLeft = vaultState.lockedAmountLeft - capitalUsed; // todo: udpate events diff --git a/packages/contracts/contracts/core/LyraVaultFuerte.sol b/packages/contracts/contracts/core/LyraVaultFuerte.sol deleted file mode 100644 index f162dfc..0000000 --- a/packages/contracts/contracts/core/LyraVaultFuerte.sol +++ /dev/null @@ -1,125 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -// import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -// import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; - -// import {BaseVault} from "./BaseVault.sol"; -// import {Vault} from "../libraries/Vault.sol"; - -// import {IStrategyFuerte} from "../interfaces/IStrategyFuerte.sol"; - -// /// @notice LyraVault help users run option-selling strategies on Lyra AMM. -// contract LyraVaultFuerte is Ownable, BaseVault { -// IERC20 public immutable premiumAsset; -// IERC20 public immutable collateralAsset; - -// IStrategyFuerte public strategy; -// address public lyraRewardRecipient; - -// // Amount locked for scheduled withdrawals last week; -// uint128 public lastQueuedWithdrawAmount; -// // % of funds to be used for weekly option purchase -// uint public optionAllocation; - -// event StrategyUpdated(address strategy); - -// event Trade(address user, uint callPositionId, uint putPositionId, uint premium, uint capitalUsed); - -// event RoundStarted(uint16 roundId, uint104 lockAmount); - -// event RoundClosed(uint16 roundId, uint104 lockAmount); - -// constructor( -// address _susd, -// address _feeRecipient, -// uint _roundDuration, -// string memory _tokenName, -// string memory _tokenSymbol, -// Vault.VaultParams memory _vaultParams -// ) BaseVault(_feeRecipient, _roundDuration, _tokenName, _tokenSymbol, _vaultParams) { -// premiumAsset = IERC20(_susd); -// collateralAsset = IERC20(_vaultParams.asset); -// } - -// /// @dev set strategy contract. This function can only be called by owner. -// /// @param _strategy new strategy contract address -// function setStrategy(address _strategy) external onlyOwner { -// if (address(strategy) != address(0)) { -// collateralAsset.approve(address(strategy), 0); -// } - -// strategy = IStrategyFuerte(_strategy); -// collateralAsset.approve(_strategy, type(uint).max); -// emit StrategyUpdated(_strategy); -// } - -// /// @dev anyone can trigger a trade -// /// @param callStrikeId the strike id to sell -// /// @param putStrikeId the strike id to sell -// function trade(uint callStrikeId, uint putStrikeId) external { -// require(vaultState.roundInProgress, "round closed"); -// // perform trade through strategy -// //(uint positionId, uint premiumReceived, uint capitalUsed) -// (uint callPositionId, -// uint putPositionId, -// uint premiumReceived, -// uint callCollateral, -// uint putCollateral) = strategy.doTrade( -// callStrikeId, -// putStrikeId, -// lyraRewardRecipient -// ); - -// // update the remaining locked amount -// vaultState.lockedAmountLeft = vaultState.lockedAmountLeft - callCollateral - putCollateral; - -// // todo: udpate events -// emit Trade(msg.sender, positionId, premiumReceived, capitalUsed); -// } - -// /// @dev anyone close part of the position with premium made by the strategy if a position is dangerous -// /// @param positionId the positiion to close -// function reducePosition(uint positionId, uint closeAmount) external { -// strategy.reducePosition(positionId, closeAmount, lyraRewardRecipient); -// } - -// /// @dev close the current round, enable user to deposit for the next round -// function closeRound() external { -// uint104 lockAmount = vaultState.lockedAmount; -// vaultState.lastLockedAmount = lockAmount; -// vaultState.lockedAmountLeft = 0; -// vaultState.lockedAmount = 0; -// vaultState.nextRoundReadyTimestamp = block.timestamp + Vault.ROUND_DELAY; -// vaultState.roundInProgress = false; - -// // won't be able to close if positions are not settled -// strategy.returnFundsAndClearStrikes(); - -// emit RoundClosed(vaultState.round, lockAmount); -// } - -// /// @notice start the next round -// /// @param boardId board id (asset + expiry) for next round. -// function startNextRound(uint boardId) external onlyOwner { -// require(!vaultState.roundInProgress, "round opened"); -// require(block.timestamp > vaultState.nextRoundReadyTimestamp, "CD"); - -// strategy.setBoard(boardId); - -// (uint lockedBalance, uint queuedWithdrawAmount) = _rollToNextRound(uint(lastQueuedWithdrawAmount)); - -// vaultState.lockedAmount = uint104(lockedBalance); -// vaultState.lockedAmountLeft = lockedBalance; -// vaultState.roundInProgress = true; -// lastQueuedWithdrawAmount = uint128(queuedWithdrawAmount); - -// emit RoundStarted(vaultState.round, uint104(lockedBalance)); -// } - -// /// @notice set set new address to receive Lyra trading reward on behalf of the vault -// /// @param recipient recipient address -// function setLyraRewardRecipient(address recipient) external onlyOwner { -// lyraRewardRecipient = recipient; -// } -// } diff --git a/packages/contracts/contracts/interfaces/IDelegateApprovals.sol b/packages/contracts/contracts/interfaces/IDelegateApprovals.sol new file mode 100644 index 0000000..4520cf5 --- /dev/null +++ b/packages/contracts/contracts/interfaces/IDelegateApprovals.sol @@ -0,0 +1,46 @@ +pragma solidity >=0.4.24; + +// https://docs.synthetix.io/contracts/source/interfaces/idelegateapprovals +interface IDelegateApprovals { + // Views + function canBurnFor(address authoriser, address delegate) + external + view + returns (bool); + + function canIssueFor(address authoriser, address delegate) + external + view + returns (bool); + + function canClaimFor(address authoriser, address delegate) + external + view + returns (bool); + + function canExchangeFor(address authoriser, address delegate) + external + view + returns (bool); + + // Mutative + function approveAllDelegatePowers(address delegate) external; + + function removeAllDelegatePowers(address delegate) external; + + function approveBurnOnBehalf(address delegate) external; + + function removeBurnOnBehalf(address delegate) external; + + function approveIssueOnBehalf(address delegate) external; + + function removeIssueOnBehalf(address delegate) external; + + function approveClaimOnBehalf(address delegate) external; + + function removeClaimOnBehalf(address delegate) external; + + function approveExchangeOnBehalf(address delegate) external; + + function removeExchangeOnBehalf(address delegate) external; +} diff --git a/packages/contracts/contracts/poly.sol b/packages/contracts/contracts/poly.sol new file mode 100644 index 0000000..957a513 --- /dev/null +++ b/packages/contracts/contracts/poly.sol @@ -0,0 +1,2067 @@ +/** + *Submitted for verification at optimistic.etherscan.io on 2022-03-29 + */ + +// Sources flattened with hardhat v2.8.3 https://hardhat.org + +// File @rari-capital/solmate/src/auth/Auth.sol@v6.2.0 + +pragma solidity >=0.8.0; + +/// @notice Provides a flexible and updatable auth pattern which is completely separate from application logic. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol) +/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) +abstract contract Auth { + event OwnerUpdated(address indexed user, address indexed newOwner); + + event AuthorityUpdated( + address indexed user, + Authority indexed newAuthority + ); + + address public owner; + + Authority public authority; + + constructor(address _owner, Authority _authority) { + owner = _owner; + authority = _authority; + + emit OwnerUpdated(msg.sender, _owner); + emit AuthorityUpdated(msg.sender, _authority); + } + + modifier requiresAuth() { + require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED"); + + _; + } + + function isAuthorized(address user, bytes4 functionSig) + internal + view + virtual + returns (bool) + { + Authority auth = authority; // Memoizing authority saves us a warm SLOAD, around 100 gas. + + // Checking if the caller is the owner only after calling the authority saves gas in most cases, but be + // aware that this makes protected functions uncallable even to the owner if the authority is out of order. + return + (address(auth) != address(0) && + auth.canCall(user, address(this), functionSig)) || + user == owner; + } + + function setAuthority(Authority newAuthority) public virtual { + // We check if the caller is the owner first because we want to ensure they can + // always swap out the authority even if it's reverting or using up a lot of gas. + require( + msg.sender == owner || + authority.canCall(msg.sender, address(this), msg.sig) + ); + + authority = newAuthority; + + emit AuthorityUpdated(msg.sender, newAuthority); + } + + function setOwner(address newOwner) public virtual requiresAuth { + owner = newOwner; + + emit OwnerUpdated(msg.sender, newOwner); + } +} + +/// @notice A generic interface for a contract which provides authorization data to an Auth instance. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol) +/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) +interface Authority { + function canCall( + address user, + address target, + bytes4 functionSig + ) external view returns (bool); +} + +// File @rari-capital/solmate/src/tokens/ERC20.sol@v6.2.0 + +pragma solidity >=0.8.0; + +/// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) +/// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. +abstract contract ERC20 { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 amount); + + event Approval( + address indexed owner, + address indexed spender, + uint256 amount + ); + + /*/////////////////////////////////////////////////////////////// + METADATA STORAGE + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + uint8 public immutable decimals; + + /*/////////////////////////////////////////////////////////////// + ERC20 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(address => mapping(address => uint256)) public allowance; + + /*/////////////////////////////////////////////////////////////// + EIP-2612 STORAGE + //////////////////////////////////////////////////////////////*/ + + bytes32 public constant PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + uint256 internal immutable INITIAL_CHAIN_ID; + + bytes32 internal immutable INITIAL_DOMAIN_SEPARATOR; + + mapping(address => uint256) public nonces; + + /*/////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals + ) { + name = _name; + symbol = _symbol; + decimals = _decimals; + + INITIAL_CHAIN_ID = block.chainid; + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + } + + /*/////////////////////////////////////////////////////////////// + ERC20 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 amount) + public + virtual + returns (bool) + { + allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) + public + virtual + returns (bool) + { + balanceOf[msg.sender] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual returns (bool) { + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) + allowance[from][msg.sender] = allowed - amount; + + balanceOf[from] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(from, to, amount); + + return true; + } + + /*/////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + //////////////////////////////////////////////////////////////*/ + + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); + + // Unchecked because the only math done is incrementing + // the owner's nonce which cannot realistically overflow. + unchecked { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + PERMIT_TYPEHASH, + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ); + + address recoveredAddress = ecrecover(digest, v, r, s); + + require( + recoveredAddress != address(0) && recoveredAddress == owner, + "INVALID_SIGNER" + ); + + allowance[recoveredAddress][spender] = value; + } + + emit Approval(owner, spender, value); + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return + block.chainid == INITIAL_CHAIN_ID + ? INITIAL_DOMAIN_SEPARATOR + : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return + keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes(name)), + keccak256("1"), + block.chainid, + address(this) + ) + ); + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 amount) internal virtual { + totalSupply += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[to] += amount; + } + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + balanceOf[from] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply -= amount; + } + + emit Transfer(from, address(0), amount); + } +} + +// File @rari-capital/solmate/src/utils/FixedPointMathLib.sol@v6.2.0 + +pragma solidity >=0.8.0; + +/// @notice Arithmetic library with operations for fixed-point numbers. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol) +library FixedPointMathLib { + /*/////////////////////////////////////////////////////////////// + COMMON BASE UNITS + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant YAD = 1e8; + uint256 internal constant WAD = 1e18; + uint256 internal constant RAY = 1e27; + uint256 internal constant RAD = 1e45; + + /*/////////////////////////////////////////////////////////////// + FIXED POINT OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function fmul( + uint256 x, + uint256 y, + uint256 baseUnit + ) internal pure returns (uint256 z) { + assembly { + // Store x * y in z for now. + z := mul(x, y) + + // Equivalent to require(x == 0 || (x * y) / x == y) + if iszero(or(iszero(x), eq(div(z, x), y))) { + revert(0, 0) + } + + // If baseUnit is zero this will return zero instead of reverting. + z := div(z, baseUnit) + } + } + + function fdiv( + uint256 x, + uint256 y, + uint256 baseUnit + ) internal pure returns (uint256 z) { + assembly { + // Store x * baseUnit in z for now. + z := mul(x, baseUnit) + + // Equivalent to require(y != 0 && (x == 0 || (x * baseUnit) / x == baseUnit)) + if iszero( + and(iszero(iszero(y)), or(iszero(x), eq(div(z, x), baseUnit))) + ) { + revert(0, 0) + } + + // We ensure y is not zero above, so there is never division by zero here. + z := div(z, y) + } + } + + function fpow( + uint256 x, + uint256 n, + uint256 baseUnit + ) internal pure returns (uint256 z) { + assembly { + switch x + case 0 { + switch n + case 0 { + // 0 ** 0 = 1 + z := baseUnit + } + default { + // 0 ** n = 0 + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + // If n is even, store baseUnit in z for now. + z := baseUnit + } + default { + // If n is odd, store x in z for now. + z := x + } + + // Shifting right by 1 is like dividing by 2. + let half := shr(1, baseUnit) + + for { + // Shift n right by 1 before looping to halve it. + n := shr(1, n) + } n { + // Shift n right by 1 each iteration to halve it. + n := shr(1, n) + } { + // Revert immediately if x ** 2 would overflow. + // Equivalent to iszero(eq(div(xx, x), x)) here. + if shr(128, x) { + revert(0, 0) + } + + // Store x squared. + let xx := mul(x, x) + + // Round to the nearest number. + let xxRound := add(xx, half) + + // Revert if xx + half overflowed. + if lt(xxRound, xx) { + revert(0, 0) + } + + // Set x to scaled xxRound. + x := div(xxRound, baseUnit) + + // If n is even: + if mod(n, 2) { + // Compute z * x. + let zx := mul(z, x) + + // If z * x overflowed: + if iszero(eq(div(zx, x), z)) { + // Revert if x is non-zero. + if iszero(iszero(x)) { + revert(0, 0) + } + } + + // Round to the nearest number. + let zxRound := add(zx, half) + + // Revert if zx + half overflowed. + if lt(zxRound, zx) { + revert(0, 0) + } + + // Return properly scaled zxRound. + z := div(zxRound, baseUnit) + } + } + } + } + } + + /*/////////////////////////////////////////////////////////////// + GENERAL NUMBER UTILITIES + //////////////////////////////////////////////////////////////*/ + + function sqrt(uint256 x) internal pure returns (uint256 z) { + assembly { + // Start off with z at 1. + z := 1 + + // Used below to help find a nearby power of 2. + let y := x + + // Find the lowest power of 2 that is at least sqrt(x). + if iszero(lt(y, 0x100000000000000000000000000000000)) { + y := shr(128, y) // Like dividing by 2 ** 128. + z := shl(64, z) + } + if iszero(lt(y, 0x10000000000000000)) { + y := shr(64, y) // Like dividing by 2 ** 64. + z := shl(32, z) + } + if iszero(lt(y, 0x100000000)) { + y := shr(32, y) // Like dividing by 2 ** 32. + z := shl(16, z) + } + if iszero(lt(y, 0x10000)) { + y := shr(16, y) // Like dividing by 2 ** 16. + z := shl(8, z) + } + if iszero(lt(y, 0x100)) { + y := shr(8, y) // Like dividing by 2 ** 8. + z := shl(4, z) + } + if iszero(lt(y, 0x10)) { + y := shr(4, y) // Like dividing by 2 ** 4. + z := shl(2, z) + } + if iszero(lt(y, 0x8)) { + // Equivalent to 2 ** z. + z := shl(1, z) + } + + // Shifting right by 1 is like dividing by 2. + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + + // Compute a rounded down version of z. + let zRoundDown := div(x, z) + + // If zRoundDown is smaller, use it. + if lt(zRoundDown, z) { + z := zRoundDown + } + } + } +} + +// File @rari-capital/solmate/src/utils/ReentrancyGuard.sol@v6.2.0 + +pragma solidity >=0.8.0; + +/// @notice Gas optimized reentrancy protection for smart contracts. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/ReentrancyGuard.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) +abstract contract ReentrancyGuard { + uint256 private reentrancyStatus = 1; + + modifier nonReentrant() { + require(reentrancyStatus == 1, "REENTRANCY"); + + reentrancyStatus = 2; + + _; + + reentrancyStatus = 1; + } +} + +// File @rari-capital/solmate/src/utils/SafeTransferLib.sol@v6.2.0 + +pragma solidity >=0.8.0; + +/// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol) +/// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol) +/// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. +library SafeTransferLib { + /*/////////////////////////////////////////////////////////////// + ETH OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function safeTransferETH(address to, uint256 amount) internal { + bool callStatus; + + assembly { + // Transfer the ETH and store if it succeeded or not. + callStatus := call(gas(), to, amount, 0, 0, 0, 0) + } + + require(callStatus, "ETH_TRANSFER_FAILED"); + } + + /*/////////////////////////////////////////////////////////////// + ERC20 OPERATIONS + //////////////////////////////////////////////////////////////*/ + + function safeTransferFrom( + ERC20 token, + address from, + address to, + uint256 amount + ) internal { + bool callStatus; + + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata to memory piece by piece: + mstore( + freeMemoryPointer, + 0x23b872dd00000000000000000000000000000000000000000000000000000000 + ) // Begin with the function selector. + mstore( + add(freeMemoryPointer, 4), + and(from, 0xffffffffffffffffffffffffffffffffffffffff) + ) // Mask and append the "from" argument. + mstore( + add(freeMemoryPointer, 36), + and(to, 0xffffffffffffffffffffffffffffffffffffffff) + ) // Mask and append the "to" argument. + mstore(add(freeMemoryPointer, 68), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. + + // Call the token and store if it succeeded or not. + // We use 100 because the calldata length is 4 + 32 * 3. + callStatus := call(gas(), token, 0, freeMemoryPointer, 100, 0, 0) + } + + require( + didLastOptionalReturnCallSucceed(callStatus), + "TRANSFER_FROM_FAILED" + ); + } + + function safeTransfer( + ERC20 token, + address to, + uint256 amount + ) internal { + bool callStatus; + + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata to memory piece by piece: + mstore( + freeMemoryPointer, + 0xa9059cbb00000000000000000000000000000000000000000000000000000000 + ) // Begin with the function selector. + mstore( + add(freeMemoryPointer, 4), + and(to, 0xffffffffffffffffffffffffffffffffffffffff) + ) // Mask and append the "to" argument. + mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. + + // Call the token and store if it succeeded or not. + // We use 68 because the calldata length is 4 + 32 * 2. + callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) + } + + require( + didLastOptionalReturnCallSucceed(callStatus), + "TRANSFER_FAILED" + ); + } + + function safeApprove( + ERC20 token, + address to, + uint256 amount + ) internal { + bool callStatus; + + assembly { + // Get a pointer to some free memory. + let freeMemoryPointer := mload(0x40) + + // Write the abi-encoded calldata to memory piece by piece: + mstore( + freeMemoryPointer, + 0x095ea7b300000000000000000000000000000000000000000000000000000000 + ) // Begin with the function selector. + mstore( + add(freeMemoryPointer, 4), + and(to, 0xffffffffffffffffffffffffffffffffffffffff) + ) // Mask and append the "to" argument. + mstore(add(freeMemoryPointer, 36), amount) // Finally append the "amount" argument. No mask as it's a full 32 byte value. + + // Call the token and store if it succeeded or not. + // We use 68 because the calldata length is 4 + 32 * 2. + callStatus := call(gas(), token, 0, freeMemoryPointer, 68, 0, 0) + } + + require(didLastOptionalReturnCallSucceed(callStatus), "APPROVE_FAILED"); + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL HELPER LOGIC + //////////////////////////////////////////////////////////////*/ + + function didLastOptionalReturnCallSucceed(bool callStatus) + private + pure + returns (bool success) + { + assembly { + // Get how many bytes the call returned. + let returnDataSize := returndatasize() + + // If the call reverted: + if iszero(callStatus) { + // Copy the revert message into memory. + returndatacopy(0, 0, returnDataSize) + + // Revert with the same message. + revert(0, returnDataSize) + } + + switch returnDataSize + case 32 { + // Copy the return data into memory. + returndatacopy(0, 0, returnDataSize) + + // Set success to whether it returned true. + success := iszero(iszero(mload(0))) + } + case 0 { + // There was no return data. + success := 1 + } + default { + // It returned some malformed input. + success := 0 + } + } + } +} + +// File contracts/interfaces/IPolynomialCoveredCall.sol + +pragma solidity 0.8.9; + +interface IPolynomialCoveredCall { + struct UserInfo { + uint256 depositRound; + uint256 pendingDeposit; + uint256 withdrawRound; + uint256 withdrawnShares; + uint256 totalShares; + } + + function deposit(uint256 _amt) external; + + function deposit(address _user, uint256 _amt) external; + + function requestWithdraw(uint256 _shares) external; + + function completeWithdraw() external; + + function cancelWithdraw(uint256 _shares) external; +} + +// File contracts/interfaces/lyra/ILyraDistributor.sol + +pragma solidity 0.8.9; + +interface ILyraDistributor { + function claim() external; +} + +// File contracts/interfaces/lyra/ICollateralShort.sol + +pragma solidity 0.8.9; + +interface ICollateralShort { + struct Loan { + // ID for the loan + uint id; + // Account that created the loan + address account; + // Amount of collateral deposited + uint collateral; + // The synth that was borrowed + bytes32 currency; + // Amount of synths borrowed + uint amount; + // Indicates if the position was short sold + bool short; + // interest amounts accrued + uint accruedInterest; + // last interest index + uint interestIndex; + // time of last interaction. + uint lastInteraction; + } + + function loans(uint id) + external + returns ( + uint, + address, + uint, + bytes32, + uint, + bool, + uint, + uint, + uint + ); + + function minCratio() external returns (uint); + + function minCollateral() external returns (uint); + + function issueFeeRate() external returns (uint); + + function open( + uint collateral, + uint amount, + bytes32 currency + ) external returns (uint id); + + function repay( + address borrower, + uint id, + uint amount + ) external returns (uint short, uint collateral); + + function repayWithCollateral(uint id, uint repayAmount) + external + returns (uint short, uint collateral); + + function draw(uint id, uint amount) + external + returns (uint short, uint collateral); + + // Same as before + function deposit( + address borrower, + uint id, + uint amount + ) external returns (uint short, uint collateral); + + // Same as before + function withdraw(uint id, uint amount) + external + returns (uint short, uint collateral); + + // function to return the loan details in one call, without needing to know about the collateralstate + function getShortAndCollateral(address account, uint id) + external + view + returns (uint short, uint collateral); +} + +// File contracts/interfaces/lyra/IExchangeRates.sol + +pragma solidity 0.8.9; + +// https://docs.synthetix.io/contracts/source/interfaces/iexchangerates +interface IExchangeRates { + function rateAndInvalid(bytes32 currencyKey) + external + view + returns (uint rate, bool isInvalid); +} + +// File contracts/interfaces/lyra/IExchanger.sol + +pragma solidity 0.8.9; + +// https://docs.synthetix.io/contracts/source/interfaces/iexchanger +interface IExchanger { + function feeRateForExchange( + bytes32 sourceCurrencyKey, + bytes32 destinationCurrencyKey + ) external view returns (uint exchangeFeeRate); +} + +// File contracts/interfaces/lyra/ISynthetix.sol + +pragma solidity 0.8.9; + +interface ISynthetix { + function exchange( + bytes32 sourceCurrencyKey, + uint sourceAmount, + bytes32 destinationCurrencyKey + ) external returns (uint amountReceived); + + function exchangeOnBehalf( + address exchangeForAddress, + bytes32 sourceCurrencyKey, + uint sourceAmount, + bytes32 destinationCurrencyKey + ) external returns (uint amountReceived); +} + +// File contracts/interfaces/lyra/ILyraGlobals.sol + +pragma solidity 0.8.9; + +interface ILyraGlobals { + enum ExchangeType { + BASE_QUOTE, + QUOTE_BASE, + ALL + } + + /** + * @dev Structs to help reduce the number of calls between other contracts and this one + * Grouped in usage for a particular contract/use case + */ + struct ExchangeGlobals { + uint spotPrice; + bytes32 quoteKey; + bytes32 baseKey; + ISynthetix synthetix; + ICollateralShort short; + uint quoteBaseFeeRate; + uint baseQuoteFeeRate; + } + + struct GreekCacheGlobals { + int rateAndCarry; + uint spotPrice; + } + + struct PricingGlobals { + uint optionPriceFeeCoefficient; + uint spotPriceFeeCoefficient; + uint vegaFeeCoefficient; + uint vegaNormFactor; + uint standardSize; + uint skewAdjustmentFactor; + int rateAndCarry; + int minDelta; + uint volatilityCutoff; + uint spotPrice; + } + + function synthetix() external view returns (ISynthetix); + + function exchanger() external view returns (IExchanger); + + function exchangeRates() external view returns (IExchangeRates); + + function collateralShort() external view returns (ICollateralShort); + + function isPaused() external view returns (bool); + + function tradingCutoff(address) external view returns (uint); + + function optionPriceFeeCoefficient(address) external view returns (uint); + + function spotPriceFeeCoefficient(address) external view returns (uint); + + function vegaFeeCoefficient(address) external view returns (uint); + + function vegaNormFactor(address) external view returns (uint); + + function standardSize(address) external view returns (uint); + + function skewAdjustmentFactor(address) external view returns (uint); + + function rateAndCarry(address) external view returns (int); + + function minDelta(address) external view returns (int); + + function volatilityCutoff(address) external view returns (uint); + + function quoteKey(address) external view returns (bytes32); + + function baseKey(address) external view returns (bytes32); + + function setGlobals( + ISynthetix _synthetix, + IExchanger _exchanger, + IExchangeRates _exchangeRates, + ICollateralShort _collateralShort + ) external; + + function setGlobalsForContract( + address _contractAddress, + uint _tradingCutoff, + PricingGlobals memory pricingGlobals, + bytes32 _quoteKey, + bytes32 _baseKey + ) external; + + function setPaused(bool _isPaused) external; + + function setTradingCutoff(address _contractAddress, uint _tradingCutoff) + external; + + function setOptionPriceFeeCoefficient( + address _contractAddress, + uint _optionPriceFeeCoefficient + ) external; + + function setSpotPriceFeeCoefficient( + address _contractAddress, + uint _spotPriceFeeCoefficient + ) external; + + function setVegaFeeCoefficient( + address _contractAddress, + uint _vegaFeeCoefficient + ) external; + + function setVegaNormFactor(address _contractAddress, uint _vegaNormFactor) + external; + + function setStandardSize(address _contractAddress, uint _standardSize) + external; + + function setSkewAdjustmentFactor( + address _contractAddress, + uint _skewAdjustmentFactor + ) external; + + function setRateAndCarry(address _contractAddress, int _rateAndCarry) + external; + + function setMinDelta(address _contractAddress, int _minDelta) external; + + function setVolatilityCutoff( + address _contractAddress, + uint _volatilityCutoff + ) external; + + function setQuoteKey(address _contractAddress, bytes32 _quoteKey) external; + + function setBaseKey(address _contractAddress, bytes32 _baseKey) external; + + function getSpotPriceForMarket(address _contractAddress) + external + view + returns (uint); + + function getSpotPrice(bytes32 to) external view returns (uint); + + function getPricingGlobals(address _contractAddress) + external + view + returns (PricingGlobals memory); + + function getGreekCacheGlobals(address _contractAddress) + external + view + returns (GreekCacheGlobals memory); + + function getExchangeGlobals( + address _contractAddress, + ExchangeType exchangeType + ) external view returns (ExchangeGlobals memory exchangeGlobals); + + function getGlobalsForOptionTrade(address _contractAddress, bool isBuy) + external + view + returns ( + PricingGlobals memory pricingGlobals, + ExchangeGlobals memory exchangeGlobals, + uint tradeCutoff + ); +} + +// File contracts/interfaces/lyra/ILiquidityPool.sol + +pragma solidity 0.8.9; + +interface ILiquidityPool { + struct Collateral { + uint quote; + uint base; + } + + /// @dev These are all in quoteAsset amounts. + struct Liquidity { + uint freeCollatLiquidity; + uint usedCollatLiquidity; + uint freeDeltaLiquidity; + uint usedDeltaLiquidity; + } + + enum Error { + QuoteTransferFailed, + AlreadySignalledWithdrawal, + SignallingBetweenRounds, + UnSignalMustSignalFirst, + UnSignalAlreadyBurnable, + WithdrawNotBurnable, + EndRoundWithLiveBoards, + EndRoundAlreadyEnded, + EndRoundMustExchangeBase, + EndRoundMustHedgeDelta, + StartRoundMustEndRound, + ReceivedZeroFromBaseQuoteExchange, + ReceivedZeroFromQuoteBaseExchange, + LockingMoreQuoteThanIsFree, + LockingMoreBaseThanCanBeExchanged, + FreeingMoreBaseThanLocked, + SendPremiumNotEnoughCollateral, + OnlyPoolHedger, + OnlyOptionMarket, + OnlyShortCollateral, + ReentrancyDetected, + Last + } + + function lockedCollateral() external view returns (uint, uint); + + function queuedQuoteFunds() external view returns (uint); + + function expiryToTokenValue(uint) external view returns (uint); + + function deposit(address beneficiary, uint amount) external returns (uint); + + function signalWithdrawal(uint certificateId) external; + + function unSignalWithdrawal(uint certificateId) external; + + function withdraw(address beneficiary, uint certificateId) + external + returns (uint value); + + function tokenPriceQuote() external view returns (uint); + + function endRound() external; + + function startRound(uint lastMaxExpiryTimestamp, uint newMaxExpiryTimestamp) + external; + + function exchangeBase() external; + + function lockQuote(uint amount, uint freeCollatLiq) external; + + function lockBase( + uint amount, + ILyraGlobals.ExchangeGlobals memory exchangeGlobals, + Liquidity memory liquidity + ) external; + + function freeQuoteCollateral(uint amount) external; + + function freeBase(uint amountBase) external; + + function sendPremium( + address recipient, + uint amount, + uint freeCollatLiq + ) external; + + function boardLiquidation( + uint amountQuoteFreed, + uint amountQuoteReserved, + uint amountBaseFreed + ) external; + + function sendReservedQuote(address user, uint amount) external; + + function getTotalPoolValueQuote(uint basePrice, uint usedDeltaLiquidity) + external + view + returns (uint); + + function getLiquidity(uint basePrice, ICollateralShort short) + external + view + returns (Liquidity memory); + + function transferQuoteToHedge( + ILyraGlobals.ExchangeGlobals memory exchangeGlobals, + uint amount + ) external returns (uint); +} + +// File contracts/interfaces/lyra/IOptionMarket.sol + +pragma solidity 0.8.9; + +interface IOptionMarket { + struct OptionListing { + uint id; + uint strike; + uint skew; + uint longCall; + uint shortCall; + uint longPut; + uint shortPut; + uint boardId; + } + + struct OptionBoard { + uint id; + uint expiry; + uint iv; + bool frozen; + uint[] listingIds; + } + + struct Trade { + bool isBuy; + uint amount; + uint vol; + uint expiry; + ILiquidityPool.Liquidity liquidity; + } + + enum TradeType { + LONG_CALL, + SHORT_CALL, + LONG_PUT, + SHORT_PUT + } + + enum Error { + TransferOwnerToZero, + InvalidBoardId, + InvalidBoardIdOrNotFrozen, + InvalidListingIdOrNotFrozen, + StrikeSkewLengthMismatch, + BoardMaxExpiryReached, + CannotStartNewRoundWhenBoardsExist, + ZeroAmountOrInvalidTradeType, + BoardFrozenOrTradingCutoffReached, + QuoteTransferFailed, + BaseTransferFailed, + BoardNotExpired, + BoardAlreadyLiquidated, + OnlyOwner, + Last + } + + function maxExpiryTimestamp() external view returns (uint); + + function optionBoards(uint) + external + view + returns ( + uint id, + uint expiry, + uint iv, + bool frozen + ); + + function optionListings(uint) + external + view + returns ( + uint id, + uint strike, + uint skew, + uint longCall, + uint shortCall, + uint longPut, + uint shortPut, + uint boardId + ); + + function boardToPriceAtExpiry(uint) external view returns (uint); + + function listingToBaseReturnedRatio(uint) external view returns (uint); + + function transferOwnership(address newOwner) external; + + function setBoardFrozen(uint boardId, bool frozen) external; + + function setBoardBaseIv(uint boardId, uint baseIv) external; + + function setListingSkew(uint listingId, uint skew) external; + + function createOptionBoard( + uint expiry, + uint baseIV, + uint[] memory strikes, + uint[] memory skews + ) external returns (uint); + + function addListingToBoard( + uint boardId, + uint strike, + uint skew + ) external; + + function getLiveBoards() external view returns (uint[] memory _liveBoards); + + function getBoardListings(uint boardId) + external + view + returns (uint[] memory); + + function openPosition( + uint _listingId, + TradeType tradeType, + uint amount + ) external returns (uint totalCost); + + function closePosition( + uint _listingId, + TradeType tradeType, + uint amount + ) external returns (uint totalCost); + + function liquidateExpiredBoard(uint boardId) external; + + function settleOptions(uint listingId, TradeType tradeType) external; +} + +// File contracts/interfaces/lyra/IOptionMarketPricer.sol + +pragma solidity 0.8.9; + +interface IOptionMarketPricer { + struct Pricing { + uint optionPrice; + int preTradeAmmNetStdVega; + int postTradeAmmNetStdVega; + int callDelta; + } + + function ivImpactForTrade( + IOptionMarket.OptionListing memory listing, + IOptionMarket.Trade memory trade, + ILyraGlobals.PricingGlobals memory pricingGlobals, + uint boardBaseIv + ) external pure returns (uint, uint); + + function updateCacheAndGetTotalCost( + IOptionMarket.OptionListing memory listing, + IOptionMarket.Trade memory trade, + ILyraGlobals.PricingGlobals memory pricingGlobals, + uint boardBaseIv + ) + external + returns ( + uint totalCost, + uint newBaseIv, + uint newSkew + ); + + function getPremium( + IOptionMarket.Trade memory trade, + Pricing memory pricing, + ILyraGlobals.PricingGlobals memory pricingGlobals + ) external pure returns (uint premium); + + function getVegaUtil( + IOptionMarket.Trade memory trade, + Pricing memory pricing, + ILyraGlobals.PricingGlobals memory pricingGlobals + ) external pure returns (uint vegaUtil); + + function getFee( + ILyraGlobals.PricingGlobals memory pricingGlobals, + uint amount, + uint optionPrice, + uint vegaUtil + ) external pure returns (uint fee); +} + +// File contracts/interfaces/lyra/IOptionMarketViewer.sol + +pragma solidity 0.8.9; + +interface IOptionMarketViewer { + struct TradePremiumView { + uint listingId; + uint premium; + uint basePrice; + uint vegaUtilFee; + uint optionPriceFee; + uint spotPriceFee; + uint newIv; + } + + function getPremiumForOpen( + uint _listingId, + IOptionMarket.TradeType tradeType, + uint amount + ) external view returns (TradePremiumView memory); +} + +// File contracts/utils/Pausable.sol + +pragma solidity 0.8.9; + +abstract contract Pausable { + event Paused(address account); + + event Unpaused(address account); + + bool private _paused; + + constructor() { + _paused = false; + } + + function paused() public view virtual returns (bool) { + return _paused; + } + + modifier whenNotPaused() { + require(!paused(), "PAUSED"); + _; + } + + modifier whenPaused() { + require(paused(), "NOT_PAUSED"); + _; + } + + function _pause() internal virtual whenNotPaused { + _paused = true; + emit Paused(msg.sender); + } + + function _unpause() internal virtual whenPaused { + _paused = false; + emit Unpaused(msg.sender); + } +} + +// File contracts/PolynomialCoveredCall.sol + +// SPDX-License-Identifier: MIT +pragma solidity 0.8.9; + +contract PolynomialCoveredCall is + IPolynomialCoveredCall, + ReentrancyGuard, + Auth, + Pausable +{ + /// ----------------------------------------------------------------------- + /// Library usage + /// ----------------------------------------------------------------------- + + using FixedPointMathLib for uint256; + using SafeTransferLib for ERC20; + + /// ----------------------------------------------------------------------- + /// Constants + /// ----------------------------------------------------------------------- + + /// @notice Number of weeks in a year (in 8 decimals) + uint256 private constant WEEKS_PER_YEAR = 52142857143; + + /// @notice An instance of LYRA token + ERC20 public constant LYRA_TOKEN = + ERC20(0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb); + + /// @notice An instance of LYRA distributor + ILyraDistributor public constant LYRA_CLAIMER = + ILyraDistributor(0x0BFb21f64E414Ff616aC54853e52679EEDB22Dd2); + + /// ----------------------------------------------------------------------- + /// Immutable parameters + /// ----------------------------------------------------------------------- + + /// @notice Underlying Asset + ERC20 public immutable UNDERLYING; + + /// @notice Synthetix + ISynthetix public immutable SYNTHETIX; + + /// @notice Lyra Option Market + IOptionMarket public immutable LYRA_MARKET; + + /// @notice Synthetix currency key of the underlying token + bytes32 public immutable SYNTH_KEY_UNDERLYING; + + /// @notice Synthetix currency key of the premium token + bytes32 public immutable SYNTH_KEY_PREMIUM; + + /// ----------------------------------------------------------------------- + /// Storage variables + /// ----------------------------------------------------------------------- + + /// @notice Human Readable Name of the Vault + string public name; + + /// @notice Address of the vault keeper + address public keeper; + + /// @notice Fee Reciepient + address public feeReciepient; + + /// @notice Current round + uint256 public currentRound; + + /// @notice Current Listing ID (Listing id is a specific option in Lyra Market) + uint256 public currentListingId; + + /// @notice Current Listing ID's Expiry + uint256 public currentExpiry; + + /// @notice Current Listing Strike Price + uint256 public currentStrike; + + /// @notice Total premium collected in the round (so far) + uint256 public premiumCollected; + + /// @notice Total amount of collateral for the current round + uint256 public totalFunds; + + /// @notice Funds used so far in the current round + uint256 public usedFunds; + + /// @notice Total shares issued so far + uint256 public totalShares; + + /// @notice Vault capacity + uint256 public vaultCapacity; + + /// @notice User deposit limit + uint256 public userDepositLimit; + + /// @notice Total pending deposit amounts (in UNDERLYING) + uint256 public pendingDeposits; + + /// @notice Pending withdraws (in SHARES) + uint256 public pendingWithdraws; + + /// @notice IV Slippage limit per trade + uint256 public ivLimit; + + /// @notice Performance Fee + uint256 public performanceFee; + + /// @notice Management Fee + uint256 public managementFee; + + /// @notice Mapping of User Info + mapping(address => UserInfo) public userInfos; + + /// @notice Mapping of round versus perfomance index + mapping(uint256 => uint256) public performanceIndices; + + /// ----------------------------------------------------------------------- + /// Events + /// ----------------------------------------------------------------------- + + event StartNewRound( + uint256 indexed round, + uint256 indexed listingId, + uint256 newIndex, + uint256 expiry, + uint256 strikePrice, + uint256 lostColl, + uint256 qty + ); + + event SellOptions( + uint256 indexed round, + uint256 optionsSold, + uint256 totalCost, + uint256 expiry, + uint256 strikePrice + ); + + event CompleteWithdraw( + address indexed user, + uint256 indexed withdrawnRound, + uint256 shares, + uint256 funds + ); + + event Deposit( + address indexed user, + uint256 indexed depositRound, + uint256 amt + ); + + event CancelDeposit( + address indexed user, + uint256 indexed depositRound, + uint256 amt + ); + + event RequestWithdraw( + address indexed user, + uint256 indexed withdrawnRound, + uint256 shares + ); + + event CancelWithdraw( + address indexed user, + uint256 indexed withdrawnRound, + uint256 shares + ); + + event SetCap(address indexed auth, uint256 oldCap, uint256 newCap); + + event SetUserDepositLimit( + address indexed auth, + uint256 oldDepositLimit, + uint256 newDepositLimit + ); + + event SetIvLimit(address indexed auth, uint256 oldLimit, uint256 newLimit); + + event SetFees( + address indexed auth, + uint256 oldManageFee, + uint256 oldPerfFee, + uint256 newManageFee, + uint256 newPerfFee + ); + + event SetFeeReciepient( + address indexed auth, + address oldReceipient, + address newReceipient + ); + + event SetKeeper(address indexed auth, address oldKeeper, address newKeeper); + + /// ----------------------------------------------------------------------- + /// Modifiers + /// ----------------------------------------------------------------------- + + modifier onlyKeeper() { + require(msg.sender == keeper, "NOT_KEEPER"); + _; + } + + constructor( + string memory _name, + ERC20 _underlying, + ISynthetix _synthetix, + IOptionMarket _lyraMarket, + bytes32 _underlyingKey, + bytes32 _premiumKey + ) Auth(msg.sender, Authority(address(0x0))) { + name = _name; + UNDERLYING = _underlying; + SYNTHETIX = _synthetix; + LYRA_MARKET = _lyraMarket; + SYNTH_KEY_UNDERLYING = _underlyingKey; + SYNTH_KEY_PREMIUM = _premiumKey; + + performanceIndices[0] = 1e18; + } + + /// ----------------------------------------------------------------------- + /// User actions + /// ----------------------------------------------------------------------- + + /// @notice Deposits UNDERLYING tokens to the vault + /// Shares assigned at the end of the current round (unless the round is 0) + /// @param _amt Amount of UNDERLYING tokens to deposit + function deposit(uint256 _amt) + external + override + nonReentrant + whenNotPaused + { + require(_amt > 0, "AMT_CANNOT_BE_ZERO"); + + if (currentRound == 0) { + _depositForRoundZero(msg.sender, _amt); + } else { + _deposit(msg.sender, _amt); + } + + emit Deposit(msg.sender, currentRound, _amt); + } + + /// @notice Deposits UNDERLYING tokens to the vault for another address + /// Used in periphery contracts to swap and deposit + /// @param _amt Amount of UNDERLYING tokens to deposit + function deposit(address _user, uint256 _amt) + external + override + nonReentrant + whenNotPaused + { + require(_amt > 0, "AMT_CANNOT_BE_ZERO"); + + if (currentRound == 0) { + _depositForRoundZero(_user, _amt); + } else { + _deposit(_user, _amt); + } + + emit Deposit(_user, currentRound, _amt); + } + + /// @notice Cancel a pending deposit + /// @param _amt Amount of tokens to cancel from deposit + function cancelDeposit(uint256 _amt) external nonReentrant whenNotPaused { + UserInfo storage userInfo = userInfos[msg.sender]; + + require( + userInfo.pendingDeposit >= _amt && + userInfo.depositRound == currentRound, + "NO_PENDING_DEPOSIT" + ); + + userInfo.pendingDeposit -= _amt; + pendingDeposits -= _amt; + + emit CancelDeposit(msg.sender, currentRound, _amt); + } + + /// @notice Request withdraw from the vault + /// Unless cancelled, withdraw request can be completed at the end of the round + /// @param _shares Amount of shares to withdraw + function requestWithdraw(uint256 _shares) external override nonReentrant { + UserInfo storage userInfo = userInfos[msg.sender]; + + if ( + userInfo.depositRound < currentRound && userInfo.pendingDeposit > 0 + ) { + /// Convert any pending deposit to shares + userInfo.totalShares += userInfo.pendingDeposit.fdiv( + performanceIndices[userInfo.depositRound], + 1e18 + ); + userInfo.pendingDeposit = 0; + } + + require(userInfo.totalShares >= _shares, "INSUFFICIENT_SHARES"); + + if (currentRound == 0) { + UNDERLYING.safeTransfer(msg.sender, _shares); + totalShares -= _shares; + } else { + if (userInfo.withdrawRound < currentRound) { + require( + userInfo.withdrawnShares == 0, + "INCOMPLETE_PENDING_WITHDRAW" + ); + } + userInfo.withdrawRound = currentRound; + userInfo.withdrawnShares += _shares; + pendingWithdraws += _shares; + } + userInfo.totalShares -= _shares; + + emit RequestWithdraw(msg.sender, currentRound, _shares); + } + + /// @notice Cancel a withdraw request + /// Cannot cancel a withdraw request if a round has already passed + /// @param _shares Amount of shares to cancel + function cancelWithdraw(uint256 _shares) + external + override + nonReentrant + whenNotPaused + { + UserInfo storage userInfo = userInfos[msg.sender]; + + require(userInfo.withdrawnShares >= _shares, "NO_WITHDRAW_REQUESTS"); + require( + userInfo.withdrawRound == currentRound, + "CANNOT_CANCEL_AFTER_ROUND" + ); + + userInfo.withdrawnShares -= _shares; + pendingWithdraws -= _shares; + userInfo.totalShares += _shares; + + emit CancelWithdraw(msg.sender, currentRound, _shares); + } + + /// @notice Complete withdraw request and claim UNDERLYING tokens from the vault + function completeWithdraw() external override nonReentrant { + UserInfo storage userInfo = userInfos[msg.sender]; + + require(currentRound > userInfo.withdrawRound, "ROUND_NOT_OVER"); + + /// Calculate amount to withdraw from withdrawn round's performance index + uint256 pendingWithdrawAmount = userInfo.withdrawnShares.fmul( + performanceIndices[userInfo.withdrawRound], + 1e18 + ); + UNDERLYING.safeTransfer(msg.sender, pendingWithdrawAmount); + + emit CompleteWithdraw( + msg.sender, + userInfo.withdrawRound, + userInfo.withdrawnShares, + pendingWithdrawAmount + ); + + userInfo.withdrawnShares = 0; + userInfo.withdrawRound = 0; + } + + /// ----------------------------------------------------------------------- + /// Owner actions + /// ----------------------------------------------------------------------- + + /// @notice Set vault capacity + /// @param _newCap Vault capacity amount in UNDERLYING + function setCap(uint256 _newCap) external requiresAuth { + require(_newCap > 0, "CAP_CANNOT_BE_ZERO"); + emit SetCap(msg.sender, vaultCapacity, _newCap); + vaultCapacity = _newCap; + } + + /// @notice Set user deposit limit + /// @param _depositLimit Max deposit amount per each deposit, in UNDERLYING + function setUserDepositLimit(uint256 _depositLimit) external requiresAuth { + require(_depositLimit > 0, "LIMIT_CANNOT_BE_ZERO"); + emit SetUserDepositLimit(msg.sender, userDepositLimit, _depositLimit); + userDepositLimit = _depositLimit; + } + + /// @notice Set IV Limit + /// @param _ivLimit IV Limit. 1e16 == 1% + function setIvLimit(uint256 _ivLimit) external requiresAuth { + require(_ivLimit > 0, "SLIPPAGE_CANNOT_BE_ZERO"); + emit SetIvLimit(msg.sender, ivLimit, _ivLimit); + ivLimit = _ivLimit; + } + + /// @notice Set vault fees + /// Fees use 8 decimals. 1% == 1e6, 10% == 1e7 & 100% == 1e8 + /// @param _perfomanceFee Performance fee + /// @param _managementFee Management Fee + function setFees(uint256 _perfomanceFee, uint256 _managementFee) + external + requiresAuth + { + require(_perfomanceFee <= 1e7, "PERF_FEE_TOO_HIGH"); + require(_managementFee <= 5e6, "MANAGE_FEE_TOO_HIGH"); + + emit SetFees( + msg.sender, + managementFee, + performanceFee, + _managementFee, + _perfomanceFee + ); + + performanceFee = _perfomanceFee; + managementFee = _managementFee; + } + + /// @notice Set fee reciepient address + /// @param _feeReciepient Fee reciepient address + function setFeeReciepient(address _feeReciepient) external requiresAuth { + require(_feeReciepient != address(0x0), "CANNOT_BE_VOID"); + emit SetFeeReciepient(msg.sender, feeReciepient, _feeReciepient); + feeReciepient = _feeReciepient; + } + + /// @notice Set Keeper address + /// Keeper bot sells options from the vault once a round is started + /// @param _keeper Address of the keeper + function setKeeper(address _keeper) external requiresAuth { + emit SetKeeper(msg.sender, keeper, _keeper); + keeper = _keeper; + } + + /// @notice Pause contract + /// Once paused, deposits and selling options are closed + function pause() external requiresAuth { + _pause(); + } + + /// @notice Unpause contract + function unpause() external requiresAuth { + _unpause(); + } + + /// @notice Claim LYRA from Lyra Distributor + /// @param _receiver Target Address to receive the claimed tokens + function claimLyra(address _receiver) external requiresAuth { + LYRA_CLAIMER.claim(); + uint256 received = LYRA_TOKEN.balanceOf(address(this)); + LYRA_TOKEN.transfer(_receiver, received); + } + + /// @notice Start a new round by providing listing ID for an upcoming option + /// @param _listingId Unique listing ID from Lyra Option Market + function startNewRound(uint256 _listingId) + external + requiresAuth + nonReentrant + { + /// Check if listing ID is valid & last round's expiry is over + (, uint256 strikePrice, , , , , , uint256 boardId) = LYRA_MARKET + .optionListings(_listingId); + (, uint256 expiry, , ) = LYRA_MARKET.optionBoards(boardId); + require(expiry >= block.timestamp, "INVALID_LISTING_ID"); + require(block.timestamp > currentExpiry, "ROUND_NOT_OVER"); + + /// Close position if round != 0 & Calculate funds & new index value + if (currentRound > 0) { + uint256 newIndex = performanceIndices[currentRound - 1]; + uint256 collateralWithdrawn = usedFunds; + uint256 collectedFunds = totalFunds; + uint256 totalFees; + + if (usedFunds > 0) { + uint256 preSettleBal = UNDERLYING.balanceOf(address(this)); + /// Settle all the options sold from last round + LYRA_MARKET.settleOptions( + currentListingId, + IOptionMarket.TradeType.SHORT_CALL + ); + uint256 postSettleBal = UNDERLYING.balanceOf(address(this)); + collateralWithdrawn = postSettleBal - preSettleBal; + + /// Calculate and collect fees, if the option expired OTM + if (collateralWithdrawn == usedFunds) { + uint256 currentRoundManagementFees = collateralWithdrawn + .fmul(managementFee, WEEKS_PER_YEAR); + uint256 currentRoundPerfomanceFee = premiumCollected.fmul( + performanceFee, + WEEKS_PER_YEAR + ); + totalFees = + currentRoundManagementFees + + currentRoundPerfomanceFee; + UNDERLYING.safeTransfer(feeReciepient, totalFees); + } + /// Calculate last round's performance index + uint256 unusedFunds = totalFunds - usedFunds; + collectedFunds = + collateralWithdrawn + + premiumCollected + + unusedFunds - + totalFees; + newIndex = collectedFunds.fdiv(totalShares, 1e18); + } + + performanceIndices[currentRound] = newIndex; + + /// Process pending deposits and withdrawals + totalShares += pendingDeposits.fdiv(newIndex, 1e18); + totalShares -= pendingWithdraws; + + /// Calculate available funds for the round that's starting + uint256 fundsPendingWithdraws = pendingWithdraws.fmul( + newIndex, + 1e18 + ); + totalFunds = + collectedFunds + + pendingDeposits - + fundsPendingWithdraws; + + emit StartNewRound( + currentRound + 1, + _listingId, + newIndex, + expiry, + strikePrice, + usedFunds - collateralWithdrawn, + usedFunds + ); + + pendingDeposits = 0; + pendingWithdraws = 0; + usedFunds = 0; + premiumCollected = 0; + } else { + totalFunds = UNDERLYING.balanceOf(address(this)); + + emit StartNewRound(1, _listingId, 1e18, expiry, strikePrice, 0, 0); + } + /// Set listing ID and start round + currentRound++; + currentListingId = _listingId; + currentExpiry = expiry; + currentStrike = strikePrice; + } + + /// @notice Sell options to Lyra AMM + /// Called via Keeper bot + /// @param _amt Amount of options to sell + function sellOptions(uint256 _amt) + external + onlyKeeper + nonReentrant + whenNotPaused + { + _amt = _amt > (totalFunds - usedFunds) ? totalFunds - usedFunds : _amt; + require(_amt > 0, "NO_FUNDS_REMAINING"); + + IOptionMarket.TradeType tradeType = IOptionMarket.TradeType.SHORT_CALL; + + /// Get initial board IV, and listing skew to calculate initial IV + (, , uint256 initSkew, , , , , uint256 boardId) = LYRA_MARKET + .optionListings(currentListingId); + (, , uint256 initBaseIv, ) = LYRA_MARKET.optionBoards(boardId); + + /// Sell options to Lyra AMM + UNDERLYING.safeApprove(address(LYRA_MARKET), _amt); + uint256 totalCost = LYRA_MARKET.openPosition( + currentListingId, + tradeType, + _amt + ); + + /// Get final board IV, and listing skew to calculate final IV + (, , uint256 finalSkew, , , , , ) = LYRA_MARKET.optionListings( + currentListingId + ); + (, , uint256 finalBaseIv, ) = LYRA_MARKET.optionBoards(boardId); + + /// Calculate IVs and revert if IV impact is high + uint256 initIv = initBaseIv.fmul(initSkew, 1e18); + uint256 finalIv = finalBaseIv.fmul(finalSkew, 1e18); + require(initIv - finalIv < ivLimit, "IV_LIMIT_HIT"); + + /// Swap recieved sUSD premium to UNDERLYING + uint256 totalCostInUnderlying = SYNTHETIX.exchange( + SYNTH_KEY_PREMIUM, + totalCost, + SYNTH_KEY_UNDERLYING + ); + + premiumCollected += totalCostInUnderlying; + usedFunds += _amt; + + emit SellOptions( + currentRound, + _amt, + totalCostInUnderlying, + currentExpiry, + currentStrike + ); + } + + /// ----------------------------------------------------------------------- + /// Internal Methods + /// ----------------------------------------------------------------------- + + /// @notice Deposit for round zero + /// Shares are issued during the round itself + function _depositForRoundZero(address _user, uint256 _amt) internal { + UNDERLYING.safeTransferFrom(msg.sender, address(this), _amt); + require( + UNDERLYING.balanceOf(address(this)) <= vaultCapacity, + "CAPACITY_EXCEEDED" + ); + + UserInfo storage userInfo = userInfos[_user]; + userInfo.totalShares += _amt; + require( + userInfo.totalShares <= userDepositLimit, + "USER_DEPOSIT_LIMIT_EXCEEDED" + ); + totalShares += _amt; + } + + /// @notice Internal deposit function + /// Shares issued after the current round is over + function _deposit(address _user, uint256 _amt) internal { + UNDERLYING.safeTransferFrom(msg.sender, address(this), _amt); + + pendingDeposits += _amt; + require( + totalFunds + pendingDeposits < vaultCapacity, + "CAPACITY_EXCEEDED" + ); + + UserInfo storage userInfo = userInfos[_user]; + /// Process any pending deposit, if any + if (userInfo.depositRound > 0 && userInfo.depositRound < currentRound) { + userInfo.totalShares += userInfo.pendingDeposit.fdiv( + performanceIndices[userInfo.depositRound], + 1e18 + ); + userInfo.pendingDeposit = _amt; + } else { + userInfo.pendingDeposit += _amt; + } + userInfo.depositRound = currentRound; + + uint256 totalBalance = userInfo.pendingDeposit + + userInfo.totalShares.fmul( + performanceIndices[currentRound - 1], + 1e18 + ); + require( + totalBalance <= userDepositLimit, + "USER_DEPOSIT_LIMIT_EXCEEDED" + ); + } +} diff --git a/packages/contracts/contracts/strategies/FuerteStrategy.sol b/packages/contracts/contracts/strategies/FuerteStrategy.sol deleted file mode 100644 index c7172c8..0000000 --- a/packages/contracts/contracts/strategies/FuerteStrategy.sol +++ /dev/null @@ -1,275 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; -pragma experimental ABIEncoderV2; - -// Hardhat -import "hardhat/console.sol"; - -// standard strategy interface -import "../interfaces/IStrategyFuerte.sol"; - -// Lyra -import {VaultAdapter} from "@lyrafinance/protocol/contracts/periphery/VaultAdapter.sol"; -import {GWAVOracle} from "@lyrafinance/protocol/contracts/periphery/GWAVOracle.sol"; - -// Libraries -import {Vault} from "../libraries/Vault.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {LyraVault} from "../core/LyraVault.sol"; -import {DecimalMath} from "@lyrafinance/protocol/contracts/synthetix/DecimalMath.sol"; -import {SignedDecimalMath} from "@lyrafinance/protocol/contracts/synthetix/SignedDecimalMath.sol"; - -// StrategyBase to inherit -import {StrategyBaseFuerte} from "./StrategyBaseFuerte.sol"; - -contract FuerteStrategy is StrategyBaseFuerte, IStrategyFuerte { - using DecimalMath for uint; - using SignedDecimalMath for int; - - // example strategy detail - struct FuerteStrategyDetail { - uint minTimeToExpiry; - uint maxTimeToExpiry; - int targetDelta; - uint maxDeltaGap; - uint minVol; - uint maxVol; - uint callSize; - uint putSize; - uint maxVolVariance; - uint gwavPeriod; - } - - FuerteStrategyDetail public strategyDetail; - uint public activeExpiry; - - /////////// - // ADMIN // - /////////// - - constructor( - LyraVault _vault, - //OptionType _optionType, - GWAVOracle _gwavOracle - ) StrategyBaseFuerte(_vault, _gwavOracle) {} - - // ) StrategyBase(_vault, _optionType, _gwavOracle) {} - - /** - * @dev update the strategy detail for the new round. - */ - function setStrategyDetail(FuerteStrategyDetail memory _fuerteStrategy) external onlyOwner { - (, , , , , , , bool roundInProgress) = vault.vaultState(); - require(!roundInProgress, "cannot change strategy if round is active"); - strategyDetail = _fuerteStrategy; - } - - /////////////////// - // VAULT ACTIONS // - /////////////////// - - /** - * @dev set the board id that will be traded for the next round - * @param callBoardId lyra call board Id. - * @param putBoardId lyra call board Id. - */ - function setBoard(uint callBoardId, uint putBoardId) external onlyVault { - Board memory callBoard = getBoard(callBoardId); - Board memory putBoard = getBoard(putBoardId); - require(_isValidExpiry(callBoard.expiry) && _isValidExpiry(putBoard.expiry), "invalid board"); - require(callBoard.expiry == putBoard.expiry, "!expiry"); - activeExpiry = callBoard.expiry; - } - - /** - * @dev convert premium in quote asset into collateral asset and send it back to the vault. - */ - function returnFundsAndClearStrikes() external onlyVault { - // exchange asset back to collateral asset and send it back to the vault - _returnFundsToVaut(); - - // keep internal storage data on old strikes and positions ids - _clearAllActiveStrikes(); - } - - /** - * @notice sell a fix amount of options and collect premium - * @dev the vault should pass in a strike id, and the strategy would verify if the strike is valid on-chain. - * @param callStrikeId lyra call strikeId to trade - * @param putStrikeId lyra call strikeId to trade - * @param lyraRewardRecipient address to receive trading reward. This need to be whitelisted - * @return callPositionId - * @return putPositionId - * @return premiumReceived - */ - function doTrade( - uint callStrikeId, - uint putStrikeId, - address lyraRewardRecipient - ) - external - onlyVault - returns ( - uint callPositionId, - uint putPositionId, - uint premiumReceived, - uint callCollateral, - uint putCollateral - ) - { - Strike memory callStrike = getStrikes(_toDynamic(callStrikeId))[0]; - Strike memory putStrike = getStrikes(_toDynamic(putStrikeId))[0]; - require( - isValidStrike(callStrike, OptionType.SHORT_CALL_BASE) && isValidStrike(putStrike, OptionType.SHORT_PUT_QUOTE), - "invalid strike" - ); - - callCollateral = getRequiredCollateral(callStrike, OptionType.SHORT_CALL_BASE); // base - putCollateral = getRequiredCollateral(putStrike, OptionType.SHORT_PUT_QUOTE); // quote - - require( - baseAsset.transferFrom(address(vault), address(this), callCollateral), - "baseAsset transfer from vault failed" - ); - - require( - quoteAsset.transferFrom(address(vault), address(this), putCollateral), - "putCollateral transfer from vault failed" - ); - - uint callPremiumReceived; - (callPositionId, callPremiumReceived) = _sellStrike( - callStrike, - OptionType.SHORT_CALL_BASE, - callCollateral, - lyraRewardRecipient - ); - uint putPremiumReceived; - (putPositionId, putPremiumReceived) = _sellStrike( - putStrike, - OptionType.SHORT_PUT_QUOTE, - putCollateral, - lyraRewardRecipient - ); - premiumReceived = callPremiumReceived + putPremiumReceived; - } - - /** - * @dev calculate required collateral to add in the next trade. - * sell size is fixed as strategyDetail.size - * only add collateral if the additional sell will make the position out of buffer range - * never remove collateral from an existing position - */ - function getRequiredCollateral(Strike memory strike, OptionType optionType) - public - view - returns (uint collateralToAdd) - { - uint sellAmount = optionType == OptionType.SHORT_CALL_BASE ? strategyDetail.callSize : strategyDetail.putSize; - collateralToAdd = _getFullCollateral(strike.strikePrice, sellAmount, optionType); - } - - /** - * @dev perform the trade - * @param strike strike detail - * @param setCollateralTo target collateral amount - * @param optionType optionType - * @param lyraRewardRecipient address to receive lyra trading reward - * @return positionId - * @return premiumReceived - */ - function _sellStrike( - Strike memory strike, - OptionType optionType, - uint setCollateralTo, - address lyraRewardRecipient - ) internal returns (uint, uint) { - // get minimum expected premium based on minIv - uint size = optionType == OptionType.SHORT_CALL_BASE ? strategyDetail.callSize : strategyDetail.putSize; - uint minExpectedPremium = _getPremiumLimit(strike, optionType, strategyDetail.minVol, size); - // perform trade - TradeResult memory result = openPosition( - TradeInputParameters({ - strikeId: strike.id, - positionId: strikeToPositionId[strike.id], - iterations: 4, - optionType: optionType, - amount: size, - setCollateralTo: setCollateralTo, - minTotalCost: minExpectedPremium, - maxTotalCost: type(uint).max, - rewardRecipient: lyraRewardRecipient // set to zero address if don't want to wait for whitelist - }) - ); - lastTradeTimestamp[strike.id] = block.timestamp; - - // update active strikes - _addActiveStrike(strike.id, result.positionId); - - require(result.totalCost >= minExpectedPremium, "premium received is below min expected premium"); - - return (result.positionId, result.totalCost); - } - - function reducePosition( - uint, - uint, - address - ) external pure { - revert("not supported"); - } - - ///////////////////////////// - // Trade Parameter Helpers // - ///////////////////////////// - - function _getFullCollateral( - uint strikePrice, - uint amount, - OptionType optionType - ) internal pure returns (uint fullCollat) { - // calculate required collat based on collatBuffer and collatPercent - fullCollat = _isBaseCollat(optionType) ? amount : amount.multiplyDecimal(strikePrice); - } - - ///////////////// - // Validation /// - ///////////////// - - /** - * @dev verify if the strike is valid for the strategy - * @return isValid true if vol is withint [minVol, maxVol] and delta is within targetDelta +- maxDeltaGap - */ - function isValidStrike(Strike memory strike, OptionType optionType) public view returns (bool isValid) { - if (activeExpiry != strike.expiry) { - return false; - } - - uint[] memory strikeId = _toDynamic(strike.id); - uint vol = getVols(strikeId)[0]; - int callDelta = getDeltas(strikeId)[0]; - int delta = _isCall(optionType) ? callDelta : callDelta - SignedDecimalMath.UNIT; - uint deltaGap = _abs(strategyDetail.targetDelta - delta); - return vol >= strategyDetail.minVol && vol <= strategyDetail.maxVol && deltaGap < strategyDetail.maxDeltaGap; - } - - /** - * @dev check if the vol variance for the given strike is within certain range - */ - function _isValidVolVariance(uint strikeId) internal view returns (bool isValid) { - uint volGWAV = gwavOracle.volGWAV(strikeId, strategyDetail.gwavPeriod); - uint volSpot = getVols(_toDynamic(strikeId))[0]; - - uint volDiff = (volGWAV >= volSpot) ? volGWAV - volSpot : volSpot - volGWAV; - - return isValid = volDiff < strategyDetail.maxVolVariance; - } - - /** - * @dev check if the expiry of the board is valid according to the strategy - */ - function _isValidExpiry(uint expiry) public view returns (bool isValid) { - uint secondsToExpiry = _getSecondsToExpiry(expiry); - isValid = (secondsToExpiry >= strategyDetail.minTimeToExpiry && secondsToExpiry <= strategyDetail.maxTimeToExpiry); - } -} diff --git a/packages/contracts/contracts/strategies/HackMoneyStrategy.sol b/packages/contracts/contracts/strategies/HackMoneyStrategy.sol index 5405694..e8d686e 100644 --- a/packages/contracts/contracts/strategies/HackMoneyStrategy.sol +++ b/packages/contracts/contracts/strategies/HackMoneyStrategy.sol @@ -32,6 +32,7 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { uint maxTimeToExpiry; int mintargetDelta; // 15% int maxtargetDelta; // 85% + uint maxDeltaGap; // 5% uint minVol; // 80% uint size; // 15 } @@ -165,16 +166,15 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { ); premiumReceived = premiumReceived1 + premiumReceived2; - uint additionalPremium; - (, , additionalPremium, premiumExchangeValue) = _tradePremiums( - premiumReceived, - collateralToAdd1, - collateralToAdd2 - ); + // uint additionalPremium; + // (, , additionalPremium, premiumExchangeValue) = _tradePremiums( + // premiumReceived, + // collateralToAdd1, + // collateralToAdd2 + // ); collateralToAdd = collateralToAdd1 + collateralToAdd2; // + exchangeValue; - - premiumReceived += additionalPremium; + //premiumReceived += additionalPremium; } function _tradeStrike(Strike memory strike) @@ -184,10 +184,12 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { uint premiumReceived, uint collateralToAdd ) + //uint setCollateralTo { - (collateralToAdd, ) = getRequiredCollateral(strike); + uint setCollateralTo; + (collateralToAdd, setCollateralTo) = getRequiredCollateral(strike); - (positionId, premiumReceived) = _sellStrike(strike, collateralToAdd); + (positionId, premiumReceived) = _sellStrike(strike, setCollateralTo); } /** @@ -210,7 +212,9 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { ) { // exchange susd to seth + exchangeValue = _exchangePremiums(size); + uint sellAmount = exchangeValue / 2; (Strike memory strike1, Strike memory strike2) = _getTradeStrikes(); uint premiumReceived1; @@ -246,6 +250,20 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { uint sellAmount = strategyDetail.size; collateralToAdd = _getFullCollateral(strike.strikePrice, sellAmount); setCollateralTo = collateralToAdd; + + // get existing position info if active + uint existingAmount = 0; + uint existingCollateral = 0; + if (_isActiveStrike(strike.id)) { + OptionPosition memory position = getPositions( + _toDynamic(strikeToPositionId[strike.id]) + )[0]; + existingCollateral = position.collateral; + existingAmount = position.amount; + } + + setCollateralTo += existingCollateral; + console.log("setCollateralTo:", setCollateralTo / 10**18); } /** @@ -277,7 +295,7 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { optionType: optionType, amount: strategyDetail.size, setCollateralTo: setCollateralTo, - minTotalCost: minExpectedPremium, + minTotalCost: 0, maxTotalCost: type(uint).max, rewardRecipient: lyraRewardRecipient // set to zero address if don't want to wait for whitelist }) @@ -311,7 +329,6 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { strategyDetail.minVol, size ); - uint strikeId = strike.id; uint initIv = strike.boardIv.multiplyDecimal(strike.skew); @@ -324,7 +341,7 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { optionType: optionType, amount: size, setCollateralTo: size + collateralToAdd, - minTotalCost: minExpectedPremium, + minTotalCost: 0, maxTotalCost: type(uint).max, rewardRecipient: lyraRewardRecipient // set to zero address if don't want to wait for whitelist }) @@ -338,10 +355,10 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { // update active strikes _addActiveStrike(strike.id, result.positionId); - require( - result.totalCost >= minExpectedPremium, - "premium received is below min expected premium" - ); + // require( + // result.totalCost >= minExpectedPremium, + // "premium received is below min expected premium" + // ); return (result.positionId, result.totalCost); } @@ -356,7 +373,7 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { // get small and big strike Ids uint smallStrikeId = strikeIds[0]; - uint bigStrikeId = strikeIds[strikeIds.length - 1]; + uint bigStrikeId = strikeIds[0]; // init strikes smallStrike = getStrikes(_toDynamic(smallStrikeId))[0]; @@ -364,37 +381,42 @@ contract HackMoneyStrategy is HackMoneyStrategyBase, IHackMoneyStrategy { (uint smallDeltaGap, ) = _getDeltaGap(smallStrike, true); (uint bigDeltaGap, ) = _getDeltaGap(bigStrike, false); + console.log("smallDeltaGap:", smallDeltaGap / 10**15, " / 10 %"); + console.log("bigDeltaGap:", bigDeltaGap / 10**15, "/ 10 %"); for (uint i = 1; i < strikeIds.length - 1; i++) { + // Get current Strike uint currentStrikeId = strikeIds[i]; Strike memory currentStrike = getStrikes( _toDynamic(currentStrikeId) )[0]; - (uint currentDeltaGap, ) = _getDeltaGap(currentStrike, true); - if (currentDeltaGap < smallDeltaGap) { + + // Get current delta gaps + (uint currentSmallDeltaGap, ) = _getDeltaGap(currentStrike, true); + (uint currentBigDeltaGap, ) = _getDeltaGap(currentStrike, false); + + if (currentSmallDeltaGap < smallDeltaGap) { smallStrike = currentStrike; - smallDeltaGap = currentDeltaGap; - } else { - break; + smallDeltaGap = currentSmallDeltaGap; } - } - - for (uint i = strikeIds.length - 2; i > 1; i--) { - uint currentStrikeId = strikeIds[i]; - Strike memory currentStrike = getStrikes( - _toDynamic(currentStrikeId) - )[0]; - (uint currentDeltaGap, int currentDelta) = _getDeltaGap( - currentStrike, - false - ); - if (currentDeltaGap < bigDeltaGap) { + if (currentBigDeltaGap < bigDeltaGap) { bigStrike = currentStrike; - bigDeltaGap = currentDeltaGap; - } else if (currentDelta != 0) { - break; + bigDeltaGap = currentBigDeltaGap; } + console.log("Loop iteration: ", i); + console.log("smallDeltaGap:", smallDeltaGap / 10**15, " / 10 %"); + console.log("bigDeltaGap:", bigDeltaGap / 10**15, " / 10 %"); } + + // final checks + require( + smallDeltaGap <= strategyDetail.maxDeltaGap, + "smallDeltaGap out of bound!" + ); + require( + bigDeltaGap <= strategyDetail.maxDeltaGap, + "smallDeltaGap out of bound!" + ); } ///////////////////////////// diff --git a/packages/contracts/contracts/strategies/HackMoneyStrategyBase.sol b/packages/contracts/contracts/strategies/HackMoneyStrategyBase.sol index cda7855..920eb18 100644 --- a/packages/contracts/contracts/strategies/HackMoneyStrategyBase.sol +++ b/packages/contracts/contracts/strategies/HackMoneyStrategyBase.sol @@ -11,6 +11,7 @@ import {GWAVOracle} from "@lyrafinance/protocol/contracts/periphery/GWAVOracle.s // Libraries import {Vault} from "../libraries/Vault.sol"; +import {IDelegateApprovals} from "../interfaces/IDelegateApprovals.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {HackMoneyVault} from "../core/HackMoneyVault.sol"; import {DecimalMath} from "@lyrafinance/protocol/contracts/synthetix/DecimalMath.sol"; @@ -81,9 +82,32 @@ contract HackMoneyStrategyBase is VaultAdapter { quoteAsset.approve(address(vault), type(uint).max); baseAsset.approve(address(vault), type(uint).max); + IDelegateApprovals(0x2A23bc0EA97A89abD91214E8e4d20F02Fe14743f) + .approveExchangeOnBehalf(_synthetixAdapter); collateralAsset = _isBaseCollat() ? baseAsset : quoteAsset; } + ////////////////////// + // APPROVAL ACTIONS // + ////////////////////// + + function approveERC20( + address token, + address spender, + uint amount + ) external onlyOwner { + IERC20(token).approve(spender, amount); + } + + function approveSynthetixDelegate( + address delegateApprovals, + address exchanger + ) external onlyOwner { + IDelegateApprovals(delegateApprovals).approveExchangeOnBehalf( + exchanger + ); + } + /////////////////// // VAULT ACTIONS // /////////////////// diff --git a/packages/contracts/contracts/strategies/HackMoneyStrategyImplementation.sol b/packages/contracts/contracts/strategies/HackMoneyStrategyImplementation.sol new file mode 100644 index 0000000..da1f0b2 --- /dev/null +++ b/packages/contracts/contracts/strategies/HackMoneyStrategyImplementation.sol @@ -0,0 +1,512 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; +pragma experimental ABIEncoderV2; + +// Hardhat +import "hardhat/console.sol"; + +// standard strategy interface +import "../interfaces/IHackMoneyStrategy.sol"; + +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +// Lyra +import {VaultAdapter} from "@lyrafinance/protocol/contracts/periphery/VaultAdapter.sol"; +import {GWAVOracle} from "@lyrafinance/protocol/contracts/periphery/GWAVOracle.sol"; + +// Libraries +import {Vault} from "../libraries/Vault.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {HackMoneyVault} from "../core/HackMoneyVault.sol"; +import {DecimalMath} from "@lyrafinance/protocol/contracts/synthetix/DecimalMath.sol"; +import {SignedDecimalMath} from "@lyrafinance/protocol/contracts/synthetix/SignedDecimalMath.sol"; + +// StrategyBase to inherit +import {HackMoneyStrategyBase} from "./HackMoneyStrategyBase.sol"; + +contract HackMoneyStrategyImplementation is + HackMoneyStrategyBase, + IHackMoneyStrategy, + Initializable +{ + using DecimalMath for uint256; + using SignedDecimalMath for int256; + + // example strategy detail + struct HackMoneyStrategyDetail { + uint256 minTimeToExpiry; + uint256 maxTimeToExpiry; + int256 mintargetDelta; // 15% + int256 maxtargetDelta; // 85% + uint256 maxDeltaGap; // 5% + uint256 minVol; // 80% + uint256 size; // 15 + } + + HackMoneyStrategyDetail public strategyDetail; + uint256 public activeExpiry; + uint256 public currentBoardId; + uint256 public ivLimit = 2 * 1e18; + address public lyraRewardRecipient; + + //uint public optionSize; + + /////////// + // ADMIN // + /////////// + + // Implementation having proxy is unsafe + // for setting immutable vars it's ok + // aside from that we are setting ownership + // initialize function reset ownership + constructor( + HackMoneyVault _vault, + OptionType _optionType, + GWAVOracle _gwavOracle + ) HackMoneyStrategyBase(_vault, _optionType, _gwavOracle) {} + + // Note: - We can either change immutable variable in the strategy base to no longer be immutable + // - Or let them be immutable and change how implementation constructor is done + // + function initialize() external initializer { + _transferOwnership(_msgSender()); + } + + /** + * @dev update the strategy detail for the new round. + */ + function setStrategyDetail(HackMoneyStrategyDetail memory _deltaStrategy) + external + onlyOwner + { + (, , , , , , , bool roundInProgress) = vault.vaultState(); + require(!roundInProgress, "cannot change strategy if round is active"); + strategyDetail = _deltaStrategy; + } + + /** + * @dev update the iv limit + */ + function setIvLimit(uint256 _ivLimit) external onlyOwner { + ivLimit = _ivLimit; + } + + /** + * @dev update lyra reward recipient + */ + function setLyraRewardRecipient(address _lyraRewardRecipient) + external + onlyOwner + { + lyraRewardRecipient = _lyraRewardRecipient; + } + + /////////////////// + // VAULT ACTIONS // + /////////////////// + + /** + * @dev set the board id that will be traded for the next round + * @param boardId lyra board Id. + */ + function setBoard(uint256 boardId) external onlyVault { + Board memory board = getBoard(boardId); + require(_isValidExpiry(board.expiry), "invalid board"); + activeExpiry = board.expiry; + currentBoardId = boardId; + } + + /** + * @dev convert premium in quote asset into collateral asset and send it back to the vault. + */ + function returnFundsAndClearStrikes() external onlyVault { + // exchange asset back to collateral asset and send it back to the vault + _returnFundsToVaut(); + + // keep internal storage data on old strikes and positions ids + _clearAllActiveStrikes(); + } + + /** + * @notice sell a fix aomunt of options and collect premium + * @dev the vault should pass in a strike id, and the strategy would verify if the strike is valid on-chain. + * @return positionId1 + * @return positionId2 + * @return premiumReceived + * @return collateralToAdd + */ + function doTrade(uint256 size) + external + onlyVault + returns ( + uint256 positionId1, + uint256 positionId2, + uint256 premiumReceived, + uint256 collateralToAdd, + uint256 premiumExchangeValue + ) + // uint exchangeValue + { + strategyDetail.size = size; + + ( + positionId1, + positionId2, + premiumReceived, + collateralToAdd, + premiumExchangeValue + ) = _tradeOptions(); + } + + function _tradeOptions() + internal + returns ( + uint256 positionId1, + uint256 positionId2, + uint256 premiumReceived, + uint256 collateralToAdd, + uint256 premiumExchangeValue + ) + { + (Strike memory strike1, Strike memory strike2) = _getTradeStrikes(); + + uint256 premiumReceived1; + uint256 premiumReceived2; + uint256 collateralToAdd1; + uint256 collateralToAdd2; + + (positionId1, premiumReceived1, collateralToAdd1) = _tradeStrike( + strike1 + ); + (positionId2, premiumReceived2, collateralToAdd2) = _tradeStrike( + strike2 + ); + premiumReceived = premiumReceived1 + premiumReceived2; + + // uint additionalPremium; + // (, , additionalPremium, premiumExchangeValue) = _tradePremiums( + // premiumReceived, + // collateralToAdd1, + // collateralToAdd2 + // ); + + collateralToAdd = collateralToAdd1 + collateralToAdd2; // + exchangeValue; + //premiumReceived += additionalPremium; + } + + function _tradeStrike(Strike memory strike) + internal + returns ( + uint256 positionId, + uint256 premiumReceived, + uint256 collateralToAdd + ) + //uint setCollateralTo + { + uint256 setCollateralTo; + (collateralToAdd, setCollateralTo) = getRequiredCollateral(strike); + + (positionId, premiumReceived) = _sellStrike(strike, setCollateralTo); + } + + /** + * @notice trade premiums received from a trade + * @return positionId1 + * @return positionId2 + * @return premiumReceived + */ + function _tradePremiums( + uint256 size, + uint256 collateralToAdd1, + uint256 collateralToAdd2 + ) + internal + returns ( + uint256 positionId1, + uint256 positionId2, + uint256 premiumReceived, + uint256 exchangeValue + ) + { + // exchange susd to seth + + exchangeValue = _exchangePremiums(size); + + uint256 sellAmount = exchangeValue / 2; + (Strike memory strike1, Strike memory strike2) = _getTradeStrikes(); + uint256 premiumReceived1; + (positionId1, premiumReceived1) = _sellPremiums( + strike1, + sellAmount, + collateralToAdd1 + ); + uint256 premiumReceived2; + (positionId2, premiumReceived2) = _sellPremiums( + strike2, + sellAmount, + collateralToAdd2 + ); + premiumReceived = premiumReceived1 + premiumReceived2; + } + + ///////////////////////////// + // Trade Parameter Helpers // + ///////////////////////////// + + /** + * @dev calculate required collateral to add in the next trade. + * sell size is fixed as strategyDetail.size + * only add collateral if the additional sell will make the position out of buffer range + * never remove collateral from an existing position + */ + function getRequiredCollateral(Strike memory strike) + public + view + returns (uint256 collateralToAdd, uint256 setCollateralTo) + { + uint256 sellAmount = strategyDetail.size; + collateralToAdd = _getFullCollateral(strike.strikePrice, sellAmount); + setCollateralTo = collateralToAdd; + + // get existing position info if active + uint256 existingAmount = 0; + uint256 existingCollateral = 0; + if (_isActiveStrike(strike.id)) { + OptionPosition memory position = getPositions( + _toDynamic(strikeToPositionId[strike.id]) + )[0]; + existingCollateral = position.collateral; + existingAmount = position.amount; + } + + setCollateralTo += existingCollateral; + console.log("setCollateralTo:", setCollateralTo / 10**18); + } + + /** + * @dev perform the trade + * @param strike strike detail + * @param setCollateralTo target collateral amount + * @return positionId + * @return premiumReceived + */ + function _sellStrike(Strike memory strike, uint256 setCollateralTo) + internal + returns (uint256, uint256) + { + // get minimum expected premium based on minIv + uint256 minExpectedPremium = _getPremiumLimit( + strike, + strategyDetail.minVol, + strategyDetail.size + ); + uint256 strikeId = strike.id; + uint256 initIv = strike.boardIv.multiplyDecimal(strike.skew); + + // perform trade + TradeResult memory result = openPosition( + TradeInputParameters({ + strikeId: strike.id, + positionId: strikeToPositionId[strike.id], + iterations: 4, + optionType: optionType, + amount: strategyDetail.size, + setCollateralTo: setCollateralTo, + minTotalCost: 0, + maxTotalCost: type(uint256).max, + rewardRecipient: lyraRewardRecipient // set to zero address if don't want to wait for whitelist + }) + ); + Strike memory finalStrike = getStrikes(_toDynamic(strikeId))[0]; + uint256 finalIv = finalStrike.boardIv.multiplyDecimal(finalStrike.skew); + require(initIv - finalIv < ivLimit, "IV_LIMIT_HIT"); + + lastTradeTimestamp[strike.id] = block.timestamp; + + // update active strikes + _addActiveStrike(strike.id, result.positionId); + + return (result.positionId, result.totalCost); + } + + /** + * @dev perform the trade + * @param strike strike detail + * @return positionId + * @return premiumReceived + */ + function _sellPremiums( + Strike memory strike, + uint256 size, + uint256 collateralToAdd + ) internal returns (uint256, uint256) { + // get minimum expected premium based on minIv + uint256 minExpectedPremium = _getPremiumLimit( + strike, + strategyDetail.minVol, + size + ); + uint256 strikeId = strike.id; + uint256 initIv = strike.boardIv.multiplyDecimal(strike.skew); + + // perform trade + TradeResult memory result = openPosition( + TradeInputParameters({ + strikeId: strike.id, + positionId: strikeToPositionId[strike.id], + iterations: 4, + optionType: optionType, + amount: size, + setCollateralTo: size + collateralToAdd, + minTotalCost: 0, + maxTotalCost: type(uint256).max, + rewardRecipient: lyraRewardRecipient // set to zero address if don't want to wait for whitelist + }) + ); + Strike memory finalStrike = getStrikes(_toDynamic(strikeId))[0]; + uint256 finalIv = finalStrike.boardIv.multiplyDecimal(finalStrike.skew); + require(initIv - finalIv < ivLimit, "IV_LIMIT_HIT"); + + lastTradeTimestamp[strike.id] = block.timestamp; + + // update active strikes + _addActiveStrike(strike.id, result.positionId); + + // require( + // result.totalCost >= minExpectedPremium, + // "premium received is below min expected premium" + // ); + + return (result.positionId, result.totalCost); + } + + function _getTradeStrikes() + internal + view + returns (Strike memory smallStrike, Strike memory bigStrike) + { + // get all strike Ids for current board + uint256[] memory strikeIds = optionMarket.getBoardStrikes( + currentBoardId + ); + + // get small and big strike Ids + uint256 smallStrikeId = strikeIds[0]; + uint256 bigStrikeId = strikeIds[0]; + + // init strikes + smallStrike = getStrikes(_toDynamic(smallStrikeId))[0]; + bigStrike = getStrikes(_toDynamic(bigStrikeId))[0]; + + (uint256 smallDeltaGap, ) = _getDeltaGap(smallStrike, true); + (uint256 bigDeltaGap, ) = _getDeltaGap(bigStrike, false); + console.log("smallDeltaGap:", smallDeltaGap / 10**15, " / 10 %"); + console.log("bigDeltaGap:", bigDeltaGap / 10**15, "/ 10 %"); + + for (uint256 i = 1; i < strikeIds.length - 1; i++) { + // Get current Strike + uint256 currentStrikeId = strikeIds[i]; + Strike memory currentStrike = getStrikes( + _toDynamic(currentStrikeId) + )[0]; + + // Get current delta gaps + (uint256 currentSmallDeltaGap, ) = _getDeltaGap( + currentStrike, + true + ); + (uint256 currentBigDeltaGap, ) = _getDeltaGap(currentStrike, false); + + if (currentSmallDeltaGap < smallDeltaGap) { + smallStrike = currentStrike; + smallDeltaGap = currentSmallDeltaGap; + } + if (currentBigDeltaGap < bigDeltaGap) { + bigStrike = currentStrike; + bigDeltaGap = currentBigDeltaGap; + } + console.log("Loop iteration: ", i); + console.log("smallDeltaGap:", smallDeltaGap / 10**15, " / 10 %"); + console.log("bigDeltaGap:", bigDeltaGap / 10**15, " / 10 %"); + } + + // final checks + require( + smallDeltaGap <= strategyDetail.maxDeltaGap, + "smallDeltaGap out of bound!" + ); + require( + bigDeltaGap <= strategyDetail.maxDeltaGap, + "smallDeltaGap out of bound!" + ); + } + + ///////////////////////////// + // Trade Parameter Helpers // + ///////////////////////////// + + /** + * @param strikePrice the strike price + * @param amount of options to cover + * @return fullCollat much collateral is needed for `amount` sell of options + */ + function _getFullCollateral(uint256 strikePrice, uint256 amount) + internal + view + returns (uint256 fullCollat) + { + // calculate required collat based on collatBuffer and collatPercent + fullCollat = _isBaseCollat() + ? amount + : amount.multiplyDecimal(strikePrice); + } + + /** + * @dev return delta gap + * @return deltaGap delta gap in abs value + */ + function _getDeltaGap(Strike memory strike, bool isSmallStrike) + public + view + returns (uint256 deltaGap, int256 callDelta) + { + int256 targetDelta = isSmallStrike + ? strategyDetail.maxtargetDelta + : strategyDetail.mintargetDelta; + uint256[] memory strikeId = _toDynamic(strike.id); + callDelta = getDeltas(strikeId)[0]; + + int256 delta = _isCall() + ? callDelta + : callDelta - SignedDecimalMath.UNIT; + deltaGap = _abs(targetDelta - delta); + } + + ///////////////// + // Validation /// + ///////////////// + + /** + * @dev check if the expiry of the board is valid according to the strategy + */ + function _isValidExpiry(uint256 expiry) public view returns (bool isValid) { + uint256 secondsToExpiry = _getSecondsToExpiry(expiry); + isValid = (secondsToExpiry >= strategyDetail.minTimeToExpiry && + secondsToExpiry <= strategyDetail.maxTimeToExpiry); + } + + function _exchangePremiums(uint256 size) + internal + returns (uint256 baseReceived) + { + ExchangeRateParams memory exchangeParams = getExchangeParams(); + //uint quoteBal = quoteAsset.balanceOf(address(this)); + // exchange quote asset to base asset + uint256 minQuoteExpected = size + .divideDecimal(exchangeParams.spotPrice) + .multiplyDecimal( + DecimalMath.UNIT - exchangeParams.baseQuoteFeeRate + ); + baseReceived = exchangeFromExactQuote(size, minQuoteExpected); + } +} diff --git a/packages/contracts/contracts/strategies/StrategyBaseFuerte.sol b/packages/contracts/contracts/strategies/StrategyBaseFuerte.sol deleted file mode 100644 index 838c8ae..0000000 --- a/packages/contracts/contracts/strategies/StrategyBaseFuerte.sol +++ /dev/null @@ -1,215 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; -pragma experimental ABIEncoderV2; - -// Hardhat -import "hardhat/console.sol"; - -// Lyra -import {VaultAdapter} from "@lyrafinance/protocol/contracts/periphery/VaultAdapter.sol"; -import {GWAVOracle} from "@lyrafinance/protocol/contracts/periphery/GWAVOracle.sol"; - -// Libraries -import {Vault} from "../libraries/Vault.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {LyraVault} from "../core/LyraVault.sol"; -import {DecimalMath} from "@lyrafinance/protocol/contracts/synthetix/DecimalMath.sol"; -import {SignedDecimalMath} from "@lyrafinance/protocol/contracts/synthetix/SignedDecimalMath.sol"; - -contract StrategyBaseFuerte is VaultAdapter { - using DecimalMath for uint; - using SignedDecimalMath for int; - - LyraVault public immutable vault; - //OptionType public immutable optionType; - GWAVOracle public immutable gwavOracle; - - /// @dev asset used as collateral in AMM to sell. Should be the same as vault asset - IERC20 public collateralAsset; - - mapping(uint => uint) public lastTradeTimestamp; - - uint[] public activeStrikeIds; - mapping(uint => uint) public strikeToPositionId; - - /////////// - // ADMIN // - /////////// - - modifier onlyVault() virtual { - require(msg.sender == address(vault), "only Vault"); - _; - } - - constructor( - LyraVault _vault, - // OptionType _optionType, - GWAVOracle _gwavOracle - ) VaultAdapter() { - vault = _vault; - // optionType = _optionType; - gwavOracle = _gwavOracle; - } - - function initAdapter( - address _curveSwap, - address _optionToken, - address _optionMarket, - address _liquidityPool, - address _shortCollateral, - address _synthetixAdapter, - address _optionPricer, - address _greekCache, - address _quoteAsset, - address _baseAsset, - address _feeCounter - ) external onlyOwner { - // set addressese for LyraVaultAdapter - setLyraAddresses( - _curveSwap, - _optionToken, - _optionMarket, - _liquidityPool, - _shortCollateral, - _synthetixAdapter, - _optionPricer, - _greekCache, - _quoteAsset, - _baseAsset, - _feeCounter - ); - - quoteAsset.approve(address(vault), type(uint).max); - baseAsset.approve(address(vault), type(uint).max); - collateralAsset = baseAsset; // _isBaseCollat() ? baseAsset : quoteAsset; - } - - /////////////////// - // VAULT ACTIONS // - /////////////////// - - /** - * @dev exchange asset back to collateral asset and send it back to the vault - * @dev override this function if you want to customize asset management flow - */ - function _returnFundsToVaut() internal virtual { - //ExchangeRateParams memory exchangeParams = getExchangeParams(); - uint quoteBal = quoteAsset.balanceOf(address(this)); - uint baseBal = baseAsset.balanceOf(address(this)); - require(baseAsset.transfer(address(vault), baseBal), "failed to return funds from strategy"); - require(quoteAsset.transfer(address(vault), quoteBal), "failed to return funds from strategy"); - } - - ///////////////////////////// - // Trade Parameter Helpers // - ///////////////////////////// - - /** - * @dev get minimum premium that the vault should receive. - * param listingId lyra option listing id - * param size size of trade in Lyra standard sizes - */ - function _getPremiumLimit( - Strike memory strike, - OptionType optionType, - uint vol, - uint size - ) internal view returns (uint limitPremium) { - ExchangeRateParams memory exchangeParams = getExchangeParams(); - (uint callPremium, uint putPremium) = getPurePremium( - _getSecondsToExpiry(strike.expiry), - vol, - exchangeParams.spotPrice, - strike.strikePrice - ); - - limitPremium = _isCall(optionType) ? callPremium.multiplyDecimal(size) : putPremium.multiplyDecimal(size); - } - - /** - * @dev use latest optionMarket delta cutoff to determine whether trade delta is out of bounds - */ - function _isOutsideDeltaCutoff(uint strikeId) internal view returns (bool) { - MarketParams memory marketParams = getMarketParams(); - int callDelta = getDeltas(_toDynamic(strikeId))[0]; - return callDelta > (int(DecimalMath.UNIT) - marketParams.deltaCutOff) || callDelta < marketParams.deltaCutOff; - } - - ////////////////////////////// - // Active Strike Management // - ////////////////////////////// - - /** - * @dev add strike id to activeStrikeIds array - */ - function _addActiveStrike(uint strikeId, uint tradedPositionId) internal { - if (!_isActiveStrike(strikeId)) { - strikeToPositionId[strikeId] = tradedPositionId; - activeStrikeIds.push(strikeId); - } - } - - /** - * @dev add the last traded timestamp for a specific strike. - */ - function _setLastTradedAt(uint strikeId, uint timestamp) internal { - lastTradeTimestamp[strikeId] = timestamp; - } - - /** - * @dev remove position data opened in the current round. - * this can only be called after the position is settled by lyra - **/ - function _clearAllActiveStrikes() internal { - if (activeStrikeIds.length != 0) { - for (uint i = 0; i < activeStrikeIds.length; i++) { - uint strikeId = activeStrikeIds[i]; - OptionPosition memory position = getPositions(_toDynamic(strikeToPositionId[strikeId]))[0]; - // revert if position state is not settled - require(position.state != PositionState.ACTIVE, "cannot clear active position"); - delete strikeToPositionId[strikeId]; - delete lastTradeTimestamp[i]; - } - delete activeStrikeIds; - } - } - - function _isActiveStrike(uint strikeId) internal view returns (bool isActive) { - isActive = strikeToPositionId[strikeId] != 0; - } - - ////////// - // Misc // - ////////// - - function _isBaseCollat(OptionType optionType) internal pure returns (bool isBase) { - isBase = (optionType == OptionType.SHORT_CALL_BASE) ? true : false; - } - - function _isCall(OptionType optionType) internal pure returns (bool isCall) { - isCall = (optionType == OptionType.SHORT_PUT_QUOTE || optionType == OptionType.LONG_PUT) ? false : true; - } - - function _getSecondsToExpiry(uint expiry) internal view returns (uint) { - require(block.timestamp <= expiry, "timestamp expired"); - return expiry - block.timestamp; - } - - function _abs(int val) internal pure returns (uint) { - return val >= 0 ? uint(val) : uint(-val); - } - - function _min(uint x, uint y) internal pure returns (uint) { - return (x < y) ? x : y; - } - - function _max(uint x, uint y) internal pure returns (uint) { - return (x > y) ? x : y; - } - - // temporary fix - eth core devs promised Q2 2022 fix - function _toDynamic(uint val) internal pure returns (uint[] memory dynamicArray) { - dynamicArray = new uint[](1); - dynamicArray[0] = val; - } -} diff --git a/packages/contracts/contracts/test/HackMoneyStrategyBaseTest.sol b/packages/contracts/contracts/test/HackMoneyStrategyBaseTest.sol new file mode 100644 index 0000000..72d0727 --- /dev/null +++ b/packages/contracts/contracts/test/HackMoneyStrategyBaseTest.sol @@ -0,0 +1,267 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; +pragma experimental ABIEncoderV2; + +// Hardhat +import "hardhat/console.sol"; + +// Lyra +import {VaultAdapter} from "@lyrafinance/protocol/contracts/periphery/VaultAdapter.sol"; +import {GWAVOracle} from "@lyrafinance/protocol/contracts/periphery/GWAVOracle.sol"; + +// Libraries +import {Vault} from "../libraries/Vault.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {HackMoneyVault} from "../core/HackMoneyVault.sol"; +import {DecimalMath} from "@lyrafinance/protocol/contracts/synthetix/DecimalMath.sol"; +import {SignedDecimalMath} from "@lyrafinance/protocol/contracts/synthetix/SignedDecimalMath.sol"; + +contract HackMoneyStrategyBaseTest is VaultAdapter { + using DecimalMath for uint; + using SignedDecimalMath for int; + + HackMoneyVault public immutable vault; + OptionType public immutable optionType; + GWAVOracle public immutable gwavOracle; + + /// @dev asset used as collateral in AMM to sell. Should be the same as vault asset + IERC20 public collateralAsset; + + mapping(uint => uint) public lastTradeTimestamp; + + uint[] public activeStrikeIds; + mapping(uint => uint) public strikeToPositionId; + + /////////// + // ADMIN // + /////////// + + modifier onlyVault() virtual { + require(msg.sender == address(vault), "only Vault"); + _; + } + + constructor( + HackMoneyVault _vault, + OptionType _optionType, + GWAVOracle _gwavOracle + ) VaultAdapter() { + vault = _vault; + optionType = _optionType; + gwavOracle = _gwavOracle; + } + + function initAdapter( + address _curveSwap, + address _optionToken, + address _optionMarket, + address _liquidityPool, + address _shortCollateral, + address _synthetixAdapter, + address _optionPricer, + address _greekCache, + address _quoteAsset, + address _baseAsset, + address _feeCounter + ) external onlyOwner { + // set addressese for LyraVaultAdapter + setLyraAddresses( + _curveSwap, + _optionToken, + _optionMarket, + _liquidityPool, + _shortCollateral, + _synthetixAdapter, + _optionPricer, + _greekCache, + _quoteAsset, + _baseAsset, + _feeCounter + ); + + quoteAsset.approve(address(vault), type(uint).max); + baseAsset.approve(address(vault), type(uint).max); + collateralAsset = _isBaseCollat() ? baseAsset : quoteAsset; + } + + ////////////////////// + // APPROVAL ACTIONS // + ////////////////////// + + function approveERC20( + address token, + address spender, + uint amount + ) external onlyOwner { + IERC20(token).approve(spender, amount); + } + + /////////////////// + // VAULT ACTIONS // + /////////////////// + + /** + * @dev exchange asset back to collateral asset and send it back to the vault + * @dev override this function if you want to customize asset management flow + */ + function _returnFundsToVaut() internal virtual { + ExchangeRateParams memory exchangeParams = getExchangeParams(); + uint quoteBal = quoteAsset.balanceOf(address(this)); + + if (_isBaseCollat()) { + // exchange quote asset to base asset, and send base asset back to vault + uint baseBal = baseAsset.balanceOf(address(this)); + uint minQuoteExpected = quoteBal + .divideDecimal(exchangeParams.spotPrice) + .multiplyDecimal( + DecimalMath.UNIT - exchangeParams.baseQuoteFeeRate + ); + uint baseReceived = exchangeFromExactQuote( + quoteBal, + minQuoteExpected + ); + require( + baseAsset.transfer(address(vault), baseBal + baseReceived), + "failed to return funds from strategy" + ); + } else { + // send quote balance directly + require( + quoteAsset.transfer(address(vault), quoteBal), + "failed to return funds from strategy" + ); + } + } + + ///////////////////////////// + // Trade Parameter Helpers // + ///////////////////////////// + + /** + * @dev get minimum premium that the vault should receive. + * param listingId lyra option listing id + * param size size of trade in Lyra standard sizes + */ + function _getPremiumLimit( + Strike memory strike, + uint vol, + uint size + ) internal view returns (uint limitPremium) { + ExchangeRateParams memory exchangeParams = getExchangeParams(); + (uint callPremium, uint putPremium) = getPurePremium( + _getSecondsToExpiry(strike.expiry), + vol, + exchangeParams.spotPrice, + strike.strikePrice + ); + + limitPremium = _isCall() + ? callPremium.multiplyDecimal(size) + : putPremium.multiplyDecimal(size); + } + + /** + * @dev use latest optionMarket delta cutoff to determine whether trade delta is out of bounds + */ + function _isOutsideDeltaCutoff(uint strikeId) internal view returns (bool) { + MarketParams memory marketParams = getMarketParams(); + int callDelta = getDeltas(_toDynamic(strikeId))[0]; + return + callDelta > (int(DecimalMath.UNIT) - marketParams.deltaCutOff) || + callDelta < marketParams.deltaCutOff; + } + + ////////////////////////////// + // Active Strike Management // + ////////////////////////////// + + /** + * @dev add strike id to activeStrikeIds array + */ + function _addActiveStrike(uint strikeId, uint tradedPositionId) internal { + if (!_isActiveStrike(strikeId)) { + strikeToPositionId[strikeId] = tradedPositionId; + activeStrikeIds.push(strikeId); + } + } + + /** + * @dev add the last traded timestamp for a specific strike. + */ + function _setLastTradedAt(uint strikeId, uint timestamp) internal { + lastTradeTimestamp[strikeId] = timestamp; + } + + /** + * @dev remove position data opened in the current round. + * this can only be called after the position is settled by lyra + **/ + function _clearAllActiveStrikes() internal { + if (activeStrikeIds.length != 0) { + for (uint i = 0; i < activeStrikeIds.length; i++) { + uint strikeId = activeStrikeIds[i]; + OptionPosition memory position = getPositions( + _toDynamic(strikeToPositionId[strikeId]) + )[0]; + // revert if position state is not settled + require( + position.state != PositionState.ACTIVE, + "cannot clear active position" + ); + delete strikeToPositionId[strikeId]; + delete lastTradeTimestamp[i]; + } + delete activeStrikeIds; + } + } + + function _isActiveStrike(uint strikeId) + internal + view + returns (bool isActive) + { + isActive = strikeToPositionId[strikeId] != 0; + } + + ////////// + // Misc // + ////////// + + function _isBaseCollat() internal view returns (bool isBase) { + isBase = (optionType == OptionType.SHORT_CALL_BASE) ? true : false; + } + + function _isCall() internal view returns (bool isCall) { + isCall = (optionType == OptionType.SHORT_PUT_QUOTE || + optionType == OptionType.LONG_PUT) + ? false + : true; + } + + function _getSecondsToExpiry(uint expiry) internal view returns (uint) { + require(block.timestamp <= expiry, "timestamp expired"); + return expiry - block.timestamp; + } + + function _abs(int val) internal pure returns (uint) { + return val >= 0 ? uint(val) : uint(-val); + } + + function _min(uint x, uint y) internal pure returns (uint) { + return (x < y) ? x : y; + } + + function _max(uint x, uint y) internal pure returns (uint) { + return (x > y) ? x : y; + } + + // temporary fix - eth core devs promised Q2 2022 fix + function _toDynamic(uint val) + internal + pure + returns (uint[] memory dynamicArray) + { + dynamicArray = new uint[](1); + dynamicArray[0] = val; + } +} diff --git a/packages/contracts/contracts/test/HackMoneyStrategyTest.sol b/packages/contracts/contracts/test/HackMoneyStrategyTest.sol new file mode 100644 index 0000000..5a1c45f --- /dev/null +++ b/packages/contracts/contracts/test/HackMoneyStrategyTest.sol @@ -0,0 +1,488 @@ +//SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; +pragma experimental ABIEncoderV2; + +// Hardhat +import "hardhat/console.sol"; + +// standard strategy interface +import "../interfaces/IHackMoneyStrategy.sol"; + +// Lyra +import {VaultAdapter} from "@lyrafinance/protocol/contracts/periphery/VaultAdapter.sol"; +import {GWAVOracle} from "@lyrafinance/protocol/contracts/periphery/GWAVOracle.sol"; + +// Libraries +import {Vault} from "../libraries/Vault.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {HackMoneyVault} from "../core/HackMoneyVault.sol"; +import {DecimalMath} from "@lyrafinance/protocol/contracts/synthetix/DecimalMath.sol"; +import {SignedDecimalMath} from "@lyrafinance/protocol/contracts/synthetix/SignedDecimalMath.sol"; + +// StrategyBase to inherit +import {HackMoneyStrategyBaseTest} from "./HackMoneyStrategyBaseTest.sol"; + +contract HackMoneyStrategyTest is + HackMoneyStrategyBaseTest, + IHackMoneyStrategy +{ + using DecimalMath for uint; + using SignedDecimalMath for int; + + // example strategy detail + struct HackMoneyStrategyDetail { + uint minTimeToExpiry; + uint maxTimeToExpiry; + int mintargetDelta; // 15% + int maxtargetDelta; // 85% + uint maxDeltaGap; // 5% + uint minVol; // 80% + uint size; // 15 + } + + HackMoneyStrategyDetail public strategyDetail; + uint public activeExpiry; + uint public currentBoardId; + uint public ivLimit = 2 * 1e18; + address public lyraRewardRecipient; + + //uint public optionSize; + + /////////// + // ADMIN // + /////////// + + constructor( + HackMoneyVault _vault, + OptionType _optionType, + GWAVOracle _gwavOracle + ) HackMoneyStrategyBaseTest(_vault, _optionType, _gwavOracle) {} + + /** + * @dev update the strategy detail for the new round. + */ + function setStrategyDetail(HackMoneyStrategyDetail memory _deltaStrategy) + external + onlyOwner + { + (, , , , , , , bool roundInProgress) = vault.vaultState(); + require(!roundInProgress, "cannot change strategy if round is active"); + strategyDetail = _deltaStrategy; + } + + /** + * @dev update the iv limit + */ + function setIvLimit(uint _ivLimit) external onlyOwner { + ivLimit = _ivLimit; + } + + /** + * @dev update lyra reward recipient + */ + function setLyraRewardRecipient(address _lyraRewardRecipient) + external + onlyOwner + { + lyraRewardRecipient = _lyraRewardRecipient; + } + + /////////////////// + // VAULT ACTIONS // + /////////////////// + + /** + * @dev set the board id that will be traded for the next round + * @param boardId lyra board Id. + */ + function setBoard(uint boardId) external onlyVault { + Board memory board = getBoard(boardId); + require(_isValidExpiry(board.expiry), "invalid board"); + activeExpiry = board.expiry; + currentBoardId = boardId; + } + + /** + * @dev convert premium in quote asset into collateral asset and send it back to the vault. + */ + function returnFundsAndClearStrikes() external onlyVault { + // exchange asset back to collateral asset and send it back to the vault + _returnFundsToVaut(); + + // keep internal storage data on old strikes and positions ids + _clearAllActiveStrikes(); + } + + /** + * @notice sell a fix aomunt of options and collect premium + * @dev the vault should pass in a strike id, and the strategy would verify if the strike is valid on-chain. + * @return positionId1 + * @return positionId2 + * @return premiumReceived + * @return collateralToAdd + */ + function doTrade(uint size) + external + onlyVault + returns ( + uint positionId1, + uint positionId2, + uint premiumReceived, + uint collateralToAdd, + uint premiumExchangeValue + ) + // uint exchangeValue + { + strategyDetail.size = size; + + ( + positionId1, + positionId2, + premiumReceived, + collateralToAdd, + premiumExchangeValue + ) = _tradeOptions(); + } + + function _tradeOptions() + internal + returns ( + uint positionId1, + uint positionId2, + uint premiumReceived, + uint collateralToAdd, + uint premiumExchangeValue + ) + { + (Strike memory strike1, Strike memory strike2) = _getTradeStrikes(); + + uint premiumReceived1; + uint premiumReceived2; + uint collateralToAdd1; + uint collateralToAdd2; + + (positionId1, premiumReceived1, collateralToAdd1) = _tradeStrike( + strike1 + ); + (positionId2, premiumReceived2, collateralToAdd2) = _tradeStrike( + strike2 + ); + premiumReceived = premiumReceived1 + premiumReceived2; + + // uint additionalPremium; + // (, , additionalPremium, premiumExchangeValue) = _tradePremiums( + // premiumReceived, + // collateralToAdd1, + // collateralToAdd2 + // ); + + collateralToAdd = collateralToAdd1 + collateralToAdd2; // + exchangeValue; + //premiumReceived += additionalPremium; + } + + function _tradeStrike(Strike memory strike) + internal + returns ( + uint positionId, + uint premiumReceived, + uint collateralToAdd + ) + //uint setCollateralTo + { + uint setCollateralTo; + (collateralToAdd, setCollateralTo) = getRequiredCollateral(strike); + + (positionId, premiumReceived) = _sellStrike(strike, setCollateralTo); + } + + /** + * @notice trade premiums received from a trade + * @return positionId1 + * @return positionId2 + * @return premiumReceived + */ + function _tradePremiums( + uint size, + uint collateralToAdd1, + uint collateralToAdd2 + ) + internal + returns ( + uint positionId1, + uint positionId2, + uint premiumReceived, + uint exchangeValue + ) + { + // exchange susd to seth + + exchangeValue = _exchangePremiums(size); + + uint sellAmount = exchangeValue / 2; + (Strike memory strike1, Strike memory strike2) = _getTradeStrikes(); + uint premiumReceived1; + (positionId1, premiumReceived1) = _sellPremiums( + strike1, + sellAmount, + collateralToAdd1 + ); + uint premiumReceived2; + (positionId2, premiumReceived2) = _sellPremiums( + strike2, + sellAmount, + collateralToAdd2 + ); + premiumReceived = premiumReceived1 + premiumReceived2; + } + + ///////////////////////////// + // Trade Parameter Helpers // + ///////////////////////////// + + /** + * @dev calculate required collateral to add in the next trade. + * sell size is fixed as strategyDetail.size + * only add collateral if the additional sell will make the position out of buffer range + * never remove collateral from an existing position + */ + function getRequiredCollateral(Strike memory strike) + public + view + returns (uint collateralToAdd, uint setCollateralTo) + { + uint sellAmount = strategyDetail.size; + collateralToAdd = _getFullCollateral(strike.strikePrice, sellAmount); + setCollateralTo = collateralToAdd; + + // get existing position info if active + uint existingAmount = 0; + uint existingCollateral = 0; + if (_isActiveStrike(strike.id)) { + OptionPosition memory position = getPositions( + _toDynamic(strikeToPositionId[strike.id]) + )[0]; + existingCollateral = position.collateral; + existingAmount = position.amount; + } + + setCollateralTo += existingCollateral; + console.log("setCollateralTo:", setCollateralTo / 10**18); + } + + /** + * @dev perform the trade + * @param strike strike detail + * @param setCollateralTo target collateral amount + * @return positionId + * @return premiumReceived + */ + function _sellStrike(Strike memory strike, uint setCollateralTo) + internal + returns (uint, uint) + { + // get minimum expected premium based on minIv + uint minExpectedPremium = _getPremiumLimit( + strike, + strategyDetail.minVol, + strategyDetail.size + ); + uint strikeId = strike.id; + uint initIv = strike.boardIv.multiplyDecimal(strike.skew); + + // perform trade + TradeResult memory result = openPosition( + TradeInputParameters({ + strikeId: strike.id, + positionId: strikeToPositionId[strike.id], + iterations: 4, + optionType: optionType, + amount: strategyDetail.size, + setCollateralTo: setCollateralTo, + minTotalCost: 0, + maxTotalCost: type(uint).max, + rewardRecipient: lyraRewardRecipient // set to zero address if don't want to wait for whitelist + }) + ); + Strike memory finalStrike = getStrikes(_toDynamic(strikeId))[0]; + uint finalIv = finalStrike.boardIv.multiplyDecimal(finalStrike.skew); + require(initIv - finalIv < ivLimit, "IV_LIMIT_HIT"); + + lastTradeTimestamp[strike.id] = block.timestamp; + + // update active strikes + _addActiveStrike(strike.id, result.positionId); + + return (result.positionId, result.totalCost); + } + + /** + * @dev perform the trade + * @param strike strike detail + * @return positionId + * @return premiumReceived + */ + function _sellPremiums( + Strike memory strike, + uint size, + uint collateralToAdd + ) internal returns (uint, uint) { + // get minimum expected premium based on minIv + uint minExpectedPremium = _getPremiumLimit( + strike, + strategyDetail.minVol, + size + ); + uint strikeId = strike.id; + uint initIv = strike.boardIv.multiplyDecimal(strike.skew); + + // perform trade + TradeResult memory result = openPosition( + TradeInputParameters({ + strikeId: strike.id, + positionId: strikeToPositionId[strike.id], + iterations: 4, + optionType: optionType, + amount: size, + setCollateralTo: size + collateralToAdd, + minTotalCost: 0, + maxTotalCost: type(uint).max, + rewardRecipient: lyraRewardRecipient // set to zero address if don't want to wait for whitelist + }) + ); + Strike memory finalStrike = getStrikes(_toDynamic(strikeId))[0]; + uint finalIv = finalStrike.boardIv.multiplyDecimal(finalStrike.skew); + require(initIv - finalIv < ivLimit, "IV_LIMIT_HIT"); + + lastTradeTimestamp[strike.id] = block.timestamp; + + // update active strikes + _addActiveStrike(strike.id, result.positionId); + + // require( + // result.totalCost >= minExpectedPremium, + // "premium received is below min expected premium" + // ); + + return (result.positionId, result.totalCost); + } + + function _getTradeStrikes() + internal + view + returns (Strike memory smallStrike, Strike memory bigStrike) + { + // get all strike Ids for current board + uint[] memory strikeIds = optionMarket.getBoardStrikes(currentBoardId); + + // get small and big strike Ids + uint smallStrikeId = strikeIds[0]; + uint bigStrikeId = strikeIds[0]; + + // init strikes + smallStrike = getStrikes(_toDynamic(smallStrikeId))[0]; + bigStrike = getStrikes(_toDynamic(bigStrikeId))[0]; + + (uint smallDeltaGap, ) = _getDeltaGap(smallStrike, true); + (uint bigDeltaGap, ) = _getDeltaGap(bigStrike, false); + console.log("smallDeltaGap:", smallDeltaGap / 10**15, " / 10 %"); + console.log("bigDeltaGap:", bigDeltaGap / 10**15, "/ 10 %"); + + for (uint i = 1; i < strikeIds.length - 1; i++) { + // Get current Strike + uint currentStrikeId = strikeIds[i]; + Strike memory currentStrike = getStrikes( + _toDynamic(currentStrikeId) + )[0]; + + // Get current delta gaps + (uint currentSmallDeltaGap, ) = _getDeltaGap(currentStrike, true); + (uint currentBigDeltaGap, ) = _getDeltaGap(currentStrike, false); + + if (currentSmallDeltaGap < smallDeltaGap) { + smallStrike = currentStrike; + smallDeltaGap = currentSmallDeltaGap; + } + if (currentBigDeltaGap < bigDeltaGap) { + bigStrike = currentStrike; + bigDeltaGap = currentBigDeltaGap; + } + console.log("Loop iteration: ", i); + console.log("smallDeltaGap:", smallDeltaGap / 10**15, "/ 10 %"); + console.log("bigDeltaGap:", bigDeltaGap / 10**15, " / 10%"); + } + + // final checks + require( + smallDeltaGap <= strategyDetail.maxDeltaGap, + "smallDeltaGap out of bound!" + ); + require( + bigDeltaGap <= strategyDetail.maxDeltaGap, + "smallDeltaGap out of bound!" + ); + } + + ///////////////////////////// + // Trade Parameter Helpers // + ///////////////////////////// + + /** + * @param strikePrice the strike price + * @param amount of options to cover + * @return fullCollat much collateral is needed for `amount` sell of options + */ + function _getFullCollateral(uint strikePrice, uint amount) + internal + view + returns (uint fullCollat) + { + // calculate required collat based on collatBuffer and collatPercent + fullCollat = _isBaseCollat() + ? amount + : amount.multiplyDecimal(strikePrice); + } + + /** + * @dev return delta gap + * @return deltaGap delta gap in abs value + */ + function _getDeltaGap(Strike memory strike, bool isSmallStrike) + public + view + returns (uint deltaGap, int callDelta) + { + int targetDelta = isSmallStrike + ? strategyDetail.maxtargetDelta + : strategyDetail.mintargetDelta; + uint[] memory strikeId = _toDynamic(strike.id); + callDelta = getDeltas(strikeId)[0]; + + int delta = _isCall() ? callDelta : callDelta - SignedDecimalMath.UNIT; + deltaGap = _abs(targetDelta - delta); + } + + ///////////////// + // Validation /// + ///////////////// + + /** + * @dev check if the expiry of the board is valid according to the strategy + */ + function _isValidExpiry(uint expiry) public view returns (bool isValid) { + uint secondsToExpiry = _getSecondsToExpiry(expiry); + isValid = (secondsToExpiry >= strategyDetail.minTimeToExpiry && + secondsToExpiry <= strategyDetail.maxTimeToExpiry); + } + + function _exchangePremiums(uint size) internal returns (uint baseReceived) { + ExchangeRateParams memory exchangeParams = getExchangeParams(); + //uint quoteBal = quoteAsset.balanceOf(address(this)); + // exchange quote asset to base asset + uint minQuoteExpected = size + .divideDecimal(exchangeParams.spotPrice) + .multiplyDecimal( + DecimalMath.UNIT - exchangeParams.baseQuoteFeeRate + ); + baseReceived = exchangeFromExactQuote(size, minQuoteExpected); + } +} diff --git a/packages/contracts/tasks/accounts.ts b/packages/contracts/tasks/accounts.ts index 1972ae7..593936b 100644 --- a/packages/contracts/tasks/accounts.ts +++ b/packages/contracts/tasks/accounts.ts @@ -1,11 +1,11 @@ -import { task } from "hardhat/config"; +// import { task } from "hardhat/config"; -task("accounts", "Prints the list of accounts", async (args, hre) => { - const accounts = await hre.ethers.getSigners(); +// task("accounts", "Prints the list of accounts", async (args, hre) => { +// const accounts = await hre.ethers.getSigners(); - for (const account of accounts) { - console.log(account.address); - } -}); +// for (const account of accounts) { +// console.log(account.address); +// } +// }); -module.exports = {}; +// module.exports = {}; diff --git a/packages/contracts/tasks/closeRound.ts b/packages/contracts/tasks/closeRound.ts index 78a2627..2c5489f 100644 --- a/packages/contracts/tasks/closeRound.ts +++ b/packages/contracts/tasks/closeRound.ts @@ -1,21 +1,21 @@ -import "@nomiclabs/hardhat-ethers"; -import "@nomiclabs/hardhat-web3"; -import { task } from "hardhat/config"; -import { DEPLOYED_CONTRACTS } from "../constants"; -import { HackMoneyVault__factory } from "../typechain-types/factories/HackMoneyVault__factory"; +// import "@nomiclabs/hardhat-ethers"; +// import "@nomiclabs/hardhat-web3"; +// import { task } from "hardhat/config"; +// import { DEPLOYED_CONTRACTS } from "../constants"; +// import { HackMoneyVault__factory } from "../typechain-types/factories/HackMoneyVault__factory"; -task("closeRound", "Close round").setAction(async (_, hre) => { - const chainId = hre.network.config.chainId; - if (!chainId) return; - const vaultAddress = DEPLOYED_CONTRACTS.LyraVault[chainId]; - const [deployer] = await hre.ethers.getSigners(); +// task("closeRound", "Close round").setAction(async (_, hre) => { +// const chainId = hre.network.config.chainId; +// if (!chainId) return; +// const vaultAddress = DEPLOYED_CONTRACTS.LyraVault[chainId]; +// const [deployer] = await hre.ethers.getSigners(); - const hackMoneyVault = HackMoneyVault__factory.connect( - vaultAddress, - deployer - ); +// const hackMoneyVault = HackMoneyVault__factory.connect( +// vaultAddress, +// deployer +// ); - await hackMoneyVault.closeRound(); -}); +// await hackMoneyVault.closeRound(); +// }); -module.exports = {}; +// module.exports = {}; diff --git a/packages/contracts/tasks/getStrategyCollateral.ts b/packages/contracts/tasks/getStrategyCollateral.ts index 153cef0..9ef2471 100644 --- a/packages/contracts/tasks/getStrategyCollateral.ts +++ b/packages/contracts/tasks/getStrategyCollateral.ts @@ -1,21 +1,21 @@ -import "@nomiclabs/hardhat-ethers"; -import "@nomiclabs/hardhat-web3"; -import { task } from "hardhat/config"; -import { DEPLOYED_CONTRACTS } from "../constants"; -import { HackMoneyStrategy__factory } from "../typechain-types/factories/HackMoneyStrategy__factory"; +// import "@nomiclabs/hardhat-ethers"; +// import "@nomiclabs/hardhat-web3"; +// import { task } from "hardhat/config"; +// import { DEPLOYED_CONTRACTS } from "../constants"; +// import { HackMoneyStrategy__factory } from "../typechain-types/factories/HackMoneyStrategy__factory"; -task("collateral", "Get strategy collateral").setAction(async (_, hre) => { - const chainId = hre.network.config.chainId; - if (!chainId) return; - const strategyAddress = DEPLOYED_CONTRACTS.HackMoneyStrategy[chainId]; - const [deployer] = await hre.ethers.getSigners(); +// task("collateral", "Get strategy collateral").setAction(async (_, hre) => { +// const chainId = hre.network.config.chainId; +// if (!chainId) return; +// const strategyAddress = DEPLOYED_CONTRACTS.HackMoneyStrategy[chainId]; +// const [deployer] = await hre.ethers.getSigners(); - const hackMoneyVault = HackMoneyStrategy__factory.connect( - strategyAddress, - deployer - ); +// const hackMoneyVault = HackMoneyStrategy__factory.connect( +// strategyAddress, +// deployer +// ); - console.log(await hackMoneyVault.collateralAsset()); -}); +// console.log(await hackMoneyVault.collateralAsset()); +// }); -module.exports = {}; +// module.exports = {}; diff --git a/packages/contracts/tasks/startNewRound.ts b/packages/contracts/tasks/startNewRound.ts index 2c8ba73..c56d49e 100644 --- a/packages/contracts/tasks/startNewRound.ts +++ b/packages/contracts/tasks/startNewRound.ts @@ -1,23 +1,23 @@ -import "@nomiclabs/hardhat-ethers"; -import "@nomiclabs/hardhat-web3"; -import { task } from "hardhat/config"; -import { DEPLOYED_CONTRACTS } from "../constants"; -import { HackMoneyVault__factory } from "../typechain-types/factories/HackMoneyVault__factory"; +// import "@nomiclabs/hardhat-ethers"; +// import "@nomiclabs/hardhat-web3"; +// import { task } from "hardhat/config"; +// import { DEPLOYED_CONTRACTS } from "../constants"; +// import { HackMoneyVault__factory } from "../typechain-types/factories/HackMoneyVault__factory"; -task("startNewRound", "Start new round") - .addParam("boardid", "Board ID") - .setAction(async ({ boardid }, hre) => { - const chainId = hre.network.config.chainId; - if (!chainId) return; - const vaultAddress = DEPLOYED_CONTRACTS.LyraVault[chainId]; - const [deployer] = await hre.ethers.getSigners(); +// task("startNewRound", "Start new round") +// .addParam("boardid", "Board ID") +// .setAction(async ({ boardid }, hre) => { +// const chainId = hre.network.config.chainId; +// if (!chainId) return; +// const vaultAddress = DEPLOYED_CONTRACTS.LyraVault[chainId]; +// const [deployer] = await hre.ethers.getSigners(); - const hackMoneyVault = HackMoneyVault__factory.connect( - vaultAddress, - deployer - ); +// const hackMoneyVault = HackMoneyVault__factory.connect( +// vaultAddress, +// deployer +// ); - await hackMoneyVault.startNextRound(boardid); - }); +// await hackMoneyVault.startNextRound(boardid); +// }); -module.exports = {}; +// module.exports = {}; diff --git a/packages/contracts/tasks/trade.ts b/packages/contracts/tasks/trade.ts index ea07d18..20967e5 100644 --- a/packages/contracts/tasks/trade.ts +++ b/packages/contracts/tasks/trade.ts @@ -1,21 +1,21 @@ -import "@nomiclabs/hardhat-ethers"; -import "@nomiclabs/hardhat-web3"; -import { task } from "hardhat/config"; -import { DEPLOYED_CONTRACTS } from "../constants"; -import { HackMoneyVault__factory } from "../typechain-types/factories/HackMoneyVault__factory"; +// import "@nomiclabs/hardhat-ethers"; +// import "@nomiclabs/hardhat-web3"; +// import { task } from "hardhat/config"; +// import { DEPLOYED_CONTRACTS } from "../constants"; +// import { HackMoneyVault__factory } from "../typechain-types/factories/HackMoneyVault__factory"; -task("trade", "Trade in new round").setAction(async (_, hre) => { - const chainId = hre.network.config.chainId; - if (!chainId) return; - const vaultAddress = DEPLOYED_CONTRACTS.LyraVault[chainId]; - const [deployer] = await hre.ethers.getSigners(); +// task("trade", "Trade in new round").setAction(async (_, hre) => { +// const chainId = hre.network.config.chainId; +// if (!chainId) return; +// const vaultAddress = DEPLOYED_CONTRACTS.LyraVault[chainId]; +// const [deployer] = await hre.ethers.getSigners(); - const hackMoneyVault = HackMoneyVault__factory.connect( - vaultAddress, - deployer - ); +// const hackMoneyVault = HackMoneyVault__factory.connect( +// vaultAddress, +// deployer +// ); - await hackMoneyVault.trade(2); -}); +// await hackMoneyVault.trade(2); +// }); -module.exports = {}; +// module.exports = {}; diff --git a/packages/contracts/test/integration-tests/strategies/hack-money-strategy.ts b/packages/contracts/test/integration-tests/strategies/hack-money-strategy.ts index 9c0b762..0e1d2fe 100644 --- a/packages/contracts/test/integration-tests/strategies/hack-money-strategy.ts +++ b/packages/contracts/test/integration-tests/strategies/hack-money-strategy.ts @@ -8,17 +8,18 @@ import { expect } from "chai"; import { BigNumber } from "ethers"; import { ethers } from "hardhat"; import { - HackMoneyStrategy, + HackMoneyStrategyTest, HackMoneyVault, MockERC20, } from "../../../typechain-types"; -import { HackMoneyStrategyDetailStruct } from "../../../typechain-types/HackMoneyStrategy"; +import { HackMoneyStrategyDetailStruct } from "../../../typechain-types/HackMoneyStrategyTest"; const strategyDetail: HackMoneyStrategyDetailStruct = { minTimeToExpiry: lyraConstants.DAY_SEC, maxTimeToExpiry: lyraConstants.WEEK_SEC * 2, mintargetDelta: toBN("0.15"), maxtargetDelta: toBN("0.85"), + maxDeltaGap: toBN("0.05"), minVol: toBN("0.1"), // min vol to sell. (also used to calculate min premium for call selling vault) size: toBN("100"), }; @@ -32,7 +33,7 @@ describe("Hack Money Strategy integration test", async () => { // let lyraGlobal: LyraGlobal; // let lyraETHMarkets: LyraMarket; let vault: HackMoneyVault; - let strategy: HackMoneyStrategy; + let strategy: HackMoneyStrategyTest; // roles let deployer: SignerWithAddress; @@ -133,7 +134,7 @@ describe("Hack Money Strategy integration test", async () => { before("deploy strategy", async () => { strategy = (await ( - await ethers.getContractFactory("HackMoneyStrategy", { + await ethers.getContractFactory("HackMoneyStrategyTest", { libraries: { BlackScholes: lyraTestSystem.blackScholes.address, }, @@ -144,7 +145,7 @@ describe("Hack Money Strategy integration test", async () => { vault.address, TestSystem.OptionType.SHORT_CALL_BASE, lyraTestSystem.GWAVOracle.address - )) as HackMoneyStrategy; + )) as HackMoneyStrategyTest; }); before("initialize strategy and adaptor", async () => { @@ -227,15 +228,6 @@ describe("Hack Money Strategy integration test", async () => { }); it("should trade when called first time", async () => { - // const strikeObj1 = await strikeIdToDetail(lyraTestSystem.optionMarket, strikes[1]); - // const strikeObj2 = await strikeIdToDetail(lyraTestSystem.optionMarket, strikes[5]); - // const [collateralToAdd1] = await strategy.getRequiredCollateral(strikeObj1); - // const [collateralToAdd2] = await strategy.getRequiredCollateral(strikeObj2); - // const collateralToAdd = collateralToAdd1.add(collateralToAdd2); - - const { smallStrikePrice, bigStrikePrice } = await strategy.getStrikes(); - console.log(smallStrikePrice.toString(), bigStrikePrice.toString()); - const strategySETHBalanceBefore = await seth.balanceOf(strategy.address); console.log( "strategySETHBalanceBefore:", @@ -285,12 +277,6 @@ describe("Hack Money Strategy integration test", async () => { ethers.utils.formatEther(strategySUSDBalanceAfter) ); - // check state.lockAmount left is updated - //expect(vaultStateBefore.lockedAmountLeft.sub(vaultStateAfter.lockedAmountLeft).eq(collateralToAdd)).to.be.true; - // check that we receive sUSD - // expect(strategySUSDBalanceAfter.sub(strategySUSDBalanceBefore).gt(0)).to - // .be.true; - // active strike is updated const storedStrikeId1 = await strategy.activeStrikeIds(0); // expect(storedStrikeId1.eq(strikeObj1.id)).to.be.true; @@ -306,22 +292,8 @@ describe("Hack Money Strategy integration test", async () => { const [position2] = await lyraTestSystem.optionToken.getOptionPositions([ positionId2, ]); - - //expect(strategySUSDBalanceAfter.sub(strategySUSDBalanceBefore).gt(0)).to.be.true; - - // expect(position1.amount.sub(strategyDetail.size).gt(0)).to.be.true; - // expect(position2.amount.sub(strategyDetail.size).gt(0)).to.be.true; - - //expect(position1.amount.eq(strategyDetail.size)).to.be.true; - //expect(position1.collateral.eq(collateralToAdd1)).to.be.true; - //expect(position2.amount.eq(strategyDetail.size)).to.be.true; - //expect(position2.collateral.eq(collateralToAdd2)).to.be.true; }); - // it('should revert when user try to make another trade during same period', async () => { - // await expect(vault.connect(randomUser).trade(strategyDetail.size)).to.be.revertedWith('Wait for options to settle'); - // }); - const additionalDepositAmount = toBN("25000"); it("can add more deposit during the round", async () => { await vault.connect(randomUser).deposit(additionalDepositAmount); diff --git a/packages/contracts/test/integration-tests/strategies/hack-money-vault.ts b/packages/contracts/test/integration-tests/strategies/hack-money-vault.ts index 29e0c87..839d668 100644 --- a/packages/contracts/test/integration-tests/strategies/hack-money-vault.ts +++ b/packages/contracts/test/integration-tests/strategies/hack-money-vault.ts @@ -1,8 +1,4 @@ -import { - getGlobalDeploys, - getMarketDeploys, - lyraConstants, -} from "@lyrafinance/protocol"; +import { lyraConstants } from "@lyrafinance/protocol"; import { MAX_UINT, OptionType, @@ -11,30 +7,28 @@ import { } from "@lyrafinance/protocol/dist/scripts/util/web3utils"; import { BasicFeeCounter__factory, - ERC20, + ERC20__factory, } from "@lyrafinance/protocol/dist/typechain-types"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { expect } from "chai"; -import { Contract } from "ethers"; import { ethers, network } from "hardhat"; -import { DEPLOYED_CONTRACTS } from "../../../constants"; import { HackMoneyVault__factory } from "../../../typechain-types"; import { HackMoneyStrategyLibraryAddresses, HackMoneyStrategy__factory, } from "../../../typechain-types/factories/HackMoneyStrategy__factory"; import { HackMoneyStrategyDetailStruct } from "../../../typechain-types/HackMoneyStrategy"; +import { targets as lyraGlobal } from "./lyra-mainnet.json"; + +const lyraMarket = lyraGlobal.markets.sETH; const strategyDetail: HackMoneyStrategyDetailStruct = { - maxVolVariance: toBN("0.1"), - gwavPeriod: 600, minTimeToExpiry: lyraConstants.DAY_SEC, maxTimeToExpiry: lyraConstants.WEEK_SEC * 2, mintargetDelta: toBN("0.15"), maxtargetDelta: toBN("0.85"), maxDeltaGap: toBN("0.05"), // accept delta from 0.10~0.20 or 0.80~0.90 minVol: toBN("0.8"), // min vol to sell. (also used to calculate min premium for call selling vault) - maxVol: toBN("1.3"), // max vol to sell. size: toBN("100"), }; @@ -53,8 +47,8 @@ describe("Hack Money Vault integration test", async () => { }); it("deploy on kovan fork, deposit and try to trade", async () => { - const chainId = 69; - const networkId = "kovan-ovm"; + const chainId = 10; + const networkId = "mainnet-ovm"; await network.provider.request({ method: "hardhat_reset", params: [ @@ -62,14 +56,14 @@ describe("Hack Money Vault integration test", async () => { forking: { // TODO: Remove into env jsonRpcUrl: - "https://opt-kovan.g.alchemy.com/v2/pfOaw3tye3hUXAWJcP115s1h1RsoYp8A", - blockNumber: 3236641, + "https://opt-mainnet.g.alchemy.com/v2/dW3_J05iOi-kuyk1Zgy7bwXip_gXMeSX", + blockNumber: 8663305, }, }, ], }); - const whaleAddress = "0x15aDBea538f541271dA5E4436E41285e386E3336"; + const whaleAddress = "0xa5f7a39e55d7878bc5bd754ee5d6bd7a7662355b"; // const balance = await ethers.provider.getBalance( // "0xD34F2e9916473C5eFA8A255f5b8738eCd4205317" @@ -80,24 +74,29 @@ describe("Hack Money Vault integration test", async () => { params: [whaleAddress], }); - const lyraGlobal = await getGlobalDeploys(networkId); - console.log("contract name:", lyraGlobal.SynthetixAdapter.contractName); - console.log("address:", lyraGlobal.SynthetixAdapter.address); - - const lyraMarket = await getMarketDeploys(networkId, "sETH"); const whale = await ethers.getSigner(whaleAddress); - const sETH = new Contract( - lyraMarket.BaseAsset.address, - lyraMarket.BaseAsset.abi, - whale - ) as ERC20; - const sUSD = new Contract( - lyraGlobal.QuoteAsset.address, - lyraGlobal.QuoteAsset.abi, + const sETH = ERC20__factory.connect( + "0xE405de8F52ba7559f9df3C368500B6E6ae6Cee49", deployer ); + // const sETH = new Contract( + // lyraMarket.BaseAsset.address, + // lyraMarket.BaseAsset.abi, + // whale + // ) as ERC20; + const sUSD = ERC20__factory.connect( + "0x8c6f28f2F1A3C87F0f938b96d27520d9751ec8d9", + deployer + ); + // const sUSD = new Contract( + // lyraGlobal.QuoteAsset.address, + // lyraGlobal.QuoteAsset.abi, + // deployer + // ); - await sETH.transfer(deployer.address, ethers.utils.parseEther("100")); + await sETH + .connect(whale) + .transfer(deployer.address, ethers.utils.parseEther("100")); const HackMoneyVaultFactory = new HackMoneyVault__factory(deployer); const decimals = 18; @@ -117,16 +116,17 @@ describe("Hack Money Vault integration test", async () => { const linkAddresses: HackMoneyStrategyLibraryAddresses = { "@lyrafinance/protocol/contracts/lib/BlackScholes.sol:BlackScholes": - lyraGlobal.BlackScholes.address, + "0x409f9A1Ee61E94B91b11e3696DF2108EFc7C3EF5", }; const LyraStrategyFactory = new HackMoneyStrategy__factory( linkAddresses, deployer ); + console.log("pre strategy deploy"); const lyraStrategy = await LyraStrategyFactory.connect(deployer).deploy( lyraVault.address, OptionType.SHORT_CALL_BASE, - lyraGlobal.GWAV.address + "0xD2CaAaD2A055Be091f514D240799Ca155Da75a24" ); const BasicFeeCounterFactory = new BasicFeeCounter__factory(deployer); @@ -135,7 +135,7 @@ describe("Hack Money Vault integration test", async () => { await lyraStrategy .connect(deployer) .initAdapter( - DEPLOYED_CONTRACTS.CurveAddress[chainId], + "0x0100fBf414071977B19fC38e6fc7c32FE444F5C9", lyraMarket.OptionToken.address, lyraMarket.OptionMarket.address, lyraMarket.LiquidityPool.address, @@ -169,9 +169,23 @@ describe("Hack Money Vault integration test", async () => { vaultBalance = await lyraVault.totalBalance(); expect(vaultBalance).to.equal(ethers.utils.parseEther("11")); - await expect(lyraVault.trade(5)) - .to.emit(lyraVault, "Trade") - .withArgs(deployer.address, 182, 183, 1817, 10); + + let strategySethBalance; + strategySethBalance = await sETH.balanceOf(lyraStrategy.address); + console.log("strategySethBalance:", strategySethBalance.toString()); + + await lyraVault.trade(ethers.utils.parseEther("2")); + console.log("Traded1"); + strategySethBalance = await sETH.balanceOf(lyraStrategy.address); + console.log("strategySethBalance:", strategySethBalance.toString()); + await lyraVault.trade(ethers.utils.parseEther("1")); + console.log("Traded2"); + strategySethBalance = await sETH.balanceOf(lyraStrategy.address); + console.log("strategySethBalance:", strategySethBalance.toString()); + + // await expect(lyraVault.trade(ethers.utils.parseEther("5"))) + // .to.emit(lyraVault, "Trade") + // .withArgs(deployer.address, 182, 183, 1817, 10); vaultBalance = await lyraVault.totalBalance(); expect(vaultBalance).to.equal(ethers.utils.parseEther("11")); diff --git a/packages/contracts/test/integration-tests/strategies/lyra-mainnet.json b/packages/contracts/test/integration-tests/strategies/lyra-mainnet.json new file mode 100644 index 0000000..860c31d --- /dev/null +++ b/packages/contracts/test/integration-tests/strategies/lyra-mainnet.json @@ -0,0 +1,120 @@ +{ + "targets": { + "SynthetixAdapter": { + "contractName": "SynthetixAdapter", + "source": "SynthetixAdapter", + "address": "0x86Bae98f21172eE1065Ef5111640b568CFe61686", + "txn": "0xe81ce7979b2908898f596841aaa33494361343373d8ad8741c55539a997a6e2b", + "blockNumber": 8457721 + }, + "OptionMarketViewer": { + "contractName": "OptionMarketViewer", + "source": "OptionMarketViewer", + "address": "0xAa644fe9FCe214D17Aba12Ff3dd3B885D2c35357", + "txn": "0x01f94fd6e36c85a90b99756f250410dfbc4a335a61fefaaf02841f38efc3e2b2", + "blockNumber": 8457731 + }, + "OptionMarketWrapper": { + "contractName": "OptionMarketWrapper", + "source": "OptionMarketWrapper", + "address": "0xE8Be7CEDf930305CB056981C2547322F82804f4F", + "txn": "0xba913812eb9f13a913258af43e1ac71d430ff2208abde77ad9193e7238aeda6f", + "blockNumber": 8457811 + }, + "LyraRegistry": { + "contractName": "LyraRegistry", + "source": "LyraRegistry", + "address": "0x7c7AbDdbCb6c731237f7546d3e4c5165531fb0c1", + "txn": "0xfbae6e0cc962cb47a1d380c202e863971cd60ea9520d3c1974e1ee05b3ba409e", + "blockNumber": 8457824 + }, + "GWAV": { + "contractName": "GWAV", + "source": "GWAV", + "address": "0xD2CaAaD2A055Be091f514D240799Ca155Da75a24", + "txn": "0xf24ce9573cde4b99e00fd662e8b0517e3d163930636909515cc3255eea6e5bfd", + "blockNumber": 8457832 + }, + "BlackScholes": { + "contractName": "BlackScholes", + "source": "BlackScholes", + "address": "0x409f9A1Ee61E94B91b11e3696DF2108EFc7C3EF5", + "txn": "0xb13be5898f1706f251c72101d2f3b118956d0e4dbef905afd20800ca01ef0e89", + "blockNumber": 8457840 + }, + "markets": { + "sETH": { + "OptionMarket": { + "contractName": "OptionMarket", + "source": "OptionMarket", + "address": "0xF24eCF73Fd8E7FC9d8F94cd9DF4f03107704D309", + "txn": "0xe7266443c54ae37bbc3900773df516201fc8bf0a0addb752c86be7fbc43415b0", + "blockNumber": 8457996 + }, + "OptionMarketPricer": { + "contractName": "OptionMarketPricer", + "source": "OptionMarketPricer", + "address": "0x6Cd5Af44b0009756080d10278c5A84ed8BE4dDDE", + "txn": "0x80f5f635d46db5561ccadf98c15b6a49aae6bd205cc23fd2c3417e5217fc8902", + "blockNumber": 8458006 + }, + "OptionGreekCache": { + "contractName": "OptionGreekCache", + "source": "OptionGreekCache", + "address": "0x988d525dC8739d68903013374556a39731c7f7A8", + "txn": "0x7271feec44d9f38d496b1472c2fa34220742cc53b6c0c49344dc23af5c501ca5", + "blockNumber": 8458017 + }, + "OptionToken": { + "contractName": "OptionToken", + "source": "OptionToken", + "address": "0x8A92e520BA9dFBa024De3bD7e0926bDcC4911fCC", + "txn": "0xf8ce9c103eb7377e9349aee1be5426e4865a13dbff463ead01e9367fd7755357", + "blockNumber": 8458029 + }, + "LiquidityPool": { + "contractName": "LiquidityPool", + "source": "LiquidityPool", + "address": "0xB6A45238b043b60F0f209dD2FACeF623cc230CCB", + "txn": "0x40a29e123697887966250d0187708971a420ee9cd0051d3d78302be40b8a467f", + "blockNumber": 8458039 + }, + "LiquidityTokens": { + "contractName": "LiquidityTokens", + "source": "LiquidityTokens", + "address": "0x39032BD0315F595276F4b612EB29251df739dE44", + "txn": "0x9db6a8a265cb0eae58e92da2735d2e15d640a2a584177d72d1c410727082a604", + "blockNumber": 8458060 + }, + "ShortCollateral": { + "contractName": "ShortCollateral", + "source": "ShortCollateral", + "address": "0xb016A673ad68b3456da1Ba3c17F9b7F4d65f063F", + "txn": "0x42f4c7e23ac5286af577c46cd5da0b8d9044a81b8ac60ebbab1a5a7c8beb39ae", + "blockNumber": 8458078 + }, + "PoolHedger": { + "contractName": "PoolHedger", + "source": "PoolHedger", + "address": "0x8337C76bbCcd65d7DD566c50e01252f6122d9EC9", + "txn": "0xd8bc3cda35f2ce230f5a6a1a736236959e2e8961f28596361abad168f6731069", + "blockNumber": 8458094 + }, + "KeeperHelper": { + "contractName": "KeeperHelper", + "source": "KeeperHelper", + "address": "0x2Bb55956F859Fe12AcB73F14975b9c86a6C08666", + "txn": "0xdc4bab21bd2502b3fbfc7c3eccb31f0b6cae73769ac80549a105ad1ddd1fe5ee", + "blockNumber": 8458107 + }, + "GWAVOracle": { + "contractName": "GWAVOracle", + "source": "GWAVOracle", + "address": "0x0C259dd157A8224a480e1e7d783Fa25e45bD3d01", + "txn": "0xeed884334d62324aad8c38c48dbc0e9c2648e89d36d59b71eaf69f70fdebf18c", + "blockNumber": 8458120 + } + } + } + } +}