Skip to content

Commit

Permalink
✨ Add fee switch (#142)
Browse files Browse the repository at this point in the history
Co-authored-by: Alcibiades Athens <[email protected]>
  • Loading branch information
neodaoist and 0xAlcibiades authored Dec 9, 2022
1 parent 05f8f56 commit 5edcb4d
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 18 deletions.
61 changes: 44 additions & 17 deletions src/OptionSettlementEngine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,20 @@ contract OptionSettlementEngine is ERC1155, IOptionSettlementEngine {
//////////////////////////////////////////////////////////////*/

/// @notice The protocol fee
uint8 public immutable feeBps = 5;
uint8 public constant feeBps = 5;

/// @notice Fee balance for a given token
mapping(address => uint256) public feeBalance;
/// @notice Whether or not the protocol fee switch is enabled
bool public feeSwitch;

/// @notice The address fees accrue to
address public feeTo;

/// @notice The contract for token uri generation
ITokenURIGenerator public tokenURIGenerator;

/// @notice Fee balance for a given token
mapping(address => uint256) public feeBalance;

/*//////////////////////////////////////////////////////////////
// State variables - Internal
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -75,6 +78,19 @@ contract OptionSettlementEngine is ERC1155, IOptionSettlementEngine {
/// @notice Accessor for mapping a claim id to its ClaimIndices
mapping(uint256 => OptionLotClaimIndex[]) internal _claimIdToClaimIndexArray;

/*//////////////////////////////////////////////////////////////
// Modifiers
//////////////////////////////////////////////////////////////*/

/// @notice This modifier restricts function access to the feeTo address.
modifier onlyFeeTo() {
if (msg.sender != feeTo) {
revert AccessControlViolation(msg.sender, feeTo);
}

_;
}

/*//////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////*/
Expand Down Expand Up @@ -348,8 +364,11 @@ contract OptionSettlementEngine is ERC1155, IOptionSettlementEngine {
// Add underlying asset to stack
address underlyingAsset = optionRecord.underlyingAsset;

// Calculate Fee and emit events
uint256 fee = _calculateRecordAndEmitFee(underlyingAsset, rxAmount);
// Assess fee (if fee switch enabled) and emit events
uint256 fee = 0;
if (feeSwitch) {
fee = _calculateRecordAndEmitFee(underlyingAsset, rxAmount);
}
emit OptionsWritten(encodedOptionId, msg.sender, encodedClaimId, amount);

if (claimNum == 0) {
Expand Down Expand Up @@ -409,8 +428,11 @@ contract OptionSettlementEngine is ERC1155, IOptionSettlementEngine {

_assignExercise(optionKey, optionRecord, amount);

// Update counters and emit events
uint256 fee = _calculateRecordAndEmitFee(exerciseAsset, rxAmount);
// Assess fee (if fee switch enabled) and emit events
uint256 fee = 0;
if (feeSwitch) {
fee = _calculateRecordAndEmitFee(exerciseAsset, rxAmount);
}
emit OptionsExercised(optionId, msg.sender, amount);

_burn(msg.sender, optionId, amount);
Expand Down Expand Up @@ -482,18 +504,24 @@ contract OptionSettlementEngine is ERC1155, IOptionSettlementEngine {
//////////////////////////////////////////////////////////////*/

/// @inheritdoc IOptionSettlementEngine
function setFeeTo(address newFeeTo) public {
if (msg.sender != feeTo) {
revert AccessControlViolation(msg.sender, feeTo);
}
function setFeeSwitch(bool enabled) external onlyFeeTo {
feeSwitch = enabled;

emit FeeSwitchUpdated(enabled);
}

/// @inheritdoc IOptionSettlementEngine
function setFeeTo(address newFeeTo) external onlyFeeTo {
if (newFeeTo == address(0)) {
revert InvalidFeeToAddress(address(0));
}
feeTo = newFeeTo;

emit FeeToUpdated(newFeeTo);
}

/// @inheritdoc IOptionSettlementEngine
function sweepFees(address[] memory tokens) public {
function sweepFees(address[] memory tokens) external {
address sendFeeTo = feeTo;
address token;
uint256 fee;
Expand All @@ -517,10 +545,7 @@ contract OptionSettlementEngine is ERC1155, IOptionSettlementEngine {
}

/// @inheritdoc IOptionSettlementEngine
function setTokenURIGenerator(address newTokenURIGenerator) public {
if (msg.sender != feeTo) {
revert AccessControlViolation(msg.sender, feeTo);
}
function setTokenURIGenerator(address newTokenURIGenerator) external onlyFeeTo {
if (newTokenURIGenerator == address(0)) {
revert InvalidTokenURIGeneratorAddress(address(0));
}
Expand All @@ -533,10 +558,12 @@ contract OptionSettlementEngine is ERC1155, IOptionSettlementEngine {
//////////////////////////////////////////////////////////////*/

/// @dev Internal helper function to calculate, record, and emit event for fee accrual
/// when writing (on underlying asset) and when exercising (on exercise asset)
/// when writing (on underlying asset) and when exercising (on exercise asset). Checks
/// that fee switch is enabled, otherwise returns fee of 0 and does not record or emit.
function _calculateRecordAndEmitFee(address assetAddress, uint256 assetAmount) internal returns (uint256 fee) {
fee = ((assetAmount * feeBps) / 10_000);
feeBalance[assetAddress] += fee;

emit FeeAccrued(assetAddress, msg.sender, fee);
}

Expand Down
26 changes: 25 additions & 1 deletion src/interfaces/IOptionSettlementEngine.sol
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,18 @@ interface IOptionSettlementEngine {
*/
event FeeAccrued(address indexed asset, address indexed payor, uint256 amount);

/**
* @notice Emitted when fee switch is updated.
* @param enabled Whether the fee switch is enabled or disabled.
*/
event FeeSwitchUpdated(bool indexed enabled);

/**
* @notice Emitted when feeTo address is updated.
* @param newFeeTo The new feeTo address.
*/
event FeeToUpdated(address indexed newFeeTo);

/**
* @notice Emitted when a claim is redeemed.
* @param claimId The id of the claim being redeemed.
Expand Down Expand Up @@ -419,6 +431,18 @@ interface IOptionSettlementEngine {
// Protocol Admin
//////////////////////////////////////////////////////////////*/

/**
* @notice Check if the protocol fee switch is enabled.
* @return Whether or not the protocol fee switch is enabled.
*/
function feeSwitch() external view returns (bool);

/**
* @notice Sets the protocol fee on / off.
* @param enabled Whether or not the protocol fee switch should be enabled.
*/
function setFeeSwitch(bool enabled) external;

/**
* @notice The protocol fee, expressed in basis points.
* @return The fee in basis points.
Expand All @@ -435,7 +459,7 @@ interface IOptionSettlementEngine {

/**
* @notice Returns the address to which protocol fees are swept.
* @return The address to which fees are swept
* @return The address to which fees are swept.
*/
function feeTo() external view returns (address);

Expand Down
153 changes: 153 additions & 0 deletions test/OptionSettlementEngine.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,15 @@ contract OptionSettlementTest is Test, NFTreceiver {

// Approve test contract approval for all on settlement engine ERC1155 token balances
engine.setApprovalForAll(address(this), true);

// Enable fee switch
vm.prank(FEE_TO);
engine.setFeeSwitch(true);
}

function testInitial() public {
assertEq(engine.feeTo(), FEE_TO);
assertEq(engine.feeSwitch(), true);
}

// **********************************************************************
Expand Down Expand Up @@ -519,10 +524,144 @@ contract OptionSettlementTest is Test, NFTreceiver {
_assertClaimAmountExercised(claimIds[6], 0);
}

function testWriteRecordFees() public {
// Alice writes 2
vm.prank(ALICE);
engine.write(testOptionId, 2);

// Fee is recorded on underlying asset, not on exercise assset
uint256 writeAmount = 2 * testUnderlyingAmount;
uint256 writeFee = (writeAmount * engine.feeBps()) / 10_000;
assertEq(engine.feeBalance(address(WETH)), writeFee);
assertEq(engine.feeBalance(address(DAI)), 0);
}

function testWriteNoFeesRecordedWhenFeeSwitchIsDisabled() public {
// precondition check, fee is recorded and emitted on write
vm.prank(ALICE);
engine.write(testOptionId, 2);

uint256 writeAmount = 2 * testUnderlyingAmount;
uint256 writeFee = (writeAmount * engine.feeBps()) / 10_000;
assertEq(engine.feeBalance(address(WETH)), writeFee);

// Disable fee switch
vm.prank(FEE_TO);
engine.setFeeSwitch(false);

// Write 3 more, no fee is recorded or emitted
vm.prank(ALICE);
engine.write(testOptionId, 3);
assertEq(engine.feeBalance(address(WETH)), writeFee); // no change

// Re-enable
vm.prank(FEE_TO);
engine.setFeeSwitch(true);

// Write 5 more, fee is again recorded and emitted
vm.prank(ALICE);
engine.write(testOptionId, 5);
writeAmount = 5 * testUnderlyingAmount;
writeFee += (writeAmount * engine.feeBps()) / 10_000;
assertEq(engine.feeBalance(address(WETH)), writeFee); // includes fee on writing 5 more
}

function testExerciseRecordFees() public {
// Alice writes 2 and transfers to Bob
vm.startPrank(ALICE);
engine.write(testOptionId, 2);
engine.safeTransferFrom(ALICE, BOB, testOptionId, 2, "");
vm.stopPrank();

// Bob exercises 2
vm.warp(testExerciseTimestamp + 1 seconds);
vm.prank(BOB);
engine.exercise(testOptionId, 2);

// Fee is recorded on exercise asset
uint256 exerciseAmount = 2 * testExerciseAmount;
uint256 exerciseFee = (exerciseAmount * engine.feeBps()) / 10_000;
assertEq(engine.feeBalance(address(DAI)), exerciseFee);
}

function testExerciseNoFeesRecordedWhenFeeSwitchIsDisabled() public {
// precondition check, fee is recorded and emitted on exercise
vm.startPrank(ALICE);
engine.write(testOptionId, 10);
engine.safeTransferFrom(ALICE, BOB, testOptionId, 10, "");
vm.stopPrank();

vm.warp(testExerciseTimestamp + 1 seconds);
vm.prank(BOB);
engine.exercise(testOptionId, 2);

uint256 exerciseAmount = 2 * testExerciseAmount;
uint256 exerciseFee = (exerciseAmount * engine.feeBps()) / 10_000;
assertEq(engine.feeBalance(address(DAI)), exerciseFee);

// Disable fee switch
vm.prank(FEE_TO);
engine.setFeeSwitch(false);

// Exercise 3 more, no fee is recorded or emitted
vm.prank(BOB);
engine.exercise(testOptionId, 3);
assertEq(engine.feeBalance(address(DAI)), exerciseFee); // no change

// Re-enable
vm.prank(FEE_TO);
engine.setFeeSwitch(true);

// Exercise 5 more, fee is again recorded and emitted
vm.prank(BOB);
engine.exercise(testOptionId, 5);
exerciseAmount = 5 * testExerciseAmount;
exerciseFee += (exerciseAmount * engine.feeBps()) / 10_000;
assertEq(engine.feeBalance(address(DAI)), exerciseFee); // includes fee on exercising 5 more
}

// **********************************************************************
// PROTOCOL ADMIN
// **********************************************************************

function testSetFeeSwitch() public {
// precondition check -- in test suite, fee switch is enabled by default
assertTrue(engine.feeSwitch());

// disable
vm.startPrank(FEE_TO);
engine.setFeeSwitch(false);

assertFalse(engine.feeSwitch());

// enable
engine.setFeeSwitch(true);

assertTrue(engine.feeSwitch());
}

function testEventSetFeeSwitch() public {
vm.expectEmit(true, true, true, true);
emit FeeSwitchUpdated(false);

// disable
vm.startPrank(FEE_TO);
engine.setFeeSwitch(false);

vm.expectEmit(true, true, true, true);
emit FeeSwitchUpdated(true);

// enable
engine.setFeeSwitch(true);
}

function testRevertSetFeeSwitchWhenNotFeeTo() public {
vm.expectRevert(abi.encodeWithSelector(IOptionSettlementEngine.AccessControlViolation.selector, ALICE, FEE_TO));

vm.prank(ALICE);
engine.setFeeSwitch(true);
}

function testRevertConstructorWhenFeeToIsZeroAddress() public {
TokenURIGenerator localGenerator = new TokenURIGenerator();

Expand All @@ -549,6 +688,16 @@ contract OptionSettlementTest is Test, NFTreceiver {
assertEq(engine.feeTo(), address(0xCAFE));
}

function testEventSetFeeTo() public {
vm.expectEmit(true, true, true, true);
emit FeeToUpdated(address(0xCAFE));

vm.prank(FEE_TO);
engine.setFeeTo(address(0xCAFE));

assertEq(engine.feeTo(), address(0xCAFE));
}

function testRevertSetFeeToWhenNotCurrentFeeTo() public {
vm.expectRevert(abi.encodeWithSelector(IOptionSettlementEngine.AccessControlViolation.selector, ALICE, FEE_TO));
vm.prank(ALICE);
Expand Down Expand Up @@ -1853,4 +2002,8 @@ contract OptionSettlementTest is Test, NFTreceiver {
uint96 exerciseAmount,
uint96 underlyingAmount
);

event FeeSwitchUpdated(bool indexed enabled);

event FeeToUpdated(address indexed newFeeTo);
}

0 comments on commit 5edcb4d

Please sign in to comment.