From 4fa97b56de08197a742f18ad326edb2457fd2fb9 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Fri, 25 Mar 2022 19:01:35 -0400 Subject: [PATCH 1/4] add snapshot --- .gas-snapshot | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .gas-snapshot diff --git a/.gas-snapshot b/.gas-snapshot new file mode 100644 index 0000000..3c8950a --- /dev/null +++ b/.gas-snapshot @@ -0,0 +1,12 @@ +OptionSettlementTest:testExercise() (gas: 190808) +OptionSettlementTest:testFailDuplicateChain() (gas: 13549) +OptionSettlementTest:testFailExercise(uint256,uint256) (runs: 256, μ: 69954, ~: 23037) +OptionSettlementTest:testFailUri() (gas: 7715) +OptionSettlementTest:testFuzzExercise(uint256,uint256) (runs: 256, μ: 216523, ~: 224859) +OptionSettlementTest:testFuzzNewChain(uint256,uint256,uint256,uint64,uint64) (runs: 256, μ: 196582, ~: 203112) +OptionSettlementTest:testFuzzRedeem(uint256) (runs: 256, μ: 173605, ~: 173605) +OptionSettlementTest:testFuzzWrite(uint256) (runs: 256, μ: 178276, ~: 178276) +OptionSettlementTest:testNewChain() (gas: 192312) +OptionSettlementTest:testRedeem() (gas: 158836) +OptionSettlementTest:testUri() (gas: 10303) +OptionSettlementTest:testWrite() (gas: 174411) From bbcc83e8195b0f89fe7fbc487a6ceed0d97792b8 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Fri, 25 Mar 2022 19:28:07 -0400 Subject: [PATCH 2/4] optimize storage layout --- .gas-snapshot | 24 +++++------ src/OptionSettlement.sol | 22 +++++------ src/test/OptionSettlement.t.sol | 70 ++++++++++++++++----------------- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 3c8950a..bd4971c 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,12 +1,12 @@ -OptionSettlementTest:testExercise() (gas: 190808) -OptionSettlementTest:testFailDuplicateChain() (gas: 13549) -OptionSettlementTest:testFailExercise(uint256,uint256) (runs: 256, μ: 69954, ~: 23037) -OptionSettlementTest:testFailUri() (gas: 7715) -OptionSettlementTest:testFuzzExercise(uint256,uint256) (runs: 256, μ: 216523, ~: 224859) -OptionSettlementTest:testFuzzNewChain(uint256,uint256,uint256,uint64,uint64) (runs: 256, μ: 196582, ~: 203112) -OptionSettlementTest:testFuzzRedeem(uint256) (runs: 256, μ: 173605, ~: 173605) -OptionSettlementTest:testFuzzWrite(uint256) (runs: 256, μ: 178276, ~: 178276) -OptionSettlementTest:testNewChain() (gas: 192312) -OptionSettlementTest:testRedeem() (gas: 158836) -OptionSettlementTest:testUri() (gas: 10303) -OptionSettlementTest:testWrite() (gas: 174411) +OptionSettlementTest:testExercise() (gas: 182892) +OptionSettlementTest:testFailDuplicateChain() (gas: 13642) +OptionSettlementTest:testFailExercise(uint112,uint112) (runs: 256, μ: 88228, ~: 111289) +OptionSettlementTest:testFailUri() (gas: 7693) +OptionSettlementTest:testFuzzExercise(uint112,uint112) (runs: 256, μ: 210415, ~: 217421) +OptionSettlementTest:testFuzzNewChain(uint160,uint96,uint96,uint40,uint40) (runs: 256, μ: 159428, ~: 159428) +OptionSettlementTest:testFuzzRedeem(uint112) (runs: 256, μ: 146653, ~: 146655) +OptionSettlementTest:testFuzzWrite(uint112) (runs: 256, μ: 174449, ~: 174449) +OptionSettlementTest:testNewChain() (gas: 148239) +OptionSettlementTest:testRedeem() (gas: 134716) +OptionSettlementTest:testUri() (gas: 10245) +OptionSettlementTest:testWrite() (gas: 170342) diff --git a/src/OptionSettlement.sol b/src/OptionSettlement.sol index 8ff63a6..2bc6cf1 100644 --- a/src/OptionSettlement.sol +++ b/src/OptionSettlement.sol @@ -31,17 +31,17 @@ struct Option { // The underlying asset to be received address underlyingAsset; // The timestamp after which this option may be exercised - uint64 exerciseTimestamp; + uint40 exerciseTimestamp; + // The timestamp before which this option must be exercised + uint40 expiryTimestamp; // The address of the asset needed for exercise address exerciseAsset; - // The timestamp before which this option must be exercised - uint64 expiryTimestamp; - // Random seed created at the time of option chain creation - uint256 settlementSeed; // The amount of the underlying asset contained within an option contract of this type - uint256 underlyingAmount; + uint96 underlyingAmount; + // Random seed created at the time of option chain creation + uint160 settlementSeed; // The amount of the exercise asset required to exercise this option - uint256 exerciseAmount; + uint96 exerciseAmount; } struct Claim { @@ -49,9 +49,9 @@ struct Claim { uint256 option; // These are 1:1 contracts with the underlying Option struct // The number of contracts written in this claim - uint256 amountWritten; + uint112 amountWritten; // The amount of contracts assigned for exercise to this claim - uint256 amountExercised; + uint112 amountExercised; // The two amounts above along with the option info, can be used to calculate the underlying assets bool claimed; } @@ -161,7 +161,7 @@ contract OptionSettlementEngine is ERC1155 { chainMap[chainKey] = true; } - function write(uint256 optionId, uint256 amount) external { + function write(uint256 optionId, uint112 amount) external { require(tokenType[optionId] == Type.Option, "Token is not an option"); require( option[optionId].settlementSeed != 0, @@ -199,7 +199,7 @@ contract OptionSettlementEngine is ERC1155 { tokens[1] = claimId; uint256[] memory amounts = new uint256[](2); - amounts[0] = amount; + amounts[0] = uint256(amount); amounts[1] = 1; bytes memory data = new bytes(0); diff --git a/src/test/OptionSettlement.t.sol b/src/test/OptionSettlement.t.sol index be60013..e56e06c 100644 --- a/src/test/OptionSettlement.t.sol +++ b/src/test/OptionSettlement.t.sol @@ -76,11 +76,11 @@ contract OptionSettlementTest is DSTest, NFTreceiver { Option memory info = Option({ underlyingAsset: address(weth), exerciseAsset: address(dai), - settlementSeed: 1, - underlyingAmount: 1 ether, - exerciseAmount: 3000 ether, - exerciseTimestamp: uint64(block.timestamp), - expiryTimestamp: (uint64(block.timestamp) + 604800) + settlementSeed: uint160(1), + underlyingAmount: uint96(1 ether), + exerciseAmount: uint96(3000 ether), + exerciseTimestamp: uint40(block.timestamp), + expiryTimestamp: (uint40(block.timestamp) + 604800) }); engine.newChain(info); @@ -106,31 +106,31 @@ contract OptionSettlementTest is DSTest, NFTreceiver { Option memory info = Option({ underlyingAsset: address(weth), exerciseAsset: address(dai), - settlementSeed: 0, - underlyingAmount: 1 ether, - exerciseAmount: 3100 ether, - exerciseTimestamp: uint64(block.timestamp), - expiryTimestamp: (uint64(block.timestamp) + 604800) + settlementSeed: uint160(1), + underlyingAmount: uint96(1 ether), + exerciseAmount: uint96(3100 ether), + exerciseTimestamp: uint40(block.timestamp), + expiryTimestamp: (uint40(block.timestamp) + 604800) }); uint256 tokenId = engine.newChain(info); ( , - uint64 testExerciseTimestamp, + uint40 testExerciseTimestamp, + uint40 testExpiryTimestamp, , - uint64 testExpiryTimestamp, - uint256 testSettlementSeed, - uint256 testUnderlyingAmount, - uint256 testExerciseAmount + uint96 testUnderlyingAmount, + uint160 testSettlementSeed, + uint96 testExerciseAmount ) = engine.option(nextTokenId); assertTrue(engine.chainMap(keccak256(abi.encode(info)))); assertEq(engine.nextTokenId(), nextTokenId + 1); assertEq(tokenId, engine.nextTokenId() - 1); - assertEq(testExerciseTimestamp, uint64(block.timestamp)); - assertEq(testExpiryTimestamp, (uint64(block.timestamp) + 604800)); + assertEq(testExerciseTimestamp, uint40(block.timestamp)); + assertEq(testExpiryTimestamp, (uint40(block.timestamp) + 604800)); assertEq(testUnderlyingAmount, 1 ether); assertEq(testExerciseAmount, 3100 ether); assertEq(testSettlementSeed, 42); @@ -140,19 +140,19 @@ contract OptionSettlementTest is DSTest, NFTreceiver { } function testFuzzNewChain( - uint256 settlementSeed, - uint256 underlyingAmount, - uint256 exerciseAmount, - uint64 exerciseTimestamp, - uint64 expiryTimestamp + uint160 settlementSeed, + uint96 underlyingAmount, + uint96 exerciseAmount, + uint40 exerciseTimestamp, + uint40 expiryTimestamp ) public { uint256 nextTokenId = engine.nextTokenId(); VM.assume(expiryTimestamp >= block.timestamp + 86400); VM.assume(exerciseTimestamp >= block.timestamp); VM.assume(exerciseTimestamp <= expiryTimestamp - 86400); - VM.assume(expiryTimestamp <= type(uint64).max); - VM.assume(exerciseTimestamp <= type(uint64).max); + VM.assume(expiryTimestamp <= type(uint40).max); + VM.assume(exerciseTimestamp <= type(uint40).max); VM.assume(underlyingAmount <= wethTotalSupply); VM.assume(exerciseAmount <= daiTotalSupply); VM.assume(type(uint256).max - underlyingAmount >= wethTotalSupply); @@ -172,12 +172,12 @@ contract OptionSettlementTest is DSTest, NFTreceiver { ( , - uint64 testExerciseTimestamp, + uint40 testExerciseTimestamp, + uint40 testExpiryTimestamp, , - uint64 testExpiryTimestamp, - uint256 testSettlementSeed, - uint256 testUnderlyingAmount, - uint256 testExerciseAmount + uint96 testUnderlyingAmount, + uint160 testSettlementSeed, + uint96 testExerciseAmount ) = engine.option(nextTokenId); assertTrue(engine.chainMap(keccak256(abi.encode(info)))); @@ -202,8 +202,8 @@ contract OptionSettlementTest is DSTest, NFTreceiver { settlementSeed: 1, underlyingAmount: 1 ether, exerciseAmount: 3000 ether, - exerciseTimestamp: uint64(block.timestamp), - expiryTimestamp: (uint64(block.timestamp) + 604800) + exerciseTimestamp: uint40(block.timestamp), + expiryTimestamp: (uint40(block.timestamp) + 604800) }); engine.newChain(info); } @@ -259,7 +259,7 @@ contract OptionSettlementTest is DSTest, NFTreceiver { assertTrue(true); } - function testFuzzWrite(uint256 amountWrite) public { + function testFuzzWrite(uint112 amountWrite) public { uint256 nextTokenId = engine.nextTokenId(); uint256 wethBalanceEngine = IERC20(weth).balanceOf(address(engine)); uint256 wethFeeTo = IERC20(weth).balanceOf(address(engine.feeTo())); @@ -345,7 +345,7 @@ contract OptionSettlementTest is DSTest, NFTreceiver { assertEq(engine.balanceOf(address(this), 1), 1); } - function testFuzzExercise(uint256 amountWrite, uint256 amountExercise) + function testFuzzExercise(uint112 amountWrite, uint112 amountExercise) public { // TODO(add checks after updating exercise()) @@ -405,7 +405,7 @@ contract OptionSettlementTest is DSTest, NFTreceiver { assertEq(engine.balanceOf(address(this), 1), 1); } - function testFailExercise(uint256 amountWrite, uint256 amountExercise) + function testFailExercise(uint112 amountWrite, uint112 amountExercise) public { VM.assume(amountExercise > amountWrite); @@ -441,7 +441,7 @@ contract OptionSettlementTest is DSTest, NFTreceiver { if (engine.tokenType(1) == Type.None) assertTrue(true); } - function testFuzzRedeem(uint256 amountWrite) public { + function testFuzzRedeem(uint112 amountWrite) public { // TODO(add checks after updating exercise()) uint256 wethBalanceEngine = IERC20(weth).balanceOf(address(engine)); uint256 daiBalanceEngine = IERC20(dai).balanceOf(address(engine)); From 5d20193f927439adfabd3207a2894616b023e19a Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Fri, 25 Mar 2022 20:42:58 -0400 Subject: [PATCH 3/4] add first cut of assignment algorithm --- src/OptionSettlement.sol | 79 +++++++++++++++++++++++++++++++-- src/test/OptionSettlement.t.sol | 4 ++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/OptionSettlement.sol b/src/OptionSettlement.sol index 2bc6cf1..d718278 100644 --- a/src/OptionSettlement.sol +++ b/src/OptionSettlement.sol @@ -72,13 +72,15 @@ contract OptionSettlementEngine is ERC1155 { // To increment the next available token id uint256 public nextTokenId; - // TODO(Null values here should return None from the enum, or design needs to change.) // Is this an option or a claim? mapping(uint256 => Type) public tokenType; // Accessor for Option contract details mapping(uint256 => Option) public option; + // The list of claims for an option + mapping(uint256 => uint256[]) internal optionToClaims; + // 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) public chainMap; @@ -167,6 +169,7 @@ contract OptionSettlementEngine is ERC1155 { option[optionId].settlementSeed != 0, "Settlement seed not populated" ); + // TODO(We shouldn't be able to write an expired option) Option storage optionRecord = option[optionId]; @@ -215,13 +218,81 @@ contract OptionSettlementEngine is ERC1155 { amountExercised: 0, claimed: false }); + optionToClaims[optionId].push(claimId); // TODO(Emit event about the writing) // Increment the next token ID ++nextTokenId; } - function exercise(uint256 optionId, uint256 amount) external { + function assignExercise( + uint256 optionId, + uint112 amount, + uint160 settlementSeed + ) internal { + // TODO(Fuzz this in testing and flush out any bugs) + // Initial storage pointer + Claim storage claimRecord; + + // Number of claims enqueued for this option + uint256 claimsLen = optionToClaims[optionId].length; + + // TODO(Is this needed?) + require(claimsLen > 0, "No claims to assign."); + + // Counter for randomness + uint256 i; + + // To keep track of the slot to overwrite + uint256 overwrite; + + // Last index in the claims list + uint256 lastIndex; + + // While there are still options to exercise + while (amount > 0) { + // Get the claim number to assign + uint256 claimNum; + if (claimsLen == 1) { + lastIndex = 0; + claimNum = optionToClaims[optionId][lastIndex]; + } else { + lastIndex = settlementSeed % claimsLen; + claimNum = optionToClaims[optionId][lastIndex]; + } + + claimRecord = claim[claimNum]; + + uint112 amountWritten = claimRecord.amountWritten; + if (amountWritten <= amount) { + amount -= amountWritten; + claimRecord.amountExercised = amountWritten; + // We pop the end off and overwrite the old slot + uint256 newLen = claimsLen - 1; + if (newLen > 0) { + overwrite = optionToClaims[optionId][newLen]; + // Would be nice if I could pop onto the stack here + optionToClaims[optionId].pop(); + claimsLen = newLen; + optionToClaims[optionId][lastIndex] = overwrite; + } + } else { + claimRecord.amountExercised = amount; + amount = 0; + } + + // Increment for the next loop + settlementSeed = uint160( + uint256(keccak256(abi.encode(settlementSeed, i))) + ); + i++; + } + + // Update the settlement seed in storage for the next exercise. + option[optionId].settlementSeed = settlementSeed; + } + + function exercise(uint256 optionId, uint112 amount) external { require(tokenType[optionId] == Type.Option, "Token is not an option"); Option storage optionRecord = option[optionId]; @@ -259,7 +330,8 @@ contract OptionSettlementEngine is ERC1155 { tx_amount ); - // TODO(Exercise assignment and claims update) + assignExercise(optionId, amount, optionRecord.settlementSeed); + _burn(msg.sender, optionId, amount); // TODO(Emit events for indexing and frontend) } @@ -303,7 +375,6 @@ contract OptionSettlementEngine is ERC1155 { ); } - tokenType[claimId] = Type.None; claimRecord.claimed = true; _burn(msg.sender, claimId, 1); diff --git a/src/test/OptionSettlement.t.sol b/src/test/OptionSettlement.t.sol index e56e06c..0c3dbbf 100644 --- a/src/test/OptionSettlement.t.sol +++ b/src/test/OptionSettlement.t.sol @@ -194,6 +194,10 @@ contract OptionSettlementTest is DSTest, NFTreceiver { assertTrue(true); } + function testTokenTypeNone() public view { + assert(engine.tokenType(3) == Type.None); + } + function testFailDuplicateChain() public { // This should fail to create the second and duplicate options chain Option memory info = Option({ From 5e41af6d446035b7af63dc4c31213ce6f2ac3258 Mon Sep 17 00:00:00 2001 From: Alcibiades Athens Date: Fri, 25 Mar 2022 20:44:35 -0400 Subject: [PATCH 4/4] move out variable --- src/OptionSettlement.sol | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/OptionSettlement.sol b/src/OptionSettlement.sol index d718278..72be2c7 100644 --- a/src/OptionSettlement.sol +++ b/src/OptionSettlement.sol @@ -249,6 +249,9 @@ contract OptionSettlementEngine is ERC1155 { // Last index in the claims list uint256 lastIndex; + // The new length for the claims list + uint256 newLen; + // While there are still options to exercise while (amount > 0) { // Get the claim number to assign @@ -268,7 +271,7 @@ contract OptionSettlementEngine is ERC1155 { amount -= amountWritten; claimRecord.amountExercised = amountWritten; // We pop the end off and overwrite the old slot - uint256 newLen = claimsLen - 1; + newLen = claimsLen - 1; if (newLen > 0) { overwrite = optionToClaims[optionId][newLen]; // Would be nice if I could pop onto the stack here