Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic ability to write options #2

Merged
merged 2 commits into from
Mar 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 69 additions & 33 deletions src/OptionSettlement.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.11;
import "base64/Base64.sol";
import "solmate/tokens/ERC20.sol";
import "solmate/tokens/ERC1155.sol";
import "solmate/utils/SafeTransferLib.sol";

/**
Valorem Options V1 is a DeFi money lego enabling writing covered call and covered put, physically settled, options.
Expand Down Expand Up @@ -59,6 +60,13 @@ contract OptionSettlementEngine is ERC1155 {

// TODO(Events for subgraph)

// TODO(Do we need to track internal balances)

uint8 public immutable feeBps = 5;

// TODO(Implement setters for this and a real address)
address public feeTo = 0x36273803306a3C22bc848f8Db761e974697ece0d;

// To increment the next available token id
uint256 private _nextTokenId;

Expand All @@ -69,6 +77,7 @@ contract OptionSettlementEngine is ERC1155 {
// Accessor for Option contract details
mapping(uint256 => Option) public option;

// TODO(Should this be a public uint256 lookup of the token id if exists?)
// This is used to check if an Option chain already exists
mapping(bytes32 => bool) private chainMap;

Expand All @@ -84,45 +93,18 @@ contract OptionSettlementEngine is ERC1155 {
returns (string memory)
{
require(tokenType[tokenId] != Type.None, "Token does not exist");
// https://eips.ethereum.org/EIPS/eip-1155#metadata
// Return base64 encoded json blob with metadata for rendering on the frontend
//{
// "title": "Token Metadata",
//"type": "object",
//"properties": {
//"name": {
//"type": "string",
//"description": "Identifies the asset to which this token represents"
//},
//"decimals": {
//"type": "integer",
//"description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation."
//},
//"description": {
//"type": "string",
//"description": "Describes the asset to which this token represents"
//},
//"image": {
//"type": "string",
//"description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
//},
//"properties": {
//"type": "object",
//"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
//}
//}
// }
// TODO(Implement metadata/uri builder)
string memory json = Base64.encode(
bytes(string(abi.encodePacked("{}")))
);
return string(abi.encodePacked("data:application/json;base64,", json));
}

// TODO(random number should be generated from vrf)
function newOptionsChain(Option memory optionInfo)
external
returns (uint256 tokenId)
{
// TODO(Transfer the link fee here)
// Check that a duplicate chain doesn't exist, and if it does, revert
bytes32 chainKey = keccak256(abi.encode(optionInfo));
require(chainMap[chainKey] == false, "This option chain exists");
Expand All @@ -146,6 +128,9 @@ contract OptionSettlementEngine is ERC1155 {
// Zero out random number for gas savings and to await randomness
optionInfo.settlementSeed = 0;

// TODO(random number should be generated from vrf and stored to settlementSeed here)
optionInfo.settlementSeed = 42;

// Create option token and increment
tokenType[_nextTokenId] = Type.Option;

Expand All @@ -170,16 +155,67 @@ contract OptionSettlementEngine is ERC1155 {
tokenId = _nextTokenId;

// Increment the next token id to be used
_nextTokenId += 1;
++_nextTokenId;
chainMap[chainKey] = true;
}

function writeOption(uint256 tokenId) external view {
function writeOptions(uint256 tokenId, uint256 amount) external {
require(tokenType[tokenId] == Type.Option, "Token is not an option");
require(
option[tokenId].settlementSeed != 0,
"Settlement seed not populated"
);

Option storage optionRecord = option[tokenId];

uint256 tx_amount = amount * optionRecord.underlyingAmount;

// Transfer the requisite underlying asset
SafeTransferLib.safeTransferFrom(
ERC20(optionRecord.underlyingAsset),
msg.sender,
address(this),
tx_amount
);

// TODO(Consider an internal balance counter here and aggregating these in a fee sweep)
// TODO(Ensure rounding down or precise math here)
// Transfer fee to writer
SafeTransferLib.safeTransfer(
ERC20(optionRecord.underlyingAsset),
feeTo,
((tx_amount / 10000) * feeBps)
);
// TODO(Do we need any other internal balance counters?)

uint256 claimTokenId = _nextTokenId;

// Mint the options contracts and claim token
uint256[] memory tokens = new uint256[](2);
tokens[0] = tokenId;
tokens[1] = claimTokenId;

uint256[] memory amounts = new uint256[](2);
amounts[0] = amount;
amounts[1] = 1;

bytes memory data = new bytes(0);

// Send tokens to writer
_batchMint(msg.sender, tokens, amounts, data);

// Store info about the claim
tokenType[claimTokenId] = Type.Claim;
claim[claimTokenId] = Claim({
option: tokenId,
amountWritten: amount,
amountExercised: 0,
claimed: false
});

// TODO(Transfer the requisite underlying asset)
// TODO(Create and transfer the claim token)
// TODO(Emit event about the writing)
// Increment the next token ID
++_nextTokenId;
}

// TODO(Exercise option)
Expand Down
63 changes: 62 additions & 1 deletion src/test/OptionSettlement.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,39 @@ import "../interfaces/IERC20.sol";
import "../interfaces/IWETH.sol";
import "../OptionSettlement.sol";

contract OptionSettlementTest is DSTest {
/// @notice Receiver hook utility for NFT 'safe' transfers
abstract contract NFTreceiver {
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external pure returns (bytes4) {
return 0x150b7a02;
}

function onERC1155Received(
address,
address,
uint256,
uint256,
bytes calldata
) external pure returns (bytes4) {
return 0xf23a6e61;
}

function onERC1155BatchReceived(
address,
address,
uint256[] calldata,
uint256[] calldata,
bytes calldata
) external pure returns (bytes4) {
return 0xbc197c81;
}
}

contract OptionSettlementTest is DSTest, NFTreceiver {
// These are just happy path functional tests ATM
// TODO(Fuzzing)
// TODO(correctness)
Expand All @@ -17,6 +49,21 @@ contract OptionSettlementTest is DSTest {
IERC20 public dai;
OptionSettlementEngine public engine;

using stdStorage for StdStorage;
StdStorage stdstore;

function writeTokenBalance(
address who,
address token,
uint256 amt
) internal {
stdstore
.target(token)
.sig(IERC20(token).balanceOf.selector)
.with_key(who)
.checked_write(amt);
}

function setUp() public {
// Setup WETH
weth = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
Expand All @@ -34,6 +81,10 @@ contract OptionSettlementTest is DSTest {
expiryTimestamp: (uint64(block.timestamp) + 604800)
});
engine.newOptionsChain(info);
// Now we have 1B DAI
writeTokenBalance(address(this), address(dai), 1000000000 * 1e18);
// And 10 M WETH
writeTokenBalance(address(this), address(weth), 10000000 * 1e18);
}

function testNewOptionsChain(uint256 settlementSeed) public {
Expand Down Expand Up @@ -70,4 +121,14 @@ contract OptionSettlementTest is DSTest {
function testFailUri() public view {
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));
// 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);
}
}