diff --git a/.solcover.js b/.solcover.js index 9d80c88..04e51b9 100644 --- a/.solcover.js +++ b/.solcover.js @@ -21,6 +21,7 @@ module.exports = { 'packages/Ownable.sol', 'packages/Roles.sol', 'packages/SafeMath.sol', + 'packages/WETH.sol', 'mocks/MockOracle.sol', 'mocks/MockERC20.sol', 'mocks/MockCtoken.sol', diff --git a/contracts/Exerciser.sol b/contracts/Exerciser.sol new file mode 100644 index 0000000..d762ae3 --- /dev/null +++ b/contracts/Exerciser.sol @@ -0,0 +1,46 @@ +pragma solidity 0.5.10; + +import "./interfaces/OtokenInterface.sol"; +import "./interfaces/WethInterface.sol"; +import "./packages/IERC20.sol"; + + +contract Exerciser { + WethInterface public weth; + + event WrapperExercise( + address indexed otoken, + uint256 indexed otokenAmount, + uint256 indexed collateralExercised, + address user + ); + + constructor(address payable _weth) public { + weth = WethInterface(_weth); + } + + function exercise( + address _otoken, + uint256 _amount, + address payable[] calldata _owners + ) external payable returns (uint256) { + // 1. pull token from user's address + OtokenInterface otoken = OtokenInterface(_otoken); + otoken.transferFrom(msg.sender, address(this), _amount); + // 2. convert eth to weth + weth.deposit.value(msg.value)(); + // 3. exercise + weth.approve(_otoken, msg.value); + otoken.exercise(_amount, _owners); + // 4. transfer collateral to user + address collateral = otoken.collateral(); + uint256 amountToTakeOut = IERC20(collateral).balanceOf(address(this)); + IERC20(collateral).transfer(msg.sender, amountToTakeOut); + // 5. transfer remaining weth back to user + if (weth.balanceOf(address(this)) > 0) { + weth.transfer(msg.sender, weth.balanceOf(address(this))); + } + + emit WrapperExercise(_otoken, _amount, amountToTakeOut, msg.sender); + } +} diff --git a/contracts/interfaces/OtokenInterface.sol b/contracts/interfaces/OtokenInterface.sol new file mode 100644 index 0000000..8128557 --- /dev/null +++ b/contracts/interfaces/OtokenInterface.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.5.10; + + +interface OtokenInterface { + function exercise( + uint256 oTokensToExercise, + address payable[] calldata vaultsToExerciseFrom + ) external payable; + + function collateral() external view returns (address); + + function transferFrom( + address sender, + address recipient, + uint256 amount + ) external returns (bool); +} diff --git a/contracts/interfaces/WethInterface.sol b/contracts/interfaces/WethInterface.sol new file mode 100644 index 0000000..6335f3a --- /dev/null +++ b/contracts/interfaces/WethInterface.sol @@ -0,0 +1,14 @@ +pragma solidity ^0.5.10; + + +interface WethInterface { + function deposit() external payable; + + function approve(address sender, uint256 amount) external; + + function balanceOf(address account) external view returns (uint256); + + function transfer(address recipient, uint256 amount) + external + returns (bool); +} diff --git a/contracts/packages/WETH.sol b/contracts/packages/WETH.sol new file mode 100644 index 0000000..ac5e28b --- /dev/null +++ b/contracts/packages/WETH.sol @@ -0,0 +1,125 @@ +// Copyright (C) 2015, 2016, 2017 Dapphub + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// SPDX-License-Identifier: GNU GPL +pragma solidity 0.5.10; + + +/** + * @author Opyn Team + * @title WETH contract + * @dev A wrapper to use ETH as collateral + */ +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + /// @notice emmitted when a sender approve WETH transfer + event Approval(address indexed src, address indexed guy, uint256 wad); + /// @notice emmitted when a sender transfer WETH + event Transfer(address indexed src, address indexed dst, uint256 wad); + /// @notice emitted when a sender deposit ETH into this contract + event Deposit(address indexed dst, uint256 wad); + /// @notice emmited when a sender withdraw ETH from this contract + event Withdrawal(address indexed src, uint256 wad); + + /// @notice mapping between address and WETH balance + mapping(address => uint256) public balanceOf; + /// @notice mapping between addresses and allowance amount + mapping(address => mapping(address => uint256)) public allowance; + + /** + * @notice Wrap deposited ETH into WETH + */ + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice withdraw ETH from contract + * @dev Unwrap from WETH to ETH + * @param _wad amount WETH to unwrap and withdraw + */ + function withdraw(uint256 _wad) public { + require( + balanceOf[msg.sender] >= _wad, + "WETH9: insufficient sender balance" + ); + balanceOf[msg.sender] -= _wad; + msg.sender.transfer(_wad); + emit Withdrawal(msg.sender, _wad); + } + + /** + * @notice get ETH total supply + * @return total supply + */ + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + /** + * @notice approve transfer + * @param _guy address to approve + * @param _wad amount of WETH + * @return true if tx succeeded + */ + function approve(address _guy, uint256 _wad) public returns (bool) { + allowance[msg.sender][_guy] = _wad; + emit Approval(msg.sender, _guy, _wad); + return true; + } + + /** + * @notice transfer WETH + * @param _dst destination address + * @param _wad amount to transfer + * @return true if tx succeeded + */ + function transfer(address _dst, uint256 _wad) public returns (bool) { + return transferFrom(msg.sender, _dst, _wad); + } + + /** + * @notice transfer from address + * @param _src source address + * @param _dst destination address + * @param _wad amount to transfer + * @return true if tx succeeded + */ + function transferFrom( + address _src, + address _dst, + uint256 _wad + ) public returns (bool) { + require(balanceOf[_src] >= _wad, "WETH9: insufficient source balance"); + + if (_src != msg.sender && allowance[_src][msg.sender] != uint256(-1)) { + require( + allowance[_src][msg.sender] >= _wad, + "WETH9: invalid allowance" + ); + allowance[_src][msg.sender] -= _wad; + } + + balanceOf[_src] -= _wad; + balanceOf[_dst] += _wad; + + emit Transfer(_src, _dst, _wad); + + return true; + } +} diff --git a/test/series/weth-put.test.ts b/test/series/weth-put.test.ts index f73dbc9..513f8ae 100644 --- a/test/series/weth-put.test.ts +++ b/test/series/weth-put.test.ts @@ -1,7 +1,9 @@ import { OptionsFactoryInstance, OTokenInstance, - Erc20MintableInstance + Erc20MintableInstance, + ExerciserInstance, + Weth9Instance } from '../../build/types/truffle-types'; import BigNumber from 'bignumber.js'; @@ -11,11 +13,10 @@ const OTokenContract = artifacts.require('oToken'); const OptionsFactory = artifacts.require('OptionsFactory'); const MockERC20 = artifacts.require('MockERC20'); -import Reverter from '../utils/reverter'; +const Exerciser = artifacts.require('Exerciser'); +const WETH = artifacts.require('WETH9'); contract('OptionsContract: weth put', accounts => { - const reverter = new Reverter(web3); - const creatorAddress = accounts[0]; const firstOwner = accounts[1]; const tokenHolder = accounts[2]; @@ -23,11 +24,13 @@ contract('OptionsContract: weth put', accounts => { let optionsFactory: OptionsFactoryInstance; let oWeth: OTokenInstance; // let oracle: MockwethoundOracleInstance; - let weth: Erc20MintableInstance; + + let exerciser: ExerciserInstance; + + let weth: Weth9Instance; let usdc: Erc20MintableInstance; - const usdcAmount = '1000000000'; // 1000 USDC - const wethAmount = '1000000000000000000000'; // 1000 weth + const usdcAmount = '1000000000'; // 10000 USDC const _name = 'TH put 250'; const _symbol = 'oEth 250'; @@ -39,9 +42,8 @@ contract('OptionsContract: weth put', accounts => { // 1. Deploy mock contracts // 1.2 Mock weth contract - weth = await MockERC20.new('weth', 'weth', 18); - await weth.mint(creatorAddress, wethAmount); // 1000 weth - await weth.mint(tokenHolder, wethAmount); + weth = await WETH.new(); + exerciser = await Exerciser.new(weth.address); // 1.3 Mock USDC contract usdc = await MockERC20.new('USDC', 'USDC', 6); @@ -71,16 +73,10 @@ contract('OptionsContract: weth put', accounts => { const optionsContractAddr = optionsContractResult.logs[1].args[0]; oWeth = await OTokenContract.at(optionsContractAddr); - - await reverter.snapshot(); }); describe('New option parameter test', () => { it('should have basic setting', async () => { - await oWeth.setDetails(_name, _symbol, { - from: creatorAddress - }); - assert.equal(await oWeth.name(), String(_name), 'set name error'); assert.equal(await oWeth.symbol(), String(_symbol), 'set symbol error'); }); @@ -141,34 +137,37 @@ contract('OptionsContract: weth put', accounts => { assert.equal(vault[2].toString(), '0'); }); - it('should not exercise without underlying allowance', async () => { - await oWeth.transfer(tokenHolder, '4000000', {from: firstOwner}); // transfer 80 oWeth - - await expectRevert( - oWeth.exercise('4000000', [firstOwner], { - from: tokenHolder - }), - 'transfer amount exceeds allowance.' - ); - }); - - it('should be able to exercise', async () => { + it('should be able to exercise from wrapper exerciser ', async () => { const amountToExercise = '4000000'; + await oWeth.transfer(tokenHolder, amountToExercise, {from: firstOwner}); + // weth const underlyingRequired = ( await oWeth.underlyingRequiredToExercise(amountToExercise) ).toString(); - await weth.approve(oWeth.address, underlyingRequired, { + // approve exerciser to spend otoken + await oWeth.approve(exerciser.address, amountToExercise, { from: tokenHolder }); - const exerciseTx = await oWeth.exercise(amountToExercise, [firstOwner], { - from: tokenHolder - }); + const usdcBefore = (await usdc.balanceOf(oWeth.address)).toString(); + const wethBefore = (await weth.balanceOf(oWeth.address)).toString(); + + const exerciseTx = await exerciser.exercise( + oWeth.address, + amountToExercise, + [firstOwner], + { + value: underlyingRequired, + from: tokenHolder + } + ); - expectEvent(exerciseTx, 'Exercise', { - amtUnderlyingToPay: underlyingRequired, - amtCollateralToPay: '1000000000' + expectEvent(exerciseTx, 'WrapperExercise', { + otoken: oWeth.address, + otokenAmount: amountToExercise, + collateralExercised: '1000000000', + user: tokenHolder }); // test that the vault's balances have been updated. @@ -176,6 +175,21 @@ contract('OptionsContract: weth put', accounts => { assert.equal(vault[0].toString(), '0'); assert.equal(vault[1].toString(), '0'); assert.equal(vault[2].toString(), underlyingRequired); + + const usdcAfter = (await usdc.balanceOf(oWeth.address)).toString(); + const wethAfter = (await weth.balanceOf(oWeth.address)).toString(); + + assert.equal( + new BigNumber(usdcBefore).minus(new BigNumber('1000000000')).toString(), + new BigNumber(usdcAfter).toString() + ); + + assert.equal( + new BigNumber(wethBefore) + .plus(new BigNumber(underlyingRequired)) + .toString(), + new BigNumber(wethAfter).toString() + ); }); }); });