From ec14c12112a645475e08bbebca737760dcd57a3b Mon Sep 17 00:00:00 2001 From: Alcibiades <89996683+0xAlcibiades@users.noreply.github.com> Date: Sun, 20 Mar 2022 20:04:47 -0400 Subject: [PATCH] Ability to exercise option (#3) --- src/OptionSettlement.sol | 94 +++++++++++++++++++++++++-------- src/test/OptionSettlement.t.sol | 32 +++++++---- 2 files changed, 95 insertions(+), 31 deletions(-) diff --git a/src/OptionSettlement.sol b/src/OptionSettlement.sol index 2c97944..bf01802 100644 --- a/src/OptionSettlement.sol +++ b/src/OptionSettlement.sol @@ -22,8 +22,10 @@ enum Type { Claim } +// TODO(Right now fee is taken on top of written amount and exercised amount, should the model be different?) // TODO(Consider converting require strings to errors) // TODO(An enum here indicating if the option is a put or a call would be redundant, but maybe useful?) +// TODO(Consider rebase tokens, fee on transfer tokens, or other tokens which may break assumptions) struct Option { // The underlying asset to be received @@ -100,7 +102,7 @@ contract OptionSettlementEngine is ERC1155 { return string(abi.encodePacked("data:application/json;base64,", json)); } - function newOptionsChain(Option memory optionInfo) + function newChain(Option memory optionInfo) external returns (uint256 tokenId) { @@ -135,16 +137,16 @@ contract OptionSettlementEngine is ERC1155 { tokenType[_nextTokenId] = Type.Option; // Check that both tokens are ERC20 by instantiating them and checking supply - ERC20 underlying = ERC20(optionInfo.underlyingAsset); - ERC20 exercise = ERC20(optionInfo.exerciseAsset); + ERC20 underlyingToken = ERC20(optionInfo.underlyingAsset); + ERC20 exerciseToken = ERC20(optionInfo.exerciseAsset); // Check total supplies and ensure the option will be exercisable require( - underlying.totalSupply() >= optionInfo.underlyingAmount, + underlyingToken.totalSupply() >= optionInfo.underlyingAmount, "Invalid Supply" ); require( - exercise.totalSupply() >= optionInfo.exerciseAmount, + exerciseToken.totalSupply() >= optionInfo.exerciseAmount, "Invalid Supply" ); @@ -159,23 +161,24 @@ contract OptionSettlementEngine is ERC1155 { chainMap[chainKey] = true; } - function writeOptions(uint256 tokenId, uint256 amount) external { - require(tokenType[tokenId] == Type.Option, "Token is not an option"); + function write(uint256 optionId, uint256 amount) external { + require(tokenType[optionId] == Type.Option, "Token is not an option"); require( - option[tokenId].settlementSeed != 0, + option[optionId].settlementSeed != 0, "Settlement seed not populated" ); - Option storage optionRecord = option[tokenId]; + Option storage optionRecord = option[optionId]; - uint256 tx_amount = amount * optionRecord.underlyingAmount; + uint256 rx_amount = amount * optionRecord.underlyingAmount; + uint256 fee = ((rx_amount / 10000) * feeBps); // Transfer the requisite underlying asset SafeTransferLib.safeTransferFrom( ERC20(optionRecord.underlyingAsset), msg.sender, address(this), - tx_amount + (rx_amount + fee) ); // TODO(Consider an internal balance counter here and aggregating these in a fee sweep) @@ -184,16 +187,16 @@ contract OptionSettlementEngine is ERC1155 { SafeTransferLib.safeTransfer( ERC20(optionRecord.underlyingAsset), feeTo, - ((tx_amount / 10000) * feeBps) + fee ); // TODO(Do we need any other internal balance counters?) - uint256 claimTokenId = _nextTokenId; + uint256 claimId = _nextTokenId; // Mint the options contracts and claim token uint256[] memory tokens = new uint256[](2); - tokens[0] = tokenId; - tokens[1] = claimTokenId; + tokens[0] = optionId; + tokens[1] = claimId; uint256[] memory amounts = new uint256[](2); amounts[0] = amount; @@ -205,9 +208,9 @@ contract OptionSettlementEngine is ERC1155 { _batchMint(msg.sender, tokens, amounts, data); // Store info about the claim - tokenType[claimTokenId] = Type.Claim; - claim[claimTokenId] = Claim({ - option: tokenId, + tokenType[claimId] = Type.Claim; + claim[claimId] = Claim({ + option: optionId, amountWritten: amount, amountExercised: 0, claimed: false @@ -218,11 +221,58 @@ contract OptionSettlementEngine is ERC1155 { ++_nextTokenId; } - // TODO(Exercise option) + function exercise(uint256 optionId, uint256 amount) external { + require(tokenType[optionId] == Type.Option, "Token is not an option"); + + Option storage optionRecord = option[optionId]; - // TODO(Redeem claim) + // Require that we have reached the exercise timestamp - // TODO(Get info about options contract) + require( + optionRecord.exerciseTimestamp <= block.timestamp, + "Too early to exercise" + ); + uint256 rx_amount = optionRecord.exerciseAmount * amount; + uint256 tx_amount = optionRecord.underlyingAmount * amount; + uint256 fee = ((rx_amount / 10000) * feeBps); + + // Transfer in the requisite exercise asset + SafeTransferLib.safeTransferFrom( + ERC20(optionRecord.exerciseAsset), + msg.sender, + address(this), + (rx_amount + fee) + ); + + // TODO(Consider aggregating this) + // Transfer out protocol fee + SafeTransferLib.safeTransfer( + ERC20(optionRecord.exerciseAsset), + feeTo, + fee + ); - // TODO(Get info about a claim) + // Transfer out the underlying + SafeTransferLib.safeTransfer( + ERC20(optionRecord.underlyingAsset), + msg.sender, + tx_amount + ); + + // TODO(Exercise assignment and claims update) + _burn(msg.sender, optionId, amount); + // TODO(Emit events for indexing and frontend) + } + + function redeem(uint256 claimId) external view { + require(tokenType[claimId] == Type.Claim, "Token is not an claim"); + // TODO(Implement) + } + + function underlying(uint256 tokenId) external view { + require(tokenType[tokenId] != Type.None, "Token does not exist"); + // TODO(Get info about underlying assets) + // TODO(Get info about options contract) + // TODO(Get info about a claim) + } } diff --git a/src/test/OptionSettlement.t.sol b/src/test/OptionSettlement.t.sol index 64fa589..f820987 100644 --- a/src/test/OptionSettlement.t.sol +++ b/src/test/OptionSettlement.t.sol @@ -80,14 +80,17 @@ contract OptionSettlementTest is DSTest, NFTreceiver { exerciseTimestamp: uint64(block.timestamp), expiryTimestamp: (uint64(block.timestamp) + 604800) }); - engine.newOptionsChain(info); + engine.newChain(info); // Now we have 1B DAI writeTokenBalance(address(this), address(dai), 1000000000 * 1e18); // And 10 M WETH writeTokenBalance(address(this), address(weth), 10000000 * 1e18); + // Issue approvals + IERC20(weth).approve(address(engine), type(uint256).max); + IERC20(dai).approve(address(engine), type(uint256).max); } - function testNewOptionsChain(uint256 settlementSeed) public { + function testNewChain(uint256 settlementSeed) public { Option memory info = Option({ underlyingAsset: address(weth), exerciseAsset: address(dai), @@ -97,10 +100,10 @@ contract OptionSettlementTest is DSTest, NFTreceiver { exerciseTimestamp: uint64(block.timestamp), expiryTimestamp: (uint64(block.timestamp) + 604800) }); - engine.newOptionsChain(info); + engine.newChain(info); } - function testFailDuplicateOptionsChain() public { + function testFailDuplicateChain() public { // This should fail to create the second and duplicate options chain Option memory info = Option({ underlyingAsset: address(weth), @@ -111,7 +114,7 @@ contract OptionSettlementTest is DSTest, NFTreceiver { exerciseTimestamp: uint64(block.timestamp), expiryTimestamp: (uint64(block.timestamp) + 604800) }); - engine.newOptionsChain(info); + engine.newChain(info); } function testUri() public view { @@ -122,13 +125,24 @@ contract OptionSettlementTest is DSTest, NFTreceiver { engine.uri(1); } - // TODO(Why is gasreport not working on this function) - function testWriteOptions(uint16 amountToWrite) public { - IERC20(weth).approve(address(engine), type(uint256).max); - engine.writeOptions(0, uint256(amountToWrite)); + // TODO(Why is gas report not working on this function) + function testWrite(uint16 amountToWrite) public { + engine.write(0, uint256(amountToWrite)); + // Assert that we have the contracts + assert(engine.balanceOf(address(this), 0) == amountToWrite); + // Assert that we have the claim + assert(engine.balanceOf(address(this), 1) == 1); + } + + function testExercise(uint16 amountToWrite) public { + engine.write(0, uint256(amountToWrite)); // Assert that we have the contracts assert(engine.balanceOf(address(this), 0) == amountToWrite); // Assert that we have the claim assert(engine.balanceOf(address(this), 1) == 1); + uint256 bal = IERC20(weth).balanceOf(address(this)); + engine.exercise(0, amountToWrite); + uint256 newBal = IERC20(weth).balanceOf(address(this)); + assert(newBal == (bal + (1 ether * uint256(amountToWrite)))); } }