From 47eaa08f4ed9b8316dbce1c6899ae7fc251d0e9b Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 26 Dec 2023 17:16:00 +0400 Subject: [PATCH 01/34] WIP tree tests for versioned modules --- src/modules/Modules.sol | 21 +++++++++- test/modules/WithModules/Keycode.tree | 4 ++ test/modules/WithModules/MockModule.sol | 13 ++++++ test/modules/WithModules/MockWithModules.sol | 9 +++++ .../WithModules/getModuleForKeycode.t.sol | 36 +++++++++++++++++ .../WithModules/getModuleForKeycode.tree | 6 +++ .../getModuleForVersionedKeycode.t.sol | 40 +++++++++++++++++++ .../getModuleForVersionedKeycode.tree | 8 ++++ 8 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 test/modules/WithModules/Keycode.tree create mode 100644 test/modules/WithModules/MockModule.sol create mode 100644 test/modules/WithModules/MockWithModules.sol create mode 100644 test/modules/WithModules/getModuleForKeycode.t.sol create mode 100644 test/modules/WithModules/getModuleForKeycode.tree create mode 100644 test/modules/WithModules/getModuleForVersionedKeycode.t.sol create mode 100644 test/modules/WithModules/getModuleForVersionedKeycode.tree diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 90d51155..fb207ebf 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -7,6 +7,19 @@ import {Owned} from "lib/solmate/src/auth/Owned.sol"; // Keycode functions +// Type VersionedKeycode: 5 byte characters, 2 bytes for versions +// Descendent contracts will need to store the versioned keycode +// WithModules will track the versions of the modules installed + +/// @notice 5 byte/character identifier for the Module +type ModuleKeycode is bytes5; + +/// @notice Uniquely identifies a specific module version +struct Keycode { + ModuleKeycode moduleKeycode; + uint8 version; +} + type Keycode is bytes10; // 3-10 characters, A-Z only first 3, A-Z, blank, ., or 0-9 for the rest error TargetNotAContract(address target_); @@ -55,6 +68,10 @@ abstract contract WithModules is Owned { error ModuleExecutionReverted(bytes error_); error ModuleAlreadySunset(Keycode keycode_); + // ========= CONSTRUCTOR ========= // + + constructor(address owner_) Owned(owner_) {} + // ========= MODULE MANAGEMENT ========= // /// @notice Array of all modules currently installed. @@ -94,12 +111,12 @@ abstract contract WithModules is Owned { // Set the module to sunset moduleSunset[keycode_] = true; } - + // TODO may need to use proxies instead of this design to allow for upgrading due to a bug and keeping collateral in a derivative contract // The downside is that you have the additional gas costs and potential for exploits in OCG // It may be better to just not have the modules be upgradable - // Having a shutdown mechanism for a specific module and an entire auctionhouse version might be good as well. + // Having a shutdown mechanism for a specific module and an entire auctionhouse version might be good as well. // Though it would still need to allow for claiming of outstanding derivative tokens. // NOTE: don't use upgradable modules. simply require a new module to be installed and sunset the old one to migrate functionality. diff --git a/test/modules/WithModules/Keycode.tree b/test/modules/WithModules/Keycode.tree new file mode 100644 index 00000000..c30bf4d4 --- /dev/null +++ b/test/modules/WithModules/Keycode.tree @@ -0,0 +1,4 @@ +KeycodeTest +# Encodes a ModuleKeycode and version to a Keycode +# Decodes a Keycode to a ModuleKeycode and version +# Encodes a string to a ModuleKeycode \ No newline at end of file diff --git a/test/modules/WithModules/MockModule.sol b/test/modules/WithModules/MockModule.sol new file mode 100644 index 00000000..7ef53217 --- /dev/null +++ b/test/modules/WithModules/MockModule.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Contracts +import {Module, Keycode, toKeycode} from "src/modules/Modules.sol"; + +contract MockModule is Module { + constructor(address _owner) Module(_owner) {} + + function KEYCODE() public pure override returns (Keycode) { + return toKeycode("MOCK"); + } +} diff --git a/test/modules/WithModules/MockWithModules.sol b/test/modules/WithModules/MockWithModules.sol new file mode 100644 index 00000000..59e65746 --- /dev/null +++ b/test/modules/WithModules/MockWithModules.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Contracts +import {WithModules} from "src/modules/Modules.sol"; + +contract MockWithModules is WithModules { + constructor(address _owner) WithModules(_owner) {} +} diff --git a/test/modules/WithModules/getModuleForKeycode.t.sol b/test/modules/WithModules/getModuleForKeycode.t.sol new file mode 100644 index 00000000..ffb421e2 --- /dev/null +++ b/test/modules/WithModules/getModuleForKeycode.t.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/WithModules/MockWithModules.sol"; +import {MockModule} from "test/modules/WithModules/MockModule.sol"; + +// Contracts +import {WithModules} from "src/modules/Modules.sol"; + +contract GetModuleForKeycodeTest is Test { + WithModules internal withModules; + Module internal mockModule; + + function setUp() external { + withModules = new MockWithModules(address(this)); + mockModule = new MockModule(address(withModules)); + } + + modifier whenAModuleIsInstalled() { + // Install the module + withModules.installModule(mockModule); + _; + } + + function test_WhenAMatchingModuleCannotBeFound() external whenAModuleIsInstalled { + // It should revert + } + + function test_WhenAMatchingModuleAndVersionIsFound() external whenAModuleIsInstalled { + // It should return the versioned keycode and module + } +} diff --git a/test/modules/WithModules/getModuleForKeycode.tree b/test/modules/WithModules/getModuleForKeycode.tree new file mode 100644 index 00000000..e2da0ef6 --- /dev/null +++ b/test/modules/WithModules/getModuleForKeycode.tree @@ -0,0 +1,6 @@ +GetModuleForKeycodeTest +└── When a module is installed + ├── When a matching module cannot be found + │ └── It should revert + └── When a matching module and version is found + └── It should return the versioned keycode and module diff --git a/test/modules/WithModules/getModuleForVersionedKeycode.t.sol b/test/modules/WithModules/getModuleForVersionedKeycode.t.sol new file mode 100644 index 00000000..9c39dbcc --- /dev/null +++ b/test/modules/WithModules/getModuleForVersionedKeycode.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/WithModules/MockWithModules.sol"; +import {MockModule} from "test/modules/WithModules/MockModule.sol"; + +// Contracts +import {WithModules} from "src/modules/Modules.sol"; + +contract GetModuleForVersionedKeycodeTest is Test { + WithModules internal withModules; + Module internal mockModule; + + function setUp() external { + withModules = new MockWithModules(address(this)); + mockModule = new MockModule(address(withModules)); + } + + modifier whenAModuleIsInstalled() { + // Install the module + withModules.installModule(mockModule); + _; + } + + function test_WhenAMatchingModuleAndVersionCannotBeFound() external whenAModuleIsInstalled { + // It should revert. + } + + function test_WhenAMatchingModuleIsFoundButNoVersion() external whenAModuleIsInstalled { + // It should revert. + } + + function test_WhenAMatchingModuleAndVersionIsFound() external whenAModuleIsInstalled { + // It should return the module. + } +} diff --git a/test/modules/WithModules/getModuleForVersionedKeycode.tree b/test/modules/WithModules/getModuleForVersionedKeycode.tree new file mode 100644 index 00000000..a2a45651 --- /dev/null +++ b/test/modules/WithModules/getModuleForVersionedKeycode.tree @@ -0,0 +1,8 @@ +GetModuleForVersionedKeycodeTest +└── When a module is installed + ├── When a matching module and version cannot be found + │ └── It should revert. + ├── When a matching module is found but no version + │ └── It should revert. + └── When a matching module and version is found + └── It should return the module. From c085d8e8e12c3ed4d0d287362e5d28b8d4d2fc32 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 26 Dec 2023 22:35:59 +0400 Subject: [PATCH 02/34] More tree tests. Messing with types. --- src/modules/Modules.sol | 35 ++++++++++++++------- test/modules/WithModules/Keycode.tree | 9 +++--- test/modules/WithModules/ModuleKeycode.tree | 11 +++++++ 3 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 test/modules/WithModules/ModuleKeycode.tree diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index fb207ebf..d20b4339 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -7,32 +7,43 @@ import {Owned} from "lib/solmate/src/auth/Owned.sol"; // Keycode functions -// Type VersionedKeycode: 5 byte characters, 2 bytes for versions +// Type VersionedKeycode: 5 byte characters, 1 bytes for versions // Descendent contracts will need to store the versioned keycode // WithModules will track the versions of the modules installed /// @notice 5 byte/character identifier for the Module +/// @dev 3-5 characters from A-Z type ModuleKeycode is bytes5; -/// @notice Uniquely identifies a specific module version -struct Keycode { - ModuleKeycode moduleKeycode; - uint8 version; -} - -type Keycode is bytes10; // 3-10 characters, A-Z only first 3, A-Z, blank, ., or 0-9 for the rest +/// @notice 6 byte identifier for the Module, including version +/// @dev ModuleKeycode, followed by 1 byte for version +type Keycode is bytes6; error TargetNotAContract(address target_); error InvalidKeycode(Keycode keycode_); +function toModuleKeycode(bytes5 moduleKeycode_) pure returns (ModuleKeycode) { + return ModuleKeycode.wrap(moduleKeycode_); +} + +function fromModuleKeycode(ModuleKeycode moduleKeycode_) pure returns (bytes5) { + return ModuleKeycode.unwrap(moduleKeycode_); +} + // solhint-disable-next-line func-visibility -function toKeycode(bytes10 keycode_) pure returns (Keycode) { - return Keycode.wrap(keycode_); +function toKeycode(ModuleKeycode moduleKeycode_, uint8 version_) pure returns (Keycode) { + return Keycode.wrap(bytes6(abi.encode(moduleKeycode_, version_))); } // solhint-disable-next-line func-visibility -function fromKeycode(Keycode keycode_) pure returns (bytes10) { - return Keycode.unwrap(keycode_); +function fromKeycode(Keycode keycode_) pure returns (ModuleKeycode, uint8) { + bytes6 unwrappedKeycode = Keycode.unwrap(keycode_); + bytes memory keycodeBytes = new bytes(6); + for (uint256 i; i < 6; i++) { + keycodeBytes[i] = unwrappedKeycode[i]; + } + + return abi.decode(keycodeBytes, (ModuleKeycode, uint8)); } // solhint-disable-next-line func-visibility diff --git a/test/modules/WithModules/Keycode.tree b/test/modules/WithModules/Keycode.tree index c30bf4d4..803aae87 100644 --- a/test/modules/WithModules/Keycode.tree +++ b/test/modules/WithModules/Keycode.tree @@ -1,4 +1,5 @@ -KeycodeTest -# Encodes a ModuleKeycode and version to a Keycode -# Decodes a Keycode to a ModuleKeycode and version -# Encodes a string to a ModuleKeycode \ No newline at end of file +# KeycodeTest +## When +## Encodes a ModuleKeycode and version to a Keycode +## Encodes a ModuleKeycode and a two-digit version number +## Decodes a Keycode to a ModuleKeycode and version diff --git a/test/modules/WithModules/ModuleKeycode.tree b/test/modules/WithModules/ModuleKeycode.tree new file mode 100644 index 00000000..0384cbab --- /dev/null +++ b/test/modules/WithModules/ModuleKeycode.tree @@ -0,0 +1,11 @@ +# ModuleKeycodeTest +## When a string is given +### It should encode the string to a ModuleKeycode +## When a ModuleKeycode is given +### It should decode the ModuleKeycode into bytes5 +## When a valid ModuleKeycode is given +### It should pass +## When any of the first three characters are not in the alphabet +### It should revert +## When any of the last two characters are not in the alphabet or blank +### It should revert From 33044bf94eb38e58c2349aca004418ad9bd8df0c Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 2 Jan 2024 10:38:01 +0400 Subject: [PATCH 03/34] Fix syntax --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 0009733e..c2a579b8 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,7 +3,7 @@ src = "src" out = "out" libs = ["lib"] -"remappings": [ +remappings = [ "ds-test/=lib/forge-std/lib/ds-test/src/", "forge-std/=lib/forge-std/src/", "solady/=lib/solady/src/", From 177d268fb40733e64bff50826360da013ece6840 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 2 Jan 2024 13:16:41 +0400 Subject: [PATCH 04/34] Codebase compiles --- src/AuctionHouse.sol | 102 ++- src/bases/Auctioneer.sol | 69 +- src/bases/Derivatizer.sol | 2 +- src/modules/Derivative.sol | 4 +- src/modules/Modules.sol | 15 +- src/modules/auctions/FPA.sol | 102 +-- src/modules/auctions/GDA.sol | 610 +++++++-------- src/modules/auctions/OFDA.sol | 256 +++--- src/modules/auctions/OSDA.sol | 390 +++++----- src/modules/auctions/SDA.sol | 736 +++++++++--------- src/modules/auctions/TVGDA.sol | 240 +++--- src/modules/auctions/bases/AtomicAuction.sol | 114 +-- src/modules/auctions/bases/BatchAuction.sol | 164 ++-- .../auctions/bases/DiscreteAuction.sol | 334 ++++---- src/modules/derivatives/CliffVesting.sol | 208 ++--- test/modules/WithModules/MockModule.sol | 4 +- 16 files changed, 1709 insertions(+), 1641 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 32a2ab67..eb196d7a 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -1,14 +1,21 @@ /// SPDX-License-Identifier: AGPL-3.0 pragma solidity 0.8.19; -import {ERC20} from "solady/tokens/ERC20.sol"; -import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol"; -import {EIP712} from "solady/utils/EIP712.sol"; -import {SignatureCheckerLib} from "solady/utils/SignatureCheckerLib.sol"; +import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; +import {SafeTransferLib} from "lib/solmate/src/utils/SafeTransferLib.sol"; +import {EIP712} from "lib/solady/src/utils/EIP712.sol"; +import {SignatureCheckerLib} from "lib/solady/src/utils/SignatureCheckerLib.sol"; +import {Owned} from "lib/solmate/src/auth/Owned.sol"; -import "src/bases/Derivatizer.sol"; -import "src/bases/Auctioneer.sol"; -import "src/modules/Condenser.sol"; +import {Derivatizer} from "src/bases/Derivatizer.sol"; +import {Auctioneer} from "src/bases/Auctioneer.sol"; +import {CondenserModule} from "src/modules/Condenser.sol"; + +import {DerivativeModule} from "src/modules/Derivative.sol"; + +import {Auction, AuctionModule} from "src/modules/Auction.sol"; + +import {fromKeycode, WithModules} from "src/modules/Modules.sol"; abstract contract FeeManager { // TODO write fee logic in separate contract to keep it organized @@ -39,7 +46,7 @@ abstract contract Router is FeeManager { // Address the protocol receives fees at // TODO make this updatable - address internal immutable _protocol; + address internal _protocol; // ========== ATOMIC AUCTIONS ========== // @@ -57,16 +64,39 @@ abstract contract Router is FeeManager { function settle(uint256 id_, Auction.Bid[] memory bids_) external virtual returns (uint256[] memory amountsOut); } -contract AuctionHouse is Derivatizer, Auctioneer, Router { +// contract AuctionHouse is Derivatizer, Auctioneer, Router { +abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { + using SafeTransferLib for ERC20; + /// Implement the router functionality here since it combines all of the base functionality + // ========== ERRORS ========== // + + error AuctionHouse_AmountLessThanMinimum(); + + // ========== EVENTS ========== // + + event Purchase( + uint256 indexed id, + address indexed buyer, + address indexed referrer, + uint256 amount, + uint256 payout + ); + + // ========== CONSTRUCTOR ========== // + + constructor() WithModules(msg.sender) { + // + } + // ========== DIRECT EXECUTION ========== // function purchase(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override returns (uint256 payout) { AuctionModule module = _getModuleForId(id_); // TODO should this not check if the auction is atomic? - // Response: No, my thought was that the module will just revert on `purchase` if it's not atomic. Vice versa + // Response: No, my thought was that the module will just revert on `purchase` if it's not atomic. Vice versa // Calculate fees for purchase // 1. Calculate referrer fee @@ -81,7 +111,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { Routing memory routing = lotRouting[id_]; // Send purchase to auction house and get payout plus any extra output - (payout, auctionOutput) = module.purchase(id_, amount_ - toReferrer - toProtocol, auctionData_); + (payout) = module.purchase(recipient_, referrer_, amount_ - toReferrer - toProtocol, id_, auctionData_, approval_); // Check that payout is at least minimum amount out // @dev Moved the slippage check from the auction to the AuctionHouse to allow different routing and purchase logic @@ -95,7 +125,7 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { _handleTransfers(routing, amount_, payout, toReferrer + toProtocol, approval_); // Handle payout to user, including creation of derivative tokens - _handlePayout(id_, routing, recipient_, payout, auctionOutput); + // _handlePayout(id_, routing, recipient_, payout, auctionOutput); // Emit event emit Purchase(id_, msg.sender, referrer_, amount_, payout); @@ -120,41 +150,41 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { // Check if approval signature has been provided, if so use it increase allowance // TODO a bunch of extra data has to be provided for Permit. - if (approval_ != bytes(0)) + // if (approval_ != bytes(0)) // Have to transfer to teller first since fee is in quote token // Check balance before and after to ensure full amount received, revert if not // Handles edge cases like fee-on-transfer tokens (which are not supported) - uint256 quoteBalance = routing.quoteToken.balanceOf(address(this)); - routing.quoteToken.safeTransferFrom(msg.sender, address(this), amount_); - if (routing.quoteToken.balanceOf(address(this)) < quoteBalance + amount_) - revert Router_UnsupportedToken(); + uint256 quoteBalance = routing_.quoteToken.balanceOf(address(this)); + routing_.quoteToken.safeTransferFrom(msg.sender, address(this), amount_); + // if (routing_.quoteToken.balanceOf(address(this)) < quoteBalance + amount_) + // revert Router_UnsupportedToken(); // If callback address supplied, transfer tokens from teller to callback, then execute callback function, // and ensure proper amount of tokens transferred in. // TODO substitute callback for hooks (and implement in more places)? - if (routing.callbackAddr != address(0)) { + if (address(routing_.hooks) != address(0)) { // Send quote token to callback (transferred in first to allow use during callback) - routing.quoteToken.safeTransfer(routing.callbackAddr, amountLessFee); + routing_.quoteToken.safeTransfer(address(routing_.hooks), amountLessFee); // Call the callback function to receive payout tokens for payout - uint256 payoutBalance = routing.payoutToken.balanceOf(address(this)); - IBondCallback(routing.callbackAddr).callback(id_, amountLessFee, payout_); + // uint256 payoutBalance = routing_.payoutToken.balanceOf(address(this)); + // IBondCallback(routing_.callbackAddr).callback(id_, amountLessFee, payout_); // Check to ensure that the callback sent the requested amount of payout tokens back to the teller - if (routing.payoutToken.balanceOf(address(this)) < (payoutBalance + payout_)) - revert Teller_InvalidCallback(); + // if (routing_.payoutToken.balanceOf(address(this)) < (payoutBalance + payout_)) + // revert Teller_InvalidCallback(); } else { // If no callback is provided, transfer tokens from market owner to this contract // for payout. // Check balance before and after to ensure full amount received, revert if not // Handles edge cases like fee-on-transfer tokens (which are not supported) - uint256 payoutBalance = routing.payoutToken.balanceOf(address(this)); - routing.payoutToken.safeTransferFrom(routing.owner, address(this), payout_); - if (routing.payoutToken.balanceOf(address(this)) < (payoutBalance + payout_)) - revert Router_UnsupportedToken(); + // uint256 payoutBalance = routing_.payoutToken.balanceOf(address(this)); + // routing_.payoutToken.safeTransferFrom(routing_.owner, address(this), payout_); + // if (routing_.payoutToken.balanceOf(address(this)) < (payoutBalance + payout_)) + // revert Router_UnsupportedToken(); - routing.quoteToken.safeTransfer(routing.owner, amountLessFee); + routing_.quoteToken.safeTransfer(routing_.owner, amountLessFee); } } @@ -167,18 +197,18 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { ) internal { // If no derivative, then the payout is sent directly to the recipient // Otherwise, send parameters and payout to the derivative to mint to recipient - if (routing_.derivativeType == toKeycode("")) { + if (fromKeycode(routing_.derivativeType) == bytes6(0)) { // No derivative, send payout to recipient - routing_.payoutToken.safeTransfer(recipient_, payout_); + // routing_.payoutToken.safeTransfer(recipient_, payout_); } else { // Get the module for the derivative type // We assume that the module type has been checked when the lot was created - DerivativeModule module = DerivativeModule(_getModuleIfInstalled(lotDerivative.dType)); + DerivativeModule module = DerivativeModule(_getModuleIfInstalled(routing_.derivativeType)); bytes memory derivativeParams = routing_.derivativeParams; - + // If condenser specified, condense auction output and derivative params before sending to derivative module - if (routing_.condenserType != toKeycode("")) { + if (fromKeycode(routing_.condenserType) != bytes6(0)) { // Get condenser module CondenserModule condenser = CondenserModule(_getModuleIfInstalled(routing_.condenserType)); @@ -187,10 +217,10 @@ contract AuctionHouse is Derivatizer, Auctioneer, Router { } // Approve the module to transfer payout tokens - routing_.payoutToken.safeApprove(address(module), payout_); + // routing_.payoutToken.safeApprove(address(module), payout_); // Call the module to mint derivative tokens to the recipient - module.mint(recipient_, payout_, derivativeParams, routing_.wrapDerivative); - } + // module.mint(recipient_, payout_, derivativeParams, routing_.wrapDerivative); + } } } \ No newline at end of file diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index 6b4cee33..b96709f0 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -5,8 +5,38 @@ import {ERC20} from "lib/solmate/src/tokens/ERC20.sol"; import "src/modules/Auction.sol"; +import {fromKeycode} from "src/modules/Modules.sol"; + +import {DerivativeModule} from "src/modules/Derivative.sol"; + +interface IHooks { + +} + +interface IAllowlist { + +} + abstract contract Auctioneer is WithModules { + // ========= ERRORS ========= // + + error HOUSE_AuctionTypeSunset(Keycode auctionType); + + error HOUSE_NotAuctionOwner(address caller); + + error HOUSE_InvalidLotId(uint256 id); + + error Auctioneer_InvalidParams(); + + // ========= EVENTS ========= // + + event AuctionCreated( + uint256 indexed id, + address indexed baseToken, + address indexed quoteToken + ); + // ========= DATA STRUCTURES ========== // /// @notice Auction routing information for a lot @@ -56,25 +86,26 @@ abstract contract Auctioneer is WithModules { // ========== AUCTION MANAGEMENT ========== // - function auction(RoutingParams calldata routing_, Auction.AuctionParams calldata params_) external override returns (uint256 id) { + function auction(RoutingParams calldata routing_, Auction.AuctionParams calldata params_) external returns (uint256 id) { // Load auction type module, this checks that it is installed. // We load it here vs. later to avoid two checks. - AuctionModule auctionModule = AuctionModule(_getModuleIfInstalled(routing_.auctionType)); + Keycode auctionType = routing_.auctionType; + AuctionModule auctionModule = AuctionModule(_getModuleIfInstalled(auctionType)); // Check that the auction type is allowing new auctions to be created - if (typeSunset[auctionType_]) revert HOUSE_AuctionTypeSunset(routing_.auctionType); + if (typeSunset[auctionType]) revert HOUSE_AuctionTypeSunset(auctionType); // Increment lot count and get ID id = lotCounter++; // Call module auction function to store implementation-specific data - module.auction(id, params_); + auctionModule.auction(id, params_); // Validate routing information // Confirm tokens are within the required decimal range - uint8 baseTokenDecimals = params_.baseToken.decimals(); - uint8 quoteTokenDecimals = params_.quoteToken.decimals(); + uint8 baseTokenDecimals = routing_.baseToken.decimals(); + uint8 quoteTokenDecimals = routing_.quoteToken.decimals(); if (baseTokenDecimals < 6 || baseTokenDecimals > 18) revert Auctioneer_InvalidParams(); @@ -82,7 +113,7 @@ abstract contract Auctioneer is WithModules { revert Auctioneer_InvalidParams(); // If payout is a derivative, validate derivative data on the derivative module - if (routing_.derivativeType != toKeycode("")) { + if (fromKeycode(routing_.derivativeType) != bytes6(0)) { // Load derivative module, this checks that it is installed. DerivativeModule derivativeModule = DerivativeModule(_getModuleIfInstalled(routing_.derivativeType)); @@ -97,16 +128,16 @@ abstract contract Auctioneer is WithModules { // Store routing information Routing storage routing = lotRouting[id]; - routing.auctionType = auctionType_; + routing.auctionType = auctionType; routing.owner = msg.sender; routing.baseToken = routing_.baseToken; routing.quoteToken = routing_.quoteToken; routing.hooks = routing_.hooks; - emit AuctionCreated(id_, address(routing.baseToken), address(routing.quoteToken)); + emit AuctionCreated(id, address(routing.baseToken), address(routing.quoteToken)); } - function cancel(uint256 id_) external override { + function cancel(uint256 id_) external { // Check that caller is the auction owner if (msg.sender != lotRouting[id_].owner) revert HOUSE_NotAuctionOwner(msg.sender); @@ -118,7 +149,7 @@ abstract contract Auctioneer is WithModules { // ========== AUCTION INFORMATION ========== // - function getRouting(uint256 id_) external view override returns (Routing memory) { + function getRouting(uint256 id_) external view returns (Routing memory) { // Check that lot ID is valid if (id_ >= lotCounter) revert HOUSE_InvalidLotId(id_); @@ -127,42 +158,42 @@ abstract contract Auctioneer is WithModules { } // TODO need to add the fee calculations back in at this level for all of these functions - function payoutFor(uint256 id_, uint256 amount_) external view override returns (uint256) { + function payoutFor(uint256 id_, uint256 amount_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get payout from module return module.payoutFor(id_, amount_); } - function priceFor(uint256 id_, uint256 payout_) external view override returns (uint256) { + function priceFor(uint256 id_, uint256 payout_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get price from module return module.priceFor(id_, payout_); } - function maxPayout(uint256 id_) external view override returns (uint256) { + function maxPayout(uint256 id_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get max payout from module return module.maxPayout(id_); } - function maxAmountAccepted(uint256 id_) external view override returns (uint256) { + function maxAmountAccepted(uint256 id_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get max amount accepted from module return module.maxAmountAccepted(id_); } - function isLive(uint256 id_) external view override returns (bool) { + function isLive(uint256 id_) external view returns (bool) { AuctionModule module = _getModuleForId(id_); // Get isLive from module return module.isLive(id_); } - function ownerOf(uint256 id_) external view override returns (address) { + function ownerOf(uint256 id_) external view returns (address) { // Check that lot ID is valid if (id_ >= lotCounter) revert HOUSE_InvalidLotId(id_); @@ -170,7 +201,7 @@ abstract contract Auctioneer is WithModules { return lotRouting[id_].owner; } - function remainingCapacity(id_) external view override returns (uint256) { + function remainingCapacity(uint256 id_) external view returns (uint256) { AuctionModule module = _getModuleForId(id_); // Get remaining capacity from module @@ -181,7 +212,7 @@ abstract contract Auctioneer is WithModules { function _getModuleForId(uint256 id_) internal view returns (AuctionModule) { // Confirm lot ID is valid - if (id_ >= lotCounter) revert HOUSE_InvalidLotId(id_); + if (id_ >= lotCounter) revert HOUSE_InvalidLotId(id_); // Load module, will revert if not installed return AuctionModule(_getModuleIfInstalled(lotRouting[id_].auctionType)); diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index a03fa6b1..92611b79 100644 --- a/src/bases/Derivatizer.sol +++ b/src/bases/Derivatizer.sol @@ -12,7 +12,7 @@ abstract contract Derivatizer is WithModules { // Load the derivative module, will revert if not installed Derivative derivative = Derivative(address(_getModuleIfInstalled(dType))); - // Check that the type hasn't been sunset + // Check that the type hasn't been sunset if (moduleSunset[dType]) revert("Derivatizer: type sunset"); // Call the deploy function on the derivative module diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index b37388bc..ae7bc9e6 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -63,7 +63,7 @@ abstract contract Derivative { /// @notice Reclaim posted collateral for a derivative token which can no longer be exercised /// @notice Access controlled: only callable by the derivative issuer via the auction house. - /// @dev + /// @dev function reclaim(uint256 tokenId_) external virtual; /// @notice Transforms an existing derivative issued by this contract into something else. Derivative is burned and collateral sent to the auction house. @@ -91,7 +91,7 @@ abstract contract Derivative { abstract contract DerivativeModule is Derivative, ERC6909, Module { - + function validate(bytes memory params_) external view virtual returns (bool); } diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index d20b4339..d46ee349 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -36,7 +36,12 @@ function toKeycode(ModuleKeycode moduleKeycode_, uint8 version_) pure returns (K } // solhint-disable-next-line func-visibility -function fromKeycode(Keycode keycode_) pure returns (ModuleKeycode, uint8) { +function fromKeycode(Keycode keycode_) pure returns (bytes6) { + return Keycode.unwrap(keycode_); +} + +// solhint-disable-next-line func-visibility +function unwrapKeycode(Keycode keycode_) pure returns (ModuleKeycode, uint8) { bytes6 unwrappedKeycode = Keycode.unwrap(keycode_); bytes memory keycodeBytes = new bytes(6); for (uint256 i; i < 6; i++) { @@ -156,8 +161,8 @@ abstract contract WithModules is Owned { Keycode keycode_, bytes memory callData_ ) external onlyOwner returns (bytes memory) { - Module module = _getModuleIfInstalled(keycode_); - (bool success, bytes memory returnData) = address(module).call(callData_); + address module = _getModuleIfInstalled(keycode_); + (bool success, bytes memory returnData) = module.call(callData_); if (!success) revert ModuleExecutionReverted(returnData); return returnData; } @@ -172,10 +177,10 @@ abstract contract WithModules is Owned { return address(module) != address(0); } - function _getModuleIfInstalled(Keycode keycode_) internal view returns (Module) { + function _getModuleIfInstalled(Keycode keycode_) internal view returns (address) { Module module = getModuleForKeycode[keycode_]; if (address(module) == address(0)) revert ModuleNotInstalled(keycode_); - return module; + return address(module); } function _validateModule(Module newModule_) internal view returns (Keycode) { diff --git a/src/modules/auctions/FPA.sol b/src/modules/auctions/FPA.sol index b714be2c..9f27cd35 100644 --- a/src/modules/auctions/FPA.sol +++ b/src/modules/auctions/FPA.sol @@ -1,66 +1,66 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; -import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; +// import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; -interface IFixedPriceAuctioneer is IMaxPayoutAuctioneer { - /// @notice Calculate current market price of payout token in quote tokens - /// @param id_ ID of market - /// @return Price for market in configured decimals (see MarketParams) - /// @dev price is derived from the equation: - // - // p = f_p - // - // where - // p = price - // f_p = fixed price provided on creation - // - function marketPrice(uint256 id_) external view override returns (uint256); -} +// interface IFixedPriceAuctioneer is IMaxPayoutAuctioneer { +// /// @notice Calculate current market price of payout token in quote tokens +// /// @param id_ ID of market +// /// @return Price for market in configured decimals (see MarketParams) +// /// @dev price is derived from the equation: +// // +// // p = f_p +// // +// // where +// // p = price +// // f_p = fixed price provided on creation +// // +// function marketPrice(uint256 id_) external view override returns (uint256); +// } -contract FixedPriceAuctioneer is MaxPayoutAuctioneer, IFixedPriceAuctioneer { - /* ========== STATE ========== */ +// contract FixedPriceAuctioneer is MaxPayoutAuctioneer, IFixedPriceAuctioneer { +// /* ========== STATE ========== */ - mapping(uint256 id => uint256 price) internal fixedPrices; +// mapping(uint256 id => uint256 price) internal fixedPrices; - /* ========== CONSTRUCTOR ========== */ +// /* ========== CONSTRUCTOR ========== */ - constructor( - IAggregator aggregator_, - address guardian_, - Authority authority_ - ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} +// constructor( +// IAggregator aggregator_, +// address guardian_, +// Authority authority_ +// ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} - /* ========== MARKET FUNCTIONS ========== */ +// /* ========== MARKET FUNCTIONS ========== */ - function __createMarket( - uint256 id_, - CoreData memory core_, - StyleData memory style_, - bytes memory params_ - ) internal override { - // Decode provided params - uint256 fixedPrice = abi.decode(params_, (uint256)); +// function __createMarket( +// uint256 id_, +// CoreData memory core_, +// StyleData memory style_, +// bytes memory params_ +// ) internal override { +// // Decode provided params +// uint256 fixedPrice = abi.decode(params_, (uint256)); - // Validate that fixed price is not zero - if (fixedPrice == 0) revert Auctioneer_InvalidParams(); +// // Validate that fixed price is not zero +// if (fixedPrice == 0) revert Auctioneer_InvalidParams(); - // Set fixed price - fixedPrices[id_] = fixedPrice; - } +// // Set fixed price +// fixedPrices[id_] = fixedPrice; +// } - /* ========== TELLER FUNCTIONS ========== */ +// /* ========== TELLER FUNCTIONS ========== */ - function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { - // Calculate the payout from the fixed price and return - return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); - } +// function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { +// // Calculate the payout from the fixed price and return +// return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); +// } - /* ========== VIEW FUNCTIONS ========== */ +// /* ========== VIEW FUNCTIONS ========== */ - /// @inheritdoc IFixedPriceAuctioneer - function marketPrice(uint256 id_) public view override returns (uint256) { - return fixedPrices[id_]; - } -} +// /// @inheritdoc IFixedPriceAuctioneer +// function marketPrice(uint256 id_) public view override returns (uint256) { +// return fixedPrices[id_]; +// } +// } diff --git a/src/modules/auctions/GDA.sol b/src/modules/auctions/GDA.sol index d149916e..648e1023 100644 --- a/src/modules/auctions/GDA.sol +++ b/src/modules/auctions/GDA.sol @@ -1,305 +1,305 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -import "src/modules/auctions/bases/AtomicAuction.sol"; -import {SD59x18, sd, convert, uUNIT} from "prb-math/SD59x18.sol"; - -abstract contract GDA { - /* ========== DATA STRUCTURES ========== */ - enum Decay { - Linear, - Exponential - } - - /// @notice Auction pricing data - struct AuctionData { - uint256 equilibriumPrice; // price at which the auction is balanced - uint256 minimumPrice; // minimum price the auction can reach - uint256 payoutScale; - uint256 quoteScale; - uint48 lastAuctionStart; - Decay decayType; // type of decay to use for the market - SD59x18 decayConstant; // speed at which the price decays, as SD59x18. - SD59x18 emissionsRate; // number of tokens released per second, as SD59x18. Calculated as capacity / duration. - } - - /* ========== STATE ========== */ - - SD59x18 public constant ONE = convert(int256(1)); - mapping(uint256 lotId => AuctionData) public auctionData; -} - -contract GradualDutchAuctioneer is AtomicAuctionModule, GDA { - /* ========== ERRORS ========== */ - - - - - /* ========== CONSTRUCTOR ========== */ - - constructor( - address auctionHouse_ - ) Module(auctionHouse_) {} - - /* ========== MARKET FUNCTIONS ========== */ - - function _auction( - uint256 id_, - Lot memory lot_, - bytes memory params_ - ) internal override { - // Decode params - ( - uint256 equilibriumPrice_, // quote tokens per payout token, so would be in quote token decimals? - Decay decayType_, - SD59x18 decayConstant_ - ) = abi.decode(params_, (uint256, Decay, SD59x18)); - - // Validate params - // TODO - - // Calculate scale from payout token decimals - uint256 payoutScale = 10 ** uint256(lot_.payoutToken.decimals()); - uint256 quoteScale = 10 ** uint256(lot_.quoteToken.decimals()); - - - // Calculate emissions rate - uint256 payoutCapacity = lot_.capacityInQuote ? lot_.capacity.mulDiv(payoutScale, equilibriumPrice_) : lot_.capacity; - SD59x18 emissionsRate = sd(int256(payoutCapacity.mulDiv(uUNIT, (lot_.conclusion - lot_.start) * payoutScale))); - - // Set auction data - AuctionData storage auction = auctionData[id_]; - auction.equilibriumPrice = equilibriumPrice_; - auction.payoutScale = payoutScale; - auction.quoteScale = quoteScale; - auction.lastAuctionStart = uint48(block.timestamp); - auction.decayType = decayType_; - auction.decayConstant = decayConstant_; - auction.emissionsRate = emissionsRate; - } - - /* ========== TELLER FUNCTIONS ========== */ - - function _purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { - // Calculate payout amount for quote amount and seconds of emissions using GDA formula - (uint256 payout, uint48 secondsOfEmissions) = _payoutAndEmissionsFor(id_, amount_); - - // Update last auction start with seconds of emissions - // Do not have to check that too many seconds have passed here - // since payout/amount is checked against capacity in the top-level function - auctionData[id_].lastAuctionStart += secondsOfEmissions; - - return payout; - } - - /* ========== PRICE FUNCTIONS ========== */ - - function priceFor(uint256 id_, uint256 payout_) external view returns (uint256) { - Decay decayType = auctionData[id_].decayType; - - uint256 amount; - if (decayType == Decay.EXPONENTIAL) { - amount = _exponentialPriceFor(id_, payout_); - } else if (decayType == Decay.LINEAR) { - amount = _linearPriceFor(id_, payout_); - } - - // Check that amount in or payout do not exceed remaining capacity - Lot memory lot = lotData[id_]; - if (lot.capacityInQuote ? amount > lot.capacity : payout_ > lot.capacity) - revert Auctioneer_InsufficientCapacity(); - } - - // For Continuos GDAs with exponential decay, the price of a given token t seconds after being emitted is: p(t) = p0 * e^(-k*t) - // Integrating this function from the last auction start time for a particular number of tokens, gives the multiplier for the token price to determine amount of quote tokens required to purchase - // P(T) = (p0 / k) * (e^(k*q/r) - 1) / e^(k*T) where T is the time since the last auction start, q is the number of tokens to purchase, p0 is the initial price, and r is the emissions rate - function _exponentialPriceFor(uint256 id_, uint256 payout_) internal view returns (uint256) { - Lot memory lot = lotData[id_]; - AuctionData memory auction = auctionData[id_]; - - // Convert payout to SD59x18. We scale first to 18 decimals from the payout token decimals - SD59x18 payout = sd(int256(payout_.mulDiv(uUNIT, auction.payoutScale))); - - // Calculate time since last auction start - SD59x18 timeSinceLastAuctionStart = convert( - int256(block.timestamp - uint256(auction.lastAuctionStart)) - ); - - // Calculate the first numerator factor - SD59x18 num1 = sd(int256(auction.equilibriumPrice.mulDiv(uUNIT, auction.quoteScale))).div( - auction.decayConstant - ); - - // Calculate the second numerator factor - SD59x18 num2 = auction.decayConstant.mul(payout).div(auction.emissionsRate).exp().sub(ONE); - - // Calculate the denominator - SD59x18 denominator = auction.decayConstant.mul(timeSinceLastAuctionStart).exp(); - - // Calculate return value - // This value should always be positive, therefore, we can safely cast to uint256 - // We scale the return value back to payout token decimals - return num1.mul(num2).div(denominator).intoUint256().mulDiv(auction.quoteScale, uUNIT); - } - - // p(t) = p0 * (1 - k*t) where p0 is the initial price, k is the decay constant, and t is the time since the last auction start - // P(T) = (p0 * q / r) * (1 - k*T + k*q/2r) where T is the time since the last auction start, q is the number of tokens to purchase, - // r is the emissions rate, and p0 is the initial price - function _linearPriceFor(uint256 id_, uint256 payout_) internal view returns (uint256) { - Lot memory lot = lotData[id_]; - AuctionData memory auction = auctionData[id_]; - - // Convert payout to SD59x18. We scale first to 18 decimals from the payout token decimals - SD59x18 payout = sd(int256(payout_.mulDiv(uUNIT, auction.payoutScale))); - - // Calculate time since last auction start - SD59x18 timeSinceLastAuctionStart = convert( - int256(block.timestamp - uint256(auction.lastAuctionStart)) - ); - - // Calculate decay factor - // TODO can we confirm this will be positive? - SD59x18 decayFactor = ONE.sub(auction.decayConstant.mul(timeSinceLastAuctionStart)).add( - auction.decayConstant.mul(payout).div(convert(int256(2)).mul(auction.emissionsRate)) - ); - - // Calculate payout factor - SD59x18 payoutFactor = payout.mul( - sd(int256(auction.equilibriumPrice.mulDiv(uUNIT, auction.quoteScale))) - ).div(auction.emissionsRate); // TODO do we lose precision here by dividing early? - - // Calculate final return value and convert back to market scale - return - payoutFactor.mul(decayFactor).intoUint256().mulDiv( - auction.quoteScale, - uUNIT - ); - } - - /* ========== PAYOUT CALCULATIONS ========== */ - - function _payoutFor(uint256 id_, uint256 amount_) internal view override returns (uint256) { - (uint256 payout, ) = _payoutAndEmissionsFor(id_, amount_); - - // Check that amount in or payout do not exceed remaining capacity - Lot memory lot = lotData[id_]; - if (lot.capacityInQuote ? amount_ > lot.capacity : payout > lot.capacity) - revert Auctioneer_InsufficientCapacity(); - - return payout; - } - - function _payoutAndEmissionsFor( - uint256 id_, - uint256 amount_ - ) internal view returns (uint256, uint48) { - Decay decayType = auctionData[id_].decayType; - - if (decayType == Decay.EXPONENTIAL) { - return _payoutForExpDecay(id_, amount_); - } else if (decayType == Decay.LINEAR) { - return _payoutForLinearDecay(id_, amount_); - } else { - revert Auctioneer_InvalidParams(); - } - } - - // TODO check this math again - // P = (r / k) * ln(Q * k / p0 * e^(k*T) + 1) where P is the number of payout tokens, Q is the number of quote tokens, r is the emissions rate, k is the decay constant, - // p0 is the price target of the market, and T is the time since the last auction start - function _payoutForExpDecay( - uint256 id_, - uint256 amount_ - ) internal view returns (uint256, uint48) { - CoreData memory core = coreData[id_]; - AuctionData memory auction = auctionData[id_]; - - // Convert to 18 decimals for fixed math by pre-computing the Q / p0 factor - SD59x18 scaledQ = sd( - int256( - amount_.mulDiv(auction.uUNIT, auction.equilibriumPrice) - ) - ); - - // Calculate time since last auction start - SD59x18 timeSinceLastAuctionStart = convert( - int256(block.timestamp - uint256(auction.lastAuctionStart)) - ); - - // Calculate the logarithm - SD59x18 logFactor = auction - .decayConstant - .mul(timeSinceLastAuctionStart) - .exp() - .mul(scaledQ) - .mul(auction.decayConstant) - .add(ONE) - .ln(); - - // Calculate the payout - SD59x18 payout = logFactor.mul(auction.emissionsRate).div(auction.decayConstant); - - // Scale back to payout token decimals - // TODO verify we can safely cast to uint256 - return payout.intoUint256().mulDiv(auction.payoutScale, uUNIT); - - // Calculate seconds of emissions from payout or amount (depending on capacity type) - // TODO emissionsRate is being set only in payout tokens over, need to think about this - // Might just use equilibrium price to convert the quote amount here to payout amount and then divide by emissions rate - uint48 secondsOfEmissions; - if (core.capacityInQuote) { - // Convert amount to SD59x18 - SD59x18 amount = sd(int256(amount_.mulDiv(uUNIT, auction.quoteScale))); - secondsOfEmissions = uint48(amount.div(auction.emissionsRate).intoUint256()); - } else { - secondsOfEmissions = uint48(payout.div(auction.emissionsRate).intoUint256()); - } - - // Scale payout to payout token decimals and return - // Payout should always be positive since it is atleast 1, therefore, we can safely cast to uint256 - return (payout.intoUint256().mulDiv(auction.payoutScale, uUNIT), secondsOfEmissions); - } - - // TODO check this math again - // P = (r / k) * (sqrt(2 * k * Q / p0) + k * T - 1) where P is the number of payout tokens, Q is the number of quote tokens, r is the emissions rate, k is the decay constant, - // p0 is the price target of the market, and T is the time since the last auction start - function _payoutForLinearDecay(uint256 id_, uint256 amount_) internal view returns (uint256) { - Lot memory lot = lotData[id_]; - AuctionData memory auction = auctionData[id_]; - - // Convert to 18 decimals for fixed math by pre-computing the Q / p0 factor - SD59x18 scaledQ = sd( - int256(amount_.mulDiv(uUNIT, auction.equilibriumPrice)) - ); - - // Calculate time since last auction start - SD59x18 timeSinceLastAuctionStart = convert( - int256(block.timestamp - uint256(auction.lastAuctionStart)) - ); - - // Calculate factors - SD59x18 sqrtFactor = convert(int256(2)).mul(auction.decayConstant).mul(scaledQ).sqrt(); - SD59x18 factor = sqrtFactor.add(auction.decayConstant.mul(timeSinceLastAuctionStart)).sub( - ONE - ); - - // Calculate payout - SD59x18 payout = auction.emissionsRate.div(auction.decayConstant).mul(factor); - - // Calculate seconds of emissions from payout or amount (depending on capacity type) - // TODO same as in the above function - uint48 secondsOfEmissions; - if (core.capacityInQuote) { - // Convert amount to SD59x18 - SD59x18 amount = sd(int256(amount_.mulDiv(uUNIT, auction.scale))); - secondsOfEmissions = uint48(amount.div(auction.emissionsRate).intoUint256()); - } else { - secondsOfEmissions = uint48(payout.div(auction.emissionsRate).intoUint256()); - } - - // Scale payout to payout token decimals and return - return (payout.intoUint256().mulDiv(auction.payoutScale, uUNIT), secondsOfEmissions); - } - /* ========== ADMIN FUNCTIONS ========== */ - /* ========== VIEW FUNCTIONS ========== */ -} +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; + +// import "src/modules/auctions/bases/AtomicAuction.sol"; +// import {SD59x18, sd, convert, uUNIT} from "prb-math/SD59x18.sol"; + +// abstract contract GDA { +// /* ========== DATA STRUCTURES ========== */ +// enum Decay { +// Linear, +// Exponential +// } + +// /// @notice Auction pricing data +// struct AuctionData { +// uint256 equilibriumPrice; // price at which the auction is balanced +// uint256 minimumPrice; // minimum price the auction can reach +// uint256 payoutScale; +// uint256 quoteScale; +// uint48 lastAuctionStart; +// Decay decayType; // type of decay to use for the market +// SD59x18 decayConstant; // speed at which the price decays, as SD59x18. +// SD59x18 emissionsRate; // number of tokens released per second, as SD59x18. Calculated as capacity / duration. +// } + +// /* ========== STATE ========== */ + +// SD59x18 public constant ONE = convert(int256(1)); +// mapping(uint256 lotId => AuctionData) public auctionData; +// } + +// contract GradualDutchAuctioneer is AtomicAuctionModule, GDA { +// /* ========== ERRORS ========== */ + + + + +// /* ========== CONSTRUCTOR ========== */ + +// constructor( +// address auctionHouse_ +// ) Module(auctionHouse_) {} + +// /* ========== MARKET FUNCTIONS ========== */ + +// function _auction( +// uint256 id_, +// Lot memory lot_, +// bytes memory params_ +// ) internal override { +// // Decode params +// ( +// uint256 equilibriumPrice_, // quote tokens per payout token, so would be in quote token decimals? +// Decay decayType_, +// SD59x18 decayConstant_ +// ) = abi.decode(params_, (uint256, Decay, SD59x18)); + +// // Validate params +// // TODO + +// // Calculate scale from payout token decimals +// uint256 payoutScale = 10 ** uint256(lot_.payoutToken.decimals()); +// uint256 quoteScale = 10 ** uint256(lot_.quoteToken.decimals()); + + +// // Calculate emissions rate +// uint256 payoutCapacity = lot_.capacityInQuote ? lot_.capacity.mulDiv(payoutScale, equilibriumPrice_) : lot_.capacity; +// SD59x18 emissionsRate = sd(int256(payoutCapacity.mulDiv(uUNIT, (lot_.conclusion - lot_.start) * payoutScale))); + +// // Set auction data +// AuctionData storage auction = auctionData[id_]; +// auction.equilibriumPrice = equilibriumPrice_; +// auction.payoutScale = payoutScale; +// auction.quoteScale = quoteScale; +// auction.lastAuctionStart = uint48(block.timestamp); +// auction.decayType = decayType_; +// auction.decayConstant = decayConstant_; +// auction.emissionsRate = emissionsRate; +// } + +// /* ========== TELLER FUNCTIONS ========== */ + +// function _purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { +// // Calculate payout amount for quote amount and seconds of emissions using GDA formula +// (uint256 payout, uint48 secondsOfEmissions) = _payoutAndEmissionsFor(id_, amount_); + +// // Update last auction start with seconds of emissions +// // Do not have to check that too many seconds have passed here +// // since payout/amount is checked against capacity in the top-level function +// auctionData[id_].lastAuctionStart += secondsOfEmissions; + +// return payout; +// } + +// /* ========== PRICE FUNCTIONS ========== */ + +// function priceFor(uint256 id_, uint256 payout_) external view returns (uint256) { +// Decay decayType = auctionData[id_].decayType; + +// uint256 amount; +// if (decayType == Decay.EXPONENTIAL) { +// amount = _exponentialPriceFor(id_, payout_); +// } else if (decayType == Decay.LINEAR) { +// amount = _linearPriceFor(id_, payout_); +// } + +// // Check that amount in or payout do not exceed remaining capacity +// Lot memory lot = lotData[id_]; +// if (lot.capacityInQuote ? amount > lot.capacity : payout_ > lot.capacity) +// revert Auctioneer_InsufficientCapacity(); +// } + +// // For Continuos GDAs with exponential decay, the price of a given token t seconds after being emitted is: p(t) = p0 * e^(-k*t) +// // Integrating this function from the last auction start time for a particular number of tokens, gives the multiplier for the token price to determine amount of quote tokens required to purchase +// // P(T) = (p0 / k) * (e^(k*q/r) - 1) / e^(k*T) where T is the time since the last auction start, q is the number of tokens to purchase, p0 is the initial price, and r is the emissions rate +// function _exponentialPriceFor(uint256 id_, uint256 payout_) internal view returns (uint256) { +// Lot memory lot = lotData[id_]; +// AuctionData memory auction = auctionData[id_]; + +// // Convert payout to SD59x18. We scale first to 18 decimals from the payout token decimals +// SD59x18 payout = sd(int256(payout_.mulDiv(uUNIT, auction.payoutScale))); + +// // Calculate time since last auction start +// SD59x18 timeSinceLastAuctionStart = convert( +// int256(block.timestamp - uint256(auction.lastAuctionStart)) +// ); + +// // Calculate the first numerator factor +// SD59x18 num1 = sd(int256(auction.equilibriumPrice.mulDiv(uUNIT, auction.quoteScale))).div( +// auction.decayConstant +// ); + +// // Calculate the second numerator factor +// SD59x18 num2 = auction.decayConstant.mul(payout).div(auction.emissionsRate).exp().sub(ONE); + +// // Calculate the denominator +// SD59x18 denominator = auction.decayConstant.mul(timeSinceLastAuctionStart).exp(); + +// // Calculate return value +// // This value should always be positive, therefore, we can safely cast to uint256 +// // We scale the return value back to payout token decimals +// return num1.mul(num2).div(denominator).intoUint256().mulDiv(auction.quoteScale, uUNIT); +// } + +// // p(t) = p0 * (1 - k*t) where p0 is the initial price, k is the decay constant, and t is the time since the last auction start +// // P(T) = (p0 * q / r) * (1 - k*T + k*q/2r) where T is the time since the last auction start, q is the number of tokens to purchase, +// // r is the emissions rate, and p0 is the initial price +// function _linearPriceFor(uint256 id_, uint256 payout_) internal view returns (uint256) { +// Lot memory lot = lotData[id_]; +// AuctionData memory auction = auctionData[id_]; + +// // Convert payout to SD59x18. We scale first to 18 decimals from the payout token decimals +// SD59x18 payout = sd(int256(payout_.mulDiv(uUNIT, auction.payoutScale))); + +// // Calculate time since last auction start +// SD59x18 timeSinceLastAuctionStart = convert( +// int256(block.timestamp - uint256(auction.lastAuctionStart)) +// ); + +// // Calculate decay factor +// // TODO can we confirm this will be positive? +// SD59x18 decayFactor = ONE.sub(auction.decayConstant.mul(timeSinceLastAuctionStart)).add( +// auction.decayConstant.mul(payout).div(convert(int256(2)).mul(auction.emissionsRate)) +// ); + +// // Calculate payout factor +// SD59x18 payoutFactor = payout.mul( +// sd(int256(auction.equilibriumPrice.mulDiv(uUNIT, auction.quoteScale))) +// ).div(auction.emissionsRate); // TODO do we lose precision here by dividing early? + +// // Calculate final return value and convert back to market scale +// return +// payoutFactor.mul(decayFactor).intoUint256().mulDiv( +// auction.quoteScale, +// uUNIT +// ); +// } + +// /* ========== PAYOUT CALCULATIONS ========== */ + +// function _payoutFor(uint256 id_, uint256 amount_) internal view override returns (uint256) { +// (uint256 payout, ) = _payoutAndEmissionsFor(id_, amount_); + +// // Check that amount in or payout do not exceed remaining capacity +// Lot memory lot = lotData[id_]; +// if (lot.capacityInQuote ? amount_ > lot.capacity : payout > lot.capacity) +// revert Auctioneer_InsufficientCapacity(); + +// return payout; +// } + +// function _payoutAndEmissionsFor( +// uint256 id_, +// uint256 amount_ +// ) internal view returns (uint256, uint48) { +// Decay decayType = auctionData[id_].decayType; + +// if (decayType == Decay.EXPONENTIAL) { +// return _payoutForExpDecay(id_, amount_); +// } else if (decayType == Decay.LINEAR) { +// return _payoutForLinearDecay(id_, amount_); +// } else { +// revert Auctioneer_InvalidParams(); +// } +// } + +// // TODO check this math again +// // P = (r / k) * ln(Q * k / p0 * e^(k*T) + 1) where P is the number of payout tokens, Q is the number of quote tokens, r is the emissions rate, k is the decay constant, +// // p0 is the price target of the market, and T is the time since the last auction start +// function _payoutForExpDecay( +// uint256 id_, +// uint256 amount_ +// ) internal view returns (uint256, uint48) { +// CoreData memory core = coreData[id_]; +// AuctionData memory auction = auctionData[id_]; + +// // Convert to 18 decimals for fixed math by pre-computing the Q / p0 factor +// SD59x18 scaledQ = sd( +// int256( +// amount_.mulDiv(auction.uUNIT, auction.equilibriumPrice) +// ) +// ); + +// // Calculate time since last auction start +// SD59x18 timeSinceLastAuctionStart = convert( +// int256(block.timestamp - uint256(auction.lastAuctionStart)) +// ); + +// // Calculate the logarithm +// SD59x18 logFactor = auction +// .decayConstant +// .mul(timeSinceLastAuctionStart) +// .exp() +// .mul(scaledQ) +// .mul(auction.decayConstant) +// .add(ONE) +// .ln(); + +// // Calculate the payout +// SD59x18 payout = logFactor.mul(auction.emissionsRate).div(auction.decayConstant); + +// // Scale back to payout token decimals +// // TODO verify we can safely cast to uint256 +// return payout.intoUint256().mulDiv(auction.payoutScale, uUNIT); + +// // Calculate seconds of emissions from payout or amount (depending on capacity type) +// // TODO emissionsRate is being set only in payout tokens over, need to think about this +// // Might just use equilibrium price to convert the quote amount here to payout amount and then divide by emissions rate +// uint48 secondsOfEmissions; +// if (core.capacityInQuote) { +// // Convert amount to SD59x18 +// SD59x18 amount = sd(int256(amount_.mulDiv(uUNIT, auction.quoteScale))); +// secondsOfEmissions = uint48(amount.div(auction.emissionsRate).intoUint256()); +// } else { +// secondsOfEmissions = uint48(payout.div(auction.emissionsRate).intoUint256()); +// } + +// // Scale payout to payout token decimals and return +// // Payout should always be positive since it is atleast 1, therefore, we can safely cast to uint256 +// return (payout.intoUint256().mulDiv(auction.payoutScale, uUNIT), secondsOfEmissions); +// } + +// // TODO check this math again +// // P = (r / k) * (sqrt(2 * k * Q / p0) + k * T - 1) where P is the number of payout tokens, Q is the number of quote tokens, r is the emissions rate, k is the decay constant, +// // p0 is the price target of the market, and T is the time since the last auction start +// function _payoutForLinearDecay(uint256 id_, uint256 amount_) internal view returns (uint256) { +// Lot memory lot = lotData[id_]; +// AuctionData memory auction = auctionData[id_]; + +// // Convert to 18 decimals for fixed math by pre-computing the Q / p0 factor +// SD59x18 scaledQ = sd( +// int256(amount_.mulDiv(uUNIT, auction.equilibriumPrice)) +// ); + +// // Calculate time since last auction start +// SD59x18 timeSinceLastAuctionStart = convert( +// int256(block.timestamp - uint256(auction.lastAuctionStart)) +// ); + +// // Calculate factors +// SD59x18 sqrtFactor = convert(int256(2)).mul(auction.decayConstant).mul(scaledQ).sqrt(); +// SD59x18 factor = sqrtFactor.add(auction.decayConstant.mul(timeSinceLastAuctionStart)).sub( +// ONE +// ); + +// // Calculate payout +// SD59x18 payout = auction.emissionsRate.div(auction.decayConstant).mul(factor); + +// // Calculate seconds of emissions from payout or amount (depending on capacity type) +// // TODO same as in the above function +// uint48 secondsOfEmissions; +// if (core.capacityInQuote) { +// // Convert amount to SD59x18 +// SD59x18 amount = sd(int256(amount_.mulDiv(uUNIT, auction.scale))); +// secondsOfEmissions = uint48(amount.div(auction.emissionsRate).intoUint256()); +// } else { +// secondsOfEmissions = uint48(payout.div(auction.emissionsRate).intoUint256()); +// } + +// // Scale payout to payout token decimals and return +// return (payout.intoUint256().mulDiv(auction.payoutScale, uUNIT), secondsOfEmissions); +// } +// /* ========== ADMIN FUNCTIONS ========== */ +// /* ========== VIEW FUNCTIONS ========== */ +// } diff --git a/src/modules/auctions/OFDA.sol b/src/modules/auctions/OFDA.sol index e9f8c0c2..b1f4706e 100644 --- a/src/modules/auctions/OFDA.sol +++ b/src/modules/auctions/OFDA.sol @@ -1,128 +1,128 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; - -interface IOracleFixedDiscountAuctioneer is IMaxPayoutAuctioneer { - /// @notice Auction pricing data - struct AuctionData { - IBondOracle oracle; - uint48 fixedDiscount; - bool conversionMul; - uint256 conversionFactor; - uint256 minPrice; - } - - /// @notice Calculate current market price of payout token in quote tokens - /// @param id_ ID of market - /// @return Price for market in configured decimals (see MarketParams) - /// @dev price is derived from the equation: - // - // p = max(min_p, o_p * (1 - d)) - // - // where - // p = price - // min_p = minimum price - // o_p = oracle price - // d = fixed discount - // - // if price is below minimum price, minimum price is returned - function marketPrice(uint256 id_) external view returns (uint256); -} - - - -import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; -import {IFixedPriceAuctioneer} from "src/interfaces/IFixedPriceAuctioneer.sol"; -import {OracleHelper} from "src/lib/OracleHelper.sol"; - -contract OracleFixedDiscountAuctioneer is MaxPayoutAuctioneer, IOracleFixedDiscountAuctioneer { - /* ========== ERRORS ========== */ - error Auctioneer_OraclePriceZero(); - - /* ========== STATE ========== */ - - mapping(uint256 id => AuctionData) internal auctionData; - - /* ========== CONSTRUCTOR ========== */ - - constructor( - IAggregator aggregator_, - address guardian_, - Authority authority_ - ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} - - /* ========== MARKET FUNCTIONS ========== */ - - function __createMarket( - uint256 id_, - CoreData memory core_, - StyleData memory style_, - bytes memory params_ - ) internal override { - // Decode params - (IBondOracle oracle, uint48 fixedDiscount, uint48 maxDiscountFromCurrent) = abi.decode( - params_, - (IBondOracle, uint48, uint48) - ); - - // Validate oracle - (uint256 oraclePrice, uint256 conversionFactor, bool conversionMul) = OracleHelper - .validateOracle(id_, oracle, core_.quoteToken, core_.payoutToken, fixedDiscount); - - // Validate discounts - if ( - fixedDiscount >= ONE_HUNDRED_PERCENT || - maxDiscountFromCurrent > ONE_HUNDRED_PERCENT || - fixedDiscount > maxDiscountFromCurrent - ) revert Auctioneer_InvalidParams(); - - // Set auction data - AuctionData storage auction = auctionData[id_]; - auction.oracle = oracle; - auction.fixedDiscount = fixedDiscount; - auction.conversionMul = conversionMul; - auction.conversionFactor = conversionFactor; - auction.minPrice = oraclePrice.mulDivUp( - ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, - ONE_HUNDRED_PERCENT - ); - } - - /* ========== TELLER FUNCTIONS ========== */ - - function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { - // Calculate the payout from the market price and return - return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); - } - - /* ========== VIEW FUNCTIONS ========== */ - - /// @inheritdoc IOracleFixedDiscountAuctioneer - function marketPrice(uint256 id_) public view override returns (uint256) { - // Get auction data - AuctionData memory auction = auctionData[id_]; - - // Get oracle price - uint256 oraclePrice = auction.oracle.currentPrice(id_); - - // Revert if oracle price is 0 - if (oraclePrice == 0) revert Auctioneer_OraclePriceZero(); - - // Apply conversion factor - if (auction.conversionMul) { - oraclePrice *= auction.conversionFactor; - } else { - oraclePrice /= auction.conversionFactor; - } - - // Apply fixed discount - uint256 price = oraclePrice.mulDivUp( - uint256(ONE_HUNDRED_PERCENT - auction.fixedDiscount), - uint256(ONE_HUNDRED_PERCENT) - ); - - // Compare the current price to the minimum price and return the maximum - return price > auction.minPrice ? price : auction.minPrice; - } -} +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; + +// import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; + +// interface IOracleFixedDiscountAuctioneer is IMaxPayoutAuctioneer { +// /// @notice Auction pricing data +// struct AuctionData { +// IBondOracle oracle; +// uint48 fixedDiscount; +// bool conversionMul; +// uint256 conversionFactor; +// uint256 minPrice; +// } + +// /// @notice Calculate current market price of payout token in quote tokens +// /// @param id_ ID of market +// /// @return Price for market in configured decimals (see MarketParams) +// /// @dev price is derived from the equation: +// // +// // p = max(min_p, o_p * (1 - d)) +// // +// // where +// // p = price +// // min_p = minimum price +// // o_p = oracle price +// // d = fixed discount +// // +// // if price is below minimum price, minimum price is returned +// function marketPrice(uint256 id_) external view returns (uint256); +// } + + + +// import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; +// import {IFixedPriceAuctioneer} from "src/interfaces/IFixedPriceAuctioneer.sol"; +// import {OracleHelper} from "src/lib/OracleHelper.sol"; + +// contract OracleFixedDiscountAuctioneer is MaxPayoutAuctioneer, IOracleFixedDiscountAuctioneer { +// /* ========== ERRORS ========== */ +// error Auctioneer_OraclePriceZero(); + +// /* ========== STATE ========== */ + +// mapping(uint256 id => AuctionData) internal auctionData; + +// /* ========== CONSTRUCTOR ========== */ + +// constructor( +// IAggregator aggregator_, +// address guardian_, +// Authority authority_ +// ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} + +// /* ========== MARKET FUNCTIONS ========== */ + +// function __createMarket( +// uint256 id_, +// CoreData memory core_, +// StyleData memory style_, +// bytes memory params_ +// ) internal override { +// // Decode params +// (IBondOracle oracle, uint48 fixedDiscount, uint48 maxDiscountFromCurrent) = abi.decode( +// params_, +// (IBondOracle, uint48, uint48) +// ); + +// // Validate oracle +// (uint256 oraclePrice, uint256 conversionFactor, bool conversionMul) = OracleHelper +// .validateOracle(id_, oracle, core_.quoteToken, core_.payoutToken, fixedDiscount); + +// // Validate discounts +// if ( +// fixedDiscount >= ONE_HUNDRED_PERCENT || +// maxDiscountFromCurrent > ONE_HUNDRED_PERCENT || +// fixedDiscount > maxDiscountFromCurrent +// ) revert Auctioneer_InvalidParams(); + +// // Set auction data +// AuctionData storage auction = auctionData[id_]; +// auction.oracle = oracle; +// auction.fixedDiscount = fixedDiscount; +// auction.conversionMul = conversionMul; +// auction.conversionFactor = conversionFactor; +// auction.minPrice = oraclePrice.mulDivUp( +// ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, +// ONE_HUNDRED_PERCENT +// ); +// } + +// /* ========== TELLER FUNCTIONS ========== */ + +// function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { +// // Calculate the payout from the market price and return +// return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); +// } + +// /* ========== VIEW FUNCTIONS ========== */ + +// /// @inheritdoc IOracleFixedDiscountAuctioneer +// function marketPrice(uint256 id_) public view override returns (uint256) { +// // Get auction data +// AuctionData memory auction = auctionData[id_]; + +// // Get oracle price +// uint256 oraclePrice = auction.oracle.currentPrice(id_); + +// // Revert if oracle price is 0 +// if (oraclePrice == 0) revert Auctioneer_OraclePriceZero(); + +// // Apply conversion factor +// if (auction.conversionMul) { +// oraclePrice *= auction.conversionFactor; +// } else { +// oraclePrice /= auction.conversionFactor; +// } + +// // Apply fixed discount +// uint256 price = oraclePrice.mulDivUp( +// uint256(ONE_HUNDRED_PERCENT - auction.fixedDiscount), +// uint256(ONE_HUNDRED_PERCENT) +// ); + +// // Compare the current price to the minimum price and return the maximum +// return price > auction.minPrice ? price : auction.minPrice; +// } +// } diff --git a/src/modules/auctions/OSDA.sol b/src/modules/auctions/OSDA.sol index 5598d61c..4dd3c882 100644 --- a/src/modules/auctions/OSDA.sol +++ b/src/modules/auctions/OSDA.sol @@ -1,195 +1,195 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; - -interface IOracleSequentialDutchAuctioneer is IMaxPayoutAuctioneer { - /// @notice Auction pricing data - struct AuctionData { - IBondOracle oracle; // oracle to use for equilibrium price - uint48 baseDiscount; // base discount from the oracle price to be used to determine equilibrium price - bool conversionMul; // whether to multiply (true) or divide (false) oracle price by conversion factor - uint256 conversionFactor; // conversion factor for oracle price to market price scale - uint256 minPrice; // minimum price for the auction - uint48 decaySpeed; // market price decay speed (discount achieved over a target deposit interval) - bool tuning; // whether or not the auction tunes the equilibrium price - } - - /* ========== VIEW FUNCTIONS ========== */ - - /// @notice Calculate current market price of payout token in quote tokens - /// @param id_ ID of market - /// @return Price for market in configured decimals (see MarketParams) - /// @dev price is derived from the equation - // - // p(t) = max(min_p, p_o * (1 - d) * (1 + k * r(t))) - // - // where - // p: price - // min_p: minimum price - // p_o: oracle price - // d: base discount - // - // k: decay speed - // k = l / i_d * t_d - // where - // l: market length - // i_d: deposit interval - // t_d: target interval discount - // - // r(t): percent difference of expected capacity and actual capacity at time t - // r(t) = (ec(t) - c(t)) / ic - // where - // ec(t): expected capacity at time t (assumes capacity is expended linearly over the duration) - // ec(t) = ic * (l - t) / l - // c(t): capacity remaining at time t - // ic = initial capacity - // - // if price is below minimum price, minimum price is returned - function marketPrice(uint256 id_) external view override returns (uint256); -} - - -import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; -import {IFixedPriceAuctioneer} from "src/interfaces/IFixedPriceAuctioneer.sol"; -import {OracleHelper} from "src/lib/OracleHelper.sol"; - -contract OracleSequentialDutchAuctioneer is MaxPayoutAuctioneer, IOracleSequentialDutchAuctioneer { - /* ========== ERRORS ========== */ - error Auctioneer_OraclePriceZero(); - - /* ========== STATE ========== */ - - mapping(uint256 id => AuctionData) internal auctionData; - - /* ========== CONSTRUCTOR ========== */ - - constructor( - IAggregator aggregator_, - address guardian_, - Authority authority_ - ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} - - /* ========== MARKET FUNCTIONS ========== */ - - function __createMarket( - uint256 id_, - CoreData memory core_, - StyleData memory style_, - bytes memory params_ - ) internal override { - // Decode params - ( - IBondOracle oracle, - uint48 baseDiscount, - uint48 maxDiscountFromCurrent, - uint48 targetIntervalDiscount - ) = abi.decode(params_, (IBondOracle, uint48, uint48)); - - // Validate oracle - (uint256 oraclePrice, uint256 conversionFactor, bool conversionMul) = OracleHelper - .validateOracle(id_, oracle, core_.quoteToken, core_.payoutToken, fixedDiscount); - - // Validate discounts - if ( - baseDiscount >= ONE_HUNDRED_PERCENT || - maxDiscountFromCurrent > ONE_HUNDRED_PERCENT || - baseDiscount > maxDiscountFromCurrent - ) revert Auctioneer_InvalidParams(); - - // Set auction data - AuctionData storage auction = auctionData[id_]; - auction.oracle = oracle; - auction.baseDiscount = baseDiscount; - auction.conversionMul = conversionMul; - auction.conversionFactor = conversionFactor; - auction.minPrice = oraclePrice.mulDivUp( - ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, - ONE_HUNDRED_PERCENT - ); - } - - /* ========== TELLER FUNCTIONS ========== */ - - function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { - // Calculate the payout from the market price and return - return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); - } - - /* ========== VIEW FUNCTIONS ========== */ - - /// @inheritdoc IOracleSequentialDutchAuctioneer - function marketPrice(uint256 id_) public view override returns (uint256) { - // Get auction data - AuctionData memory auction = auctionData[id_]; - - // Get oracle price - uint256 price = auction.oracle.currentPrice(id_); - - // Revert if oracle price is 0 - if (price == 0) revert Auctioneer_OraclePriceZero(); - - // Apply conversion factor - if (auction.conversionMul) { - price *= auction.conversionFactor; - } else { - price /= auction.conversionFactor; - } - - // Apply base discount - price = price.mulDivUp( - uint256(ONE_HUNDRED_PERCENT - auction.baseDiscount), - uint256(ONE_HUNDRED_PERCENT) - ); - - // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point - uint256 initialCapacity = market.capacity + - (market.capacityInQuote ? market.purchased : market.sold); - - // Compute seconds remaining until market will conclude - uint256 conclusion = uint256(term.conclusion); - uint256 timeRemaining = conclusion - block.timestamp; - - // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point - // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold - uint256 expectedCapacity = initialCapacity.mulDiv( - timeRemaining, - conclusion - uint256(term.start) - ); - - // Price is increased or decreased based on how far the market is ahead or behind - // Intuition: - // If the time neutral capacity is higher than the initial capacity, then the market is undersold and price should be discounted - // If the time neutral capacity is lower than the initial capacity, then the market is oversold and price should be increased - // - // This implementation uses a linear price decay - // P(t) = P(0) * (1 + k * (X(t) - C(t) / C(0))) - // P(t): price at time t - // P(0): target price of the market provided by oracle + base discount (see IOSDA.MarketParams) - // k: decay speed of the market - // k = L / I * d, where L is the duration/length of the market, I is the deposit interval, and d is the target interval discount. - // X(t): expected capacity of the market at time t. - // X(t) = C(0) * t / L. - // C(t): actual capacity of the market at time t. - // C(0): initial capacity of the market provided by the user (see IOSDA.MarketParams). - uint256 decay; - if (expectedCapacity > core.capacity) { - decay = - ONE_HUNDRED_PERCENT + - (auction.decaySpeed * (expectedCapacity - core.capacity)) / - initialCapacity; - } else { - // If actual capacity is greater than expected capacity, we need to check for underflows - // The decay has a minimum value of 0 since that will reduce the price to 0 as well. - uint256 factor = (auction.decaySpeed * (core.capacity - expectedCapacity)) / - initialCapacity; - decay = ONE_HUNDRED_PERCENT > factor ? ONE_HUNDRED_PERCENT - factor : 0; - } - - // Apply decay to price (could be negative decay - i.e. a premium to the equilibrium) - price = price.mulDivUp(decay, ONE_HUNDRED_PERCENT); - - // Compare the current price to the minimum price and return the maximum - return price > auction.minPrice ? price : auction.minPrice; - } -} +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; + +// import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; + +// interface IOracleSequentialDutchAuctioneer is IMaxPayoutAuctioneer { +// /// @notice Auction pricing data +// struct AuctionData { +// IBondOracle oracle; // oracle to use for equilibrium price +// uint48 baseDiscount; // base discount from the oracle price to be used to determine equilibrium price +// bool conversionMul; // whether to multiply (true) or divide (false) oracle price by conversion factor +// uint256 conversionFactor; // conversion factor for oracle price to market price scale +// uint256 minPrice; // minimum price for the auction +// uint48 decaySpeed; // market price decay speed (discount achieved over a target deposit interval) +// bool tuning; // whether or not the auction tunes the equilibrium price +// } + +// /* ========== VIEW FUNCTIONS ========== */ + +// /// @notice Calculate current market price of payout token in quote tokens +// /// @param id_ ID of market +// /// @return Price for market in configured decimals (see MarketParams) +// /// @dev price is derived from the equation +// // +// // p(t) = max(min_p, p_o * (1 - d) * (1 + k * r(t))) +// // +// // where +// // p: price +// // min_p: minimum price +// // p_o: oracle price +// // d: base discount +// // +// // k: decay speed +// // k = l / i_d * t_d +// // where +// // l: market length +// // i_d: deposit interval +// // t_d: target interval discount +// // +// // r(t): percent difference of expected capacity and actual capacity at time t +// // r(t) = (ec(t) - c(t)) / ic +// // where +// // ec(t): expected capacity at time t (assumes capacity is expended linearly over the duration) +// // ec(t) = ic * (l - t) / l +// // c(t): capacity remaining at time t +// // ic = initial capacity +// // +// // if price is below minimum price, minimum price is returned +// function marketPrice(uint256 id_) external view override returns (uint256); +// } + + +// import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; +// import {IFixedPriceAuctioneer} from "src/interfaces/IFixedPriceAuctioneer.sol"; +// import {OracleHelper} from "src/lib/OracleHelper.sol"; + +// contract OracleSequentialDutchAuctioneer is MaxPayoutAuctioneer, IOracleSequentialDutchAuctioneer { +// /* ========== ERRORS ========== */ +// error Auctioneer_OraclePriceZero(); + +// /* ========== STATE ========== */ + +// mapping(uint256 id => AuctionData) internal auctionData; + +// /* ========== CONSTRUCTOR ========== */ + +// constructor( +// IAggregator aggregator_, +// address guardian_, +// Authority authority_ +// ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} + +// /* ========== MARKET FUNCTIONS ========== */ + +// function __createMarket( +// uint256 id_, +// CoreData memory core_, +// StyleData memory style_, +// bytes memory params_ +// ) internal override { +// // Decode params +// ( +// IBondOracle oracle, +// uint48 baseDiscount, +// uint48 maxDiscountFromCurrent, +// uint48 targetIntervalDiscount +// ) = abi.decode(params_, (IBondOracle, uint48, uint48)); + +// // Validate oracle +// (uint256 oraclePrice, uint256 conversionFactor, bool conversionMul) = OracleHelper +// .validateOracle(id_, oracle, core_.quoteToken, core_.payoutToken, fixedDiscount); + +// // Validate discounts +// if ( +// baseDiscount >= ONE_HUNDRED_PERCENT || +// maxDiscountFromCurrent > ONE_HUNDRED_PERCENT || +// baseDiscount > maxDiscountFromCurrent +// ) revert Auctioneer_InvalidParams(); + +// // Set auction data +// AuctionData storage auction = auctionData[id_]; +// auction.oracle = oracle; +// auction.baseDiscount = baseDiscount; +// auction.conversionMul = conversionMul; +// auction.conversionFactor = conversionFactor; +// auction.minPrice = oraclePrice.mulDivUp( +// ONE_HUNDRED_PERCENT - maxDiscountFromCurrent, +// ONE_HUNDRED_PERCENT +// ); +// } + +// /* ========== TELLER FUNCTIONS ========== */ + +// function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { +// // Calculate the payout from the market price and return +// return amount_.mulDiv(styleData[id_].scale, marketPrice(id_)); +// } + +// /* ========== VIEW FUNCTIONS ========== */ + +// /// @inheritdoc IOracleSequentialDutchAuctioneer +// function marketPrice(uint256 id_) public view override returns (uint256) { +// // Get auction data +// AuctionData memory auction = auctionData[id_]; + +// // Get oracle price +// uint256 price = auction.oracle.currentPrice(id_); + +// // Revert if oracle price is 0 +// if (price == 0) revert Auctioneer_OraclePriceZero(); + +// // Apply conversion factor +// if (auction.conversionMul) { +// price *= auction.conversionFactor; +// } else { +// price /= auction.conversionFactor; +// } + +// // Apply base discount +// price = price.mulDivUp( +// uint256(ONE_HUNDRED_PERCENT - auction.baseDiscount), +// uint256(ONE_HUNDRED_PERCENT) +// ); + +// // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point +// uint256 initialCapacity = market.capacity + +// (market.capacityInQuote ? market.purchased : market.sold); + +// // Compute seconds remaining until market will conclude +// uint256 conclusion = uint256(term.conclusion); +// uint256 timeRemaining = conclusion - block.timestamp; + +// // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point +// // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold +// uint256 expectedCapacity = initialCapacity.mulDiv( +// timeRemaining, +// conclusion - uint256(term.start) +// ); + +// // Price is increased or decreased based on how far the market is ahead or behind +// // Intuition: +// // If the time neutral capacity is higher than the initial capacity, then the market is undersold and price should be discounted +// // If the time neutral capacity is lower than the initial capacity, then the market is oversold and price should be increased +// // +// // This implementation uses a linear price decay +// // P(t) = P(0) * (1 + k * (X(t) - C(t) / C(0))) +// // P(t): price at time t +// // P(0): target price of the market provided by oracle + base discount (see IOSDA.MarketParams) +// // k: decay speed of the market +// // k = L / I * d, where L is the duration/length of the market, I is the deposit interval, and d is the target interval discount. +// // X(t): expected capacity of the market at time t. +// // X(t) = C(0) * t / L. +// // C(t): actual capacity of the market at time t. +// // C(0): initial capacity of the market provided by the user (see IOSDA.MarketParams). +// uint256 decay; +// if (expectedCapacity > core.capacity) { +// decay = +// ONE_HUNDRED_PERCENT + +// (auction.decaySpeed * (expectedCapacity - core.capacity)) / +// initialCapacity; +// } else { +// // If actual capacity is greater than expected capacity, we need to check for underflows +// // The decay has a minimum value of 0 since that will reduce the price to 0 as well. +// uint256 factor = (auction.decaySpeed * (core.capacity - expectedCapacity)) / +// initialCapacity; +// decay = ONE_HUNDRED_PERCENT > factor ? ONE_HUNDRED_PERCENT - factor : 0; +// } + +// // Apply decay to price (could be negative decay - i.e. a premium to the equilibrium) +// price = price.mulDivUp(decay, ONE_HUNDRED_PERCENT); + +// // Compare the current price to the minimum price and return the maximum +// return price > auction.minPrice ? price : auction.minPrice; +// } +// } diff --git a/src/modules/auctions/SDA.sol b/src/modules/auctions/SDA.sol index 194e6735..214bd6bd 100644 --- a/src/modules/auctions/SDA.sol +++ b/src/modules/auctions/SDA.sol @@ -1,368 +1,368 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; - -interface ISequentialDutchAuctioneer is IMaxPayoutAuctioneer { - /// @notice Auction pricing data - struct AuctionData { - uint256 equilibriumPrice; // price at which the auction is balanced - uint256 minPrice; // minimum price for the auction - uint48 decaySpeed; // market price decay speed (discount achieved over a target deposit interval) - bool tuning; // whether or not the auction tunes the equilibrium price - } - - /// @notice Data needed for tuning equilibrium price - struct TuneData { - uint48 lastTune; // last timestamp when control variable was tuned - uint48 tuneInterval; // frequency of tuning - uint48 tuneAdjustmentDelay; // time to implement downward tuning adjustments - uint48 tuneGain; // parameter that controls how aggressive the tuning mechanism is, provided as a percentage with 3 decimals, i.e. 1% = 1_000 - uint256 tuneIntervalCapacity; // capacity expected to be used during a tuning interval - uint256 tuneBelowCapacity; // capacity that the next tuning will occur at - } - - /// @notice Equilibrium price adjustment data - struct Adjustment { - uint256 change; - uint48 lastAdjustment; - uint48 timeToAdjusted; // how long until adjustment is complete - bool active; - } - - /* ========== ADMIN FUNCTIONS ========== */ - - // TODO determine if we should have minimum values for tune parameters - - /* ========== VIEW FUNCTIONS ========== */ - - /// @notice Calculate current market price of payout token in quote tokens - /// @param id_ ID of market - /// @return Price for market in configured decimals (see MarketParams) - /// @dev price is derived from the equation - // - // p(t) = max(min_p, p_eq * (1 + k * r(t))) - // - // where - // p: price - // min_p: minimum price - // p_eq: equilibrium price - // - // k: decay speed - // k = l / i_d * t_d - // where - // l: market length - // i_d: deposit interval - // t_d: target interval discount - // - // r(t): percent difference of expected capacity and actual capacity at time t - // r(t) = (ec(t) - c(t)) / ic - // where - // ec(t): expected capacity at time t (assumes capacity is expended linearly over the duration) - // ec(t) = ic * (l - t) / l - // c(t): capacity remaining at time t - // ic = initial capacity - // - // if price is below minimum price, minimum price is returned - function marketPrice(uint256 id_) external view override returns (uint256); -} - -import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; -import {ISequentialDutchAuctioneer} from "src/interfaces/ISequentialDutchAuctioneer.sol"; - -contract SequentialDutchAuctioneer is MaxPayoutAuctioneer, ISequentialDutchAuctioneer { - /* ========== STATE ========== */ - - mapping(uint256 id => AuctionData) public auctionData; - mapping(uint256 id => TuneData) public tuneData; - mapping(uint256 id => Adjustment) public adjustments; - - /* ========== CONSTRUCTOR ========== */ - - constructor( - IAggregator aggregator_, - address guardian_, - Authority authority_ - ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} - - /* ========== MARKET FUNCTIONS ========== */ - - function __createMarket( - uint256 id_, - CoreData memory core_, - StyleData memory style_, - bytes memory params_ - ) internal override { - // Decode provided params - // TODO - should we use a struct for this? that way it can be specified in the interface - ( - uint256 initialPrice, - uint256 minPrice, - uint48 targetIntervalDiscount, - bool tuning, - uint48 tuneInterval, - uint48 tuneAdjustmentDelay, - uint48 tuneGain - ) = abi.decode(params_, (uint256, uint256, uint48, bool, uint48, uint48, uint48)); - - // Validate auction data - if (initialPrice == 0) revert Auctioneer_InvalidParams(); - if (initialPrice < minPrice) revert Auctioneer_InitialPriceLessThanMin(); - if (targetIntervalDiscount >= ONE_HUNDRED_PERCENT) revert Auctioneer_InvalidParams(); - - // Set auction data - uint48 duration = core_.conclusion - core_.start; - - AuctionData storage auction = auctionData[id_]; - auction.equilibriumPrice = initialPrice; - auction.minPrice = minPrice; - auction.decaySpeed = (duration * targetIntervalDiscount) / style_.depositInterval; - auction.tuning = tuning; - - // Check if tuning is enabled - if (tuning) { - // Tune interval must be at least the deposit interval - // and atleast the minimum global tune interval - if (tuneInterval < style_.depositInterval || tuneInterval < minTuneInterval) - revert Auctioneer_InvalidParams(); - - // Tune adjustment delay must be less than or equal to the tune interval - if (tuneAdjustmentDelay > tuneInterval) revert Auctioneer_InvalidParams(); - - // Set tune data - TuneData storage tune = tuneData[id_]; - tune.lastTune = uint48(block.timestamp); - tune.tuneInterval = tuneInterval; - tune.tuneAdjustmentDelay = tuneAdjustmentDelay; - tune.tuneIntervalCapacity = core_.capacity.mulDiv(tuneInterval, duration); - tune.tuneBelowCapacity = core_.capacity - tune.tuneIntervalCapacity; - // TODO should we enforce a maximum tune gain? there is likely a level above which it will greatly misbehave - tune.tuneGain = tuneGain; - } - } - - /* ========== TELLER FUNCTIONS ========== */ - - function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { - // If tuning, apply any active adjustments to the equilibrium price - if (auctionData[id_].tuning) { - // The market equilibrium price can optionally be tuned to keep the market on schedule. - // When it is lowered, the change is carried out smoothly over the tuneAdjustmentDelay. - Adjustment storage adjustment = adjustments[id_]; - if (adjustment.active) { - // Update equilibrium price with adjusted price - auctionData[id_].equilibriumPrice = _adjustedEquilibriumPrice( - auctionData[id_].equilibriumPrice, - adjustment - ); - - // Update adjustment data - if (stillActive) { - adjustment.change -= adjustBy; - adjustment.timeToAdjusted -= secondsSince; - adjustment.lastAdjustment = time_; - } else { - adjustment.active = false; - } - } - } - - // Calculate payout - uint256 price = marketPrice(id_); - uint256 payout = amount_.mulDiv(styleData[id_].scale, price); - - // If tuning, attempt to tune the market - // The payout value is required and capacity isn't updated until we provide this data back to the top level function. - // Therefore, the function manually handles updates to capacity when tuning. - if (auction.tuning) _tune(id_, price); - - return payout; - } - - function _tune(uint256 id_, uint256 price_, uint256 amount_, uint256 payout_) internal { - CoreData memory core = coreData[id_]; - StyleData memory style = styleData[id_]; - AuctionData memory auction = auctionData[id_]; - TuneData storage tune = tuneData[id_]; - - // Market tunes in 2 situations: - // 1. If capacity has exceeded target since last tune adjustment and the market is oversold - // 2. If a tune interval has passed since last tune adjustment and the market is undersold - // - // Markets are created with a target capacity with the expectation that capacity will - // be utilized evenly over the duration of the market. - // The intuition with tuning is: - // - When the market is ahead of target capacity, we should tune based on capacity. - // - When the market is behind target capacity, we should tune based on time. - // - // Tuning is equivalent to using a P controller to adjust the price to stay on schedule with selling capacity. - // We don't want to make adjustments when the market is close to on schedule to avoid overcorrections. - // Adjustments should undershoot rather than overshoot the target. - - // Compute seconds remaining until market will conclude and total duration of market - uint256 currentTime = block.timestamp; - uint256 timeRemaining = uint256(core.conclusion - currentTime); - uint256 duration = uint256(core.conclusion - core.start); - - // Subtract amount / payout for this purchase from capacity since it hasn't been updated in the state yet. - // If it is greater than capacity, revert. - if (core.capacityInQuote ? amount_ > capacity : payout_ > capacity) - revert Auctioneer_InsufficientCapacity(); - uint256 capacity = capacityInQuote ? core.capacity - amount_ : core.capacity - payout_; - - // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point - uint256 initialCapacity = capacity + - (core.capacityInQuote ? core.purchased + amount_ : core.sold + payout_); - - // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point - // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold - uint256 expectedCapacity = initialCapacity.mulDiv(timeRemaining, duration); - - if ( - (capacity < tune.tuneBelowCapacity && capacity < expectedCapacity) || - (currentTime >= tune.lastTune + tune.tuneInterval && capacity > expectedCapacity) - ) { - // Calculate and apply tune adjustment - - // Calculate the percent delta expected and current capacity - uint256 delta = capacity > expectedCapacity - ? ((capacity - expectedCapacity) * ONE_HUNDRED_PERCENT) / initialCapacity - : ((expectedCapacity - capacity) * ONE_HUNDRED_PERCENT) / initialCapacity; - - // Do not tune if the delta is within a reasonable range based on the deposit interval - // Market capacity does not decrease continuously, but follows a step function - // based on purchases. If the capacity deviation is less than the amount of capacity in a - // deposit interval, then we should not tune. - if (delta < (style.depositInterval * ONE_HUNDRED_PERCENT) / duration) return; - - // Apply the controller gain to the delta to determine the amount of change - delta = (delta * tune.gain) / ONE_HUNDRED_PERCENT; - if (capacity > expectedCapacity) { - // Apply a tune adjustment since the market is undersold - - // Create an adjustment to lower the equilibrium price by delta percent over the tune adjustment delay - Adjustment storage adjustment = adjustments[id_]; - adjustment.active = true; - adjustment.change = auction.equilibriumPrice.mulDiv(delta, ONE_HUNDRED_PERCENT); - adjustment.lastAdjustment = currentTime; - adjustment.timeToAdjusted = tune.tuneAdjustmentDelay; - } else { - // Immediately tune up since the market is oversold - - // Increase equilibrium price by delta percent - auctionData[id_].equilibriumPrice = auction.equilibriumPrice.mulDiv( - ONE_HUNDRED_PERCENT + delta, - ONE_HUNDRED_PERCENT - ); - - // Set current adjustment to inactive (e.g. if we are re-tuning early) - adjustment.active = false; - } - - // Update tune data - tune.lastTune = currentTime; - tune.tuneBelowCapacity = capacity > tune.tuneIntervalCapacity - ? capacity - tune.tuneIntervalCapacity - : 0; - - // Calculate the correct payout to complete on time assuming each bond - // will be max size in the desired deposit interval for the remaining time - // - // i.e. market has 10 days remaining. deposit interval is 1 day. capacity - // is 10,000 TOKEN. max payout would be 1,000 TOKEN (10,000 * 1 / 10). - uint256 payoutCapacity = core.capacityInQuote - ? capacity.mulDiv(style.scale, price_) - : capacity; - styleData[id_].maxPayout = payoutCapacity.mulDiv( - uint256(style.depositInterval), - timeRemaining - ); - - emit Tuned(id_); - } - } - - /* ========== ADMIN FUNCTIONS ========== */ - /* ========== VIEW FUNCTIONS ========== */ - - function _adjustedEquilibriumPrice( - uint256 equilibriumPrice_, - Adjustment memory adjustment_ - ) internal view returns (uint256) { - // Adjustment should be active if passed to this function. - // Calculate the change to apply based on the time elapsed since the last adjustment. - uint256 timeElapsed = block.timestamp - adjustment_.lastAdjustment; - - // If timeElapsed is zero, return early since the adjustment has already been applied up to the present. - if (timeElapsed == 0) return equilibriumPrice_; - - uint256 timeToAdjusted = adjustment_.timeToAdjusted; - bool stillActive = timeElapsed < timeToAdjusted; - uint256 change = stillActive - ? adjustment_.change.mulDiv(timeElapsed, timeToAdjusted) - : adjustment_.change; - return equilibriumPrice_ - change; - } - - /// @inheritdoc ISequentialDutchAuctioneer - function marketPrice(uint256 id_) public view override returns (uint256) { - CoreData memory core = coreData[id_]; - AuctionData memory auction = auctionData[id_]; - - // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point - uint256 initialCapacity = core.capacity + - (core.capacityInQuote ? core.purchased : core.sold); - - // Compute seconds remaining until market will conclude - uint256 timeRemaining = core.conclusion - block.timestamp; - - // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point - // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold - uint256 expectedCapacity = initialCapacity.mulDiv( - timeRemaining, - uint256(core.conclusion) - uint256(core.start) - ); - - // If tuning, apply any active adjustments to the equilibrium price before decaying - uint256 price = auction.equilibriumPrice; - if (auction.tuning) { - Adjustment memory adjustment = adjustments[id_]; - if (adjustment.active) price = _adjustedEquilibriumPrice(price, adjustment); - } - - // Price is increased or decreased based on how far the market is ahead or behind - // Intuition: - // If the time neutral capacity is higher than the initial capacity, then the market is undersold and price should be discounted - // If the time neutral capacity is lower than the initial capacity, then the market is oversold and price should be increased - // - // This implementation uses a linear price decay - // P(t) = P_eq * (1 + k * (X(t) - C(t) / C(0))) - // P(t): price at time t - // P_eq: equilibrium price of the market, initialized by issuer on market creation and potential updated via tuning - // k: decay speed of the market - // k = L / I * d, where L is the duration/length of the market, I is the deposit interval, and d is the target interval discount. - // X(t): expected capacity of the market at time t. - // X(t) = C(0) * t / L. - // C(t): actual capacity of the market at time t. - // C(0): initial capacity of the market provided by the user (see IOSDA.MarketParams). - uint256 decay; - if (expectedCapacity > core.capacity) { - decay = - ONE_HUNDRED_PERCENT + - (auction.decaySpeed * (expectedCapacity - core.capacity)) / - initialCapacity; - } else { - // If actual capacity is greater than expected capacity, we need to check for underflows - // The decay has a minimum value of 0 since that will reduce the price to 0 as well. - uint256 factor = (auction.decaySpeed * (core.capacity - expectedCapacity)) / - initialCapacity; - decay = ONE_HUNDRED_PERCENT > factor ? ONE_HUNDRED_PERCENT - factor : 0; - } - - // Apply decay to price (could be negative decay - i.e. a premium to the equilibrium) - price = price.mulDivUp(decay, ONE_HUNDRED_PERCENT); - - // Compare the current price to the minimum price and return the maximum - return price > auction.minPrice ? price : auction.minPrice; - } -} +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; + +// import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; + +// interface ISequentialDutchAuctioneer is IMaxPayoutAuctioneer { +// /// @notice Auction pricing data +// struct AuctionData { +// uint256 equilibriumPrice; // price at which the auction is balanced +// uint256 minPrice; // minimum price for the auction +// uint48 decaySpeed; // market price decay speed (discount achieved over a target deposit interval) +// bool tuning; // whether or not the auction tunes the equilibrium price +// } + +// /// @notice Data needed for tuning equilibrium price +// struct TuneData { +// uint48 lastTune; // last timestamp when control variable was tuned +// uint48 tuneInterval; // frequency of tuning +// uint48 tuneAdjustmentDelay; // time to implement downward tuning adjustments +// uint48 tuneGain; // parameter that controls how aggressive the tuning mechanism is, provided as a percentage with 3 decimals, i.e. 1% = 1_000 +// uint256 tuneIntervalCapacity; // capacity expected to be used during a tuning interval +// uint256 tuneBelowCapacity; // capacity that the next tuning will occur at +// } + +// /// @notice Equilibrium price adjustment data +// struct Adjustment { +// uint256 change; +// uint48 lastAdjustment; +// uint48 timeToAdjusted; // how long until adjustment is complete +// bool active; +// } + +// /* ========== ADMIN FUNCTIONS ========== */ + +// // TODO determine if we should have minimum values for tune parameters + +// /* ========== VIEW FUNCTIONS ========== */ + +// /// @notice Calculate current market price of payout token in quote tokens +// /// @param id_ ID of market +// /// @return Price for market in configured decimals (see MarketParams) +// /// @dev price is derived from the equation +// // +// // p(t) = max(min_p, p_eq * (1 + k * r(t))) +// // +// // where +// // p: price +// // min_p: minimum price +// // p_eq: equilibrium price +// // +// // k: decay speed +// // k = l / i_d * t_d +// // where +// // l: market length +// // i_d: deposit interval +// // t_d: target interval discount +// // +// // r(t): percent difference of expected capacity and actual capacity at time t +// // r(t) = (ec(t) - c(t)) / ic +// // where +// // ec(t): expected capacity at time t (assumes capacity is expended linearly over the duration) +// // ec(t) = ic * (l - t) / l +// // c(t): capacity remaining at time t +// // ic = initial capacity +// // +// // if price is below minimum price, minimum price is returned +// function marketPrice(uint256 id_) external view override returns (uint256); +// } + +// import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; +// import {ISequentialDutchAuctioneer} from "src/interfaces/ISequentialDutchAuctioneer.sol"; + +// contract SequentialDutchAuctioneer is MaxPayoutAuctioneer, ISequentialDutchAuctioneer { +// /* ========== STATE ========== */ + +// mapping(uint256 id => AuctionData) public auctionData; +// mapping(uint256 id => TuneData) public tuneData; +// mapping(uint256 id => Adjustment) public adjustments; + +// /* ========== CONSTRUCTOR ========== */ + +// constructor( +// IAggregator aggregator_, +// address guardian_, +// Authority authority_ +// ) MaxPayoutAuctioneer(aggregator_, guardian_, authority_) {} + +// /* ========== MARKET FUNCTIONS ========== */ + +// function __createMarket( +// uint256 id_, +// CoreData memory core_, +// StyleData memory style_, +// bytes memory params_ +// ) internal override { +// // Decode provided params +// // TODO - should we use a struct for this? that way it can be specified in the interface +// ( +// uint256 initialPrice, +// uint256 minPrice, +// uint48 targetIntervalDiscount, +// bool tuning, +// uint48 tuneInterval, +// uint48 tuneAdjustmentDelay, +// uint48 tuneGain +// ) = abi.decode(params_, (uint256, uint256, uint48, bool, uint48, uint48, uint48)); + +// // Validate auction data +// if (initialPrice == 0) revert Auctioneer_InvalidParams(); +// if (initialPrice < minPrice) revert Auctioneer_InitialPriceLessThanMin(); +// if (targetIntervalDiscount >= ONE_HUNDRED_PERCENT) revert Auctioneer_InvalidParams(); + +// // Set auction data +// uint48 duration = core_.conclusion - core_.start; + +// AuctionData storage auction = auctionData[id_]; +// auction.equilibriumPrice = initialPrice; +// auction.minPrice = minPrice; +// auction.decaySpeed = (duration * targetIntervalDiscount) / style_.depositInterval; +// auction.tuning = tuning; + +// // Check if tuning is enabled +// if (tuning) { +// // Tune interval must be at least the deposit interval +// // and atleast the minimum global tune interval +// if (tuneInterval < style_.depositInterval || tuneInterval < minTuneInterval) +// revert Auctioneer_InvalidParams(); + +// // Tune adjustment delay must be less than or equal to the tune interval +// if (tuneAdjustmentDelay > tuneInterval) revert Auctioneer_InvalidParams(); + +// // Set tune data +// TuneData storage tune = tuneData[id_]; +// tune.lastTune = uint48(block.timestamp); +// tune.tuneInterval = tuneInterval; +// tune.tuneAdjustmentDelay = tuneAdjustmentDelay; +// tune.tuneIntervalCapacity = core_.capacity.mulDiv(tuneInterval, duration); +// tune.tuneBelowCapacity = core_.capacity - tune.tuneIntervalCapacity; +// // TODO should we enforce a maximum tune gain? there is likely a level above which it will greatly misbehave +// tune.tuneGain = tuneGain; +// } +// } + +// /* ========== TELLER FUNCTIONS ========== */ + +// function __purchase(uint256 id_, uint256 amount_) internal override returns (uint256) { +// // If tuning, apply any active adjustments to the equilibrium price +// if (auctionData[id_].tuning) { +// // The market equilibrium price can optionally be tuned to keep the market on schedule. +// // When it is lowered, the change is carried out smoothly over the tuneAdjustmentDelay. +// Adjustment storage adjustment = adjustments[id_]; +// if (adjustment.active) { +// // Update equilibrium price with adjusted price +// auctionData[id_].equilibriumPrice = _adjustedEquilibriumPrice( +// auctionData[id_].equilibriumPrice, +// adjustment +// ); + +// // Update adjustment data +// if (stillActive) { +// adjustment.change -= adjustBy; +// adjustment.timeToAdjusted -= secondsSince; +// adjustment.lastAdjustment = time_; +// } else { +// adjustment.active = false; +// } +// } +// } + +// // Calculate payout +// uint256 price = marketPrice(id_); +// uint256 payout = amount_.mulDiv(styleData[id_].scale, price); + +// // If tuning, attempt to tune the market +// // The payout value is required and capacity isn't updated until we provide this data back to the top level function. +// // Therefore, the function manually handles updates to capacity when tuning. +// if (auction.tuning) _tune(id_, price); + +// return payout; +// } + +// function _tune(uint256 id_, uint256 price_, uint256 amount_, uint256 payout_) internal { +// CoreData memory core = coreData[id_]; +// StyleData memory style = styleData[id_]; +// AuctionData memory auction = auctionData[id_]; +// TuneData storage tune = tuneData[id_]; + +// // Market tunes in 2 situations: +// // 1. If capacity has exceeded target since last tune adjustment and the market is oversold +// // 2. If a tune interval has passed since last tune adjustment and the market is undersold +// // +// // Markets are created with a target capacity with the expectation that capacity will +// // be utilized evenly over the duration of the market. +// // The intuition with tuning is: +// // - When the market is ahead of target capacity, we should tune based on capacity. +// // - When the market is behind target capacity, we should tune based on time. +// // +// // Tuning is equivalent to using a P controller to adjust the price to stay on schedule with selling capacity. +// // We don't want to make adjustments when the market is close to on schedule to avoid overcorrections. +// // Adjustments should undershoot rather than overshoot the target. + +// // Compute seconds remaining until market will conclude and total duration of market +// uint256 currentTime = block.timestamp; +// uint256 timeRemaining = uint256(core.conclusion - currentTime); +// uint256 duration = uint256(core.conclusion - core.start); + +// // Subtract amount / payout for this purchase from capacity since it hasn't been updated in the state yet. +// // If it is greater than capacity, revert. +// if (core.capacityInQuote ? amount_ > capacity : payout_ > capacity) +// revert Auctioneer_InsufficientCapacity(); +// uint256 capacity = capacityInQuote ? core.capacity - amount_ : core.capacity - payout_; + +// // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point +// uint256 initialCapacity = capacity + +// (core.capacityInQuote ? core.purchased + amount_ : core.sold + payout_); + +// // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point +// // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold +// uint256 expectedCapacity = initialCapacity.mulDiv(timeRemaining, duration); + +// if ( +// (capacity < tune.tuneBelowCapacity && capacity < expectedCapacity) || +// (currentTime >= tune.lastTune + tune.tuneInterval && capacity > expectedCapacity) +// ) { +// // Calculate and apply tune adjustment + +// // Calculate the percent delta expected and current capacity +// uint256 delta = capacity > expectedCapacity +// ? ((capacity - expectedCapacity) * ONE_HUNDRED_PERCENT) / initialCapacity +// : ((expectedCapacity - capacity) * ONE_HUNDRED_PERCENT) / initialCapacity; + +// // Do not tune if the delta is within a reasonable range based on the deposit interval +// // Market capacity does not decrease continuously, but follows a step function +// // based on purchases. If the capacity deviation is less than the amount of capacity in a +// // deposit interval, then we should not tune. +// if (delta < (style.depositInterval * ONE_HUNDRED_PERCENT) / duration) return; + +// // Apply the controller gain to the delta to determine the amount of change +// delta = (delta * tune.gain) / ONE_HUNDRED_PERCENT; +// if (capacity > expectedCapacity) { +// // Apply a tune adjustment since the market is undersold + +// // Create an adjustment to lower the equilibrium price by delta percent over the tune adjustment delay +// Adjustment storage adjustment = adjustments[id_]; +// adjustment.active = true; +// adjustment.change = auction.equilibriumPrice.mulDiv(delta, ONE_HUNDRED_PERCENT); +// adjustment.lastAdjustment = currentTime; +// adjustment.timeToAdjusted = tune.tuneAdjustmentDelay; +// } else { +// // Immediately tune up since the market is oversold + +// // Increase equilibrium price by delta percent +// auctionData[id_].equilibriumPrice = auction.equilibriumPrice.mulDiv( +// ONE_HUNDRED_PERCENT + delta, +// ONE_HUNDRED_PERCENT +// ); + +// // Set current adjustment to inactive (e.g. if we are re-tuning early) +// adjustment.active = false; +// } + +// // Update tune data +// tune.lastTune = currentTime; +// tune.tuneBelowCapacity = capacity > tune.tuneIntervalCapacity +// ? capacity - tune.tuneIntervalCapacity +// : 0; + +// // Calculate the correct payout to complete on time assuming each bond +// // will be max size in the desired deposit interval for the remaining time +// // +// // i.e. market has 10 days remaining. deposit interval is 1 day. capacity +// // is 10,000 TOKEN. max payout would be 1,000 TOKEN (10,000 * 1 / 10). +// uint256 payoutCapacity = core.capacityInQuote +// ? capacity.mulDiv(style.scale, price_) +// : capacity; +// styleData[id_].maxPayout = payoutCapacity.mulDiv( +// uint256(style.depositInterval), +// timeRemaining +// ); + +// emit Tuned(id_); +// } +// } + +// /* ========== ADMIN FUNCTIONS ========== */ +// /* ========== VIEW FUNCTIONS ========== */ + +// function _adjustedEquilibriumPrice( +// uint256 equilibriumPrice_, +// Adjustment memory adjustment_ +// ) internal view returns (uint256) { +// // Adjustment should be active if passed to this function. +// // Calculate the change to apply based on the time elapsed since the last adjustment. +// uint256 timeElapsed = block.timestamp - adjustment_.lastAdjustment; + +// // If timeElapsed is zero, return early since the adjustment has already been applied up to the present. +// if (timeElapsed == 0) return equilibriumPrice_; + +// uint256 timeToAdjusted = adjustment_.timeToAdjusted; +// bool stillActive = timeElapsed < timeToAdjusted; +// uint256 change = stillActive +// ? adjustment_.change.mulDiv(timeElapsed, timeToAdjusted) +// : adjustment_.change; +// return equilibriumPrice_ - change; +// } + +// /// @inheritdoc ISequentialDutchAuctioneer +// function marketPrice(uint256 id_) public view override returns (uint256) { +// CoreData memory core = coreData[id_]; +// AuctionData memory auction = auctionData[id_]; + +// // Calculate initial capacity based on remaining capacity and amount sold/purchased up to this point +// uint256 initialCapacity = core.capacity + +// (core.capacityInQuote ? core.purchased : core.sold); + +// // Compute seconds remaining until market will conclude +// uint256 timeRemaining = core.conclusion - block.timestamp; + +// // Calculate expectedCapacity as the capacity expected to be bought or sold up to this point +// // Higher than current capacity means the market is undersold, lower than current capacity means the market is oversold +// uint256 expectedCapacity = initialCapacity.mulDiv( +// timeRemaining, +// uint256(core.conclusion) - uint256(core.start) +// ); + +// // If tuning, apply any active adjustments to the equilibrium price before decaying +// uint256 price = auction.equilibriumPrice; +// if (auction.tuning) { +// Adjustment memory adjustment = adjustments[id_]; +// if (adjustment.active) price = _adjustedEquilibriumPrice(price, adjustment); +// } + +// // Price is increased or decreased based on how far the market is ahead or behind +// // Intuition: +// // If the time neutral capacity is higher than the initial capacity, then the market is undersold and price should be discounted +// // If the time neutral capacity is lower than the initial capacity, then the market is oversold and price should be increased +// // +// // This implementation uses a linear price decay +// // P(t) = P_eq * (1 + k * (X(t) - C(t) / C(0))) +// // P(t): price at time t +// // P_eq: equilibrium price of the market, initialized by issuer on market creation and potential updated via tuning +// // k: decay speed of the market +// // k = L / I * d, where L is the duration/length of the market, I is the deposit interval, and d is the target interval discount. +// // X(t): expected capacity of the market at time t. +// // X(t) = C(0) * t / L. +// // C(t): actual capacity of the market at time t. +// // C(0): initial capacity of the market provided by the user (see IOSDA.MarketParams). +// uint256 decay; +// if (expectedCapacity > core.capacity) { +// decay = +// ONE_HUNDRED_PERCENT + +// (auction.decaySpeed * (expectedCapacity - core.capacity)) / +// initialCapacity; +// } else { +// // If actual capacity is greater than expected capacity, we need to check for underflows +// // The decay has a minimum value of 0 since that will reduce the price to 0 as well. +// uint256 factor = (auction.decaySpeed * (core.capacity - expectedCapacity)) / +// initialCapacity; +// decay = ONE_HUNDRED_PERCENT > factor ? ONE_HUNDRED_PERCENT - factor : 0; +// } + +// // Apply decay to price (could be negative decay - i.e. a premium to the equilibrium) +// price = price.mulDivUp(decay, ONE_HUNDRED_PERCENT); + +// // Compare the current price to the minimum price and return the maximum +// return price > auction.minPrice ? price : auction.minPrice; +// } +// } diff --git a/src/modules/auctions/TVGDA.sol b/src/modules/auctions/TVGDA.sol index 915be855..3e616e0b 100644 --- a/src/modules/auctions/TVGDA.sol +++ b/src/modules/auctions/TVGDA.sol @@ -1,122 +1,122 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; - -import "src/modules/auctions/bases/AtomicAuction.sol"; -import {SD59x18, sd, convert, uUNIT} from "prb-math/SD59x18.sol"; - -/// @notice Two variable GDA. Price is dependent on time. The other variable is independent of time. -abstract contract TVGDA { - /* ========== DATA STRUCTURES ========== */ - enum Decay { - Linear, - Exponential - } - - /// @notice Auction pricing data - struct AuctionData { - uint256 equilibriumPrice; // price at which the auction is balanced when emissions rate is on schedule and the independent variable is zero - uint256 minimumPrice; // minimum price the auction can reach - uint256 payoutScale; - uint256 quoteScale; - uint48 lastAuctionStart; - Decay priceDecayType; // type of decay to use for the market price - Decay variableDecayType; // type of decay to use for the independent variable - SD59x18 priceDecayConstant; // speed at which the price decays, as SD59x18. - SD59x18 variableDecayConstant; // speed at which the independent variable decays, as SD59x18. - SD59x18 emissionsRate; // number of tokens released per second, as SD59x18. Calculated as capacity / duration. - } -} - -contract TwoVariableGradualDutchAuctioneer is AtomicAuctionModule, TVGDA { - - /* ========== CONSTRUCTOR ========== */ - - constructor( - address auctionHouse_ - ) Module(auctionHouse_) {} - - /* ========== AUCTION FUNCTIONS ========== */ - - function _auction( - uint256 lotId_, - Lot memory lot_, - bytes memory params_ - ) internal override { - // Decode params - ( - uint256 equilibriumPrice_, // quote tokens per payout token, in quote token decimals - uint256 minimumPrice_, // fewest quote tokens per payout token acceptable for the auction, in quote token decimals - Decay priceDecayType_, - Decay variableDecayType_, - SD59x18 priceDecayConstant_, - SD59x18 variableDecayConstant_, - ) = abi.decode(params_, (uint256, uint256, Decay, Decay, SD59x18, SD59x18)); - - // Validate params - // TODO - - // Calculate scale from payout token decimals - uint256 payoutScale = 10 ** uint256(lot_.payoutToken.decimals()); - uint256 quoteScale = 10 ** uint256(lot_.quoteToken.decimals()); - - // Calculate emissions rate - uint256 payoutCapacity = lot_.capacityInQuote ? lot_.capacity.mulDiv(payoutScale, equilibriumPrice_) : lot_.capacity; - SD59x18 emissionsRate = sd(int256(payoutCapacity.mulDiv(uUNIT, (lot_.conclusion - lot_.start) * payoutScale))); - - // Set auction data - AuctionData storage auction = auctionData[lotId_]; - auction.equilibriumPrice = equilibriumPrice_; - auction.minimumPrice = minimumPrice_; - auction.payoutScale = payoutScale; - auction.quoteScale = quoteScale; - auction.lastAuctionStart = uint48(block.timestamp); - auction.priceDecayType = priceDecayType_; - auction.variableDecayType = variableDecayType_; - auction.priceDecayConstant = priceDecayConstant_; - auction.variableDecayConstant = variableDecayConstant_; - auction.emissionsRate = emissionsRate; - } - - function _purchase(uint256 lotId_, uint256 amount_, bytes memory variableInput_) internal override returns (uint256) { - // variableInput should be a single uint256 - uint256 variableInput = abi.decode(variableInput_, (uint256)); - - // Calculate payout amount for quote amount and seconds of emissions using GDA formula - (uint256 payout, uint48 secondsOfEmissions) = _payoutAndEmissionsFor(id_, amount_, variableInput); - - // Update last auction start with seconds of emissions - // Do not have to check that too many seconds have passed here - // since payout/amount is checked against capacity in the top-level function - auctionData[id_].lastAuctionStart += secondsOfEmissions; - - return payout; - } - - function _payoutAndEmissionsFor(uint256 lotId_, uint256 amount_, uint256 variableInput_) internal view override returns (uint256) { - // Load decay types for lot - priceDecayType = auctionData[lotId_].priceDecayType; - variableDecayType = auctionData[lotId_].variableDecayType; - - // Get payout information based on the various combinations of decay types - if (priceDecayType == Decay.Linear && variableDecayType == Decay.Linear) { - return _payoutForLinLin(auction, amount_, variableInput_); - } else if (priceDecayType == Decay.Linear && variableDecayType == Decay.Exponential) { - return _payoutForLinExp(auction, amount_, variableInput_); - } else if (priceDecayType == Decay.Exponential && variableDecayType == Decay.Linear) { - return _payoutForExpLin(auction, amount_, variableInput_); - } else { - return _payoutForExpExp(auction, amount_, variableInput_); - } - } - - // TODO problem with having a minimum price -> messes up the math and the inverse solution is not closed form - function _payoutForExpExp( - uint256 lotId_, - uint256 amount_, - uint256 variableInput_ - ) internal view returns (uint256, uint48) { - - } -} +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; + +// import "src/modules/auctions/bases/AtomicAuction.sol"; +// import {SD59x18, sd, convert, uUNIT} from "prb-math/SD59x18.sol"; + +// /// @notice Two variable GDA. Price is dependent on time. The other variable is independent of time. +// abstract contract TVGDA { +// /* ========== DATA STRUCTURES ========== */ +// enum Decay { +// Linear, +// Exponential +// } + +// /// @notice Auction pricing data +// struct AuctionData { +// uint256 equilibriumPrice; // price at which the auction is balanced when emissions rate is on schedule and the independent variable is zero +// uint256 minimumPrice; // minimum price the auction can reach +// uint256 payoutScale; +// uint256 quoteScale; +// uint48 lastAuctionStart; +// Decay priceDecayType; // type of decay to use for the market price +// Decay variableDecayType; // type of decay to use for the independent variable +// SD59x18 priceDecayConstant; // speed at which the price decays, as SD59x18. +// SD59x18 variableDecayConstant; // speed at which the independent variable decays, as SD59x18. +// SD59x18 emissionsRate; // number of tokens released per second, as SD59x18. Calculated as capacity / duration. +// } +// } + +// contract TwoVariableGradualDutchAuctioneer is AtomicAuctionModule, TVGDA { + +// /* ========== CONSTRUCTOR ========== */ + +// constructor( +// address auctionHouse_ +// ) Module(auctionHouse_) {} + +// /* ========== AUCTION FUNCTIONS ========== */ + +// function _auction( +// uint256 lotId_, +// Lot memory lot_, +// bytes memory params_ +// ) internal override { +// // Decode params +// ( +// uint256 equilibriumPrice_, // quote tokens per payout token, in quote token decimals +// uint256 minimumPrice_, // fewest quote tokens per payout token acceptable for the auction, in quote token decimals +// Decay priceDecayType_, +// Decay variableDecayType_, +// SD59x18 priceDecayConstant_, +// SD59x18 variableDecayConstant_, +// ) = abi.decode(params_, (uint256, uint256, Decay, Decay, SD59x18, SD59x18)); + +// // Validate params +// // TODO + +// // Calculate scale from payout token decimals +// uint256 payoutScale = 10 ** uint256(lot_.payoutToken.decimals()); +// uint256 quoteScale = 10 ** uint256(lot_.quoteToken.decimals()); + +// // Calculate emissions rate +// uint256 payoutCapacity = lot_.capacityInQuote ? lot_.capacity.mulDiv(payoutScale, equilibriumPrice_) : lot_.capacity; +// SD59x18 emissionsRate = sd(int256(payoutCapacity.mulDiv(uUNIT, (lot_.conclusion - lot_.start) * payoutScale))); + +// // Set auction data +// AuctionData storage auction = auctionData[lotId_]; +// auction.equilibriumPrice = equilibriumPrice_; +// auction.minimumPrice = minimumPrice_; +// auction.payoutScale = payoutScale; +// auction.quoteScale = quoteScale; +// auction.lastAuctionStart = uint48(block.timestamp); +// auction.priceDecayType = priceDecayType_; +// auction.variableDecayType = variableDecayType_; +// auction.priceDecayConstant = priceDecayConstant_; +// auction.variableDecayConstant = variableDecayConstant_; +// auction.emissionsRate = emissionsRate; +// } + +// function _purchase(uint256 lotId_, uint256 amount_, bytes memory variableInput_) internal override returns (uint256) { +// // variableInput should be a single uint256 +// uint256 variableInput = abi.decode(variableInput_, (uint256)); + +// // Calculate payout amount for quote amount and seconds of emissions using GDA formula +// (uint256 payout, uint48 secondsOfEmissions) = _payoutAndEmissionsFor(id_, amount_, variableInput); + +// // Update last auction start with seconds of emissions +// // Do not have to check that too many seconds have passed here +// // since payout/amount is checked against capacity in the top-level function +// auctionData[id_].lastAuctionStart += secondsOfEmissions; + +// return payout; +// } + +// function _payoutAndEmissionsFor(uint256 lotId_, uint256 amount_, uint256 variableInput_) internal view override returns (uint256) { +// // Load decay types for lot +// priceDecayType = auctionData[lotId_].priceDecayType; +// variableDecayType = auctionData[lotId_].variableDecayType; + +// // Get payout information based on the various combinations of decay types +// if (priceDecayType == Decay.Linear && variableDecayType == Decay.Linear) { +// return _payoutForLinLin(auction, amount_, variableInput_); +// } else if (priceDecayType == Decay.Linear && variableDecayType == Decay.Exponential) { +// return _payoutForLinExp(auction, amount_, variableInput_); +// } else if (priceDecayType == Decay.Exponential && variableDecayType == Decay.Linear) { +// return _payoutForExpLin(auction, amount_, variableInput_); +// } else { +// return _payoutForExpExp(auction, amount_, variableInput_); +// } +// } + +// // TODO problem with having a minimum price -> messes up the math and the inverse solution is not closed form +// function _payoutForExpExp( +// uint256 lotId_, +// uint256 amount_, +// uint256 variableInput_ +// ) internal view returns (uint256, uint48) { + +// } +// } diff --git a/src/modules/auctions/bases/AtomicAuction.sol b/src/modules/auctions/bases/AtomicAuction.sol index dee69fe6..b6329fc4 100644 --- a/src/modules/auctions/bases/AtomicAuction.sol +++ b/src/modules/auctions/bases/AtomicAuction.sol @@ -1,82 +1,82 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; -import "src/modules/Auction.sol"; +// import "src/modules/Auction.sol"; -abstract contract AtomicAuction { +// abstract contract AtomicAuction { - // ========== AUCTION INFORMATION ========== // +// // ========== AUCTION INFORMATION ========== // - function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); +// function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); - function priceFor(uint256 id_, uint256 payout_) public view virtual returns (uint256); +// function priceFor(uint256 id_, uint256 payout_) public view virtual returns (uint256); - function maxPayout(uint256 id_) public view virtual returns (uint256); +// function maxPayout(uint256 id_) public view virtual returns (uint256); - function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); -} +// function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); +// } -abstract contract AtomicAuctionModule is AuctionModule, AtomicAuction { +// abstract contract AtomicAuctionModule is AuctionModule, AtomicAuction { - // ========== AUCTION EXECUTION ========== // +// // ========== AUCTION EXECUTION ========== // - function purchase(uint256 id_, uint256 amount_, bytes calldata auctionData_) external override onlyParent returns (uint256 payout, bytes memory auctionOutput) { - Lot storage lot = lotData[id_]; +// function purchase(uint256 id_, uint256 amount_, bytes calldata auctionData_) external override onlyParent returns (uint256 payout, bytes memory auctionOutput) { +// Lot storage lot = lotData[id_]; - // Check if market is live, if not revert - if (!isLive(id_)) revert Auction_MarketNotActive(); +// // Check if market is live, if not revert +// if (!isLive(id_)) revert Auction_MarketNotActive(); - // Get payout from implementation-specific auction logic - payout = _purchase(id_, amount_); +// // Get payout from implementation-specific auction logic +// payout = _purchase(id_, amount_); - // Update Capacity +// // Update Capacity - // Capacity is either the number of payout tokens that the market can sell - // (if capacity in quote is false), - // - // or the number of quote tokens that the market can buy - // (if capacity in quote is true) +// // Capacity is either the number of payout tokens that the market can sell +// // (if capacity in quote is false), +// // +// // or the number of quote tokens that the market can buy +// // (if capacity in quote is true) - // If amount/payout is greater than capacity remaining, revert - if (lot.capacityInQuote ? amount_ > lot.capacity : payout > lot.capacity) - revert Auction_NotEnoughCapacity(); - // Capacity is decreased by the deposited or paid amount - lot.capacity -= lot.capacityInQuote ? amount_ : payout; +// // If amount/payout is greater than capacity remaining, revert +// if (lot.capacityInQuote ? amount_ > lot.capacity : payout > lot.capacity) +// revert Auction_NotEnoughCapacity(); +// // Capacity is decreased by the deposited or paid amount +// lot.capacity -= lot.capacityInQuote ? amount_ : payout; - // Markets keep track of how many quote tokens have been - // purchased, and how many payout tokens have been sold - lot.purchased += amount_; - lot.sold += payout; - } +// // Markets keep track of how many quote tokens have been +// // purchased, and how many payout tokens have been sold +// lot.purchased += amount_; +// lot.sold += payout; +// } - /// @dev implementation-specific purchase logic can be inserted by overriding this function - function _purchase( - uint256 id_, - uint256 amount_, - uint256 minAmountOut_ - ) internal virtual returns (uint256); +// /// @dev implementation-specific purchase logic can be inserted by overriding this function +// function _purchase( +// uint256 id_, +// uint256 amount_, +// uint256 minAmountOut_ +// ) internal virtual returns (uint256); - function bid(uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_) external override onlyParent { - revert Auction_NotImplemented(); - } +// function bid(uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_) external override onlyParent { +// revert Auction_NotImplemented(); +// } - function settle(uint256 id_, Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { - revert Auction_NotImplemented(); - } +// function settle(uint256 id_, Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { +// revert Auction_NotImplemented(); +// } - function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { - revert Auction_NotImplemented(); - } +// function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { +// revert Auction_NotImplemented(); +// } - // ========== AUCTION INFORMATION ========== // +// // ========== AUCTION INFORMATION ========== // - // These functions do not include fees. Policies can call these functions with the after-fee amount to get a payout value. - // TODO - // function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); +// // These functions do not include fees. Policies can call these functions with the after-fee amount to get a payout value. +// // TODO +// // function payoutFor(uint256 id_, uint256 amount_) public view virtual returns (uint256); - // function priceFor(uint256 id_, uint256 payout_) public view virtual returns (uint256); +// // function priceFor(uint256 id_, uint256 payout_) public view virtual returns (uint256); - // function maxPayout(uint256 id_) public view virtual returns (uint256); +// // function maxPayout(uint256 id_) public view virtual returns (uint256); - // function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); -} \ No newline at end of file +// // function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); +// } \ No newline at end of file diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index 029f0ed9..e17ba8e7 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -1,110 +1,110 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; -import "src/modules/Auction.sol"; +// import "src/modules/Auction.sol"; -// Spec -// - Allow issuers to create batch auctions to sell a payout token (or a derivative of it) for a quote token -// - Purchasers will submit orders off-chain that will be batched and submitted at the end of the auction by a Teller. All Tellers should be able to execute batches of orders? -// - The issuer will provide all relevant information for the running of the batch auction to this contract. Some parameters for derivatives of the payout will be passed onto and processed by the Teller. -// - The issuer should be able to auction different variables in the purchase. -// I need to determine if this should be handled by different batch auctioneers. -// - There are some overlap with the variables used in Live Auctions, so those should be abstracted and inherited so we don't repeat ourselves. -// - Data needed for a batch auction: -// - capacity - amount of tokens being sold (or bought?) -// - quote token -// - payout token -// - teller -// - teller params -// - duration (start & conclusion) -// - allowlist -// - amount sold & amount purchased - do we need to track this since it is just for historical purposes? can we emit the data in an event? -// - minimum value to settle auction - minimum value for whatever parameter is being auctioned. -// need to think if we need to have a maximum value option, but it can probably just use an inverse. -// - info to tell the teller what the auctioned value is and how to settle the auction. need to think on this more +// // Spec +// // - Allow issuers to create batch auctions to sell a payout token (or a derivative of it) for a quote token +// // - Purchasers will submit orders off-chain that will be batched and submitted at the end of the auction by a Teller. All Tellers should be able to execute batches of orders? +// // - The issuer will provide all relevant information for the running of the batch auction to this contract. Some parameters for derivatives of the payout will be passed onto and processed by the Teller. +// // - The issuer should be able to auction different variables in the purchase. +// // I need to determine if this should be handled by different batch auctioneers. +// // - There are some overlap with the variables used in Live Auctions, so those should be abstracted and inherited so we don't repeat ourselves. +// // - Data needed for a batch auction: +// // - capacity - amount of tokens being sold (or bought?) +// // - quote token +// // - payout token +// // - teller +// // - teller params +// // - duration (start & conclusion) +// // - allowlist +// // - amount sold & amount purchased - do we need to track this since it is just for historical purposes? can we emit the data in an event? +// // - minimum value to settle auction - minimum value for whatever parameter is being auctioned. +// // need to think if we need to have a maximum value option, but it can probably just use an inverse. +// // - info to tell the teller what the auctioned value is and how to settle the auction. need to think on this more -abstract contract BatchAuction { - error BatchAuction_NotConcluded(); +// abstract contract BatchAuction { +// error BatchAuction_NotConcluded(); - // ========== STATE VARIABLES ========== // +// // ========== STATE VARIABLES ========== // - mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; +// mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; - // ========== AUCTION INFORMATION ========== // +// // ========== AUCTION INFORMATION ========== // - // TODO add batch auction specific getters -} +// // TODO add batch auction specific getters +// } -abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { +// abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { - function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { - // TODO - // Validate inputs +// function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { +// // TODO +// // Validate inputs - // Execute user approval if provided? +// // Execute user approval if provided? - // Call implementation specific bid logic +// // Call implementation specific bid logic - // Store bid data - } +// // Store bid data +// } - function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { - // TODO - // Validate inputs +// function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { +// // TODO +// // Validate inputs - // Call implementation specific settle logic +// // Call implementation specific settle logic - // Store settle data - } +// // Store settle data +// } - function settle(uint256 id_, Auction.Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { - revert Auction_NotImplemented(); - } -} +// function settle(uint256 id_, Auction.Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { +// revert Auction_NotImplemented(); +// } +// } -abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { +// abstract contract OffChainBatchAuctionModule is AuctionModule, BatchAuction { - // ========== AUCTION EXECUTION ========== // +// // ========== AUCTION EXECUTION ========== // - function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { - revert Auction_NotImplemented(); - } +// function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { +// revert Auction_NotImplemented(); +// } - function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { - revert Auction_NotImplemented(); - } - - /// @notice Settle a batch auction with the provided bids - function settle(uint256 id_, Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { - Lot storage lot = lotData[id_]; +// function settle(uint256 id_) external override onlyParent returns (uint256[] memory amountsOut) { +// revert Auction_NotImplemented(); +// } - // Must be past the conclusion time to settle - if (uint48(block.timestamp) < lotData[id_].conclusion) revert BatchAuction_NotConcluded(); +// /// @notice Settle a batch auction with the provided bids +// function settle(uint256 id_, Bid[] memory bids_) external override onlyParent returns (uint256[] memory amountsOut) { +// Lot storage lot = lotData[id_]; - // Bids must not be greater than the capacity - uint256 len = bids_.length; - uint256 sum; - if (lot.capacityInQuote) { - for (uint256 i; i < len; i++) { - sum += bids_[i].amount; - } - if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); - } else { - for (uint256 i; i < len; i++) { - sum += bids_[i].minAmountOut; - } - if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); - } +// // Must be past the conclusion time to settle +// if (uint48(block.timestamp) < lotData[id_].conclusion) revert BatchAuction_NotConcluded(); - // TODO other generic validation? - // Check approvals in the Auctioneer since it handles token transfers +// // Bids must not be greater than the capacity +// uint256 len = bids_.length; +// uint256 sum; +// if (lot.capacityInQuote) { +// for (uint256 i; i < len; i++) { +// sum += bids_[i].amount; +// } +// if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); +// } else { +// for (uint256 i; i < len; i++) { +// sum += bids_[i].minAmountOut; +// } +// if (sum > lot.capacity) revert Auction_NotEnoughCapacity(); +// } - // Get amounts out from implementation-specific auction logic - amountsOut = _settle(id_, bids_); - } +// // TODO other generic validation? +// // Check approvals in the Auctioneer since it handles token transfers - function _settle(uint256 id_, Bid[] memory bids_) internal virtual returns (uint256[] memory amountsOut); +// // Get amounts out from implementation-specific auction logic +// amountsOut = _settle(id_, bids_); +// } -} \ No newline at end of file +// function _settle(uint256 id_, Bid[] memory bids_) internal virtual returns (uint256[] memory amountsOut); + +// } \ No newline at end of file diff --git a/src/modules/auctions/bases/DiscreteAuction.sol b/src/modules/auctions/bases/DiscreteAuction.sol index 9d1bed23..72067da5 100644 --- a/src/modules/auctions/bases/DiscreteAuction.sol +++ b/src/modules/auctions/bases/DiscreteAuction.sol @@ -1,183 +1,185 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; -import "src/modules/auctions/bases/AtomicAuction.sol"; +// import "src/modules/auctions/bases/AtomicAuction.sol"; -abstract contract DiscreteAuction { - /* ========== ERRORS ========== */ - error Auction_MaxPayoutExceeded(); +// import {Auction} from "src/modules/Auction.sol"; - /* ========== DATA STRUCTURES ========== */ - struct StyleData { - uint48 depositInterval; // target interval between deposits - uint256 maxPayout; // maximum payout for a single purchase - uint256 scale; // stored scale for auction price - } +// abstract contract DiscreteAuction { +// /* ========== ERRORS ========== */ +// error Auction_MaxPayoutExceeded(); - /* ========== STATE ========== */ +// /* ========== DATA STRUCTURES ========== */ +// struct StyleData { +// uint48 depositInterval; // target interval between deposits +// uint256 maxPayout; // maximum payout for a single purchase +// uint256 scale; // stored scale for auction price +// } - /// @notice Minimum deposit interval for a discrete auction - uint48 public minDepositInterval; +// /* ========== STATE ========== */ - mapping(uint256 lotId => StyleData style) public styleData; +// /// @notice Minimum deposit interval for a discrete auction +// uint48 public minDepositInterval; - /* ========== ADMIN FUNCTIONS ========== */ +// mapping(uint256 lotId => StyleData style) public styleData; - /// @notice Set the minimum deposit interval - /// @notice Access controlled - /// @param depositInterval_ Minimum deposit interval in seconds - function setMinDepositInterval(uint48 depositInterval_) external; +// /* ========== ADMIN FUNCTIONS ========== */ - /* ========== VIEW FUNCTIONS ========== */ +// /// @notice Set the minimum deposit interval +// /// @notice Access controlled +// /// @param depositInterval_ Minimum deposit interval in seconds +// function setMinDepositInterval(uint48 depositInterval_) external; - /// @notice Calculate current auction price of base token in quote tokens - /// @param id_ ID of auction - /// @return Price for auction in configured decimals - function auctionPrice(uint256 id_) external view returns (uint256); +// /* ========== VIEW FUNCTIONS ========== */ - /// @notice Scale value to use when converting between quote token and base token amounts with auctionPrice() - /// @param id_ ID of auction - /// @return Scaling factor for auction in configured decimals - function auctionScale(uint256 id_) external view returns (uint256); +// /// @notice Calculate current auction price of base token in quote tokens +// /// @param id_ ID of auction +// /// @return Price for auction in configured decimals +// function auctionPrice(uint256 id_) external view returns (uint256); - function maxPayout(uint256 id_) external view returns(uint256); -} +// /// @notice Scale value to use when converting between quote token and base token amounts with auctionPrice() +// /// @param id_ ID of auction +// /// @return Scaling factor for auction in configured decimals +// function auctionScale(uint256 id_) external view returns (uint256); -abstract contract DiscreteAuctionModule is AtomicAuctionModule, DiscreteAuction { +// function maxPayout(uint256 id_) external view returns(uint256); +// } - /* ========== CONSTRUCTOR ========== */ +// abstract contract DiscreteAuctionModule is AtomicAuctionModule, DiscreteAuction { - constructor( - address auctionHouse_ - ) AtomicAuctionModule(auctionHouse_) { - minDepositInterval = 1 hours; - } +// /* ========== CONSTRUCTOR ========== */ - /* ========== MARKET FUNCTIONS ========== */ +// constructor( +// address auctionHouse_ +// ) AtomicAuctionModule(auctionHouse_) { +// minDepositInterval = 1 hours; +// } - function _auction( - uint256 id_, - LotData memory lot_, - bytes calldata params_ - ) internal override { - // Decode provided params - (uint48 depositInterval, bytes calldata params) = abi.decode(params_, (uint48, bytes)); - - // Validate that deposit interval is in-bounds - uint48 duration = lot_.conclusion - lot_.start; - if (depositInterval < MIN_DEPOSIT_INTERVAL || depositInterval > duration) - revert Auctioneer_InvalidParams(); - - // Set style data - StyleData memory style = styleData[id_]; - style.depositInterval = depositInterval; - style.scale = 10 ** lot_.quoteToken.decimals(); - - // Call internal __createMarket function to store implementation-specific data - __createMarket(id, lot_, style, params); - - // Set max payout (depends on auctionPrice being available so must be done after __createMarket) - style.maxPayout = _baseCapacity(lot_).mulDiv(depositInterval, duration); - } - - /// @dev implementation-specific auction creation logic can be inserted by overriding this function - function __auction( - uint256 id_, - LotData memory lot_, - StyleData memory style_, - bytes memory params_ - ) internal virtual; - - /* ========== TELLER FUNCTIONS ========== */ - - function _purchase(uint256 id_, uint256 amount_) internal returns (uint256) { - // Get payout from implementation-specific purchase logic - uint256 payout = __purchaseBond(id_, amount_); - - // Check that payout is less than or equal to max payout - if (payout > styleData[id_].maxPayout) revert Auction_MaxPayoutExceeded(); - - return payout; - } - - /// @dev implementation-specific purchase logic can be inserted by overriding this function - function __purchase(uint256 id_, uint256 amount_) internal virtual returns (uint256); - - /* ========== ADMIN FUNCTIONS ========== */ - - /// @inheritdoc DiscreteAuction - function setMinDepositInterval(uint48 depositInterval_) external override onlyParent { - // Restricted to authorized addresses - - // Require min deposit interval to be less than minimum auction duration and at least 1 hour - if (depositInterval_ > minAuctionDuration || depositInterval_ < 1 hours) - revert Auction_InvalidParams(); - - minDepositInterval = depositInterval_; - } - - /* ========== VIEW FUNCTIONS ========== */ - - function _baseCapacity(LotData memory lot_) internal view returns (uint256) { - // Calculate capacity in terms of base tokens - // If capacity is in quote tokens, convert to base tokens with auction price - // Otherwise, return capacity as-is - return - lot_.capacityInQuote - ? lot_.capacity.mulDiv(styleData[id_].scale, auctionPrice(id_)) - : lot_.capacity; - } - - /// @inheritdoc DiscreteAuction - function auctionPrice(uint256 id_) public view virtual returns (uint256); - - /// @inheritdoc DiscreteAuction - function auctionScale(uint256 id_) external view override returns (uint256) { - return styleData[id_].scale; - } - - /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract - function payoutFor(uint256 id_, uint256 amount_) public view override onlyParent returns (uint256) { - // TODO handle payout greater than max payout - revert? - - // Calculate payout for amount of quote tokens - return amount_.mulDiv(styleData[id_].scale, auctionPrice(id_)); - } - - /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract - function priceFor(uint256 id_, uint256 payout_) public view override onlyParent returns (uint256) { - // TODO handle payout greater than max payout - revert? - - // Calculate price for payout in quote tokens - return payout_.mulDiv(auctionPrice(id_), styleData[id_].scale); - } - - /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract - function maxAmountAccepted(uint256 id_) external view override onlyParent returns (uint256) { - // Calculate maximum amount of quote tokens that correspond to max bond size - // Maximum of the maxPayout and the remaining capacity converted to quote tokens - LotData memory lot = lotData[id_]; - StyleData memory style = styleData[id_]; - uint256 price = auctionPrice(id_); - uint256 quoteCapacity = lot.capacityInQuote - ? lot.capacity - : lot.capacity.mulDiv(price, style.scale); - uint256 maxQuote = style.maxPayout.mulDiv(price, style.scale); - uint256 amountAccepted = quoteCapacity < maxQuote ? quoteCapacity : maxQuote; - - return amountAccepted; - } - - /// @notice Calculate max payout of the auction in base tokens - /// @dev Returns a dynamically calculated payout or the maximum set by the creator, whichever is less. - /// @param id_ ID of auction - /// @return Current max payout for the auction in base tokens - /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract - function maxPayout(uint256 id_) public view override onlyParent returns (uint256) { - // Convert capacity to base token units for comparison with max payout - uint256 capacity = _baseCapacity(lotData[id_]); - - // Cap max payout at the remaining capacity - return styleData[id_].maxPayout > capacity ? capacity : styleData[id_].maxPayout; - } -} \ No newline at end of file +// /* ========== MARKET FUNCTIONS ========== */ + +// function _auction( +// uint256 id_, +// Auction.Lot memory lot_, +// bytes calldata params_ +// ) internal override { +// // Decode provided params +// (uint48 depositInterval, bytes calldata params) = abi.decode(params_, (uint48, bytes)); + +// // Validate that deposit interval is in-bounds +// uint48 duration = lot_.conclusion - lot_.start; +// if (depositInterval < MIN_DEPOSIT_INTERVAL || depositInterval > duration) +// revert Auctioneer_InvalidParams(); + +// // Set style data +// StyleData memory style = styleData[id_]; +// style.depositInterval = depositInterval; +// style.scale = 10 ** lot_.quoteToken.decimals(); + +// // Call internal __createMarket function to store implementation-specific data +// __createMarket(id, lot_, style, params); + +// // Set max payout (depends on auctionPrice being available so must be done after __createMarket) +// style.maxPayout = _baseCapacity(lot_).mulDiv(depositInterval, duration); +// } + +// /// @dev implementation-specific auction creation logic can be inserted by overriding this function +// function __auction( +// uint256 id_, +// Auction.Lot memory lot_, +// StyleData memory style_, +// bytes memory params_ +// ) internal virtual; + +// /* ========== TELLER FUNCTIONS ========== */ + +// function _purchase(uint256 id_, uint256 amount_) internal returns (uint256) { +// // Get payout from implementation-specific purchase logic +// uint256 payout = __purchaseBond(id_, amount_); + +// // Check that payout is less than or equal to max payout +// if (payout > styleData[id_].maxPayout) revert Auction_MaxPayoutExceeded(); + +// return payout; +// } + +// /// @dev implementation-specific purchase logic can be inserted by overriding this function +// function __purchase(uint256 id_, uint256 amount_) internal virtual returns (uint256); + +// /* ========== ADMIN FUNCTIONS ========== */ + +// /// @inheritdoc DiscreteAuction +// function setMinDepositInterval(uint48 depositInterval_) external override onlyParent { +// // Restricted to authorized addresses + +// // Require min deposit interval to be less than minimum auction duration and at least 1 hour +// if (depositInterval_ > minAuctionDuration || depositInterval_ < 1 hours) +// revert Auction_InvalidParams(); + +// minDepositInterval = depositInterval_; +// } + +// /* ========== VIEW FUNCTIONS ========== */ + +// function _baseCapacity(Auction.Lot memory lot_) internal view returns (uint256) { +// // Calculate capacity in terms of base tokens +// // If capacity is in quote tokens, convert to base tokens with auction price +// // Otherwise, return capacity as-is +// return +// lot_.capacityInQuote +// ? lot_.capacity.mulDiv(styleData[id_].scale, auctionPrice(id_)) +// : lot_.capacity; +// } + +// /// @inheritdoc DiscreteAuction +// function auctionPrice(uint256 id_) public view virtual returns (uint256); + +// /// @inheritdoc DiscreteAuction +// function auctionScale(uint256 id_) external view override returns (uint256) { +// return styleData[id_].scale; +// } + +// /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract +// function payoutFor(uint256 id_, uint256 amount_) public view override onlyParent returns (uint256) { +// // TODO handle payout greater than max payout - revert? + +// // Calculate payout for amount of quote tokens +// return amount_.mulDiv(styleData[id_].scale, auctionPrice(id_)); +// } + +// /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract +// function priceFor(uint256 id_, uint256 payout_) public view override onlyParent returns (uint256) { +// // TODO handle payout greater than max payout - revert? + +// // Calculate price for payout in quote tokens +// return payout_.mulDiv(auctionPrice(id_), styleData[id_].scale); +// } + +// /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract +// function maxAmountAccepted(uint256 id_) external view override onlyParent returns (uint256) { +// // Calculate maximum amount of quote tokens that correspond to max bond size +// // Maximum of the maxPayout and the remaining capacity converted to quote tokens +// Auction.Lot memory lot = lotData[id_]; +// StyleData memory style = styleData[id_]; +// uint256 price = auctionPrice(id_); +// uint256 quoteCapacity = lot.capacityInQuote +// ? lot.capacity +// : lot.capacity.mulDiv(price, style.scale); +// uint256 maxQuote = style.maxPayout.mulDiv(price, style.scale); +// uint256 amountAccepted = quoteCapacity < maxQuote ? quoteCapacity : maxQuote; + +// return amountAccepted; +// } + +// /// @notice Calculate max payout of the auction in base tokens +// /// @dev Returns a dynamically calculated payout or the maximum set by the creator, whichever is less. +// /// @param id_ ID of auction +// /// @return Current max payout for the auction in base tokens +// /// @dev This function is gated by onlyParent because it does not include any fee logic, which is applied in the parent contract +// function maxPayout(uint256 id_) public view override onlyParent returns (uint256) { +// // Convert capacity to base token units for comparison with max payout +// uint256 capacity = _baseCapacity(lotData[id_]); + +// // Cap max payout at the remaining capacity +// return styleData[id_].maxPayout > capacity ? capacity : styleData[id_].maxPayout; +// } +// } \ No newline at end of file diff --git a/src/modules/derivatives/CliffVesting.sol b/src/modules/derivatives/CliffVesting.sol index af9c1ccb..4e5a6b16 100644 --- a/src/modules/derivatives/CliffVesting.sol +++ b/src/modules/derivatives/CliffVesting.sol @@ -1,74 +1,74 @@ -/// SPDX-License-Identifier: AGPL-3.0 -pragma solidity 0.8.19; +// /// SPDX-License-Identifier: AGPL-3.0 +// pragma solidity 0.8.19; -import {ClonesWithImmutableArgs} from "src/lib/clones/ClonesWithImmutableArgs.sol"; -import "src/modules/Derivative.sol"; +// import {ClonesWithImmutableArgs} from "src/lib/clones/ClonesWithImmutableArgs.sol"; +// import "src/modules/Derivative.sol"; -// TODO this only uses the ERC20 clones, need to convert to ERC6909 with optional ERC20 via wrapping the ERC6909 -contract CliffVesting is DerivativeModule { - using ClonesWithImmutableArgs for address; +// // TODO this only uses the ERC20 clones, need to convert to ERC6909 with optional ERC20 via wrapping the ERC6909 +// contract CliffVesting is DerivativeModule { +// using ClonesWithImmutableArgs for address; - // ========== EVENTS ========== // +// // ========== EVENTS ========== // - // ========== ERRORS ========== // +// // ========== ERRORS ========== // - // ========== STATE VARIABLES ========== // +// // ========== STATE VARIABLES ========== // - struct Cliff { - ERC20 base; - uint48 expiry; - } +// struct Cliff { +// ERC20 base; +// uint48 expiry; +// } - // ========== SUBMODULE SETUP ========== // +// // ========== SUBMODULE SETUP ========== // - constructor(Module parent_) Submodule(parent_) {} +// constructor(Module parent_) Submodule(parent_) {} - function KEYCODE() public pure override returns (SubKeycode) { - return toSubKeycode("VAULT.FIXED_EXPIRY"); - } +// function KEYCODE() public pure override returns (SubKeycode) { +// return toSubKeycode("VAULT.FIXED_EXPIRY"); +// } - // ========== DERIVATIVE MANAGEMENT ========== // +// // ========== DERIVATIVE MANAGEMENT ========== // - function deploy(bytes memory params_) external override onlyParent returns (bytes32) { - // Extract parameters from data - (ERC20 base, uint48 expiry) = _decodeAndNormalize(params_); +// function deploy(bytes memory params_) external override onlyParent returns (bytes32) { +// // Extract parameters from data +// (ERC20 base, uint48 expiry) = _decodeAndNormalize(params_); - // Revert if expiry is in the past - if (uint256(expiry) < block.timestamp) revert VAULT_InvalidParams(); +// // Revert if expiry is in the past +// if (uint256(expiry) < block.timestamp) revert VAULT_InvalidParams(); - // Get id from provided parameters - uint256 id = _computeId(base, expiry); +// // Get id from provided parameters +// uint256 id = _computeId(base, expiry); - // Load derivative token data from storage - Token storage t = tokenMetadata[id]; +// // Load derivative token data from storage +// Token storage t = tokenMetadata[id]; - // Check if derivative already exists, if not deploy it - if (!t.exists) { +// // Check if derivative already exists, if not deploy it +// if (!t.exists) { - // If wrapping, deploy ERC20 clone using ID as salt - // Note: token implementations implement view functions, but they are just a passthrough to get data from the tokenMetadata mapping on the vault contract - // Therefore, we don't need to store the data redundantly on the token contract - SubKeycode dType = SUBKEYCODE(); - if (wrapped_) { - // TODO think about collisions from different contract code and salts - t.wrapped = wrappedImplementations[dType].clone3(abi.encodePacked( - id, - address(this) - ), bytes32(id)); - } +// // If wrapping, deploy ERC20 clone using ID as salt +// // Note: token implementations implement view functions, but they are just a passthrough to get data from the tokenMetadata mapping on the vault contract +// // Therefore, we don't need to store the data redundantly on the token contract +// SubKeycode dType = SUBKEYCODE(); +// if (wrapped_) { +// // TODO think about collisions from different contract code and salts +// t.wrapped = wrappedImplementations[dType].clone3(abi.encodePacked( +// id, +// address(this) +// ), bytes32(id)); +// } - // Store derivative data - t.exists = true; - (t.name, t.symbol) = _getNameAndSymbol(base, expiry); - t.decimals = base.decimals(); - t.data = abi.encode(FixedExpiry(base, expiry)); +// // Store derivative data +// t.exists = true; +// (t.name, t.symbol) = _getNameAndSymbol(base, expiry); +// t.decimals = base.decimals(); +// t.data = abi.encode(FixedExpiry(base, expiry)); - // Emit event - emit DerivativeCreated(dType, id, t.wrapped, base, expiry); - } +// // Emit event +// emit DerivativeCreated(dType, id, t.wrapped, base, expiry); +// } @@ -76,75 +76,75 @@ contract CliffVesting is DerivativeModule { - // // Get address of fixed expiry token using salt - // address feToken = ClonesWithImmutableArgs.addressOfClone3(salt); +// // // Get address of fixed expiry token using salt +// // address feToken = ClonesWithImmutableArgs.addressOfClone3(salt); - // // Check if the token already exists. If not, deploy it. - // if (feToken.code.length == 0) { - // (string memory name, string memory symbol) = _getNameAndSymbol(underlying_, expiry); - // bytes memory tokenData = abi.encodePacked( - // bytes32(bytes(name)), - // bytes32(bytes(symbol)), - // uint8(base.decimals()), - // base, - // uint256(expiry), - // address(this) - // ); - // feToken = address(dStore.implementation).clone3(tokenData, salt); - // emit FixedExpiryERC20Created(feToken, base, expiry); - // } - // return bytes32(uint256(uint160(feToken))); - } +// // // Check if the token already exists. If not, deploy it. +// // if (feToken.code.length == 0) { +// // (string memory name, string memory symbol) = _getNameAndSymbol(underlying_, expiry); +// // bytes memory tokenData = abi.encodePacked( +// // bytes32(bytes(name)), +// // bytes32(bytes(symbol)), +// // uint8(base.decimals()), +// // base, +// // uint256(expiry), +// // address(this) +// // ); +// // feToken = address(dStore.implementation).clone3(tokenData, salt); +// // emit FixedExpiryERC20Created(feToken, base, expiry); +// // } +// // return bytes32(uint256(uint160(feToken))); +// } - function create(bytes memory data, uint256 amount) external override onlyParent returns (bytes memory) {} +// function create(bytes memory data, uint256 amount) external override onlyParent returns (bytes memory) {} - function redeem(bytes memory data, uint256 amount) external override onlyParent {} +// function redeem(bytes memory data, uint256 amount) external override onlyParent {} - // function batchRedeem(bytes[] memory data, uint256[] memory amounts) external override {} +// // function batchRedeem(bytes[] memory data, uint256[] memory amounts) external override {} - function exercise(bytes memory data, uint256 amount) external override {} +// function exercise(bytes memory data, uint256 amount) external override {} - function reclaim(bytes memory data) external override {} +// function reclaim(bytes memory data) external override {} - function convert(bytes memory data, uint256 amount) external override {} +// function convert(bytes memory data, uint256 amount) external override {} - // ========== DERIVATIVE INFORMATION ========== // +// // ========== DERIVATIVE INFORMATION ========== // - function exerciseCost(bytes memory data, uint256 amount) external view override returns (uint256) {} +// function exerciseCost(bytes memory data, uint256 amount) external view override returns (uint256) {} - function convertsTo(bytes memory data, uint256 amount) external view override returns (uint256) {} +// function convertsTo(bytes memory data, uint256 amount) external view override returns (uint256) {} - function derivativeForMarket(uint256 id_) external view override returns (bytes memory) {} +// function derivativeForMarket(uint256 id_) external view override returns (bytes memory) {} - // ========== INTERNAL FUNCTIONS ========== // +// // ========== INTERNAL FUNCTIONS ========== // - // unique to this submodule by using the hash of the params and then hashing again with the subkeycode - function _computeId(ERC20 base_, uint48 expiry_) internal pure returns (uint256) { - return uint256(keccak256( - abi.encodePacked( - SUBKEYCODE(), - keccak256( - abi.encode( - base_, - expiry_ - ) - ) - ) - )); - } +// // unique to this submodule by using the hash of the params and then hashing again with the subkeycode +// function _computeId(ERC20 base_, uint48 expiry_) internal pure returns (uint256) { +// return uint256(keccak256( +// abi.encodePacked( +// SUBKEYCODE(), +// keccak256( +// abi.encode( +// base_, +// expiry_ +// ) +// ) +// ) +// )); +// } - function _decodeAndNormalize(bytes memory params_) internal pure returns (ERC20 base, uint48 expiry) { - (base, expiry) = abi.decode(params_, (ERC20, uint48)); +// function _decodeAndNormalize(bytes memory params_) internal pure returns (ERC20 base, uint48 expiry) { +// (base, expiry) = abi.decode(params_, (ERC20, uint48)); - // Expiry is rounded to the nearest day at 0000 UTC (in seconds) since fixed expiry tokens - // are only unique to a day, not a specific timestamp. - expiry = uint48(expiry / 1 days) * 1 days; - } +// // Expiry is rounded to the nearest day at 0000 UTC (in seconds) since fixed expiry tokens +// // are only unique to a day, not a specific timestamp. +// expiry = uint48(expiry / 1 days) * 1 days; +// } - function computeId(bytes memory params_) external pure override returns (uint256) { - (ERC20 base, uint48 expiry) = _decodeAndNormalize(params_); - return _computeId(base, expiry); - } +// function computeId(bytes memory params_) external pure override returns (uint256) { +// (ERC20 base, uint48 expiry) = _decodeAndNormalize(params_); +// return _computeId(base, expiry); +// } -} \ No newline at end of file +// } \ No newline at end of file diff --git a/test/modules/WithModules/MockModule.sol b/test/modules/WithModules/MockModule.sol index 7ef53217..2aa9420f 100644 --- a/test/modules/WithModules/MockModule.sol +++ b/test/modules/WithModules/MockModule.sol @@ -2,12 +2,12 @@ pragma solidity 0.8.19; // Contracts -import {Module, Keycode, toKeycode} from "src/modules/Modules.sol"; +import {Module, Keycode, toKeycode, toModuleKeycode} from "src/modules/Modules.sol"; contract MockModule is Module { constructor(address _owner) Module(_owner) {} function KEYCODE() public pure override returns (Keycode) { - return toKeycode("MOCK"); + return toKeycode(toModuleKeycode("MOCK"), 1); } } From af7d8f2fb53078f3b5e0757430338ed20e197b3d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 2 Jan 2024 13:23:24 +0400 Subject: [PATCH 05/34] Fix Foundry configuration --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index c2a579b8..b75591b1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,7 +1,7 @@ [profile.default] src = "src" out = "out" -libs = ["lib"] +# libs = ["lib"] remappings = [ "ds-test/=lib/forge-std/lib/ds-test/src/", From 2d919986b5e40bc213e12b4e9cd06b2a6b874659 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 2 Jan 2024 17:02:20 +0400 Subject: [PATCH 06/34] Complete keycode validation --- design/FEATURES.md | 11 ++ src/modules/Modules.sol | 53 ++++---- test/modules/WithModules/Keycode.t.sol | 119 ++++++++++++++++++ test/modules/WithModules/Keycode.tree | 5 - test/modules/WithModules/ModuleKeycode.tree | 11 -- .../WithModules/getModuleForKeycode.t.sol | 4 +- .../WithModules/getModuleForKeycode.tree | 6 - .../getModuleForVersionedKeycode.t.sol | 5 +- .../getModuleForVersionedKeycode.tree | 8 -- 9 files changed, 165 insertions(+), 57 deletions(-) create mode 100644 test/modules/WithModules/Keycode.t.sol delete mode 100644 test/modules/WithModules/Keycode.tree delete mode 100644 test/modules/WithModules/ModuleKeycode.tree delete mode 100644 test/modules/WithModules/getModuleForKeycode.tree delete mode 100644 test/modules/WithModules/getModuleForVersionedKeycode.tree diff --git a/design/FEATURES.md b/design/FEATURES.md index 77efd680..62001ade 100644 --- a/design/FEATURES.md +++ b/design/FEATURES.md @@ -233,6 +233,17 @@ Fees can be taken by the protocol at the following points: - e.g. early exercising of vesting token, take a cut of residual collateral - would make sense when there's a profit for the bidder +### Module Management + +- Module references + - Specific module versions are referred to by keycodes that are stored as 7 bytes + - The first 5 bytes are the module name (e.g. "TEST"), followed by 2 bytes of 1 number each + - For example, `TEST12` would refer to version 12 of the `TEST` module +- When a new record is created: + - The calling contract will have a module keycode referring to the desired module type + - The module keycode will be used to get the versioned keycode of the latest version + - The record will reference the versioned keycode, so that subsequent usage will be tied to that implementation + ## Design Principles - The architecture should be modular, enabling support for different types of auction and derivatives diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index d46ee349..4450ddb9 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -7,17 +7,13 @@ import {Owned} from "lib/solmate/src/auth/Owned.sol"; // Keycode functions -// Type VersionedKeycode: 5 byte characters, 1 bytes for versions -// Descendent contracts will need to store the versioned keycode -// WithModules will track the versions of the modules installed - /// @notice 5 byte/character identifier for the Module /// @dev 3-5 characters from A-Z type ModuleKeycode is bytes5; -/// @notice 6 byte identifier for the Module, including version -/// @dev ModuleKeycode, followed by 1 byte for version -type Keycode is bytes6; +/// @notice 7 byte identifier for the Module, including version +/// @dev ModuleKeycode, followed by 2 characters from 0-9 +type Keycode is bytes7; error TargetNotAContract(address target_); error InvalidKeycode(Keycode keycode_); @@ -32,23 +28,28 @@ function fromModuleKeycode(ModuleKeycode moduleKeycode_) pure returns (bytes5) { // solhint-disable-next-line func-visibility function toKeycode(ModuleKeycode moduleKeycode_, uint8 version_) pure returns (Keycode) { - return Keycode.wrap(bytes6(abi.encode(moduleKeycode_, version_))); -} + bytes5 moduleKeycodeBytes = fromModuleKeycode(moduleKeycode_); + bytes memory keycodeBytes = new bytes(7); -// solhint-disable-next-line func-visibility -function fromKeycode(Keycode keycode_) pure returns (bytes6) { - return Keycode.unwrap(keycode_); + // Copy moduleKeycode_ into keycodeBytes + for (uint256 i; i < 5; i++) { + keycodeBytes[i] = moduleKeycodeBytes[i]; + } + + // Get the digits of the version + uint8 firstDigit = version_ / 10; + uint8 secondDigit = version_ % 10; + + // Convert the digits to bytes + keycodeBytes[5] = bytes1(firstDigit + 0x30); + keycodeBytes[6] = bytes1(secondDigit + 0x30); + + return Keycode.wrap(bytes7(keycodeBytes)); } // solhint-disable-next-line func-visibility -function unwrapKeycode(Keycode keycode_) pure returns (ModuleKeycode, uint8) { - bytes6 unwrappedKeycode = Keycode.unwrap(keycode_); - bytes memory keycodeBytes = new bytes(6); - for (uint256 i; i < 6; i++) { - keycodeBytes[i] = unwrappedKeycode[i]; - } - - return abi.decode(keycodeBytes, (ModuleKeycode, uint8)); +function fromKeycode(Keycode keycode_) pure returns (bytes7) { + return Keycode.unwrap(keycode_); } // solhint-disable-next-line func-visibility @@ -58,15 +59,18 @@ function ensureContract(address target_) view { // solhint-disable-next-line func-visibility function ensureValidKeycode(Keycode keycode_) pure { - bytes10 unwrapped = Keycode.unwrap(keycode_); - for (uint256 i; i < 5; ) { + bytes7 unwrapped = Keycode.unwrap(keycode_); + for (uint256 i; i < 7; ) { bytes1 char = unwrapped[i]; if (i < 3) { // First 3 characters must be A-Z if (char < 0x41 || char > 0x5A) revert InvalidKeycode(keycode_); + } else if (i < 5) { + // Next 2 characters after the first 3 can be A-Z or blank, 0-9, or . + if (char != 0x00 && (char < 0x41 || char > 0x5A) && (char < 0x30 || char > 0x39)) revert InvalidKeycode(keycode_); } else { - // Characters after the first 3 can be A-Z, blank, 0-9, or . - if (char != 0x00 && (char < 0x41 || char > 0x5A) && (char < 0x30 || char > 0x39) && char != 0x2E) revert InvalidKeycode(keycode_); + // Last 2 character must be 0-9 + if (char < 0x30 || char > 0x39) revert InvalidKeycode(keycode_); } unchecked { i++; @@ -74,7 +78,6 @@ function ensureValidKeycode(Keycode keycode_) pure { } } - abstract contract WithModules is Owned { // ========= ERRORS ========= // error InvalidModule(); diff --git a/test/modules/WithModules/Keycode.t.sol b/test/modules/WithModules/Keycode.t.sol new file mode 100644 index 00000000..8d1fc2f3 --- /dev/null +++ b/test/modules/WithModules/Keycode.t.sol @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {console2} from "forge-std/console2.sol"; + +import {ModuleKeycode, toModuleKeycode, fromModuleKeycode, Keycode, toKeycode, fromKeycode, ensureValidKeycode, InvalidKeycode} from "src/modules/Modules.sol"; + +contract KeycodeTest is Test { + function test_moduleKeycode() external { + ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); + assertEq(fromModuleKeycode(moduleKeycode), "TEST"); + } + + function test_ensureValidKeycode_singleDigitNumber() external { + ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); + Keycode t1_keycode = toKeycode(moduleKeycode, 1); + + bytes7 unwrapped = Keycode.unwrap(t1_keycode); + console2.logBytes1(unwrapped[5]); + console2.logBytes1(unwrapped[6]); + + ensureValidKeycode(t1_keycode); + } + + function test_ensureValidKeycode_doubleDigitNumber() external { + ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); + Keycode t1_keycode = toKeycode(moduleKeycode, 11); + + ensureValidKeycode(t1_keycode); + } + + function _modifyModuleKeycode(bytes5 moduleKeycode_, uint8 index_, uint8 character_) internal pure returns (bytes5) { + bytes memory moduleKeycodeBytes = abi.encodePacked(moduleKeycode_); + moduleKeycodeBytes[index_] = bytes1(character_); + return bytes5(moduleKeycodeBytes); + } + + function test_ensureValidKeycode_length() external { + ModuleKeycode t1_moduleKeycode = toModuleKeycode("TES"); + Keycode t1_keycode = toKeycode(t1_moduleKeycode, 11); + ensureValidKeycode(t1_keycode); + + ModuleKeycode t2_moduleKeycode = toModuleKeycode("TEST"); + Keycode t2_keycode = toKeycode(t2_moduleKeycode, 11); + ensureValidKeycode(t2_keycode); + assertFalse(fromKeycode(t1_keycode) == fromKeycode(t2_keycode)); + + ModuleKeycode t3_moduleKeycode = toModuleKeycode("TESTT"); + Keycode t3_keycode = toKeycode(t3_moduleKeycode, 21); + ensureValidKeycode(t3_keycode); + assertFalse(fromKeycode(t2_keycode) == fromKeycode(t3_keycode)); + + ModuleKeycode t4_moduleKeycode = toModuleKeycode("TESTT"); + Keycode t4_keycode = toKeycode(t4_moduleKeycode, 1); + ensureValidKeycode(t4_keycode); + assertFalse(fromKeycode(t3_keycode) == fromKeycode(t4_keycode)); + } + + function testRevert_ensureValidKeycode_invalidRequiredCharacter(uint8 character_, uint8 index_) external { + // Only manipulating the first 3 characters + vm.assume(index_ < 3); + + // Restrict the character to outside of A-Z + vm.assume(!(character_ >= 65 && character_ <= 90)); + + // Replace the fuzzed character + bytes5 moduleKeycodeInput = _modifyModuleKeycode("TST", index_, character_); + + ModuleKeycode moduleKeycode = toModuleKeycode(moduleKeycodeInput); + Keycode t1_keycode = toKeycode(moduleKeycode, 1); + + bytes memory err = abi.encodeWithSelector( + InvalidKeycode.selector, + t1_keycode + ); + vm.expectRevert(err); + + ensureValidKeycode(t1_keycode); + } + + function testRevert_ensureValidKeycode_invalidOptionalCharacter(uint8 character_, uint8 index_) external { + // Only manipulating the characters 4-5 + vm.assume(index_ < 3); + + // Restrict the character to outside of A-Z and blank + vm.assume(!(character_ >= 65 && character_ <= 90) && character_ != 0); + + // Replace the fuzzed character + bytes5 moduleKeycodeInput = _modifyModuleKeycode("TST", index_, character_); + + ModuleKeycode moduleKeycode = toModuleKeycode(moduleKeycodeInput); + Keycode t1_keycode = toKeycode(moduleKeycode, 1); + + bytes memory err = abi.encodeWithSelector( + InvalidKeycode.selector, + t1_keycode + ); + vm.expectRevert(err); + + ensureValidKeycode(t1_keycode); + } + + function testRevert_ensureValidKeycode_invalidVersion(uint8 version_) external { + // Restrict the version to outside of 0-99 + vm.assume(!(version_ >= 0 && version_ <= 99)); + + ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); + Keycode t1_keycode = toKeycode(moduleKeycode, version_); + + bytes memory err = abi.encodeWithSelector( + InvalidKeycode.selector, + t1_keycode + ); + vm.expectRevert(err); + + ensureValidKeycode(t1_keycode); + } +} diff --git a/test/modules/WithModules/Keycode.tree b/test/modules/WithModules/Keycode.tree deleted file mode 100644 index 803aae87..00000000 --- a/test/modules/WithModules/Keycode.tree +++ /dev/null @@ -1,5 +0,0 @@ -# KeycodeTest -## When -## Encodes a ModuleKeycode and version to a Keycode -## Encodes a ModuleKeycode and a two-digit version number -## Decodes a Keycode to a ModuleKeycode and version diff --git a/test/modules/WithModules/ModuleKeycode.tree b/test/modules/WithModules/ModuleKeycode.tree deleted file mode 100644 index 0384cbab..00000000 --- a/test/modules/WithModules/ModuleKeycode.tree +++ /dev/null @@ -1,11 +0,0 @@ -# ModuleKeycodeTest -## When a string is given -### It should encode the string to a ModuleKeycode -## When a ModuleKeycode is given -### It should decode the ModuleKeycode into bytes5 -## When a valid ModuleKeycode is given -### It should pass -## When any of the first three characters are not in the alphabet -### It should revert -## When any of the last two characters are not in the alphabet or blank -### It should revert diff --git a/test/modules/WithModules/getModuleForKeycode.t.sol b/test/modules/WithModules/getModuleForKeycode.t.sol index ffb421e2..fb7d3c2e 100644 --- a/test/modules/WithModules/getModuleForKeycode.t.sol +++ b/test/modules/WithModules/getModuleForKeycode.t.sol @@ -13,7 +13,7 @@ import {WithModules} from "src/modules/Modules.sol"; contract GetModuleForKeycodeTest is Test { WithModules internal withModules; - Module internal mockModule; + MockModule internal mockModule; function setUp() external { withModules = new MockWithModules(address(this)); @@ -28,9 +28,11 @@ contract GetModuleForKeycodeTest is Test { function test_WhenAMatchingModuleCannotBeFound() external whenAModuleIsInstalled { // It should revert + assertTrue(false); } function test_WhenAMatchingModuleAndVersionIsFound() external whenAModuleIsInstalled { // It should return the versioned keycode and module + assertTrue(false); } } diff --git a/test/modules/WithModules/getModuleForKeycode.tree b/test/modules/WithModules/getModuleForKeycode.tree deleted file mode 100644 index e2da0ef6..00000000 --- a/test/modules/WithModules/getModuleForKeycode.tree +++ /dev/null @@ -1,6 +0,0 @@ -GetModuleForKeycodeTest -└── When a module is installed - ├── When a matching module cannot be found - │ └── It should revert - └── When a matching module and version is found - └── It should return the versioned keycode and module diff --git a/test/modules/WithModules/getModuleForVersionedKeycode.t.sol b/test/modules/WithModules/getModuleForVersionedKeycode.t.sol index 9c39dbcc..d8fd90bf 100644 --- a/test/modules/WithModules/getModuleForVersionedKeycode.t.sol +++ b/test/modules/WithModules/getModuleForVersionedKeycode.t.sol @@ -13,7 +13,7 @@ import {WithModules} from "src/modules/Modules.sol"; contract GetModuleForVersionedKeycodeTest is Test { WithModules internal withModules; - Module internal mockModule; + MockModule internal mockModule; function setUp() external { withModules = new MockWithModules(address(this)); @@ -28,13 +28,16 @@ contract GetModuleForVersionedKeycodeTest is Test { function test_WhenAMatchingModuleAndVersionCannotBeFound() external whenAModuleIsInstalled { // It should revert. + assertTrue(false); } function test_WhenAMatchingModuleIsFoundButNoVersion() external whenAModuleIsInstalled { // It should revert. + assertTrue(false); } function test_WhenAMatchingModuleAndVersionIsFound() external whenAModuleIsInstalled { // It should return the module. + assertTrue(false); } } diff --git a/test/modules/WithModules/getModuleForVersionedKeycode.tree b/test/modules/WithModules/getModuleForVersionedKeycode.tree deleted file mode 100644 index a2a45651..00000000 --- a/test/modules/WithModules/getModuleForVersionedKeycode.tree +++ /dev/null @@ -1,8 +0,0 @@ -GetModuleForVersionedKeycodeTest -└── When a module is installed - ├── When a matching module and version cannot be found - │ └── It should revert. - ├── When a matching module is found but no version - │ └── It should revert. - └── When a matching module and version is found - └── It should return the module. From 4b701ee87337f0ffd17572b8e963eea8dfb201ca Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Tue, 2 Jan 2024 17:44:52 +0400 Subject: [PATCH 07/34] Tests for getting module with and without the version number --- src/modules/Modules.sol | 27 +++++++++- test/modules/WithModules/Keycode.t.sol | 43 ++++++++++++++-- test/modules/WithModules/MockModule.sol | 8 +++ .../WithModules/getModuleForKeycode.t.sol | 16 +++--- .../getModuleForVersionedKeycode.t.sol | 43 ---------------- .../WithModules/getModuleLatestVersion.t.sol | 51 +++++++++++++++++++ 6 files changed, 134 insertions(+), 54 deletions(-) delete mode 100644 test/modules/WithModules/getModuleForVersionedKeycode.t.sol create mode 100644 test/modules/WithModules/getModuleLatestVersion.t.sol diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 4450ddb9..e608f5e6 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -52,6 +52,18 @@ function fromKeycode(Keycode keycode_) pure returns (bytes7) { return Keycode.unwrap(keycode_); } +function versionFromKeycode(Keycode keycode_) pure returns (uint8) { + bytes7 unwrapped = Keycode.unwrap(keycode_); + uint8 firstDigit = uint8(unwrapped[5]) - 0x30; + uint8 secondDigit = uint8(unwrapped[6]) - 0x30; + return firstDigit * 10 + secondDigit; +} + +function moduleFromKeycode(Keycode keycode_) pure returns (ModuleKeycode) { + bytes7 unwrapped = Keycode.unwrap(keycode_); + return ModuleKeycode.wrap(bytes5(unwrapped)); +} + // solhint-disable-next-line func-visibility function ensureContract(address target_) view { if (target_.code.length == 0) revert TargetNotAContract(target_); @@ -76,6 +88,10 @@ function ensureValidKeycode(Keycode keycode_) pure { i++; } } + + // Check that the version is not 0 + // This is because the version is by default 0 if the module is not installed + if (versionFromKeycode(keycode_) == 0) revert InvalidKeycode(keycode_); } abstract contract WithModules is Owned { @@ -99,6 +115,9 @@ abstract contract WithModules is Owned { /// @notice Mapping of Keycode to Module address. mapping(Keycode => Module) public getModuleForKeycode; + /// @notice Mapping of ModuleKeycode to latest version. + mapping(ModuleKeycode => uint8) public getModuleLatestVersion; + /// @notice Mapping of Keycode to whether the module is sunset. mapping(Keycode => bool) public moduleSunset; @@ -115,6 +134,10 @@ abstract contract WithModules is Owned { getModuleForKeycode[keycode] = newModule_; modules.push(keycode); + // Update latest version + ModuleKeycode moduleKeycode = moduleFromKeycode(keycode); + getModuleLatestVersion[moduleKeycode] = versionFromKeycode(keycode); + // Initialize the module newModule_.INIT(); } @@ -227,4 +250,6 @@ abstract contract Module { /// @dev This function is called when the module is installed or upgraded by the module. /// @dev MUST BE GATED BY onlyParent. Used to encompass any initialization or upgrade logic. function INIT() external virtual onlyParent {} -} \ No newline at end of file +} + +// TODO handle version number diff --git a/test/modules/WithModules/Keycode.t.sol b/test/modules/WithModules/Keycode.t.sol index 8d1fc2f3..720aea9d 100644 --- a/test/modules/WithModules/Keycode.t.sol +++ b/test/modules/WithModules/Keycode.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {console2} from "forge-std/console2.sol"; -import {ModuleKeycode, toModuleKeycode, fromModuleKeycode, Keycode, toKeycode, fromKeycode, ensureValidKeycode, InvalidKeycode} from "src/modules/Modules.sol"; +import {ModuleKeycode, toModuleKeycode, fromModuleKeycode, Keycode, toKeycode, fromKeycode, versionFromKeycode, moduleFromKeycode, ensureValidKeycode, InvalidKeycode} from "src/modules/Modules.sol"; contract KeycodeTest is Test { function test_moduleKeycode() external { @@ -17,8 +17,6 @@ contract KeycodeTest is Test { Keycode t1_keycode = toKeycode(moduleKeycode, 1); bytes7 unwrapped = Keycode.unwrap(t1_keycode); - console2.logBytes1(unwrapped[5]); - console2.logBytes1(unwrapped[6]); ensureValidKeycode(t1_keycode); } @@ -101,6 +99,19 @@ contract KeycodeTest is Test { ensureValidKeycode(t1_keycode); } + function testRevert_ensureValidKeycode_zeroVersion() external { + ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); + Keycode t1_keycode = toKeycode(moduleKeycode, 0); + + bytes memory err = abi.encodeWithSelector( + InvalidKeycode.selector, + t1_keycode + ); + vm.expectRevert(err); + + ensureValidKeycode(t1_keycode); + } + function testRevert_ensureValidKeycode_invalidVersion(uint8 version_) external { // Restrict the version to outside of 0-99 vm.assume(!(version_ >= 0 && version_ <= 99)); @@ -116,4 +127,30 @@ contract KeycodeTest is Test { ensureValidKeycode(t1_keycode); } + + function test_versionFromKeycode() external { + ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); + Keycode t1_keycode = toKeycode(moduleKeycode, 1); + assertEq(versionFromKeycode(t1_keycode), 1); + + Keycode t2_keycode = toKeycode(moduleKeycode, 11); + assertEq(versionFromKeycode(t2_keycode), 11); + + Keycode t3_keycode = toKeycode(moduleKeycode, 99); + assertEq(versionFromKeycode(t3_keycode), 99); + + Keycode t4_keycode = toKeycode(moduleKeycode, 0); + assertEq(versionFromKeycode(t4_keycode), 0); + } + + function test_moduleFromKeycode() external { + Keycode t1_keycode = toKeycode(toModuleKeycode("TEST"), 1); + assertEq(fromModuleKeycode(moduleFromKeycode(t1_keycode)), "TEST"); + + Keycode t2_keycode = toKeycode(toModuleKeycode("TES"), 11); + assertEq(fromModuleKeycode(moduleFromKeycode(t2_keycode)), "TES"); + + Keycode t3_keycode = toKeycode(toModuleKeycode("TESTT"), 11); + assertEq(fromModuleKeycode(moduleFromKeycode(t3_keycode)), "TESTT"); + } } diff --git a/test/modules/WithModules/MockModule.sol b/test/modules/WithModules/MockModule.sol index 2aa9420f..9cba95a7 100644 --- a/test/modules/WithModules/MockModule.sol +++ b/test/modules/WithModules/MockModule.sol @@ -11,3 +11,11 @@ contract MockModule is Module { return toKeycode(toModuleKeycode("MOCK"), 1); } } + +contract MockUpgradedModule is Module { + constructor(address _owner) Module(_owner) {} + + function KEYCODE() public pure override returns (Keycode) { + return toKeycode(toModuleKeycode("MOCK"), 2); + } +} diff --git a/test/modules/WithModules/getModuleForKeycode.t.sol b/test/modules/WithModules/getModuleForKeycode.t.sol index fb7d3c2e..1a638b1e 100644 --- a/test/modules/WithModules/getModuleForKeycode.t.sol +++ b/test/modules/WithModules/getModuleForKeycode.t.sol @@ -9,7 +9,7 @@ import {MockWithModules} from "test/modules/WithModules/MockWithModules.sol"; import {MockModule} from "test/modules/WithModules/MockModule.sol"; // Contracts -import {WithModules} from "src/modules/Modules.sol"; +import {WithModules, Module} from "src/modules/Modules.sol"; contract GetModuleForKeycodeTest is Test { WithModules internal withModules; @@ -26,13 +26,15 @@ contract GetModuleForKeycodeTest is Test { _; } - function test_WhenAMatchingModuleCannotBeFound() external whenAModuleIsInstalled { - // It should revert - assertTrue(false); + function test_WhenAMatchingModuleCannotBeFound() external { + Module module = withModules.getModuleForKeycode(mockModule.KEYCODE()); + + assertEq(address(module), address(0)); } - function test_WhenAMatchingModuleAndVersionIsFound() external whenAModuleIsInstalled { - // It should return the versioned keycode and module - assertTrue(false); + function test_WhenAMatchingModuleIsFound() external whenAModuleIsInstalled { + Module module = withModules.getModuleForKeycode(mockModule.KEYCODE()); + + assertEq(address(module), address(mockModule)); } } diff --git a/test/modules/WithModules/getModuleForVersionedKeycode.t.sol b/test/modules/WithModules/getModuleForVersionedKeycode.t.sol deleted file mode 100644 index d8fd90bf..00000000 --- a/test/modules/WithModules/getModuleForVersionedKeycode.t.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -// Libraries -import {Test} from "forge-std/Test.sol"; - -// Mocks -import {MockWithModules} from "test/modules/WithModules/MockWithModules.sol"; -import {MockModule} from "test/modules/WithModules/MockModule.sol"; - -// Contracts -import {WithModules} from "src/modules/Modules.sol"; - -contract GetModuleForVersionedKeycodeTest is Test { - WithModules internal withModules; - MockModule internal mockModule; - - function setUp() external { - withModules = new MockWithModules(address(this)); - mockModule = new MockModule(address(withModules)); - } - - modifier whenAModuleIsInstalled() { - // Install the module - withModules.installModule(mockModule); - _; - } - - function test_WhenAMatchingModuleAndVersionCannotBeFound() external whenAModuleIsInstalled { - // It should revert. - assertTrue(false); - } - - function test_WhenAMatchingModuleIsFoundButNoVersion() external whenAModuleIsInstalled { - // It should revert. - assertTrue(false); - } - - function test_WhenAMatchingModuleAndVersionIsFound() external whenAModuleIsInstalled { - // It should return the module. - assertTrue(false); - } -} diff --git a/test/modules/WithModules/getModuleLatestVersion.t.sol b/test/modules/WithModules/getModuleLatestVersion.t.sol new file mode 100644 index 00000000..e3fee386 --- /dev/null +++ b/test/modules/WithModules/getModuleLatestVersion.t.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/WithModules/MockWithModules.sol"; +import {MockModule, MockUpgradedModule} from "test/modules/WithModules/MockModule.sol"; + +// Contracts +import {WithModules, toModuleKeycode} from "src/modules/Modules.sol"; + +contract GetModuleLatestVersionTest is Test { + WithModules internal withModules; + MockModule internal mockModule; + + function setUp() external { + withModules = new MockWithModules(address(this)); + mockModule = new MockModule(address(withModules)); + } + + modifier whenAModuleIsInstalled() { + // Install the module + withModules.installModule(mockModule); + _; + } + + function test_WhenAMatchingModuleCannotBeFound() external { + uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); + + assertEq(version, 0); + } + + function test_WhenAMatchingModuleIsFound() external whenAModuleIsInstalled { + uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); + + assertEq(version, 1); + } + + function test_WhenMultipleVersionsAreFound() external whenAModuleIsInstalled { + // Install an upgraded module + MockUpgradedModule upgradedMockModule = new MockUpgradedModule(address(withModules)); + // TODO change to upgradeModule + withModules.installModule(upgradedMockModule); + + uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); + + assertEq(version, 2); + } +} From e084195471df15c96e8b9005069c093723f87617 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 3 Jan 2024 15:47:11 +0400 Subject: [PATCH 08/34] Rename directory --- test/modules/{WithModules => Modules}/Keycode.t.sol | 0 test/modules/{WithModules => Modules}/MockModule.sol | 0 test/modules/{WithModules => Modules}/MockWithModules.sol | 0 .../{WithModules => Modules}/getModuleForKeycode.t.sol | 4 ++-- .../{WithModules => Modules}/getModuleLatestVersion.t.sol | 4 ++-- 5 files changed, 4 insertions(+), 4 deletions(-) rename test/modules/{WithModules => Modules}/Keycode.t.sol (100%) rename test/modules/{WithModules => Modules}/MockModule.sol (100%) rename test/modules/{WithModules => Modules}/MockWithModules.sol (100%) rename test/modules/{WithModules => Modules}/getModuleForKeycode.t.sol (87%) rename test/modules/{WithModules => Modules}/getModuleLatestVersion.t.sol (89%) diff --git a/test/modules/WithModules/Keycode.t.sol b/test/modules/Modules/Keycode.t.sol similarity index 100% rename from test/modules/WithModules/Keycode.t.sol rename to test/modules/Modules/Keycode.t.sol diff --git a/test/modules/WithModules/MockModule.sol b/test/modules/Modules/MockModule.sol similarity index 100% rename from test/modules/WithModules/MockModule.sol rename to test/modules/Modules/MockModule.sol diff --git a/test/modules/WithModules/MockWithModules.sol b/test/modules/Modules/MockWithModules.sol similarity index 100% rename from test/modules/WithModules/MockWithModules.sol rename to test/modules/Modules/MockWithModules.sol diff --git a/test/modules/WithModules/getModuleForKeycode.t.sol b/test/modules/Modules/getModuleForKeycode.t.sol similarity index 87% rename from test/modules/WithModules/getModuleForKeycode.t.sol rename to test/modules/Modules/getModuleForKeycode.t.sol index 1a638b1e..e3dba314 100644 --- a/test/modules/WithModules/getModuleForKeycode.t.sol +++ b/test/modules/Modules/getModuleForKeycode.t.sol @@ -5,8 +5,8 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; // Mocks -import {MockWithModules} from "test/modules/WithModules/MockWithModules.sol"; -import {MockModule} from "test/modules/WithModules/MockModule.sol"; +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModule} from "test/modules/Modules/MockModule.sol"; // Contracts import {WithModules, Module} from "src/modules/Modules.sol"; diff --git a/test/modules/WithModules/getModuleLatestVersion.t.sol b/test/modules/Modules/getModuleLatestVersion.t.sol similarity index 89% rename from test/modules/WithModules/getModuleLatestVersion.t.sol rename to test/modules/Modules/getModuleLatestVersion.t.sol index e3fee386..4fe8a32a 100644 --- a/test/modules/WithModules/getModuleLatestVersion.t.sol +++ b/test/modules/Modules/getModuleLatestVersion.t.sol @@ -5,8 +5,8 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; // Mocks -import {MockWithModules} from "test/modules/WithModules/MockWithModules.sol"; -import {MockModule, MockUpgradedModule} from "test/modules/WithModules/MockModule.sol"; +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModule, MockUpgradedModule} from "test/modules/Modules/MockModule.sol"; // Contracts import {WithModules, toModuleKeycode} from "src/modules/Modules.sol"; From 47d0304fd29df87418d44f243798c9b57793677e Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 3 Jan 2024 16:56:43 +0400 Subject: [PATCH 09/34] Versioned module implementation for installation. Documentation. --- src/modules/Modules.sol | 34 ++++++++-- test/modules/Modules/MockModule.sol | 8 +++ test/modules/Modules/installModule.t.sol | 80 ++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 test/modules/Modules/installModule.t.sol diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index e608f5e6..5d079fe6 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -98,11 +98,15 @@ abstract contract WithModules is Owned { // ========= ERRORS ========= // error InvalidModule(); error InvalidModuleUpgrade(Keycode keycode_); - error ModuleAlreadyInstalled(Keycode keycode_); + error ModuleAlreadyInstalled(ModuleKeycode moduleKeycode_, uint8 version_); error ModuleNotInstalled(Keycode keycode_); error ModuleExecutionReverted(bytes error_); error ModuleAlreadySunset(Keycode keycode_); + // ========= EVENTS ========= // + + event ModuleInstalled(ModuleKeycode indexed moduleKeycode_, uint8 indexed version_, address indexed address_); + // ========= CONSTRUCTOR ========= // constructor(address owner_) Owned(owner_) {} @@ -121,25 +125,43 @@ abstract contract WithModules is Owned { /// @notice Mapping of Keycode to whether the module is sunset. mapping(Keycode => bool) public moduleSunset; + /// @notice Installs a new module + /// @notice Subsequent versions should be installed via upgradeModule + /// @dev This function performs the following: + /// @dev - Validates the new module + /// @dev - Checks that the module (or other versions) is not already installed + /// @dev - Stores the module details + /// + /// @dev This function reverts if: + /// @dev - The caller is not the owner + /// @dev - The module is not a contract + /// @dev - The module has an invalid Keycode + /// @dev - The module (or other versions) is already installed + /// + /// @param newModule_ The new module function installModule(Module newModule_) external onlyOwner { // Validate new module and get its subkeycode Keycode keycode = _validateModule(newModule_); + ModuleKeycode moduleKeycode = moduleFromKeycode(keycode); + uint8 moduleVersion = versionFromKeycode(keycode); - // Check that a module with this keycode is not already installed + // Check that the module is not already installed // If this reverts, then the new module should be installed via upgradeModule - if (address(getModuleForKeycode[keycode]) != address(0)) - revert ModuleAlreadyInstalled(keycode); + uint8 moduleInstalledVersion = getModuleLatestVersion[moduleKeycode]; + if (moduleInstalledVersion > 0) + revert ModuleAlreadyInstalled(moduleKeycode, moduleInstalledVersion); // Store module in module getModuleForKeycode[keycode] = newModule_; modules.push(keycode); // Update latest version - ModuleKeycode moduleKeycode = moduleFromKeycode(keycode); - getModuleLatestVersion[moduleKeycode] = versionFromKeycode(keycode); + getModuleLatestVersion[moduleKeycode] = moduleVersion; // Initialize the module newModule_.INIT(); + + emit ModuleInstalled(moduleKeycode, moduleVersion, address(newModule_)); } /// @notice Prevents future use of module, but functionality remains for existing users. Modules should implement functionality such that creation functions are disabled if sunset. diff --git a/test/modules/Modules/MockModule.sol b/test/modules/Modules/MockModule.sol index 9cba95a7..e0aa5e73 100644 --- a/test/modules/Modules/MockModule.sol +++ b/test/modules/Modules/MockModule.sol @@ -12,6 +12,14 @@ contract MockModule is Module { } } +contract MockInvalidModule is Module { + constructor(address _owner) Module(_owner) {} + + function KEYCODE() public pure override returns (Keycode) { + return toKeycode(toModuleKeycode("INVA_"), 100); + } +} + contract MockUpgradedModule is Module { constructor(address _owner) Module(_owner) {} diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol new file mode 100644 index 00000000..8843b4d3 --- /dev/null +++ b/test/modules/Modules/installModule.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModule, MockUpgradedModule, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; + +// Contracts +import {WithModules, Module, moduleFromKeycode, InvalidKeycode} from "src/modules/Modules.sol"; + +contract InstallModule is Test { + WithModules internal withModules; + MockModule internal mockModule; + + function setUp() external { + withModules = new MockWithModules(address(this)); + mockModule = new MockModule(address(withModules)); + } + + function testReverts_whenPreviousVersionIsInstalled() external { + // Install version 1 + withModules.installModule(mockModule); + + Module upgradedModule = new MockUpgradedModule(address(withModules)); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(mockModule.KEYCODE()), 1); + vm.expectRevert(err); + + // Install version 2 + withModules.installModule(upgradedModule); + } + + function testReverts_whenSameVersionIsInstalled() external { + // Install version 1 + withModules.installModule(mockModule); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(mockModule.KEYCODE()), 1); + vm.expectRevert(err); + + // Install version 1 again + withModules.installModule(mockModule); + } + + function testReverts_whenNewerVersionIsInstalled() external { + // Install version 2 + Module upgradedModule = new MockUpgradedModule(address(withModules)); + withModules.installModule(upgradedModule); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(upgradedModule.KEYCODE()), 2); + vm.expectRevert(err); + + // Install version 1 + withModules.installModule(mockModule); + } + + function testReverts_invalidKeycode() external { + Module invalidModule = new MockInvalidModule(address(withModules)); + + bytes memory err = abi.encodeWithSelector(InvalidKeycode.selector, invalidModule.KEYCODE()); + vm.expectRevert(err); + + withModules.installModule(invalidModule); + } + + function test_success_whenNoPreviousVersionIsInstalled() external { + // Install the module + withModules.installModule(mockModule); + + // Check that the module is installed + Module module = withModules.getModuleForKeycode(mockModule.KEYCODE()); + assertEq(address(module), address(mockModule)); + + // Check that the latest version is recorded + uint8 version = withModules.getModuleLatestVersion(moduleFromKeycode(mockModule.KEYCODE())); + assertEq(version, 1); + } +} From d7ef6dae794e9c36a912f2d8615fe2b394f0cbd2 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 3 Jan 2024 17:11:34 +0400 Subject: [PATCH 10/34] Change contract name --- test/modules/Modules/installModule.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol index 8843b4d3..ad933f36 100644 --- a/test/modules/Modules/installModule.t.sol +++ b/test/modules/Modules/installModule.t.sol @@ -11,7 +11,7 @@ import {MockModule, MockUpgradedModule, MockInvalidModule} from "test/modules/Mo // Contracts import {WithModules, Module, moduleFromKeycode, InvalidKeycode} from "src/modules/Modules.sol"; -contract InstallModule is Test { +contract InstallModuleTest is Test { WithModules internal withModules; MockModule internal mockModule; From e0c1fea3df62b50a5e5c8111b25ee0e14b7e4a53 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 3 Jan 2024 17:39:38 +0400 Subject: [PATCH 11/34] Implementation and tests for upgradeModule --- src/modules/Modules.sol | 74 +++++++++----- .../Modules/getModuleLatestVersion.t.sol | 3 +- test/modules/Modules/installModule.t.sol | 7 +- test/modules/Modules/upgradeModule.t.sol | 97 +++++++++++++++++++ 4 files changed, 153 insertions(+), 28 deletions(-) create mode 100644 test/modules/Modules/upgradeModule.t.sol diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 5d079fe6..6dc9759a 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -99,7 +99,7 @@ abstract contract WithModules is Owned { error InvalidModule(); error InvalidModuleUpgrade(Keycode keycode_); error ModuleAlreadyInstalled(ModuleKeycode moduleKeycode_, uint8 version_); - error ModuleNotInstalled(Keycode keycode_); + error ModuleNotInstalled(ModuleKeycode moduleKeycode_, uint8 version_); error ModuleExecutionReverted(bytes error_); error ModuleAlreadySunset(Keycode keycode_); @@ -107,6 +107,8 @@ abstract contract WithModules is Owned { event ModuleInstalled(ModuleKeycode indexed moduleKeycode_, uint8 indexed version_, address indexed address_); + event ModuleUpgraded(ModuleKeycode indexed moduleKeycode_, uint8 indexed version_, address indexed address_); + // ========= CONSTRUCTOR ========= // constructor(address owner_) Owned(owner_) {} @@ -140,7 +142,7 @@ abstract contract WithModules is Owned { /// /// @param newModule_ The new module function installModule(Module newModule_) external onlyOwner { - // Validate new module and get its subkeycode + // Validate new module and get its keycode Keycode keycode = _validateModule(newModule_); ModuleKeycode moduleKeycode = moduleFromKeycode(keycode); uint8 moduleVersion = versionFromKeycode(keycode); @@ -167,7 +169,9 @@ abstract contract WithModules is Owned { /// @notice Prevents future use of module, but functionality remains for existing users. Modules should implement functionality such that creation functions are disabled if sunset. function sunsetModule(Keycode keycode_) external onlyOwner { // Check that the module is installed - if (!_moduleIsInstalled(keycode_)) revert ModuleNotInstalled(keycode_); + ModuleKeycode moduleKeycode_ = moduleFromKeycode(keycode_); + uint8 moduleVersion_ = versionFromKeycode(keycode_); + if (!_moduleIsInstalled(keycode_)) revert ModuleNotInstalled(moduleKeycode_, moduleVersion_); // Check that the module is not already sunset if (moduleSunset[keycode_]) revert ModuleAlreadySunset(keycode_); @@ -176,32 +180,50 @@ abstract contract WithModules is Owned { moduleSunset[keycode_] = true; } + /// @notice Upgrades an existing module + /// @dev This function performs the following: + /// @dev - Validates the new module + /// @dev - Checks that a prior version of the module is already installed + /// @dev - Stores the module details + /// @dev - Marks the previous version as sunset + /// + /// @dev This function reverts if: + /// @dev - The caller is not the owner + /// @dev - The module is not a contract + /// @dev - The module has an invalid Keycode + /// @dev - The module is not already installed + /// @dev - The same or newer module version is already installed + function upgradeModule(Module newModule_) external onlyOwner { + // Validate new module and get its keycode + Keycode keycode = _validateModule(newModule_); - // TODO may need to use proxies instead of this design to allow for upgrading due to a bug and keeping collateral in a derivative contract - // The downside is that you have the additional gas costs and potential for exploits in OCG - // It may be better to just not have the modules be upgradable - // Having a shutdown mechanism for a specific module and an entire auctionhouse version might be good as well. - // Though it would still need to allow for claiming of outstanding derivative tokens. + // Check that an earlier version of the module is installed + // If this reverts, then the new module should be installed via installModule + ModuleKeycode moduleKeycode = moduleFromKeycode(keycode); + uint8 moduleVersion = versionFromKeycode(keycode); + uint8 moduleInstalledVersion = getModuleLatestVersion[moduleKeycode]; + if (moduleInstalledVersion == 0) + revert ModuleNotInstalled(moduleKeycode, moduleInstalledVersion); - // NOTE: don't use upgradable modules. simply require a new module to be installed and sunset the old one to migrate functionality. - // This doesn't allow fixing a contract for existing users / data, but it prevents malicious upgrades. - // Versions can be used in the keycode to denote a new version of the same module, e.g. SDA v1.1 - // function upgradeModule(Module newModule_) external onlyOwner { - // // Validate new module and get its keycode - // Keycode keycode = _validateModule(newModule_); + if (moduleInstalledVersion >= moduleVersion) + revert ModuleAlreadyInstalled(moduleKeycode, moduleInstalledVersion); - // // Get the existing module, ensure that it's not zero and not the same as the new module - // // If this reverts due to no module being installed, then the new module should be installed via installModule - // Module oldModule = getModuleForKeycode[keycode]; - // if (oldModule == Module(address(0)) || oldModule == newModule_) - // revert InvalidModuleUpgrade(keycode); + // Update module records + getModuleForKeycode[keycode] = newModule_; + modules.push(keycode); - // // Update module in module - // getModuleForKeycode[keycode] = newModule_; + // Update latest version + getModuleLatestVersion[moduleKeycode] = moduleVersion; - // // Initialize the module - // newModule_.INIT(); - // } + // Sunset the previous version + Keycode previousKeycode = toKeycode(moduleKeycode, moduleInstalledVersion); + moduleSunset[previousKeycode] = true; + + // Initialize the module + newModule_.INIT(); + + emit ModuleUpgraded(moduleKeycode, moduleVersion, address(newModule_)); + } // Decide if we need this function, i.e. do we need to set any parameters or call permissioned functions on any modules? // Answer: yes, e.g. when setting default values on an Auction module, like minimum duration or minimum deposit interval @@ -227,7 +249,9 @@ abstract contract WithModules is Owned { function _getModuleIfInstalled(Keycode keycode_) internal view returns (address) { Module module = getModuleForKeycode[keycode_]; - if (address(module) == address(0)) revert ModuleNotInstalled(keycode_); + ModuleKeycode moduleKeycode_ = moduleFromKeycode(keycode_); + uint8 moduleVersion_ = versionFromKeycode(keycode_); + if (address(module) == address(0)) revert ModuleNotInstalled(moduleKeycode_, moduleVersion_); return address(module); } diff --git a/test/modules/Modules/getModuleLatestVersion.t.sol b/test/modules/Modules/getModuleLatestVersion.t.sol index 4fe8a32a..141532fd 100644 --- a/test/modules/Modules/getModuleLatestVersion.t.sol +++ b/test/modules/Modules/getModuleLatestVersion.t.sol @@ -41,8 +41,7 @@ contract GetModuleLatestVersionTest is Test { function test_WhenMultipleVersionsAreFound() external whenAModuleIsInstalled { // Install an upgraded module MockUpgradedModule upgradedMockModule = new MockUpgradedModule(address(withModules)); - // TODO change to upgradeModule - withModules.installModule(upgradedMockModule); + withModules.upgradeModule(upgradedMockModule); uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol index ad933f36..55e6b133 100644 --- a/test/modules/Modules/installModule.t.sol +++ b/test/modules/Modules/installModule.t.sol @@ -9,7 +9,7 @@ import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; import {MockModule, MockUpgradedModule, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; // Contracts -import {WithModules, Module, moduleFromKeycode, InvalidKeycode} from "src/modules/Modules.sol"; +import {WithModules, Module, Keycode, fromKeycode, toKeycode, toModuleKeycode, moduleFromKeycode, InvalidKeycode} from "src/modules/Modules.sol"; contract InstallModuleTest is Test { WithModules internal withModules; @@ -76,5 +76,10 @@ contract InstallModuleTest is Test { // Check that the latest version is recorded uint8 version = withModules.getModuleLatestVersion(moduleFromKeycode(mockModule.KEYCODE())); assertEq(version, 1); + + // Check that the modules array is updated + Keycode[] memory modules = withModules.getModules(); + assertEq(modules.length, 1); + assertEq(fromKeycode(modules[0]), fromKeycode(toKeycode(toModuleKeycode("MOCK"), 1))); } } diff --git a/test/modules/Modules/upgradeModule.t.sol b/test/modules/Modules/upgradeModule.t.sol new file mode 100644 index 00000000..178f270d --- /dev/null +++ b/test/modules/Modules/upgradeModule.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModule, MockUpgradedModule, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; + +// Contracts +import {WithModules, Module, Keycode, fromKeycode, toKeycode, toModuleKeycode, moduleFromKeycode, InvalidKeycode, moduleFromKeycode} from "src/modules/Modules.sol"; + +contract UpgradeModuleTest is Test { + WithModules internal withModules; + MockModule internal mockModule; + + function setUp() external { + withModules = new MockWithModules(address(this)); + mockModule = new MockModule(address(withModules)); + } + + function test_whenPreviousVersionIsInstalled() external { + // Install version 1 + withModules.installModule(mockModule); + + Module upgradedModule = new MockUpgradedModule(address(withModules)); + + // Upgrade to version 2 + withModules.upgradeModule(upgradedModule); + + // Check that the module is installed + Module upgradedModule_ = withModules.getModuleForKeycode(upgradedModule.KEYCODE()); + assertEq(address(upgradedModule_), address(upgradedModule)); + + // Check that the version is correct + uint8 upgradedModuleVersion_ = withModules.getModuleLatestVersion(moduleFromKeycode(upgradedModule.KEYCODE())); + assertEq(upgradedModuleVersion_, 2); + + // Check that the new version is NOT sunset + bool upgradedModuleIsSunset_ = withModules.moduleSunset(upgradedModule.KEYCODE()); + assertFalse(upgradedModuleIsSunset_); + + // Check that the previous version is still installed + Module previousModule_ = withModules.getModuleForKeycode(mockModule.KEYCODE()); + assertEq(address(previousModule_), address(mockModule)); + + // Check that the previous version is sunset + bool previousModuleIsSunset_ = withModules.moduleSunset(mockModule.KEYCODE()); + assertTrue(previousModuleIsSunset_); + + // Check that the modules array is updated + Keycode[] memory modules = withModules.getModules(); + assertEq(modules.length, 2); + assertEq(fromKeycode(modules[0]), fromKeycode(toKeycode(toModuleKeycode("MOCK"), 1))); + assertEq(fromKeycode(modules[1]), fromKeycode(toKeycode(toModuleKeycode("MOCK"), 2))); + } + + function testReverts_whenSameVersionIsInstalled() external { + // Install version 1 + withModules.installModule(mockModule); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(mockModule.KEYCODE()), 1); + vm.expectRevert(err); + + // Upgrade to version 1 + withModules.upgradeModule(mockModule); + } + + function testReverts_whenNewerVersionIsInstalled() external { + // Install version 2 + Module upgradedModule = new MockUpgradedModule(address(withModules)); + withModules.installModule(upgradedModule); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(upgradedModule.KEYCODE()), 2); + vm.expectRevert(err); + + // Upgrade to version 1 + withModules.upgradeModule(mockModule); + } + + function testReverts_invalidKeycode() external { + Module invalidModule = new MockInvalidModule(address(withModules)); + + bytes memory err = abi.encodeWithSelector(InvalidKeycode.selector, invalidModule.KEYCODE()); + vm.expectRevert(err); + + withModules.upgradeModule(invalidModule); + } + + function testReverts_whenNoVersionIsInstalled() external { + bytes memory err = abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, moduleFromKeycode(mockModule.KEYCODE()), 0); + vm.expectRevert(err); + + withModules.upgradeModule(mockModule); + } +} \ No newline at end of file From 495f1f61a420492438245b6b40769b8a462b3e30 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Wed, 3 Jan 2024 17:51:51 +0400 Subject: [PATCH 12/34] Consolidate into unwrapKeycode function --- src/modules/Modules.sol | 36 ++++++++++++------------ test/modules/Modules/Keycode.t.sol | 33 +++++++++++----------- test/modules/Modules/installModule.t.sol | 10 +++---- test/modules/Modules/upgradeModule.t.sol | 10 +++---- 4 files changed, 44 insertions(+), 45 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 6dc9759a..841d81b5 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -52,16 +52,17 @@ function fromKeycode(Keycode keycode_) pure returns (bytes7) { return Keycode.unwrap(keycode_); } -function versionFromKeycode(Keycode keycode_) pure returns (uint8) { +function unwrapKeycode(Keycode keycode_) pure returns (ModuleKeycode, uint8) { bytes7 unwrapped = Keycode.unwrap(keycode_); - uint8 firstDigit = uint8(unwrapped[5]) - 0x30; - uint8 secondDigit = uint8(unwrapped[6]) - 0x30; - return firstDigit * 10 + secondDigit; -} -function moduleFromKeycode(Keycode keycode_) pure returns (ModuleKeycode) { - bytes7 unwrapped = Keycode.unwrap(keycode_); - return ModuleKeycode.wrap(bytes5(unwrapped)); + // Get the moduleKeycode + ModuleKeycode moduleKeycode = ModuleKeycode.wrap(bytes5(unwrapped)); + + // Get the version + uint8 moduleVersion = (uint8(unwrapped[5]) - 0x30) * 10; + moduleVersion += uint8(unwrapped[6]) - 0x30; + + return (moduleKeycode, moduleVersion); } // solhint-disable-next-line func-visibility @@ -91,7 +92,8 @@ function ensureValidKeycode(Keycode keycode_) pure { // Check that the version is not 0 // This is because the version is by default 0 if the module is not installed - if (versionFromKeycode(keycode_) == 0) revert InvalidKeycode(keycode_); + (, uint8 moduleVersion) = unwrapKeycode(keycode_); + if (moduleVersion == 0) revert InvalidKeycode(keycode_); } abstract contract WithModules is Owned { @@ -144,8 +146,7 @@ abstract contract WithModules is Owned { function installModule(Module newModule_) external onlyOwner { // Validate new module and get its keycode Keycode keycode = _validateModule(newModule_); - ModuleKeycode moduleKeycode = moduleFromKeycode(keycode); - uint8 moduleVersion = versionFromKeycode(keycode); + (ModuleKeycode moduleKeycode, uint8 moduleVersion) = unwrapKeycode(keycode); // Check that the module is not already installed // If this reverts, then the new module should be installed via upgradeModule @@ -168,10 +169,10 @@ abstract contract WithModules is Owned { /// @notice Prevents future use of module, but functionality remains for existing users. Modules should implement functionality such that creation functions are disabled if sunset. function sunsetModule(Keycode keycode_) external onlyOwner { + (ModuleKeycode moduleKeycode, uint8 moduleVersion) = unwrapKeycode(keycode_); + // Check that the module is installed - ModuleKeycode moduleKeycode_ = moduleFromKeycode(keycode_); - uint8 moduleVersion_ = versionFromKeycode(keycode_); - if (!_moduleIsInstalled(keycode_)) revert ModuleNotInstalled(moduleKeycode_, moduleVersion_); + if (!_moduleIsInstalled(keycode_)) revert ModuleNotInstalled(moduleKeycode, moduleVersion); // Check that the module is not already sunset if (moduleSunset[keycode_]) revert ModuleAlreadySunset(keycode_); @@ -196,11 +197,10 @@ abstract contract WithModules is Owned { function upgradeModule(Module newModule_) external onlyOwner { // Validate new module and get its keycode Keycode keycode = _validateModule(newModule_); + (ModuleKeycode moduleKeycode, uint8 moduleVersion) = unwrapKeycode(keycode); // Check that an earlier version of the module is installed // If this reverts, then the new module should be installed via installModule - ModuleKeycode moduleKeycode = moduleFromKeycode(keycode); - uint8 moduleVersion = versionFromKeycode(keycode); uint8 moduleInstalledVersion = getModuleLatestVersion[moduleKeycode]; if (moduleInstalledVersion == 0) revert ModuleNotInstalled(moduleKeycode, moduleInstalledVersion); @@ -249,8 +249,8 @@ abstract contract WithModules is Owned { function _getModuleIfInstalled(Keycode keycode_) internal view returns (address) { Module module = getModuleForKeycode[keycode_]; - ModuleKeycode moduleKeycode_ = moduleFromKeycode(keycode_); - uint8 moduleVersion_ = versionFromKeycode(keycode_); + (ModuleKeycode moduleKeycode_, uint8 moduleVersion_) = unwrapKeycode(keycode_); + if (address(module) == address(0)) revert ModuleNotInstalled(moduleKeycode_, moduleVersion_); return address(module); } diff --git a/test/modules/Modules/Keycode.t.sol b/test/modules/Modules/Keycode.t.sol index 720aea9d..e1d3dc94 100644 --- a/test/modules/Modules/Keycode.t.sol +++ b/test/modules/Modules/Keycode.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {console2} from "forge-std/console2.sol"; -import {ModuleKeycode, toModuleKeycode, fromModuleKeycode, Keycode, toKeycode, fromKeycode, versionFromKeycode, moduleFromKeycode, ensureValidKeycode, InvalidKeycode} from "src/modules/Modules.sol"; +import {ModuleKeycode, toModuleKeycode, fromModuleKeycode, Keycode, toKeycode, fromKeycode, unwrapKeycode, ensureValidKeycode, InvalidKeycode} from "src/modules/Modules.sol"; contract KeycodeTest is Test { function test_moduleKeycode() external { @@ -16,8 +16,6 @@ contract KeycodeTest is Test { ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); Keycode t1_keycode = toKeycode(moduleKeycode, 1); - bytes7 unwrapped = Keycode.unwrap(t1_keycode); - ensureValidKeycode(t1_keycode); } @@ -128,29 +126,30 @@ contract KeycodeTest is Test { ensureValidKeycode(t1_keycode); } - function test_versionFromKeycode() external { + function test_unwrapKeycode() external { ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); Keycode t1_keycode = toKeycode(moduleKeycode, 1); - assertEq(versionFromKeycode(t1_keycode), 1); + (ModuleKeycode moduleKeycode_, uint8 moduleVersion_) = unwrapKeycode(t1_keycode); + assertEq(moduleVersion_, 1); Keycode t2_keycode = toKeycode(moduleKeycode, 11); - assertEq(versionFromKeycode(t2_keycode), 11); + (moduleKeycode_, moduleVersion_) = unwrapKeycode(t2_keycode); + assertEq(moduleVersion_, 11); Keycode t3_keycode = toKeycode(moduleKeycode, 99); - assertEq(versionFromKeycode(t3_keycode), 99); + (moduleKeycode_, moduleVersion_) = unwrapKeycode(t3_keycode); + assertEq(moduleVersion_, 99); Keycode t4_keycode = toKeycode(moduleKeycode, 0); - assertEq(versionFromKeycode(t4_keycode), 0); - } - - function test_moduleFromKeycode() external { - Keycode t1_keycode = toKeycode(toModuleKeycode("TEST"), 1); - assertEq(fromModuleKeycode(moduleFromKeycode(t1_keycode)), "TEST"); + (moduleKeycode_, moduleVersion_) = unwrapKeycode(t4_keycode); + assertEq(moduleVersion_, 0); - Keycode t2_keycode = toKeycode(toModuleKeycode("TES"), 11); - assertEq(fromModuleKeycode(moduleFromKeycode(t2_keycode)), "TES"); + Keycode t5_keycode = toKeycode(toModuleKeycode("TES"), 11); + (moduleKeycode_, moduleVersion_) = unwrapKeycode(t5_keycode); + assertEq(fromModuleKeycode(moduleKeycode_), "TES"); - Keycode t3_keycode = toKeycode(toModuleKeycode("TESTT"), 11); - assertEq(fromModuleKeycode(moduleFromKeycode(t3_keycode)), "TESTT"); + Keycode t6_keycode = toKeycode(toModuleKeycode("TESTT"), 11); + (moduleKeycode_, moduleVersion_) = unwrapKeycode(t6_keycode); + assertEq(fromModuleKeycode(moduleKeycode_), "TESTT"); } } diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol index 55e6b133..ee9d176d 100644 --- a/test/modules/Modules/installModule.t.sol +++ b/test/modules/Modules/installModule.t.sol @@ -9,7 +9,7 @@ import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; import {MockModule, MockUpgradedModule, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; // Contracts -import {WithModules, Module, Keycode, fromKeycode, toKeycode, toModuleKeycode, moduleFromKeycode, InvalidKeycode} from "src/modules/Modules.sol"; +import {WithModules, Module, Keycode, fromKeycode, toKeycode, toModuleKeycode, unwrapKeycode, InvalidKeycode} from "src/modules/Modules.sol"; contract InstallModuleTest is Test { WithModules internal withModules; @@ -26,7 +26,7 @@ contract InstallModuleTest is Test { Module upgradedModule = new MockUpgradedModule(address(withModules)); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(mockModule.KEYCODE()), 1); + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 1); vm.expectRevert(err); // Install version 2 @@ -37,7 +37,7 @@ contract InstallModuleTest is Test { // Install version 1 withModules.installModule(mockModule); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(mockModule.KEYCODE()), 1); + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 1); vm.expectRevert(err); // Install version 1 again @@ -49,7 +49,7 @@ contract InstallModuleTest is Test { Module upgradedModule = new MockUpgradedModule(address(withModules)); withModules.installModule(upgradedModule); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(upgradedModule.KEYCODE()), 2); + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 2); vm.expectRevert(err); // Install version 1 @@ -74,7 +74,7 @@ contract InstallModuleTest is Test { assertEq(address(module), address(mockModule)); // Check that the latest version is recorded - uint8 version = withModules.getModuleLatestVersion(moduleFromKeycode(mockModule.KEYCODE())); + uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); assertEq(version, 1); // Check that the modules array is updated diff --git a/test/modules/Modules/upgradeModule.t.sol b/test/modules/Modules/upgradeModule.t.sol index 178f270d..a1ef7e50 100644 --- a/test/modules/Modules/upgradeModule.t.sol +++ b/test/modules/Modules/upgradeModule.t.sol @@ -9,7 +9,7 @@ import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; import {MockModule, MockUpgradedModule, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; // Contracts -import {WithModules, Module, Keycode, fromKeycode, toKeycode, toModuleKeycode, moduleFromKeycode, InvalidKeycode, moduleFromKeycode} from "src/modules/Modules.sol"; +import {WithModules, Module, Keycode, fromKeycode, toKeycode, toModuleKeycode, unwrapKeycode, InvalidKeycode} from "src/modules/Modules.sol"; contract UpgradeModuleTest is Test { WithModules internal withModules; @@ -34,7 +34,7 @@ contract UpgradeModuleTest is Test { assertEq(address(upgradedModule_), address(upgradedModule)); // Check that the version is correct - uint8 upgradedModuleVersion_ = withModules.getModuleLatestVersion(moduleFromKeycode(upgradedModule.KEYCODE())); + uint8 upgradedModuleVersion_ = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); assertEq(upgradedModuleVersion_, 2); // Check that the new version is NOT sunset @@ -60,7 +60,7 @@ contract UpgradeModuleTest is Test { // Install version 1 withModules.installModule(mockModule); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(mockModule.KEYCODE()), 1); + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 1); vm.expectRevert(err); // Upgrade to version 1 @@ -72,7 +72,7 @@ contract UpgradeModuleTest is Test { Module upgradedModule = new MockUpgradedModule(address(withModules)); withModules.installModule(upgradedModule); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, moduleFromKeycode(upgradedModule.KEYCODE()), 2); + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 2); vm.expectRevert(err); // Upgrade to version 1 @@ -89,7 +89,7 @@ contract UpgradeModuleTest is Test { } function testReverts_whenNoVersionIsInstalled() external { - bytes memory err = abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, moduleFromKeycode(mockModule.KEYCODE()), 0); + bytes memory err = abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toModuleKeycode("MOCK"), 0); vm.expectRevert(err); withModules.upgradeModule(mockModule); From 017c2a9c714470bc3b6c7595218035e0dddc799a Mon Sep 17 00:00:00 2001 From: Oighty Date: Wed, 3 Jan 2024 12:55:51 -0600 Subject: [PATCH 13/34] refactor: simplify module management and change nomenclature --- src/modules/Modules.sol | 264 ++++++++++++++++++---------------------- 1 file changed, 119 insertions(+), 145 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 841d81b5..489bb038 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -9,60 +9,54 @@ import {Owned} from "lib/solmate/src/auth/Owned.sol"; /// @notice 5 byte/character identifier for the Module /// @dev 3-5 characters from A-Z -type ModuleKeycode is bytes5; +type Keycode is bytes5; /// @notice 7 byte identifier for the Module, including version -/// @dev ModuleKeycode, followed by 2 characters from 0-9 -type Keycode is bytes7; +/// @dev 2 characters from 0-9 (a version number), followed by Keycode +type Veecode is bytes7; error TargetNotAContract(address target_); -error InvalidKeycode(Keycode keycode_); +error InvalidVeecode(Veecode veecode_); -function toModuleKeycode(bytes5 moduleKeycode_) pure returns (ModuleKeycode) { - return ModuleKeycode.wrap(moduleKeycode_); +function toKeycode(bytes5 keycode_) pure returns (Keycode) { + return Keycode.wrap(keycode_); } -function fromModuleKeycode(ModuleKeycode moduleKeycode_) pure returns (bytes5) { - return ModuleKeycode.unwrap(moduleKeycode_); +function fromKeycode(Keycode keycode_) pure returns (bytes5) { + return Keycode.unwrap(keycode_); } // solhint-disable-next-line func-visibility -function toKeycode(ModuleKeycode moduleKeycode_, uint8 version_) pure returns (Keycode) { - bytes5 moduleKeycodeBytes = fromModuleKeycode(moduleKeycode_); - bytes memory keycodeBytes = new bytes(7); - - // Copy moduleKeycode_ into keycodeBytes - for (uint256 i; i < 5; i++) { - keycodeBytes[i] = moduleKeycodeBytes[i]; - } - +function wrapVeecode(Keycode keycode_, uint8 version_) pure returns (Veecode) { // Get the digits of the version - uint8 firstDigit = version_ / 10; - uint8 secondDigit = version_ % 10; + bytes1 firstDigit = bytes1(version_ / 10 + 0x30); + bytes1 secondDigit = bytes1(version_ % 10 + 0x30); - // Convert the digits to bytes - keycodeBytes[5] = bytes1(firstDigit + 0x30); - keycodeBytes[6] = bytes1(secondDigit + 0x30); + // Pack everything and wrap as a Veecode + return Veecode.wrap(bytes7(abi.encodePacked(firstDigit, secondDigit, keycode_))); +} - return Keycode.wrap(bytes7(keycodeBytes)); +// solhint-disable-next-line func-visibility +function toVeecode(bytes7 veecode_) pure returns (Veecode) { + return Veecode.wrap(veecode_); } // solhint-disable-next-line func-visibility -function fromKeycode(Keycode keycode_) pure returns (bytes7) { - return Keycode.unwrap(keycode_); +function fromVeecode(Veecode veecode_) pure returns (bytes7) { + return Veecode.unwrap(veecode_); } -function unwrapKeycode(Keycode keycode_) pure returns (ModuleKeycode, uint8) { - bytes7 unwrapped = Keycode.unwrap(keycode_); +function unwrapVeecode(Veecode veecode_) pure returns (Keycode, uint8) { + bytes7 unwrapped = Veecode.unwrap(veecode_); - // Get the moduleKeycode - ModuleKeycode moduleKeycode = ModuleKeycode.wrap(bytes5(unwrapped)); + // Get the version from the first 2 bytes + uint8 version = (uint8(unwrapped[0]) - 0x30) * 10; + version += uint8(unwrapped[1]) - 0x30; - // Get the version - uint8 moduleVersion = (uint8(unwrapped[5]) - 0x30) * 10; - moduleVersion += uint8(unwrapped[6]) - 0x30; + // Get the Keycode by shifting the full Veecode to the left by 2 bytes + Keycode keycode = Keycode.wrap(bytes5(unwrapped << 16)); - return (moduleKeycode, moduleVersion); + return (keycode, version); } // solhint-disable-next-line func-visibility @@ -71,19 +65,19 @@ function ensureContract(address target_) view { } // solhint-disable-next-line func-visibility -function ensureValidKeycode(Keycode keycode_) pure { - bytes7 unwrapped = Keycode.unwrap(keycode_); +function ensureValidVeecode(Veecode veecode_) pure { + bytes7 unwrapped = Veecode.unwrap(veecode_); for (uint256 i; i < 7; ) { bytes1 char = unwrapped[i]; - if (i < 3) { - // First 3 characters must be A-Z - if (char < 0x41 || char > 0x5A) revert InvalidKeycode(keycode_); + if (i < 2) { + // First 2 characters must be the version, each character is a number 0-9 + if (char < 0x30 || char > 0x39) revert InvalidVeecode(veecode_); } else if (i < 5) { - // Next 2 characters after the first 3 can be A-Z or blank, 0-9, or . - if (char != 0x00 && (char < 0x41 || char > 0x5A) && (char < 0x30 || char > 0x39)) revert InvalidKeycode(keycode_); + // Next 3 characters after the first 3 can be A-Z + if (char < 0x41 || char > 0x5A) revert InvalidVeecode(veecode_); } else { - // Last 2 character must be 0-9 - if (char < 0x30 || char > 0x39) revert InvalidKeycode(keycode_); + // Last 2 character must be A-Z or blank + if (char != 0x00 && (char < 0x41 || char > 0x5A)) revert InvalidVeecode(veecode_); } unchecked { i++; @@ -92,24 +86,24 @@ function ensureValidKeycode(Keycode keycode_) pure { // Check that the version is not 0 // This is because the version is by default 0 if the module is not installed - (, uint8 moduleVersion) = unwrapKeycode(keycode_); - if (moduleVersion == 0) revert InvalidKeycode(keycode_); + (, uint8 moduleVersion) = unwrapVeecode(veecode_); + if (moduleVersion == 0) revert InvalidVeecode(veecode_); } abstract contract WithModules is Owned { // ========= ERRORS ========= // error InvalidModule(); - error InvalidModuleUpgrade(Keycode keycode_); - error ModuleAlreadyInstalled(ModuleKeycode moduleKeycode_, uint8 version_); - error ModuleNotInstalled(ModuleKeycode moduleKeycode_, uint8 version_); + error InvalidModuleInstall(Keycode keycode_, uint8 version_); + error ModuleAlreadyInstalled(Keycode keycode_, uint8 version_); + error ModuleNotInstalled(Keycode keycode_, uint8 version_); error ModuleExecutionReverted(bytes error_); error ModuleAlreadySunset(Keycode keycode_); + error ModuleSunset(Keycode keycode_); - // ========= EVENTS ========= // - event ModuleInstalled(ModuleKeycode indexed moduleKeycode_, uint8 indexed version_, address indexed address_); + // ========= EVENTS ========= // - event ModuleUpgraded(ModuleKeycode indexed moduleKeycode_, uint8 indexed version_, address indexed address_); + event ModuleInstalled(Keycode indexed keycode_, uint8 indexed version_, address indexed address_); // ========= CONSTRUCTOR ========= // @@ -117,118 +111,74 @@ abstract contract WithModules is Owned { // ========= MODULE MANAGEMENT ========= // + struct ModStatus { + uint8 latestVersion; + bool sunset; + } + /// @notice Array of all modules currently installed. Keycode[] public modules; - /// @notice Mapping of Keycode to Module address. - mapping(Keycode => Module) public getModuleForKeycode; - - /// @notice Mapping of ModuleKeycode to latest version. - mapping(ModuleKeycode => uint8) public getModuleLatestVersion; + /// @notice Mapping of Veecode to Module address. + mapping(Veecode => Module) public getModuleForVeecode; - /// @notice Mapping of Keycode to whether the module is sunset. - mapping(Keycode => bool) public moduleSunset; + /// @notice Mapping of Keycode to module status information + mapping(Keycode => ModStatus) public getModuleStatus; - /// @notice Installs a new module - /// @notice Subsequent versions should be installed via upgradeModule - /// @dev This function performs the following: - /// @dev - Validates the new module - /// @dev - Checks that the module (or other versions) is not already installed - /// @dev - Stores the module details + /// @notice Installs a module. Can be used to install a new module or upgrade an existing one. + /// @dev The version of the installed module must be one greater than the latest version. If it's a new module, then the version must be 1. + /// @dev Only one version of a module is active for creation functions at a time. Older versions continue to work for existing data. + /// @dev If a module is currently sunset, installing a new version will remove the sunset. /// /// @dev This function reverts if: /// @dev - The caller is not the owner /// @dev - The module is not a contract - /// @dev - The module has an invalid Keycode + /// @dev - The module has an invalid Veecode /// @dev - The module (or other versions) is already installed - /// - /// @param newModule_ The new module + /// @dev - The module version is not one greater than the latest version function installModule(Module newModule_) external onlyOwner { - // Validate new module and get its keycode - Keycode keycode = _validateModule(newModule_); - (ModuleKeycode moduleKeycode, uint8 moduleVersion) = unwrapKeycode(keycode); + // Validate new module is a contract, has correct parent, and has valid Keycode + ensureContract(address(newModule_)); + Veecode veecode = newModule_.VEECODE(); + ensureValidVeecode(veecode); + (Keycode keycode, uint8 version) = unwrapVeecode(veecode); - // Check that the module is not already installed - // If this reverts, then the new module should be installed via upgradeModule - uint8 moduleInstalledVersion = getModuleLatestVersion[moduleKeycode]; - if (moduleInstalledVersion > 0) - revert ModuleAlreadyInstalled(moduleKeycode, moduleInstalledVersion); + // Validate that the module version is one greater than the latest version + ModStatus storage status = getModuleStatus[keycode]; + if (version != status.latestVersion + 1) revert InvalidModuleInstall(keycode, version); - // Store module in module - getModuleForKeycode[keycode] = newModule_; - modules.push(keycode); + // Store module data and remove sunset if applied + status.latestVersion = version; + if (status.sunset) status.sunset = false; + getModuleForVeecode[veecode] = newModule_; - // Update latest version - getModuleLatestVersion[moduleKeycode] = moduleVersion; + // If the module is not already installed, add it to the list of modules + if (version == uint8(1)) modules.push(keycode); // Initialize the module newModule_.INIT(); - emit ModuleInstalled(moduleKeycode, moduleVersion, address(newModule_)); + emit ModuleInstalled(keycode, version, address(newModule_)); } /// @notice Prevents future use of module, but functionality remains for existing users. Modules should implement functionality such that creation functions are disabled if sunset. + /// @dev Sunset is used to disable a module type without installing a new one. function sunsetModule(Keycode keycode_) external onlyOwner { - (ModuleKeycode moduleKeycode, uint8 moduleVersion) = unwrapKeycode(keycode_); - // Check that the module is installed - if (!_moduleIsInstalled(keycode_)) revert ModuleNotInstalled(moduleKeycode, moduleVersion); + if (!_moduleIsInstalled(keycode_)) revert ModuleNotInstalled(keycode_, 0); // Check that the module is not already sunset - if (moduleSunset[keycode_]) revert ModuleAlreadySunset(keycode_); + ModStatus storage status = getModuleStatus[keycode_]; + if (status.sunset) revert ModuleAlreadySunset(keycode_); // Set the module to sunset - moduleSunset[keycode_] = true; - } - - /// @notice Upgrades an existing module - /// @dev This function performs the following: - /// @dev - Validates the new module - /// @dev - Checks that a prior version of the module is already installed - /// @dev - Stores the module details - /// @dev - Marks the previous version as sunset - /// - /// @dev This function reverts if: - /// @dev - The caller is not the owner - /// @dev - The module is not a contract - /// @dev - The module has an invalid Keycode - /// @dev - The module is not already installed - /// @dev - The same or newer module version is already installed - function upgradeModule(Module newModule_) external onlyOwner { - // Validate new module and get its keycode - Keycode keycode = _validateModule(newModule_); - (ModuleKeycode moduleKeycode, uint8 moduleVersion) = unwrapKeycode(keycode); - - // Check that an earlier version of the module is installed - // If this reverts, then the new module should be installed via installModule - uint8 moduleInstalledVersion = getModuleLatestVersion[moduleKeycode]; - if (moduleInstalledVersion == 0) - revert ModuleNotInstalled(moduleKeycode, moduleInstalledVersion); - - if (moduleInstalledVersion >= moduleVersion) - revert ModuleAlreadyInstalled(moduleKeycode, moduleInstalledVersion); - - // Update module records - getModuleForKeycode[keycode] = newModule_; - modules.push(keycode); - - // Update latest version - getModuleLatestVersion[moduleKeycode] = moduleVersion; - - // Sunset the previous version - Keycode previousKeycode = toKeycode(moduleKeycode, moduleInstalledVersion); - moduleSunset[previousKeycode] = true; - - // Initialize the module - newModule_.INIT(); - - emit ModuleUpgraded(moduleKeycode, moduleVersion, address(newModule_)); + status.sunset = true; } // Decide if we need this function, i.e. do we need to set any parameters or call permissioned functions on any modules? // Answer: yes, e.g. when setting default values on an Auction module, like minimum duration or minimum deposit interval function execOnModule( - Keycode keycode_, + Veecode keycode_, bytes memory callData_ ) external onlyOwner returns (bytes memory) { address module = _getModuleIfInstalled(keycode_); @@ -242,26 +192,50 @@ abstract contract WithModules is Owned { return modules; } + // Checks whether any module is installed under the keycode function _moduleIsInstalled(Keycode keycode_) internal view returns (bool) { - Module module = getModuleForKeycode[keycode_]; - return address(module) != address(0); + // Any module that has been installed will have a latest version greater than 0 + // We can check not equal here to save gas + uint8 latestVersion = getModuleStatus[keycode_].latestVersion; + return latestVersion != uint8(0); } - function _getModuleIfInstalled(Keycode keycode_) internal view returns (address) { - Module module = getModuleForKeycode[keycode_]; - (ModuleKeycode moduleKeycode_, uint8 moduleVersion_) = unwrapKeycode(keycode_); + function _getLatestModuleIfActive(Keycode keycode_) internal view returns (address) { + // Check that the module is installed + ModStatus memory status = getModuleStatus[keycode_]; + if (status.latestVersion == uint8(0)) revert ModuleNotInstalled(keycode_, 0); - if (address(module) == address(0)) revert ModuleNotInstalled(moduleKeycode_, moduleVersion_); - return address(module); + // Check that the module is not sunset + if (status.sunset) revert ModuleSunset(keycode_); + + // Wrap into a Veecode, get module address and return + // We don't need to check that the Veecode is valid because we already checked that the module is installed and pulled the version from the contract + Veecode veecode = wrapVeecode(keycode_, status.latestVersion); + return address(getModuleForVeecode[veecode]); } - function _validateModule(Module newModule_) internal view returns (Keycode) { - // Validate new module is a contract, has correct parent, and has valid Keycode - ensureContract(address(newModule_)); - Keycode keycode = newModule_.KEYCODE(); - ensureValidKeycode(keycode); + function _getModuleIfInstalled(Keycode keycode_, uint8 version_) internal view returns (address) { + // Check that the module is installed + ModStatus memory status = getModuleStatus[keycode_]; + if (status.latestVersion == uint8(0)) revert ModuleNotInstalled(keycode_, 0); - return keycode; + // Check that the module version is less than or equal to the latest version and greater than 0 + if (version_ > status.latestVersion || version_ == 0) revert ModuleNotInstalled(keycode_, version_); + + // Wrap into a Veecode, get module address and return + // We don't need to check that the Veecode is valid because we already checked that the module is installed and pulled the version from the contract + Veecode veecode = wrapVeecode(keycode_, version_); + return address(getModuleForVeecode[veecode]); + } + + function _getModuleIfInstalled(Veecode veecode_) internal view returns (address) { + // In this case, it's simpler to check that the stored address is not zero + Module mod = getModuleForVeecode[veecode_]; + if (address(mod) == address(0)) { + (Keycode keycode, uint8 version) = unwrapVeecode(veecode_); + revert ModuleNotInstalled(keycode, version); + } + return address(mod); } } @@ -289,8 +263,8 @@ abstract contract Module { _; } - /// @notice 5 byte identifier for the module. 3-5 characters from A-Z. - function KEYCODE() public pure virtual returns (Keycode) {} + /// @notice 7 byte, versioned identifier for the module. 2 characters from 0-9 that signify the version and 3-5 characters from A-Z. + function VEECODE() public pure virtual returns (Veecode) {} /// @notice Initialization function for the module /// @dev This function is called when the module is installed or upgraded by the module. From f0bde00ad035303036d358278f906fac2d166102 Mon Sep 17 00:00:00 2001 From: Oighty Date: Wed, 3 Jan 2024 13:00:36 -0600 Subject: [PATCH 14/34] fix: minor clean-up and remove unused errors --- src/modules/Modules.sol | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 489bb038..ca43403b 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -92,9 +92,7 @@ function ensureValidVeecode(Veecode veecode_) pure { abstract contract WithModules is Owned { // ========= ERRORS ========= // - error InvalidModule(); error InvalidModuleInstall(Keycode keycode_, uint8 version_); - error ModuleAlreadyInstalled(Keycode keycode_, uint8 version_); error ModuleNotInstalled(Keycode keycode_, uint8 version_); error ModuleExecutionReverted(bytes error_); error ModuleAlreadySunset(Keycode keycode_); @@ -178,10 +176,10 @@ abstract contract WithModules is Owned { // Decide if we need this function, i.e. do we need to set any parameters or call permissioned functions on any modules? // Answer: yes, e.g. when setting default values on an Auction module, like minimum duration or minimum deposit interval function execOnModule( - Veecode keycode_, + Veecode veecode_, bytes memory callData_ ) external onlyOwner returns (bytes memory) { - address module = _getModuleIfInstalled(keycode_); + address module = _getModuleIfInstalled(veecode_); (bool success, bytes memory returnData) = module.call(callData_); if (!success) revert ModuleExecutionReverted(returnData); return returnData; @@ -271,5 +269,3 @@ abstract contract Module { /// @dev MUST BE GATED BY onlyParent. Used to encompass any initialization or upgrade logic. function INIT() external virtual onlyParent {} } - -// TODO handle version number From c433e77d59f78ba301fc2c51d511031bdd3569f2 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 11:51:18 +0400 Subject: [PATCH 15/34] Post-refactor cleanup --- design/FEATURES.md | 13 +- src/AuctionHouse.sol | 4 +- src/bases/Auctioneer.sol | 6 +- src/bases/Derivatizer.sol | 5 +- test/modules/Modules/Keycode.t.sol | 155 +++++++++--------- test/modules/Modules/MockModule.sol | 36 ++-- ...eycode.t.sol => getModuleForVeecode.t.sol} | 12 +- .../Modules/getModuleLatestVersion.t.sol | 50 ------ test/modules/Modules/getModuleStatus.t.sol | 63 +++++++ test/modules/Modules/installModule.t.sol | 101 +++++++++--- test/modules/Modules/upgradeModule.t.sol | 97 ----------- 11 files changed, 264 insertions(+), 278 deletions(-) rename test/modules/Modules/{getModuleForKeycode.t.sol => getModuleForVeecode.t.sol} (68%) delete mode 100644 test/modules/Modules/getModuleLatestVersion.t.sol create mode 100644 test/modules/Modules/getModuleStatus.t.sol delete mode 100644 test/modules/Modules/upgradeModule.t.sol diff --git a/design/FEATURES.md b/design/FEATURES.md index 62001ade..b873a202 100644 --- a/design/FEATURES.md +++ b/design/FEATURES.md @@ -236,13 +236,14 @@ Fees can be taken by the protocol at the following points: ### Module Management - Module references - - Specific module versions are referred to by keycodes that are stored as 7 bytes - - The first 5 bytes are the module name (e.g. "TEST"), followed by 2 bytes of 1 number each - - For example, `TEST12` would refer to version 12 of the `TEST` module + - A module is referred to by a `Keycode`, which is an identifier stored as 5 bytes + - Specific module versions are referred to by a `Veecode`, which is the version and the `Keycode` identifier + - The first 2 bytes are the module version (e.g. "12), followed by 5 bytes for the module name (e.g. "TEST") + - For example, `12TEST` would refer to version 12 of the `TEST` module - When a new record is created: - - The calling contract will have a module keycode referring to the desired module type - - The module keycode will be used to get the versioned keycode of the latest version - - The record will reference the versioned keycode, so that subsequent usage will be tied to that implementation + - The calling contract will have a `Keycode` referring to the desired module type + - The `Keycode` will be used to get the `Veecode` of the latest version + - The record will reference the `Veecode`, so that subsequent usage will be tied to that implementation ## Design Principles diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index eb196d7a..6f96e60d 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -203,14 +203,14 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { } else { // Get the module for the derivative type // We assume that the module type has been checked when the lot was created - DerivativeModule module = DerivativeModule(_getModuleIfInstalled(routing_.derivativeType)); + DerivativeModule module = DerivativeModule(_getLatestModuleIfActive(routing_.derivativeType)); bytes memory derivativeParams = routing_.derivativeParams; // If condenser specified, condense auction output and derivative params before sending to derivative module if (fromKeycode(routing_.condenserType) != bytes6(0)) { // Get condenser module - CondenserModule condenser = CondenserModule(_getModuleIfInstalled(routing_.condenserType)); + CondenserModule condenser = CondenserModule(_getLatestModuleIfActive(routing_.condenserType)); // Condense auction output and derivative params derivativeParams = condenser.condense(auctionOutput_, derivativeParams); diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index b96709f0..e7d3eca5 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -90,7 +90,7 @@ abstract contract Auctioneer is WithModules { // Load auction type module, this checks that it is installed. // We load it here vs. later to avoid two checks. Keycode auctionType = routing_.auctionType; - AuctionModule auctionModule = AuctionModule(_getModuleIfInstalled(auctionType)); + AuctionModule auctionModule = AuctionModule(_getLatestModuleIfActive(auctionType)); // Check that the auction type is allowing new auctions to be created if (typeSunset[auctionType]) revert HOUSE_AuctionTypeSunset(auctionType); @@ -115,7 +115,7 @@ abstract contract Auctioneer is WithModules { // If payout is a derivative, validate derivative data on the derivative module if (fromKeycode(routing_.derivativeType) != bytes6(0)) { // Load derivative module, this checks that it is installed. - DerivativeModule derivativeModule = DerivativeModule(_getModuleIfInstalled(routing_.derivativeType)); + DerivativeModule derivativeModule = DerivativeModule(_getLatestModuleIfActive(routing_.derivativeType)); // Call module validate function to validate implementation-specific data derivativeModule.validate(routing_.derivativeParams); @@ -215,6 +215,6 @@ abstract contract Auctioneer is WithModules { if (id_ >= lotCounter) revert HOUSE_InvalidLotId(id_); // Load module, will revert if not installed - return AuctionModule(_getModuleIfInstalled(lotRouting[id_].auctionType)); + return AuctionModule(_getLatestModuleIfActive(lotRouting[id_].auctionType)); } } \ No newline at end of file diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index 92611b79..b9fa939b 100644 --- a/src/bases/Derivatizer.sol +++ b/src/bases/Derivatizer.sol @@ -10,10 +10,11 @@ abstract contract Derivatizer is WithModules { // Return address will be zero if not wrapped function deploy(Keycode dType, bytes memory data, bool wrapped) external virtual returns (uint256, address) { // Load the derivative module, will revert if not installed - Derivative derivative = Derivative(address(_getModuleIfInstalled(dType))); + Derivative derivative = Derivative(address(_getLatestModuleIfActive(dType))); // Check that the type hasn't been sunset - if (moduleSunset[dType]) revert("Derivatizer: type sunset"); + ModStatus storage moduleStatus = getModuleStatus[dType]; + if (moduleStatus.sunset) revert("Derivatizer: type sunset"); // Call the deploy function on the derivative module (uint256 tokenId, address wrappedToken) = derivative.deploy(data, wrapped); diff --git a/test/modules/Modules/Keycode.t.sol b/test/modules/Modules/Keycode.t.sol index e1d3dc94..1b793324 100644 --- a/test/modules/Modules/Keycode.t.sol +++ b/test/modules/Modules/Keycode.t.sol @@ -4,56 +4,56 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {console2} from "forge-std/console2.sol"; -import {ModuleKeycode, toModuleKeycode, fromModuleKeycode, Keycode, toKeycode, fromKeycode, unwrapKeycode, ensureValidKeycode, InvalidKeycode} from "src/modules/Modules.sol"; +import {Keycode, toKeycode, fromKeycode, Veecode, wrapVeecode, fromVeecode, unwrapVeecode, ensureValidVeecode, InvalidVeecode} from "src/modules/Modules.sol"; contract KeycodeTest is Test { - function test_moduleKeycode() external { - ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); - assertEq(fromModuleKeycode(moduleKeycode), "TEST"); + function test_keycode() external { + Keycode keycode = toKeycode("TEST"); + assertEq(fromKeycode(keycode), "TEST"); } - function test_ensureValidKeycode_singleDigitNumber() external { - ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); - Keycode t1_keycode = toKeycode(moduleKeycode, 1); + function test_ensureValidVeecode_singleDigitNumber() external { + Keycode keycode = toKeycode("TEST"); + Veecode t1_veecode = wrapVeecode(keycode, 1); - ensureValidKeycode(t1_keycode); + ensureValidVeecode(t1_veecode); } - function test_ensureValidKeycode_doubleDigitNumber() external { - ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); - Keycode t1_keycode = toKeycode(moduleKeycode, 11); + function test_ensureValidVeecode_doubleDigitNumber() external { + Keycode keycode = toKeycode("TEST"); + Veecode t1_veecode = wrapVeecode(keycode, 11); - ensureValidKeycode(t1_keycode); + ensureValidVeecode(t1_veecode); } - function _modifyModuleKeycode(bytes5 moduleKeycode_, uint8 index_, uint8 character_) internal pure returns (bytes5) { - bytes memory moduleKeycodeBytes = abi.encodePacked(moduleKeycode_); - moduleKeycodeBytes[index_] = bytes1(character_); - return bytes5(moduleKeycodeBytes); + function _modifyKeycode(bytes5 keycode_, uint8 index_, uint8 character_) internal pure returns (bytes5) { + bytes memory keycodeBytes = abi.encodePacked(keycode_); + keycodeBytes[index_] = bytes1(character_); + return bytes5(keycodeBytes); } - function test_ensureValidKeycode_length() external { - ModuleKeycode t1_moduleKeycode = toModuleKeycode("TES"); - Keycode t1_keycode = toKeycode(t1_moduleKeycode, 11); - ensureValidKeycode(t1_keycode); - - ModuleKeycode t2_moduleKeycode = toModuleKeycode("TEST"); - Keycode t2_keycode = toKeycode(t2_moduleKeycode, 11); - ensureValidKeycode(t2_keycode); - assertFalse(fromKeycode(t1_keycode) == fromKeycode(t2_keycode)); - - ModuleKeycode t3_moduleKeycode = toModuleKeycode("TESTT"); - Keycode t3_keycode = toKeycode(t3_moduleKeycode, 21); - ensureValidKeycode(t3_keycode); - assertFalse(fromKeycode(t2_keycode) == fromKeycode(t3_keycode)); - - ModuleKeycode t4_moduleKeycode = toModuleKeycode("TESTT"); - Keycode t4_keycode = toKeycode(t4_moduleKeycode, 1); - ensureValidKeycode(t4_keycode); - assertFalse(fromKeycode(t3_keycode) == fromKeycode(t4_keycode)); + function test_ensureValidVeecode_length() external { + Keycode t1_keycode = toKeycode("TES"); + Veecode t1_veecode = wrapVeecode(t1_keycode, 11); + ensureValidVeecode(t1_veecode); + + Keycode t2_keycode = toKeycode("TEST"); + Veecode t2_veecode = wrapVeecode(t2_keycode, 11); + ensureValidVeecode(t2_veecode); + assertFalse(fromVeecode(t1_veecode) == fromVeecode(t2_veecode)); + + Keycode t3_keycode = toKeycode("TESTT"); + Veecode t3_veecode = wrapVeecode(t3_keycode, 21); + ensureValidVeecode(t3_veecode); + assertFalse(fromVeecode(t2_veecode) == fromVeecode(t3_veecode)); + + Keycode t4_keycode = toKeycode("TESTT"); + Veecode t4_veecode = wrapVeecode(t4_keycode, 1); + ensureValidVeecode(t4_veecode); + assertFalse(fromVeecode(t3_veecode) == fromVeecode(t4_veecode)); } - function testRevert_ensureValidKeycode_invalidRequiredCharacter(uint8 character_, uint8 index_) external { + function testRevert_ensureValidVeecode_invalidRequiredCharacter(uint8 character_, uint8 index_) external { // Only manipulating the first 3 characters vm.assume(index_ < 3); @@ -61,21 +61,21 @@ contract KeycodeTest is Test { vm.assume(!(character_ >= 65 && character_ <= 90)); // Replace the fuzzed character - bytes5 moduleKeycodeInput = _modifyModuleKeycode("TST", index_, character_); + bytes5 keycodeInput = _modifyKeycode("TST", index_, character_); - ModuleKeycode moduleKeycode = toModuleKeycode(moduleKeycodeInput); - Keycode t1_keycode = toKeycode(moduleKeycode, 1); + Keycode keycode = toKeycode(keycodeInput); + Veecode t1_veecode = wrapVeecode(keycode, 1); bytes memory err = abi.encodeWithSelector( - InvalidKeycode.selector, - t1_keycode + InvalidVeecode.selector, + t1_veecode ); vm.expectRevert(err); - ensureValidKeycode(t1_keycode); + ensureValidVeecode(t1_veecode); } - function testRevert_ensureValidKeycode_invalidOptionalCharacter(uint8 character_, uint8 index_) external { + function testRevert_ensureValidVeecode_invalidOptionalCharacter(uint8 character_, uint8 index_) external { // Only manipulating the characters 4-5 vm.assume(index_ < 3); @@ -83,73 +83,74 @@ contract KeycodeTest is Test { vm.assume(!(character_ >= 65 && character_ <= 90) && character_ != 0); // Replace the fuzzed character - bytes5 moduleKeycodeInput = _modifyModuleKeycode("TST", index_, character_); + bytes5 keycodeInput = _modifyKeycode("TST", index_, character_); - ModuleKeycode moduleKeycode = toModuleKeycode(moduleKeycodeInput); - Keycode t1_keycode = toKeycode(moduleKeycode, 1); + Keycode keycode = toKeycode(keycodeInput); + Veecode t1_veecode = wrapVeecode(keycode, 1); bytes memory err = abi.encodeWithSelector( - InvalidKeycode.selector, - t1_keycode + InvalidVeecode.selector, + t1_veecode ); vm.expectRevert(err); - ensureValidKeycode(t1_keycode); + ensureValidVeecode(t1_veecode); } - function testRevert_ensureValidKeycode_zeroVersion() external { - ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); - Keycode t1_keycode = toKeycode(moduleKeycode, 0); + function testRevert_ensureValidVeecode_zeroVersion() external { + Keycode keycode = toKeycode("TEST"); + Veecode t1_veecode = wrapVeecode(keycode, 0); bytes memory err = abi.encodeWithSelector( - InvalidKeycode.selector, - t1_keycode + InvalidVeecode.selector, + t1_veecode ); vm.expectRevert(err); - ensureValidKeycode(t1_keycode); + ensureValidVeecode(t1_veecode); } - function testRevert_ensureValidKeycode_invalidVersion(uint8 version_) external { + function testRevert_ensureValidVeecode_invalidVersion(uint8 version_) external { // Restrict the version to outside of 0-99 vm.assume(!(version_ >= 0 && version_ <= 99)); - ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); - Keycode t1_keycode = toKeycode(moduleKeycode, version_); + Keycode keycode = toKeycode("TEST"); + Veecode t1_veecode = wrapVeecode(keycode, version_); bytes memory err = abi.encodeWithSelector( - InvalidKeycode.selector, - t1_keycode + InvalidVeecode.selector, + t1_veecode ); vm.expectRevert(err); - ensureValidKeycode(t1_keycode); + ensureValidVeecode(t1_veecode); } - function test_unwrapKeycode() external { - ModuleKeycode moduleKeycode = toModuleKeycode("TEST"); - Keycode t1_keycode = toKeycode(moduleKeycode, 1); - (ModuleKeycode moduleKeycode_, uint8 moduleVersion_) = unwrapKeycode(t1_keycode); + function test_unwrapVeecode() external { + Keycode keycode = toKeycode("TEST"); + Veecode t1_veecode = wrapVeecode(keycode, 1); + (Keycode keycode_, uint8 moduleVersion_) = unwrapVeecode(t1_veecode); + assertEq(fromKeycode(keycode_), "TEST"); assertEq(moduleVersion_, 1); - Keycode t2_keycode = toKeycode(moduleKeycode, 11); - (moduleKeycode_, moduleVersion_) = unwrapKeycode(t2_keycode); + Veecode t2_veecode = wrapVeecode(keycode, 11); + (keycode_, moduleVersion_) = unwrapVeecode(t2_veecode); assertEq(moduleVersion_, 11); - Keycode t3_keycode = toKeycode(moduleKeycode, 99); - (moduleKeycode_, moduleVersion_) = unwrapKeycode(t3_keycode); + Veecode t3_veecode = wrapVeecode(keycode, 99); + (keycode_, moduleVersion_) = unwrapVeecode(t3_veecode); assertEq(moduleVersion_, 99); - Keycode t4_keycode = toKeycode(moduleKeycode, 0); - (moduleKeycode_, moduleVersion_) = unwrapKeycode(t4_keycode); + Veecode t4_veecode = wrapVeecode(keycode, 0); + (keycode_, moduleVersion_) = unwrapVeecode(t4_veecode); assertEq(moduleVersion_, 0); - Keycode t5_keycode = toKeycode(toModuleKeycode("TES"), 11); - (moduleKeycode_, moduleVersion_) = unwrapKeycode(t5_keycode); - assertEq(fromModuleKeycode(moduleKeycode_), "TES"); + Veecode t5_veecode = wrapVeecode(toKeycode("TES"), 11); + (keycode_, moduleVersion_) = unwrapVeecode(t5_veecode); + assertEq(fromKeycode(keycode_), "TES"); - Keycode t6_keycode = toKeycode(toModuleKeycode("TESTT"), 11); - (moduleKeycode_, moduleVersion_) = unwrapKeycode(t6_keycode); - assertEq(fromModuleKeycode(moduleKeycode_), "TESTT"); + Veecode t6_veecode = wrapVeecode(toKeycode("TESTT"), 11); + (keycode_, moduleVersion_) = unwrapVeecode(t6_veecode); + assertEq(fromKeycode(keycode_), "TESTT"); } } diff --git a/test/modules/Modules/MockModule.sol b/test/modules/Modules/MockModule.sol index e0aa5e73..bca35509 100644 --- a/test/modules/Modules/MockModule.sol +++ b/test/modules/Modules/MockModule.sol @@ -2,28 +2,44 @@ pragma solidity 0.8.19; // Contracts -import {Module, Keycode, toKeycode, toModuleKeycode} from "src/modules/Modules.sol"; +import {Module, Veecode, toKeycode, wrapVeecode} from "src/modules/Modules.sol"; -contract MockModule is Module { +contract MockModuleV1 is Module { constructor(address _owner) Module(_owner) {} - function KEYCODE() public pure override returns (Keycode) { - return toKeycode(toModuleKeycode("MOCK"), 1); + function VEECODE() public pure override returns (Veecode) { + return wrapVeecode(toKeycode("MOCK"), 1); } } -contract MockInvalidModule is Module { +contract MockModuleV2 is Module { + constructor(address _owner) Module(_owner) {} + + function VEECODE() public pure override returns (Veecode) { + return wrapVeecode(toKeycode("MOCK"), 2); + } +} + +contract MockModuleV3 is Module { constructor(address _owner) Module(_owner) {} - function KEYCODE() public pure override returns (Keycode) { - return toKeycode(toModuleKeycode("INVA_"), 100); + function VEECODE() public pure override returns (Veecode) { + return wrapVeecode(toKeycode("MOCK"), 3); } } -contract MockUpgradedModule is Module { +contract MockModuleV0 is Module { + constructor(address _owner) Module(_owner) {} + + function VEECODE() public pure override returns (Veecode) { + return wrapVeecode(toKeycode("MOCK"), 0); + } +} + +contract MockInvalidModule is Module { constructor(address _owner) Module(_owner) {} - function KEYCODE() public pure override returns (Keycode) { - return toKeycode(toModuleKeycode("MOCK"), 2); + function VEECODE() public pure override returns (Veecode) { + return wrapVeecode(toKeycode("INVA_"), 100); } } diff --git a/test/modules/Modules/getModuleForKeycode.t.sol b/test/modules/Modules/getModuleForVeecode.t.sol similarity index 68% rename from test/modules/Modules/getModuleForKeycode.t.sol rename to test/modules/Modules/getModuleForVeecode.t.sol index e3dba314..f7bd3eed 100644 --- a/test/modules/Modules/getModuleForKeycode.t.sol +++ b/test/modules/Modules/getModuleForVeecode.t.sol @@ -6,18 +6,18 @@ import {Test} from "forge-std/Test.sol"; // Mocks import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; -import {MockModule} from "test/modules/Modules/MockModule.sol"; +import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; // Contracts import {WithModules, Module} from "src/modules/Modules.sol"; -contract GetModuleForKeycodeTest is Test { +contract GetModuleForVeecodeTest is Test { WithModules internal withModules; - MockModule internal mockModule; + MockModuleV1 internal mockModule; function setUp() external { withModules = new MockWithModules(address(this)); - mockModule = new MockModule(address(withModules)); + mockModule = new MockModuleV1(address(withModules)); } modifier whenAModuleIsInstalled() { @@ -27,13 +27,13 @@ contract GetModuleForKeycodeTest is Test { } function test_WhenAMatchingModuleCannotBeFound() external { - Module module = withModules.getModuleForKeycode(mockModule.KEYCODE()); + Module module = withModules.getModuleForVeecode(mockModule.VEECODE()); assertEq(address(module), address(0)); } function test_WhenAMatchingModuleIsFound() external whenAModuleIsInstalled { - Module module = withModules.getModuleForKeycode(mockModule.KEYCODE()); + Module module = withModules.getModuleForVeecode(mockModule.VEECODE()); assertEq(address(module), address(mockModule)); } diff --git a/test/modules/Modules/getModuleLatestVersion.t.sol b/test/modules/Modules/getModuleLatestVersion.t.sol deleted file mode 100644 index 141532fd..00000000 --- a/test/modules/Modules/getModuleLatestVersion.t.sol +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -// Libraries -import {Test} from "forge-std/Test.sol"; - -// Mocks -import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; -import {MockModule, MockUpgradedModule} from "test/modules/Modules/MockModule.sol"; - -// Contracts -import {WithModules, toModuleKeycode} from "src/modules/Modules.sol"; - -contract GetModuleLatestVersionTest is Test { - WithModules internal withModules; - MockModule internal mockModule; - - function setUp() external { - withModules = new MockWithModules(address(this)); - mockModule = new MockModule(address(withModules)); - } - - modifier whenAModuleIsInstalled() { - // Install the module - withModules.installModule(mockModule); - _; - } - - function test_WhenAMatchingModuleCannotBeFound() external { - uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); - - assertEq(version, 0); - } - - function test_WhenAMatchingModuleIsFound() external whenAModuleIsInstalled { - uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); - - assertEq(version, 1); - } - - function test_WhenMultipleVersionsAreFound() external whenAModuleIsInstalled { - // Install an upgraded module - MockUpgradedModule upgradedMockModule = new MockUpgradedModule(address(withModules)); - withModules.upgradeModule(upgradedMockModule); - - uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); - - assertEq(version, 2); - } -} diff --git a/test/modules/Modules/getModuleStatus.t.sol b/test/modules/Modules/getModuleStatus.t.sol new file mode 100644 index 00000000..065d4993 --- /dev/null +++ b/test/modules/Modules/getModuleStatus.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModuleV1, MockModuleV2} from "test/modules/Modules/MockModule.sol"; + +// Contracts +import {WithModules, toKeycode} from "src/modules/Modules.sol"; + +contract GetModuleStatusTest is Test { + WithModules internal withModules; + MockModuleV1 internal mockModule; + + function setUp() external { + withModules = new MockWithModules(address(this)); + mockModule = new MockModuleV1(address(withModules)); + } + + modifier whenAModuleIsInstalled() { + // Install the module + withModules.installModule(mockModule); + _; + } + + function test_WhenAMatchingModuleCannotBeFound() external { + (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + + assertEq(moduleLatestVersion, 0); + assertFalse(moduleIsSunset); + } + + function test_WhenAMatchingModuleIsFound() external whenAModuleIsInstalled { + (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + + assertEq(moduleLatestVersion, 1); + assertFalse(moduleIsSunset); + } + + function test_WhenMultipleVersionsAreFound() external whenAModuleIsInstalled { + // Install an upgraded module + MockModuleV2 upgradedMockModule = new MockModuleV2(address(withModules)); + withModules.installModule(upgradedMockModule); + + (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + + assertEq(moduleLatestVersion, 2); + assertFalse(moduleIsSunset); + } + + function test_WhenAModuleIsSunset() external whenAModuleIsInstalled { + // Sunset the module + withModules.sunsetModule(toKeycode("MOCK")); + + (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + + assertEq(moduleLatestVersion, 1); + assertTrue(moduleIsSunset); + } +} diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol index ee9d176d..f87bb096 100644 --- a/test/modules/Modules/installModule.t.sol +++ b/test/modules/Modules/installModule.t.sol @@ -6,80 +6,131 @@ import {Test} from "forge-std/Test.sol"; // Mocks import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; -import {MockModule, MockUpgradedModule, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; +import {MockModuleV0, MockModuleV1, MockModuleV2, MockModuleV3, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; // Contracts -import {WithModules, Module, Keycode, fromKeycode, toKeycode, toModuleKeycode, unwrapKeycode, InvalidKeycode} from "src/modules/Modules.sol"; +import {WithModules, Module, Keycode, fromKeycode, toKeycode, wrapVeecode, InvalidVeecode} from "src/modules/Modules.sol"; contract InstallModuleTest is Test { WithModules internal withModules; - MockModule internal mockModule; + MockModuleV1 internal mockModule; function setUp() external { withModules = new MockWithModules(address(this)); - mockModule = new MockModule(address(withModules)); + mockModule = new MockModuleV1(address(withModules)); } - function testReverts_whenPreviousVersionIsInstalled() external { + function testReverts_whenSameVersionIsInstalled() external { // Install version 1 withModules.installModule(mockModule); - Module upgradedModule = new MockUpgradedModule(address(withModules)); - - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 1); + bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 1); vm.expectRevert(err); - // Install version 2 - withModules.installModule(upgradedModule); + // Install version 1 again + withModules.installModule(mockModule); } - function testReverts_whenSameVersionIsInstalled() external { + function testReverts_whenNewerVersionIsInstalled() external { // Install version 1 withModules.installModule(mockModule); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 1); + // Install version 2 + Module upgradedModule = new MockModuleV2(address(withModules)); + withModules.installModule(upgradedModule); + + bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 1); vm.expectRevert(err); - // Install version 1 again + // Install version 1 withModules.installModule(mockModule); } - function testReverts_whenNewerVersionIsInstalled() external { - // Install version 2 - Module upgradedModule = new MockUpgradedModule(address(withModules)); - withModules.installModule(upgradedModule); + function testReverts_whenInitialVersionZero() external { + Module moduleZero = new MockModuleV0(address(withModules)); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 2); + bytes memory err = abi.encodeWithSelector(InvalidVeecode.selector, moduleZero.VEECODE()); vm.expectRevert(err); + // Install version 0 + withModules.installModule(moduleZero); + } + + function testReverts_whenInitialVersionIncorrect() external { + Module upgradedModule = new MockModuleV2(address(withModules)); + + bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 2); + vm.expectRevert(err); + + // Install version 2 (skips version 1) + withModules.installModule(upgradedModule); + } + + function testReverts_whenNewerVersionSkips() external { // Install version 1 withModules.installModule(mockModule); + + Module upgradedModule = new MockModuleV3(address(withModules)); + + bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 3); + vm.expectRevert(err); + + // Install version 3 + withModules.installModule(upgradedModule); } - function testReverts_invalidKeycode() external { + function testReverts_invalidVeecode() external { Module invalidModule = new MockInvalidModule(address(withModules)); - bytes memory err = abi.encodeWithSelector(InvalidKeycode.selector, invalidModule.KEYCODE()); + bytes memory err = abi.encodeWithSelector(InvalidVeecode.selector, invalidModule.VEECODE()); vm.expectRevert(err); withModules.installModule(invalidModule); } - function test_success_whenNoPreviousVersionIsInstalled() external { + function test_whenNoPreviousVersionIsInstalled() external { // Install the module withModules.installModule(mockModule); // Check that the module is installed - Module module = withModules.getModuleForKeycode(mockModule.KEYCODE()); + Module module = withModules.getModuleForVeecode(mockModule.VEECODE()); assertEq(address(module), address(mockModule)); // Check that the latest version is recorded - uint8 version = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); - assertEq(version, 1); + (uint8 moduleLatestVersion, ) = withModules.getModuleStatus(toKeycode("MOCK")); + assertEq(moduleLatestVersion, 1); // Check that the modules array is updated Keycode[] memory modules = withModules.getModules(); assertEq(modules.length, 1); - assertEq(fromKeycode(modules[0]), fromKeycode(toKeycode(toModuleKeycode("MOCK"), 1))); + assertEq(fromKeycode(modules[0]), "MOCK"); + } + + function test_whenPreviousVersionIsInstalled() external { + // Install version 1 + withModules.installModule(mockModule); + + Module upgradedModule = new MockModuleV2(address(withModules)); + + // Upgrade to version 2 + withModules.installModule(upgradedModule); + + // Check that the module is installed + Module upgradedModule_ = withModules.getModuleForVeecode(upgradedModule.VEECODE()); + assertEq(address(upgradedModule_), address(upgradedModule)); + + // Check that the version is correct + (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + assertEq(moduleLatestVersion, 2); + assertEq(moduleIsSunset, false); + + // Check that the previous version is still installed + Module previousModule_ = withModules.getModuleForVeecode(mockModule.VEECODE()); + assertEq(address(previousModule_), address(mockModule)); + + // Check that the modules array remains the same + Keycode[] memory modules = withModules.getModules(); + assertEq(modules.length, 1); + assertEq(fromKeycode(modules[0]), "MOCK"); } } diff --git a/test/modules/Modules/upgradeModule.t.sol b/test/modules/Modules/upgradeModule.t.sol deleted file mode 100644 index a1ef7e50..00000000 --- a/test/modules/Modules/upgradeModule.t.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -// Libraries -import {Test} from "forge-std/Test.sol"; - -// Mocks -import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; -import {MockModule, MockUpgradedModule, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; - -// Contracts -import {WithModules, Module, Keycode, fromKeycode, toKeycode, toModuleKeycode, unwrapKeycode, InvalidKeycode} from "src/modules/Modules.sol"; - -contract UpgradeModuleTest is Test { - WithModules internal withModules; - MockModule internal mockModule; - - function setUp() external { - withModules = new MockWithModules(address(this)); - mockModule = new MockModule(address(withModules)); - } - - function test_whenPreviousVersionIsInstalled() external { - // Install version 1 - withModules.installModule(mockModule); - - Module upgradedModule = new MockUpgradedModule(address(withModules)); - - // Upgrade to version 2 - withModules.upgradeModule(upgradedModule); - - // Check that the module is installed - Module upgradedModule_ = withModules.getModuleForKeycode(upgradedModule.KEYCODE()); - assertEq(address(upgradedModule_), address(upgradedModule)); - - // Check that the version is correct - uint8 upgradedModuleVersion_ = withModules.getModuleLatestVersion(toModuleKeycode("MOCK")); - assertEq(upgradedModuleVersion_, 2); - - // Check that the new version is NOT sunset - bool upgradedModuleIsSunset_ = withModules.moduleSunset(upgradedModule.KEYCODE()); - assertFalse(upgradedModuleIsSunset_); - - // Check that the previous version is still installed - Module previousModule_ = withModules.getModuleForKeycode(mockModule.KEYCODE()); - assertEq(address(previousModule_), address(mockModule)); - - // Check that the previous version is sunset - bool previousModuleIsSunset_ = withModules.moduleSunset(mockModule.KEYCODE()); - assertTrue(previousModuleIsSunset_); - - // Check that the modules array is updated - Keycode[] memory modules = withModules.getModules(); - assertEq(modules.length, 2); - assertEq(fromKeycode(modules[0]), fromKeycode(toKeycode(toModuleKeycode("MOCK"), 1))); - assertEq(fromKeycode(modules[1]), fromKeycode(toKeycode(toModuleKeycode("MOCK"), 2))); - } - - function testReverts_whenSameVersionIsInstalled() external { - // Install version 1 - withModules.installModule(mockModule); - - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 1); - vm.expectRevert(err); - - // Upgrade to version 1 - withModules.upgradeModule(mockModule); - } - - function testReverts_whenNewerVersionIsInstalled() external { - // Install version 2 - Module upgradedModule = new MockUpgradedModule(address(withModules)); - withModules.installModule(upgradedModule); - - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadyInstalled.selector, toModuleKeycode("MOCK"), 2); - vm.expectRevert(err); - - // Upgrade to version 1 - withModules.upgradeModule(mockModule); - } - - function testReverts_invalidKeycode() external { - Module invalidModule = new MockInvalidModule(address(withModules)); - - bytes memory err = abi.encodeWithSelector(InvalidKeycode.selector, invalidModule.KEYCODE()); - vm.expectRevert(err); - - withModules.upgradeModule(invalidModule); - } - - function testReverts_whenNoVersionIsInstalled() external { - bytes memory err = abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toModuleKeycode("MOCK"), 0); - vm.expectRevert(err); - - withModules.upgradeModule(mockModule); - } -} \ No newline at end of file From 22b83fd1710350ab656b1390787bd21b4ad5191d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 11:58:50 +0400 Subject: [PATCH 16/34] Add setup/config for linting --- .gitignore | 2 + .solhint.json | 17 ++ .solhintignore | 2 + package.json | 36 +++ pnpm-lock.yaml | 729 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 786 insertions(+) create mode 100644 .solhint.json create mode 100644 .solhintignore create mode 100644 package.json create mode 100644 pnpm-lock.yaml diff --git a/.gitignore b/.gitignore index 85198aaa..103adc72 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ docs/ # Dotenv file .env + +node_modules/ diff --git a/.solhint.json b/.solhint.json new file mode 100644 index 00000000..ab96b455 --- /dev/null +++ b/.solhint.json @@ -0,0 +1,17 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error",">=0.7.0"], + "avoid-low-level-calls": "off", + "const-name-snakecase": "off", + "var-name-mixedcase": "off", + "func-name-mixedcase": "off", + "not-rely-on-time": "off", + "func-visibility": [ "warn", { "ignoreConstructors":true }], + "no-inline-assembly": "off", + "reason-string": "off", + "no-empty-blocks": "off", + "no-console": "off", + "no-global-import": "off" + } +} diff --git a/.solhintignore b/.solhintignore new file mode 100644 index 00000000..82544125 --- /dev/null +++ b/.solhintignore @@ -0,0 +1,2 @@ +src/test/**/*.sol +src/scripts/**/*.sol diff --git a/package.json b/package.json new file mode 100644 index 00000000..4c619195 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "moonraker", + "version": "1.0.0", + "description": "", + "main": "index.js", + "engines": { + "npm": "use-pnpm", + "yarn": "use-pnpm" + }, + "scripts": { + "build": "forge build", + "prettier": "prettier --write 'src/**/*.sol' '**/*.md'", + "prettier:check": "prettier --check 'src/**/*.sol' '**/*.md'", + "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix", + "lint": "npm run prettier && npm run solhint", + "lint:check": "pnpm run prettier:check && pnpm run solhint", + "test": "forge test -vvv" + }, + "keywords": [], + "author": "", + "license": "ISC", + "prettier": { + "tabWidth": 4, + "singleQuote": false, + "bracketSpacing": false, + "printWidth": 100, + "plugins": [ + "prettier-plugin-solidity" + ] + }, + "devDependencies": { + "prettier": "^3.1.1", + "prettier-plugin-solidity": "^1.3.1", + "solhint": "^4.0.0" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..da9eff40 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,729 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +devDependencies: + prettier: + specifier: ^3.1.1 + version: 3.1.1 + prettier-plugin-solidity: + specifier: ^1.3.1 + version: 1.3.1(prettier@3.1.1) + solhint: + specifier: ^4.0.0 + version: 4.0.0 + +packages: + + /@babel/code-frame@7.23.5: + resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.23.4 + chalk: 2.4.2 + dev: true + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + dev: true + + /@babel/highlight@7.23.4: + resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + dev: true + + /@pnpm/config.env-replace@1.1.0: + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + dev: true + + /@pnpm/network.ca-file@1.0.2: + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + dependencies: + graceful-fs: 4.2.10 + dev: true + + /@pnpm/npm-conf@2.2.2: + resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} + engines: {node: '>=12'} + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + dev: true + + /@sindresorhus/is@5.6.0: + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + dev: true + + /@solidity-parser/parser@0.16.2: + resolution: {integrity: sha512-PI9NfoA3P8XK2VBkK5oIfRgKDsicwDZfkVq9ZTBCQYGOP1N2owgY2dyLGyU5/J/hQs8KRk55kdmvTLjy3Mu3vg==} + dependencies: + antlr4ts: 0.5.0-alpha.4 + dev: true + + /@solidity-parser/parser@0.17.0: + resolution: {integrity: sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw==} + dev: true + + /@szmarczak/http-timer@5.0.1: + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + dependencies: + defer-to-connect: 2.0.1 + dev: true + + /@types/http-cache-semantics@4.0.4: + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /antlr4@4.13.1: + resolution: {integrity: sha512-kiXTspaRYvnIArgE97z5YVVf/cDVQABr3abFRR6mE7yesLMkgu4ujuyV/sgxafQ8wgve0DJQUJ38Z8tkgA2izA==} + engines: {node: '>=16'} + dev: true + + /antlr4ts@0.5.0-alpha.4: + resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /ast-parents@0.0.1: + resolution: {integrity: sha512-XHusKxKz3zoYk1ic8Un640joHbFMhbqneyoZfoKnEGtf2ey9Uh/IdpcQplODdO/kENaMIWsD0nJm4+wX3UNLHA==} + dev: true + + /astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + dev: true + + /cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + dependencies: + '@types/http-cache-semantics': 4.0.4 + get-stream: 6.0.1 + http-cache-semantics: 4.1.1 + keyv: 4.5.4 + mimic-response: 4.0.0 + normalize-url: 8.0.0 + responselike: 3.0.0 + dev: true + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + dev: true + + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: true + + /cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + dev: true + + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: true + + /deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + + /defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + dev: true + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: true + + /got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + dependencies: + '@sindresorhus/is': 5.6.0 + '@szmarczak/http-timer': 5.0.1 + cacheable-lookup: 7.0.0 + cacheable-request: 10.2.14 + decompress-response: 6.0.0 + form-data-encoder: 2.1.4 + get-stream: 6.0.1 + http2-wrapper: 2.2.1 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 3.0.0 + dev: true + + /graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: true + + /http2-wrapper@2.2.1: + resolution: {integrity: sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: true + + /ignore@5.3.0: + resolution: {integrity: sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==} + engines: {node: '>= 4'} + dev: true + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: true + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + dev: true + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /latest-version@7.0.0: + resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} + engines: {node: '>=14.16'} + dependencies: + package-json: 8.1.1 + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + dev: true + + /lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: true + + /mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /normalize-url@8.0.0: + resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} + engines: {node: '>=14.16'} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + dev: true + + /package-json@8.1.1: + resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} + engines: {node: '>=14.16'} + dependencies: + got: 12.6.1 + registry-auth-token: 5.0.2 + registry-url: 6.0.1 + semver: 7.5.4 + dev: true + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.23.5 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + dev: true + + /prettier-plugin-solidity@1.3.1(prettier@3.1.1): + resolution: {integrity: sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA==} + engines: {node: '>=16'} + peerDependencies: + prettier: '>=2.3.0' + dependencies: + '@solidity-parser/parser': 0.17.0 + prettier: 3.1.1 + semver: 7.5.4 + solidity-comments-extractor: 0.0.8 + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /prettier@3.1.1: + resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + + /rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + dev: true + + /registry-auth-token@5.0.2: + resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} + engines: {node: '>=14'} + dependencies: + '@pnpm/npm-conf': 2.2.2 + dev: true + + /registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + dependencies: + rc: 1.2.8 + dev: true + + /require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: true + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + dependencies: + lowercase-keys: 3.0.0 + dev: true + + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /solhint@4.0.0: + resolution: {integrity: sha512-bFViMcFvhqVd/HK3Roo7xZXX5nbujS7Bxeg5vnZc9QvH0yCWCrQ38Yrn1pbAY9tlKROc6wFr+rK1mxYgYrjZgA==} + hasBin: true + dependencies: + '@solidity-parser/parser': 0.16.2 + ajv: 6.12.6 + antlr4: 4.13.1 + ast-parents: 0.0.1 + chalk: 4.1.2 + commander: 10.0.1 + cosmiconfig: 8.3.6 + fast-diff: 1.3.0 + glob: 8.1.0 + ignore: 5.3.0 + js-yaml: 4.1.0 + latest-version: 7.0.0 + lodash: 4.17.21 + pluralize: 8.0.0 + semver: 7.5.4 + strip-ansi: 6.0.1 + table: 6.8.1 + text-table: 0.2.0 + optionalDependencies: + prettier: 2.8.8 + transitivePeerDependencies: + - typescript + dev: true + + /solidity-comments-extractor@0.0.8: + resolution: {integrity: sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g==} + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /table@6.8.1: + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.12.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true From cdeb384c9c838e37dc19c4b621c9ac1d7863522b Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 12:01:34 +0400 Subject: [PATCH 17/34] chore: linting --- design/ARCHITECTURE.md | 27 +- design/FEATURES.md | 300 +++++++++--------- src/AuctionHouse.sol | 63 +++- src/bases/Auctioneer.sol | 32 +- src/bases/Derivatizer.sol | 26 +- src/lib/clones/Clone.sol | 34 +- src/lib/clones/CloneERC20.sol | 6 +- src/lib/clones/ClonesWithImmutableArgs.sol | 33 +- src/modules/Auction.sol | 31 +- src/modules/Condenser.sol | 9 +- src/modules/Derivative.sol | 33 +- src/modules/Modules.sol | 17 +- src/modules/auctions/FPA.sol | 5 +- src/modules/auctions/GDA.sol | 8 +- src/modules/auctions/OBB.sol | 5 +- src/modules/auctions/OFDA.sol | 6 +- src/modules/auctions/OSDA.sol | 5 +- src/modules/auctions/SBB.sol | 5 +- src/modules/auctions/SDA.sol | 4 +- src/modules/auctions/TVGDA.sol | 6 +- src/modules/auctions/bases/AtomicAuction.sol | 6 +- src/modules/auctions/bases/BatchAuction.sol | 8 +- .../auctions/bases/DiscreteAuction.sol | 6 +- src/modules/derivatives/CliffVesting.sol | 16 +- 24 files changed, 364 insertions(+), 327 deletions(-) diff --git a/design/ARCHITECTURE.md b/design/ARCHITECTURE.md index 76100c86..d2254e12 100644 --- a/design/ARCHITECTURE.md +++ b/design/ARCHITECTURE.md @@ -107,7 +107,7 @@ classDiagram AuctionModule ..> Auctioneer DerivativeModule ..> Derivatizer - + CondenserModule ..> AuctionHouse Auctioneer --|> AuctionHouse Derivatizer --|> AuctionHouse @@ -198,20 +198,20 @@ classDiagram AuctionModule --|> GDA AuctionModule --|> TVGDA - + DerivativeModule --|> CliffVesting DerivativeModule --|> StakedCliffVesting DerivativeModule --|> RageVesting DerivativeModule --|> FixedStrikeOption DerivativeModule --|> SuccessToken - + ``` ### TODOs -- [ ] Add section for Auction and Derivative module implementations after we prioritize which ones to build first -- [ ] Create a function or add return values so that a solver / user can determine the derivative token that a market will return (useful for then creating off-chain orders for that token). This also brings up a point about how certain auction view functions that rely solely on an amount need to be refactored for a multi-variate auction world, e.g. `payoutFor(uint256)` -> `payoutFor(uint256, bytes)` +- [ ] Add section for Auction and Derivative module implementations after we prioritize which ones to build first +- [ ] Create a function or add return values so that a solver / user can determine the derivative token that a market will return (useful for then creating off-chain orders for that token). This also brings up a point about how certain auction view functions that rely solely on an amount need to be refactored for a multi-variate auction world, e.g. `payoutFor(uint256)` -> `payoutFor(uint256, bytes)` ## Processes @@ -234,7 +234,7 @@ sequenceDiagram AtomicAuctionModule->>AtomicAuctionModule: _auction(uint256 id, Lot lot, bytes implParams) Note right of AtomicAuctionModule: module-specific actions - AtomicAuctionModule-->>AuctionHouse: + AtomicAuctionModule-->>AuctionHouse: deactivate AtomicAuctionModule Note over AuctionHouse: store routing information @@ -252,7 +252,7 @@ sequenceDiagram autoNumber participant Buyer participant AuctionHouse - + activate AuctionHouse Buyer->>AuctionHouse: purchase(address recipient, address referrer, uint256 auctionId, uint256 amount, uint256 minAmountOut, bytes approval) AuctionHouse->>AuctionHouse: _getModuleForId(uint256 auctionId) @@ -382,7 +382,7 @@ sequenceDiagram activate AuctionHouse AuctionOwner->>AuctionHouse: close(uint256 id) - + AuctionHouse->>AuctionHouse: _getModuleForId(id) AuctionHouse->>AuctionHouse: lotRouting(id) @@ -394,7 +394,7 @@ sequenceDiagram AuctionHouse-->>AuctionOwner: returns else AuctionHouse->>AuctionOwner: revert - end + end deactivate AuctionHouse ``` @@ -456,8 +456,8 @@ sequenceDiagram deactivate AuctionHouse ``` -Need to think about the implications of transferring to/from the AuctionOwner once here, because _handleTransfers is not currently designed for this. +Need to think about the implications of transferring to/from the AuctionOwner once here, because \_handleTransfers is not currently designed for this. ### User Redeems Derivative Token - V1 (through AuctionHouse, requires refactoring AuctionModule) @@ -482,7 +482,7 @@ sequenceDiagram DerivativeModule->>DerivativeToken: burn(user, amount) destroy DerivativeToken User-->>DerivativeToken: derivative tokens burned - else + else DerivativeModule->>DerivativeModule: burn(tokenId, user, amount) User-->>DerivativeModule: derivative tokens burned end @@ -508,7 +508,7 @@ sequenceDiagram DerivativeModule->>DerivativeToken: burn(user, amount) destroy DerivativeToken User-->>DerivativeToken: derivative tokens burned - else + else DerivativeModule->>DerivativeModule: burn(tokenId, user, amount) User-->>DerivativeModule: derivative tokens burned end @@ -554,5 +554,6 @@ sequenceDiagram ``` ### Create Auction by transforming one Derivative to another + TODO determine if it makes sense to build this as a specific workflow within the system. -It's a bit of an edge case and will add several variables to the auction creation process that are optional. \ No newline at end of file +It's a bit of an edge case and will add several variables to the auction creation process that are optional. diff --git a/design/FEATURES.md b/design/FEATURES.md index b873a202..cce4f454 100644 --- a/design/FEATURES.md +++ b/design/FEATURES.md @@ -4,57 +4,57 @@ Lot -- _What_ is being sold -- A token or group of tokens that are available for bidding through an auction -- The current design supports a single lot per auction +- _What_ is being sold +- A token or group of tokens that are available for bidding through an auction +- The current design supports a single lot per auction Auction -- The auction (specifically the type) specifies _how_ the lot is being sold +- The auction (specifically the type) specifies _how_ the lot is being sold Market -- This term was used in the V1 protocol -- `Auction` is a more expansive term that encompasses the functionality in the V2 protocol +- This term was used in the V1 protocol +- `Auction` is a more expansive term that encompasses the functionality in the V2 protocol Quote Token -- The token that is offered by the bidder in return for the payout token +- The token that is offered by the bidder in return for the payout token Payout Token -- The token that is provided to the bidder as a payment for the quote token -- A payout token is used when the payout is non-vesting, where the payout token is transferred at the time of auction settlement +- The token that is provided to the bidder as a payment for the quote token +- A payout token is used when the payout is non-vesting, where the payout token is transferred at the time of auction settlement Derivative Token -- A form of a payout token that is used when the payout vests over time +- A form of a payout token that is used when the payout vests over time ### Actions Purchase -- _Immediately_ buy tokens from an auction -- Provided the auction is open and the purchase amount is within capacity, the purchase will succeed -- Valid only for atomic auctions (since batch auctions are settled later) +- _Immediately_ buy tokens from an auction +- Provided the auction is open and the purchase amount is within capacity, the purchase will succeed +- Valid only for atomic auctions (since batch auctions are settled later) Bid -- An offer to buy tokens from an auction -- The success of the bid is not known until the auction is settled -- Valid only for batch auctions (since atomic auctions are settled at the same time) +- An offer to buy tokens from an auction +- The success of the bid is not known until the auction is settled +- Valid only for batch auctions (since atomic auctions are settled at the same time) Settle -- Finalises the bids for an auction -- Valid only for batch auctions (since atomic auctions are settled at the same time) +- Finalises the bids for an auction +- Valid only for batch auctions (since atomic auctions are settled at the same time) Close -- Ends the auction immediately/prematurely -- Further capacity cannot be sold nor bids/purchases accepted -- Valid for all auction types -- In the case of a batch auction, the auction will not be settled +- Ends the auction immediately/prematurely +- Further capacity cannot be sold nor bids/purchases accepted +- Valid for all auction types +- In the case of a batch auction, the auction will not be settled ## Features @@ -66,10 +66,10 @@ core system only supports atomic auctions. Batch auctions not supported. Except For this reason, V2 supports off-chain bids and settlement computation. This enables auctions that were not possible earlier: -- sealed bid auctions - - list of winning bidders provided to the contract for settlement -- providing Bond Protocol auctions as a liquidity source for CoW Protocol -- off-chain computation of auction settlement by solvers +- sealed bid auctions + - list of winning bidders provided to the contract for settlement +- providing Bond Protocol auctions as a liquidity source for CoW Protocol +- off-chain computation of auction settlement by solvers solver is trying to put together a set of contract interactions that gives an ideal outcome for users @@ -84,54 +84,54 @@ sealed batch auctions could be done on-chain, but are difficult Atomic -- Definition - - From the whitepaper: - > Atomic Auctions are then auctions where a bid is submitted, instantly accepted or rejected, and settled within a single transaction - - Atomic auctions are settled at the time of purchase - - Settled immediately: offered tokens are transferred at time of purchase -- Examples include: - - Sequential Dutch - > The main feature of an SDA includes splitting a large number of tokens into multiple discrete Dutch Auctions that are performed over time. This sequence of auctions uses a dynamic exchange rate for two arbitrary ERC20 tokens without the use of oracles. - - Gradual Dutch / Australian Auctions - > while SDAs split capacity into multiple discrete auctions, GDAs split capacity into infinitely many auctions - - The cumulative purchase price is increasing exponentially +- Definition + - From the whitepaper: + > Atomic Auctions are then auctions where a bid is submitted, instantly accepted or rejected, and settled within a single transaction + - Atomic auctions are settled at the time of purchase + - Settled immediately: offered tokens are transferred at time of purchase +- Examples include: + - Sequential Dutch + > The main feature of an SDA includes splitting a large number of tokens into multiple discrete Dutch Auctions that are performed over time. This sequence of auctions uses a dynamic exchange rate for two arbitrary ERC20 tokens without the use of oracles. + - Gradual Dutch / Australian Auctions + > while SDAs split capacity into multiple discrete auctions, GDAs split capacity into infinitely many auctions + - The cumulative purchase price is increasing exponentially Batch -- Definition - - From the whitepaper: - > Batch Auctions refer to the more familiar auction format of collecting bids from participants over a set duration and then settling the auction at the end based on the best received bids. “Batch” refers to the notion that proceeds are received and auction units distributed in a batch, rather than individually. - - Two major types: - - Open - - Bids are recorded on-chain - - Sealed - - Bids are recorded off-chain and submitted on-chain at the time of settlement - - The submission function will be permissioned in order to restrict who can submit the set of sealed bids - - This auction type prevents other bidders from seeing active bids - - Quote tokens are only transferred at the time of settlement - - This avoids having to provide functionality for bidders to claim a refund of their quote tokens - - Payout tokens are transferred at the time of settlement -- Examples include: - - Marginal Price Auction - > A marginal price auction, also called a uniform price auction, is a multiunit auction format where bidders place bids that include two variables: price and quantity of items to purchase. The auction is settled by awarding the items to the highest bids until capacity is expended. All winners pay the lowest accepted bid price. The price of this lowest accepted bid is also called the clearing price of the auction. - - Vickrey-Clarkes Groves - > VCG auctions are a form of second-price auction (sometimes called Vickrey auctions) extended to the multiunit domain. They require a sealed bidding process to incentivize participants to bid their best price. +- Definition + - From the whitepaper: + > Batch Auctions refer to the more familiar auction format of collecting bids from participants over a set duration and then settling the auction at the end based on the best received bids. “Batch” refers to the notion that proceeds are received and auction units distributed in a batch, rather than individually. + - Two major types: + - Open + - Bids are recorded on-chain + - Sealed + - Bids are recorded off-chain and submitted on-chain at the time of settlement + - The submission function will be permissioned in order to restrict who can submit the set of sealed bids + - This auction type prevents other bidders from seeing active bids + - Quote tokens are only transferred at the time of settlement + - This avoids having to provide functionality for bidders to claim a refund of their quote tokens + - Payout tokens are transferred at the time of settlement +- Examples include: + - Marginal Price Auction + > A marginal price auction, also called a uniform price auction, is a multiunit auction format where bidders place bids that include two variables: price and quantity of items to purchase. The auction is settled by awarding the items to the highest bids until capacity is expended. All winners pay the lowest accepted bid price. The price of this lowest accepted bid is also called the clearing price of the auction. + - Vickrey-Clarkes Groves + > VCG auctions are a form of second-price auction (sometimes called Vickrey auctions) extended to the multiunit domain. They require a sealed bidding process to incentivize participants to bid their best price. ### Auction Configuration Auctions (and auction types) will have different configuration options. This will include: -- Auction owner -- Auction type -- Starting time -- Duration -- Payout token -- Quote token -- Purchase hook addresses -- Optional whitelist of allowed bidders -- Capacity (in quote or payout token) -- Optional derivative type and parameters -- Optional condenser type (used to manipulate auction output for the derivative module) +- Auction owner +- Auction type +- Starting time +- Duration +- Payout token +- Quote token +- Purchase hook addresses +- Optional whitelist of allowed bidders +- Capacity (in quote or payout token) +- Optional derivative type and parameters +- Optional condenser type (used to manipulate auction output for the derivative module) Auction modules may have additional configuration options, too. @@ -139,47 +139,47 @@ Auction modules may have additional configuration options, too. Payout -- If an auction has a payout configured (or has a lack of a derivative type), the payout token will be transferred to the bidder at the time of settlement +- If an auction has a payout configured (or has a lack of a derivative type), the payout token will be transferred to the bidder at the time of settlement Derivative -- If an auction has a derivative type configured, the derivative token will be minted and transferred to the bidder at the time of settlement -- Structured as an ERC6909, but can be optionally wrapped as an ERC20 - - Needed because there may be many different derivatives from a single auction. For example, a long-running auction with a fixed-term derivative type would have numerous derivative tokens, each with different expiry dates. - - More gas efficient, enabling giving receipt tokens. -- Actions that can be performed on a derivative token by a token holder/bidder: - - Redeem: when the conditions are fulfilled, redeems/cashes in the derivative tokens for the payout token - - Exercise: TODO how is this different to redeem? - - Wrap: wraps the derivative token into an ERC20 - - Unwrap: unwraps the derivative token back into the underlying ERC6909 -- Actions that can be performed on a derivative token by the auction owner: - - Deploy: deploys a new derivative token - - Mint: mints an amount of the derivative token - - Reclaim: enables an auction owner to reclaim the payout tokens that have not been redeemed - - Transform: transfers the derivative into another form - - e.g. transform a vesting token into an option token and creates an auction for it. -- There are a number of different derivative types: - - Fixed expiry - - Expires on a specific date - - Fixed term - - Expires after a specific term, e.g. 3 months - - Vesting - - Cliff vesting - - At a certain expiration date, the full amount is vested - - Different users can have different cliff dates (hence it requires ERC6909) - - Linear vesting - - Rage vesting - > Rage Vesting introduces the concept of Rage Quitting, where users can unlock their proportional share of tokens vested at a point in time but forfeit the remaining balance. - - Staked vesting - - Dynamic - - Could implement an arbitrary vesting algorithm - - Call options - - Call options give buyers the opportunity to buy an asset at a specified price within a specific time period - - Only covered calls are supported: the auction owner has to provide the collateral so that call options are guaranteed to settle if they are exercised - - TODO fixed vs oracle strike - - Success token - - See [Outcome Finance](https://docs.outcome.finance/success-tokens/what-are-success-tokens) - - Combination of a vesting token and an option for those tokens +- If an auction has a derivative type configured, the derivative token will be minted and transferred to the bidder at the time of settlement +- Structured as an ERC6909, but can be optionally wrapped as an ERC20 + - Needed because there may be many different derivatives from a single auction. For example, a long-running auction with a fixed-term derivative type would have numerous derivative tokens, each with different expiry dates. + - More gas efficient, enabling giving receipt tokens. +- Actions that can be performed on a derivative token by a token holder/bidder: + - Redeem: when the conditions are fulfilled, redeems/cashes in the derivative tokens for the payout token + - Exercise: TODO how is this different to redeem? + - Wrap: wraps the derivative token into an ERC20 + - Unwrap: unwraps the derivative token back into the underlying ERC6909 +- Actions that can be performed on a derivative token by the auction owner: + - Deploy: deploys a new derivative token + - Mint: mints an amount of the derivative token + - Reclaim: enables an auction owner to reclaim the payout tokens that have not been redeemed + - Transform: transfers the derivative into another form + - e.g. transform a vesting token into an option token and creates an auction for it. +- There are a number of different derivative types: + - Fixed expiry + - Expires on a specific date + - Fixed term + - Expires after a specific term, e.g. 3 months + - Vesting + - Cliff vesting + - At a certain expiration date, the full amount is vested + - Different users can have different cliff dates (hence it requires ERC6909) + - Linear vesting + - Rage vesting + > Rage Vesting introduces the concept of Rage Quitting, where users can unlock their proportional share of tokens vested at a point in time but forfeit the remaining balance. + - Staked vesting + - Dynamic + - Could implement an arbitrary vesting algorithm + - Call options + - Call options give buyers the opportunity to buy an asset at a specified price within a specific time period + - Only covered calls are supported: the auction owner has to provide the collateral so that call options are guaranteed to settle if they are exercised + - TODO fixed vs oracle strike + - Success token + - See [Outcome Finance](https://docs.outcome.finance/success-tokens/what-are-success-tokens) + - Combination of a vesting token and an option for those tokens ### Hooks @@ -187,82 +187,82 @@ Hooks are an expansion of the callback function in Bond Protocol V1, modelled af The callback function in Bond Protocol V1 offered flexibility to the auction owner. For example: -- Enables the auction owner to mint tokens upon purchase -- Quote tokens also sent to the callback to give owner flexibility of where to send them. +- Enables the auction owner to mint tokens upon purchase +- Quote tokens also sent to the callback to give owner flexibility of where to send them. Hooks in the V2 protocol provide further flexibility and customisation for the auction owner. For example: -- Custom logic at other places in the transaction, such as before payment, between payment and payout, after payout, settlement +- Custom logic at other places in the transaction, such as before payment, between payment and payout, after payout, settlement Unlike in V1, hooks will not be gated. Security risks: -- There are inherent security risks in running arbitrary code -- There will be a need to check balances to ensure that after each step (or at the end), invariants are not broken +- There are inherent security risks in running arbitrary code +- There will be a need to check balances to ensure that after each step (or at the end), invariants are not broken ### Fees #### V1 -- Purchase -- Protocol fee (to BP treasury) -- Referrer fee (for frontends to claim later) -- Fee variables can be set at any time -- Option teller has fee on exercise of the option -- Has slippage check that would mitigate against fees being changed before a bid +- Purchase +- Protocol fee (to BP treasury) +- Referrer fee (for frontends to claim later) +- Fee variables can be set at any time +- Option teller has fee on exercise of the option +- Has slippage check that would mitigate against fees being changed before a bid #### V2 Principles: -- Fees are taken in the quote token. -- The protocol should only take a fee when value is being produced for both parties -- Don't take fees on basic actions, e.g. redemption and cliff vesting -- Referrer fees should carry over -- TODO decide if fees are locked at the time of auction creation - - If not locked, a governance action could dramatically alter the fees for open auctions and derivatives +- Fees are taken in the quote token. +- The protocol should only take a fee when value is being produced for both parties +- Don't take fees on basic actions, e.g. redemption and cliff vesting +- Referrer fees should carry over +- TODO decide if fees are locked at the time of auction creation + - If not locked, a governance action could dramatically alter the fees for open auctions and derivatives Fees can be taken by the protocol at the following points: -- Auction creation - - e.g. service fee not contingent on volume -- Atomic auction purchase -- Batch auction settlement -- Exercising of derivative token - - e.g. early exercising of vesting token, take a cut of residual collateral - - would make sense when there's a profit for the bidder +- Auction creation + - e.g. service fee not contingent on volume +- Atomic auction purchase +- Batch auction settlement +- Exercising of derivative token + - e.g. early exercising of vesting token, take a cut of residual collateral + - would make sense when there's a profit for the bidder ### Module Management -- Module references - - A module is referred to by a `Keycode`, which is an identifier stored as 5 bytes - - Specific module versions are referred to by a `Veecode`, which is the version and the `Keycode` identifier - - The first 2 bytes are the module version (e.g. "12), followed by 5 bytes for the module name (e.g. "TEST") - - For example, `12TEST` would refer to version 12 of the `TEST` module -- When a new record is created: - - The calling contract will have a `Keycode` referring to the desired module type - - The `Keycode` will be used to get the `Veecode` of the latest version - - The record will reference the `Veecode`, so that subsequent usage will be tied to that implementation +- Module references + - A module is referred to by a `Keycode`, which is an identifier stored as 5 bytes + - Specific module versions are referred to by a `Veecode`, which is the version and the `Keycode` identifier + - The first 2 bytes are the module version (e.g. "12), followed by 5 bytes for the module name (e.g. "TEST") + - For example, `12TEST` would refer to version 12 of the `TEST` module +- When a new record is created: + - The calling contract will have a `Keycode` referring to the desired module type + - The `Keycode` will be used to get the `Veecode` of the latest version + - The record will reference the `Veecode`, so that subsequent usage will be tied to that implementation ## Design Principles -- The architecture should be modular, enabling support for different types of auction and derivatives -- Only handle at the auction-level (e.g. `AuctionHouse`) what needs to be done there - - This means that at least initially, there won't be pass-through functions to auction and derivative modules - - The reasoning for this is that different auction and derivative types may have different functions and arguments, - and catering for those in the `AuctionHouse` core contract will increase complexity - - For example, it makes the most sense for quote and payout token transfers to be performed at the level of `AuctionHouse`, - while derivative token transfers be handled in the respective derivative module (due to potential variations in behaviour and conditions)\ - - Data should also be stored in a similar manner -- Third-parties will mainly interact with the auction and derivative modules +- The architecture should be modular, enabling support for different types of auction and derivatives +- Only handle at the auction-level (e.g. `AuctionHouse`) what needs to be done there + - This means that at least initially, there won't be pass-through functions to auction and derivative modules + - The reasoning for this is that different auction and derivative types may have different functions and arguments, + and catering for those in the `AuctionHouse` core contract will increase complexity + - For example, it makes the most sense for quote and payout token transfers to be performed at the level of `AuctionHouse`, + while derivative token transfers be handled in the respective derivative module (due to potential variations in behaviour and conditions)\ + - Data should also be stored in a similar manner +- Third-parties will mainly interact with the auction and derivative modules ## Security Considerations -- The goal is for the protocol to be permissionless and community-owned, which alters the security considerations -- The functions that solvers interact with (for off-chain computation and/or bid storage) will need to be permissioned - - This would likely that the form of a whitelist of addresses that can call the function -- Auctions should only be administered by the owner +- The goal is for the protocol to be permissionless and community-owned, which alters the security considerations +- The functions that solvers interact with (for off-chain computation and/or bid storage) will need to be permissioned + - This would likely that the form of a whitelist of addresses that can call the function +- Auctions should only be administered by the owner vesting token: collateral is the underlying asset that the bidder will receive after vesting is complete call option: needs to be a "covered call", meaning that the collateral needs to be provided ahead of time diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index 6f96e60d..d8c7085b 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -23,7 +23,6 @@ abstract contract FeeManager { } abstract contract Router is FeeManager { - // ========== STATE VARIABLES ========== // /// @notice Fee paid to a front end operator in basis points (3 decimals). Set by the referrer, must be less than or equal to 5% (5e3). @@ -51,17 +50,36 @@ abstract contract Router is FeeManager { // ========== ATOMIC AUCTIONS ========== // /// @param approval_ - (Optional) Permit approval signature for the quoteToken - function purchase(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external virtual returns (uint256 payout); + function purchase( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external virtual returns (uint256 payout); // ========== BATCH AUCTIONS ========== // // On-chain auction variant - function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external virtual; + function bid( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external virtual; function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); // Off-chain auction variant - function settle(uint256 id_, Auction.Bid[] memory bids_) external virtual returns (uint256[] memory amountsOut); + function settle( + uint256 id_, + Auction.Bid[] memory bids_ + ) external virtual returns (uint256[] memory amountsOut); } // contract AuctionHouse is Derivatizer, Auctioneer, Router { @@ -92,7 +110,15 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { // ========== DIRECT EXECUTION ========== // - function purchase(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override returns (uint256 payout) { + function purchase( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external override returns (uint256 payout) { AuctionModule module = _getModuleForId(id_); // TODO should this not check if the auction is atomic? @@ -103,7 +129,9 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { // 2. Calculate protocol fee as the total expected fee amount minus the referrer fee // to avoid issues with rounding from separate fee calculations // TODO think about how to reduce storage loads - uint256 toReferrer = referrer_ == address(0) ? 0 : (amount_ * referrerFees[referrer_]) / FEE_DECIMALS; + uint256 toReferrer = referrer_ == address(0) + ? 0 + : (amount_ * referrerFees[referrer_]) / FEE_DECIMALS; uint256 toProtocol = ((amount_ * (protocolFee + referrerFees[referrer_])) / FEE_DECIMALS) - toReferrer; @@ -111,7 +139,14 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { Routing memory routing = lotRouting[id_]; // Send purchase to auction house and get payout plus any extra output - (payout) = module.purchase(recipient_, referrer_, amount_ - toReferrer - toProtocol, id_, auctionData_, approval_); + (payout) = module.purchase( + recipient_, + referrer_, + amount_ - toReferrer - toProtocol, + id_, + auctionData_, + approval_ + ); // Check that payout is at least minimum amount out // @dev Moved the slippage check from the auction to the AuctionHouse to allow different routing and purchase logic @@ -131,10 +166,8 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { emit Purchase(id_, msg.sender, referrer_, amount_, payout); } - // ============ DELEGATED EXECUTION ========== // - // ============ INTERNAL EXECUTION FUNCTIONS ========== // /// @notice Handles transfer of funds from user and market owner/callback @@ -203,14 +236,18 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { } else { // Get the module for the derivative type // We assume that the module type has been checked when the lot was created - DerivativeModule module = DerivativeModule(_getLatestModuleIfActive(routing_.derivativeType)); + DerivativeModule module = DerivativeModule( + _getLatestModuleIfActive(routing_.derivativeType) + ); bytes memory derivativeParams = routing_.derivativeParams; // If condenser specified, condense auction output and derivative params before sending to derivative module if (fromKeycode(routing_.condenserType) != bytes6(0)) { - // Get condenser module - CondenserModule condenser = CondenserModule(_getLatestModuleIfActive(routing_.condenserType)); + // Get condenser module + CondenserModule condenser = CondenserModule( + _getLatestModuleIfActive(routing_.condenserType) + ); // Condense auction output and derivative params derivativeParams = condenser.condense(auctionOutput_, derivativeParams); @@ -223,4 +260,4 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { // module.mint(recipient_, payout_, derivativeParams, routing_.wrapDerivative); } } -} \ No newline at end of file +} diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index e7d3eca5..aa0017fc 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -9,16 +9,11 @@ import {fromKeycode} from "src/modules/Modules.sol"; import {DerivativeModule} from "src/modules/Derivative.sol"; -interface IHooks { +interface IHooks {} -} - -interface IAllowlist { - -} +interface IAllowlist {} abstract contract Auctioneer is WithModules { - // ========= ERRORS ========= // error HOUSE_AuctionTypeSunset(Keycode auctionType); @@ -31,11 +26,7 @@ abstract contract Auctioneer is WithModules { // ========= EVENTS ========= // - event AuctionCreated( - uint256 indexed id, - address indexed baseToken, - address indexed quoteToken - ); + event AuctionCreated(uint256 indexed id, address indexed baseToken, address indexed quoteToken); // ========= DATA STRUCTURES ========== // @@ -86,7 +77,10 @@ abstract contract Auctioneer is WithModules { // ========== AUCTION MANAGEMENT ========== // - function auction(RoutingParams calldata routing_, Auction.AuctionParams calldata params_) external returns (uint256 id) { + function auction( + RoutingParams calldata routing_, + Auction.AuctionParams calldata params_ + ) external returns (uint256 id) { // Load auction type module, this checks that it is installed. // We load it here vs. later to avoid two checks. Keycode auctionType = routing_.auctionType; @@ -107,15 +101,15 @@ abstract contract Auctioneer is WithModules { uint8 baseTokenDecimals = routing_.baseToken.decimals(); uint8 quoteTokenDecimals = routing_.quoteToken.decimals(); - if (baseTokenDecimals < 6 || baseTokenDecimals > 18) - revert Auctioneer_InvalidParams(); - if (quoteTokenDecimals < 6 || quoteTokenDecimals > 18) - revert Auctioneer_InvalidParams(); + if (baseTokenDecimals < 6 || baseTokenDecimals > 18) revert Auctioneer_InvalidParams(); + if (quoteTokenDecimals < 6 || quoteTokenDecimals > 18) revert Auctioneer_InvalidParams(); // If payout is a derivative, validate derivative data on the derivative module if (fromKeycode(routing_.derivativeType) != bytes6(0)) { // Load derivative module, this checks that it is installed. - DerivativeModule derivativeModule = DerivativeModule(_getLatestModuleIfActive(routing_.derivativeType)); + DerivativeModule derivativeModule = DerivativeModule( + _getLatestModuleIfActive(routing_.derivativeType) + ); // Call module validate function to validate implementation-specific data derivativeModule.validate(routing_.derivativeParams); @@ -217,4 +211,4 @@ abstract contract Auctioneer is WithModules { // Load module, will revert if not installed return AuctionModule(_getLatestModuleIfActive(lotRouting[id_].auctionType)); } -} \ No newline at end of file +} diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index b9fa939b..f0bd0556 100644 --- a/src/bases/Derivatizer.sol +++ b/src/bases/Derivatizer.sol @@ -4,11 +4,14 @@ pragma solidity 0.8.19; import "src/modules/Derivative.sol"; abstract contract Derivatizer is WithModules { - // ========== DERIVATIVE MANAGEMENT ========== // // Return address will be zero if not wrapped - function deploy(Keycode dType, bytes memory data, bool wrapped) external virtual returns (uint256, address) { + function deploy( + Keycode dType, + bytes memory data, + bool wrapped + ) external virtual returns (uint256, address) { // Load the derivative module, will revert if not installed Derivative derivative = Derivative(address(_getLatestModuleIfActive(dType))); @@ -22,8 +25,16 @@ abstract contract Derivatizer is WithModules { return (tokenId, wrappedToken); } - function mint(bytes memory data, uint256 amount, bool wrapped) external virtual returns (bytes memory); - function mint(uint256 tokenId, uint256 amount, bool wrapped) external virtual returns (bytes memory); + function mint( + bytes memory data, + uint256 amount, + bool wrapped + ) external virtual returns (bytes memory); + function mint( + uint256 tokenId, + uint256 amount, + bool wrapped + ) external virtual returns (bytes memory); function redeem(bytes memory data, uint256 amount) external virtual; @@ -43,10 +54,13 @@ abstract contract Derivatizer is WithModules { // TODO view function to format implementation specific token data correctly and return to user - function exerciseCost(bytes memory data, uint256 amount) external view virtual returns (uint256); + function exerciseCost( + bytes memory data, + uint256 amount + ) external view virtual returns (uint256); function convertsTo(bytes memory data, uint256 amount) external view virtual returns (uint256); // Compute unique token ID for params on the submodule function computeId(bytes memory params_) external pure virtual returns (uint256); -} \ No newline at end of file +} diff --git a/src/lib/clones/Clone.sol b/src/lib/clones/Clone.sol index 9df0b682..07771d5d 100644 --- a/src/lib/clones/Clone.sol +++ b/src/lib/clones/Clone.sol @@ -8,11 +8,7 @@ contract Clone { /// @notice Reads an immutable arg with type address /// @param argOffset The offset of the arg in the packed data /// @return arg The arg value - function _getArgAddress(uint256 argOffset) - internal - pure - returns (address arg) - { + function _getArgAddress(uint256 argOffset) internal pure returns (address arg) { uint256 offset = _getImmutableArgsOffset(); // solhint-disable-next-line no-inline-assembly assembly { @@ -23,11 +19,7 @@ contract Clone { /// @notice Reads an immutable arg with type uint256 /// @param argOffset The offset of the arg in the packed data /// @return arg The arg value - function _getArgUint256(uint256 argOffset) - internal - pure - returns (uint256 arg) - { + function _getArgUint256(uint256 argOffset) internal pure returns (uint256 arg) { uint256 offset = _getImmutableArgsOffset(); // solhint-disable-next-line no-inline-assembly assembly { @@ -39,11 +31,10 @@ contract Clone { /// @param argOffset The offset of the arg in the packed data /// @param arrLen Number of elements in the array /// @return arr The array - function _getArgUint256Array(uint256 argOffset, uint64 arrLen) - internal - pure - returns (uint256[] memory arr) - { + function _getArgUint256Array( + uint256 argOffset, + uint64 arrLen + ) internal pure returns (uint256[] memory arr) { uint256 offset = _getImmutableArgsOffset(); uint256 el; arr = new uint256[](arrLen); @@ -60,11 +51,7 @@ contract Clone { /// @notice Reads an immutable arg with type uint64 /// @param argOffset The offset of the arg in the packed data /// @return arg The arg value - function _getArgUint64(uint256 argOffset) - internal - pure - returns (uint64 arg) - { + function _getArgUint64(uint256 argOffset) internal pure returns (uint64 arg) { uint256 offset = _getImmutableArgsOffset(); // solhint-disable-next-line no-inline-assembly assembly { @@ -87,10 +74,7 @@ contract Clone { function _getImmutableArgsOffset() internal pure returns (uint256 offset) { // solhint-disable-next-line no-inline-assembly assembly { - offset := sub( - calldatasize(), - add(shr(240, calldataload(sub(calldatasize(), 2))), 2) - ) + offset := sub(calldatasize(), add(shr(240, calldataload(sub(calldatasize(), 2))), 2)) } } -} \ No newline at end of file +} diff --git a/src/lib/clones/CloneERC20.sol b/src/lib/clones/CloneERC20.sol index 205fb0e7..4854b395 100644 --- a/src/lib/clones/CloneERC20.sol +++ b/src/lib/clones/CloneERC20.sol @@ -84,11 +84,7 @@ abstract contract CloneERC20 is Clone { return true; } - function transferFrom( - address from, - address to, - uint256 amount - ) public virtual returns (bool) { + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; diff --git a/src/lib/clones/ClonesWithImmutableArgs.sol b/src/lib/clones/ClonesWithImmutableArgs.sol index 7cc71af6..e8f23461 100644 --- a/src/lib/clones/ClonesWithImmutableArgs.sol +++ b/src/lib/clones/ClonesWithImmutableArgs.sol @@ -7,8 +7,7 @@ pragma solidity ^0.8.4; /// @notice Enables creating clone contracts with immutable args library ClonesWithImmutableArgs { /// @dev The CREATE3 proxy bytecode. - uint256 private constant _CREATE3_PROXY_BYTECODE = - 0x67363d3d37363d34f03d5260086018f3; + uint256 private constant _CREATE3_PROXY_BYTECODE = 0x67363d3d37363d34f03d5260086018f3; /// @dev Hash of the `_CREATE3_PROXY_BYTECODE`. /// Equivalent to `keccak256(abi.encodePacked(hex"67363d3d37363d34f03d5260086018f3"))`. @@ -23,10 +22,10 @@ library ClonesWithImmutableArgs { /// @param implementation The implementation contract to clone /// @param data Encoded immutable args /// @return instance The address of the created clone - function clone(address implementation, bytes memory data) - internal - returns (address payable instance) - { + function clone( + address implementation, + bytes memory data + ) internal returns (address payable instance) { // unrealistic for memory ptr or data length to exceed 256 bits unchecked { uint256 extraLength = data.length + 2; // +2 bytes for telling how much data there is appended to the call @@ -43,10 +42,7 @@ library ClonesWithImmutableArgs { // ------------------------------------------------------------------------------------------------------------- // 61 runtime | PUSH2 runtime (r) | r | – - mstore( - ptr, - 0x6100000000000000000000000000000000000000000000000000000000000000 - ) + mstore(ptr, 0x6100000000000000000000000000000000000000000000000000000000000000) mstore(add(ptr, 0x01), shl(240, runSize)) // size of the contract running bytecode (16 bits) // creation size = 0a @@ -135,7 +131,7 @@ library ClonesWithImmutableArgs { copyPtr += 32; dataPtr += 32; } - uint256 mask = ~(256**(32 - counter) - 1); + uint256 mask = ~(256 ** (32 - counter) - 1); // solhint-disable-next-line no-inline-assembly assembly { mstore(copyPtr, and(mload(dataPtr), mask)) @@ -181,10 +177,7 @@ library ClonesWithImmutableArgs { // 3d | RETURNDATASIZE | 0 | – // 61 runtime | PUSH2 runtime (r) | r 0 | – - mstore( - ptr, - 0x3d61000000000000000000000000000000000000000000000000000000000000 - ) + mstore(ptr, 0x3d61000000000000000000000000000000000000000000000000000000000000) mstore(add(ptr, 0x02), shl(240, sub(creationSize, 11))) // size of the contract running bytecode (16 bits) // creation size = 0b @@ -280,7 +273,7 @@ library ClonesWithImmutableArgs { copyPtr += 32; dataPtr += 32; } - uint256 mask = ~(256**(32 - counter) - 1); + uint256 mask = ~(256 ** (32 - counter) - 1); // solhint-disable-next-line no-inline-assembly assembly { mstore(copyPtr, and(mload(dataPtr), mask)) @@ -347,11 +340,7 @@ library ClonesWithImmutableArgs { /// @notice Returns the CREATE3 deterministic address of the contract deployed via cloneDeterministic(). /// @dev Forked from https://github.com/Vectorized/solady/blob/main/src/utils/CREATE3.sol /// @param salt The salt used by the CREATE3 deployment - function addressOfClone3(bytes32 salt) - internal - view - returns (address deployed) - { + function addressOfClone3(bytes32 salt) internal view returns (address deployed) { /// @solidity memory-safe-assembly // solhint-disable-next-line no-inline-assembly assembly { @@ -379,4 +368,4 @@ library ClonesWithImmutableArgs { deployed := keccak256(0x1e, 0x17) } } -} \ No newline at end of file +} diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index a488eaa2..410cc2fd 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -4,7 +4,6 @@ pragma solidity 0.8.19; import "src/modules/Modules.sol"; abstract contract Auction { - /* ========== ERRORS ========== */ error Auction_OnlyMarketOwner(); @@ -64,18 +63,36 @@ abstract contract Auction { // ========== ATOMIC AUCTIONS ========== // /// @param approval_ - (Optional) Permit approval signature for the quoteToken - function purchase(address recipient_, address referrer_, uint256 id_, uint256 amount_, bytes calldata auctionData_, bytes calldata approval_) external virtual returns (uint256 payout); + function purchase( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external virtual returns (uint256 payout); // ========== BATCH AUCTIONS ========== // // On-chain auction variant - function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external virtual; + function bid( + address recipient_, + address referrer_, + uint256 id_, + uint256 amount_, + uint256 minAmountOut_, + bytes calldata auctionData_, + bytes calldata approval_ + ) external virtual; function settle(uint256 id_) external virtual returns (uint256[] memory amountsOut); // Off-chain auction variant // TODO use solady data packing library to make bids smaller on the actual module to store? - function settle(uint256 id_, Bid[] memory bids_) external virtual returns (uint256[] memory amountsOut); + function settle( + uint256 id_, + Bid[] memory bids_ + ) external virtual returns (uint256[] memory amountsOut); // ========== AUCTION MANAGEMENT ========== // @@ -95,11 +112,10 @@ abstract contract Auction { function isLive(uint256 id_) public view virtual returns (bool); - function remainingCapacity(uint256 id_) external view virtual returns (uint256); + function remainingCapacity(uint256 id_) external view virtual returns (uint256); } abstract contract AuctionModule is Auction, Module { - // ========== AUCTION MANAGEMENT ========== // function auction(uint256 id_, AuctionParams memory params_) external override onlyParent { @@ -110,7 +126,6 @@ abstract contract AuctionModule is Auction, Module { // Duration must be at least min duration if (params_.duration < minAuctionDuration) revert Auction_InvalidParams(); - // Create core market data Lot memory lot; lot.start = params_.start == 0 ? uint48(block.timestamp) : params_.start; @@ -156,4 +171,4 @@ abstract contract AuctionModule is Auction, Module { function remainingCapacity(uint256 id_) external view override returns (uint256) { return lotData[id_].capacity; } -} \ No newline at end of file +} diff --git a/src/modules/Condenser.sol b/src/modules/Condenser.sol index 82194962..e825d8c8 100644 --- a/src/modules/Condenser.sol +++ b/src/modules/Condenser.sol @@ -4,9 +4,10 @@ pragma solidity 0.8.19; import "src/modules/Modules.sol"; abstract contract Condenser { - function condense(bytes memory auctionOutput_, bytes memory derivativeConfig_) external pure virtual returns (bytes memory); + function condense( + bytes memory auctionOutput_, + bytes memory derivativeConfig_ + ) external pure virtual returns (bytes memory); } -abstract contract CondenserModule is Condenser, Module { - -} \ No newline at end of file +abstract contract CondenserModule is Condenser, Module {} diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index ae7bc9e6..529261d3 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -5,7 +5,6 @@ import {ERC6909} from "lib/solmate/src/tokens/ERC6909.sol"; import "src/modules/Modules.sol"; abstract contract Derivative { - // ========== DATA STRUCTURES ========== // // TODO are some of the properties not redundant? exists, decimals, name, symbol. Can be fetched from the ERC20. @@ -31,7 +30,10 @@ abstract contract Derivative { /// @param wrapped_ Whether (true) or not (false) the derivative should be wrapped in an ERC20 token for composability /// @return tokenId_ The ID of the newly created derivative token /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. - function deploy(bytes memory params_, bool wrapped_) external virtual returns (uint256, address); + function deploy( + bytes memory params_, + bool wrapped_ + ) external virtual returns (uint256, address); /// @notice Mint new derivative tokens. Deploys the derivative token if it does not already exist. /// @param params_ ABI-encoded parameters for the derivative to be created @@ -40,7 +42,11 @@ abstract contract Derivative { /// @return tokenId_ The ID of the newly created derivative token /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. /// @return amountCreated_ The amount of derivative tokens created - function mint(bytes memory params_, uint256 amount_, bool wrapped_) external virtual returns (uint256, address, uint256); + function mint( + bytes memory params_, + uint256 amount_, + bool wrapped_ + ) external virtual returns (uint256, address, uint256); /// @notice Mint new derivative tokens for a specific token Id /// @param tokenId_ The ID of the derivative token @@ -49,7 +55,11 @@ abstract contract Derivative { /// @return tokenId_ The ID of the derivative token /// @return wrappedAddress_ The address of the ERC20 wrapped derivative token, if wrapped_ is true, otherwise, it's the zero address. /// @return amountCreated_ The amount of derivative tokens created - function mint(uint256 tokenId_, uint256 amount_, bool wrapped_) external virtual returns (uint256, address, uint256); + function mint( + uint256 tokenId_, + uint256 amount_, + bool wrapped_ + ) external virtual returns (uint256, address, uint256); /// @notice Redeem derivative tokens for underlying collateral /// @param tokenId_ The ID of the derivative token to redeem @@ -68,7 +78,12 @@ abstract contract Derivative { /// @notice Transforms an existing derivative issued by this contract into something else. Derivative is burned and collateral sent to the auction house. /// @notice Access controlled: only callable by the auction house. - function transform(uint256 tokenId_, address from_, uint256 amount_, bool wrapped_) external virtual; + function transform( + uint256 tokenId_, + address from_, + uint256 amount_, + bool wrapped_ + ) external virtual; // Wrap an existing derivative into an ERC20 token for composability // Deploys the ERC20 wrapper if it does not already exist @@ -81,7 +96,10 @@ abstract contract Derivative { // TODO view function to format implementation specific token data correctly and return to user - function exerciseCost(bytes memory data, uint256 amount) external view virtual returns (uint256); + function exerciseCost( + bytes memory data, + uint256 amount + ) external view virtual returns (uint256); function convertsTo(bytes memory data, uint256 amount) external view virtual returns (uint256); @@ -90,8 +108,5 @@ abstract contract Derivative { } abstract contract DerivativeModule is Derivative, ERC6909, Module { - function validate(bytes memory params_) external view virtual returns (bool); - } - diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index ca43403b..e0bf51fa 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -30,7 +30,7 @@ function fromKeycode(Keycode keycode_) pure returns (bytes5) { function wrapVeecode(Keycode keycode_, uint8 version_) pure returns (Veecode) { // Get the digits of the version bytes1 firstDigit = bytes1(version_ / 10 + 0x30); - bytes1 secondDigit = bytes1(version_ % 10 + 0x30); + bytes1 secondDigit = bytes1((version_ % 10) + 0x30); // Pack everything and wrap as a Veecode return Veecode.wrap(bytes7(abi.encodePacked(firstDigit, secondDigit, keycode_))); @@ -98,10 +98,13 @@ abstract contract WithModules is Owned { error ModuleAlreadySunset(Keycode keycode_); error ModuleSunset(Keycode keycode_); - // ========= EVENTS ========= // - event ModuleInstalled(Keycode indexed keycode_, uint8 indexed version_, address indexed address_); + event ModuleInstalled( + Keycode indexed keycode_, + uint8 indexed version_, + address indexed address_ + ); // ========= CONSTRUCTOR ========= // @@ -212,13 +215,17 @@ abstract contract WithModules is Owned { return address(getModuleForVeecode[veecode]); } - function _getModuleIfInstalled(Keycode keycode_, uint8 version_) internal view returns (address) { + function _getModuleIfInstalled( + Keycode keycode_, + uint8 version_ + ) internal view returns (address) { // Check that the module is installed ModStatus memory status = getModuleStatus[keycode_]; if (status.latestVersion == uint8(0)) revert ModuleNotInstalled(keycode_, 0); // Check that the module version is less than or equal to the latest version and greater than 0 - if (version_ > status.latestVersion || version_ == 0) revert ModuleNotInstalled(keycode_, version_); + if (version_ > status.latestVersion || version_ == 0) + revert ModuleNotInstalled(keycode_, version_); // Wrap into a Veecode, get module address and return // We don't need to check that the Veecode is valid because we already checked that the module is installed and pulled the version from the contract diff --git a/src/modules/auctions/FPA.sol b/src/modules/auctions/FPA.sol index 9f27cd35..e93d6f1a 100644 --- a/src/modules/auctions/FPA.sol +++ b/src/modules/auctions/FPA.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; @@ -18,7 +18,6 @@ // function marketPrice(uint256 id_) external view override returns (uint256); // } - // contract FixedPriceAuctioneer is MaxPayoutAuctioneer, IFixedPriceAuctioneer { // /* ========== STATE ========== */ diff --git a/src/modules/auctions/GDA.sol b/src/modules/auctions/GDA.sol index 648e1023..a3558b9b 100644 --- a/src/modules/auctions/GDA.sol +++ b/src/modules/auctions/GDA.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import "src/modules/auctions/bases/AtomicAuction.sol"; // import {SD59x18, sd, convert, uUNIT} from "prb-math/SD59x18.sol"; @@ -32,9 +32,6 @@ // contract GradualDutchAuctioneer is AtomicAuctionModule, GDA { // /* ========== ERRORS ========== */ - - - // /* ========== CONSTRUCTOR ========== */ // constructor( @@ -62,7 +59,6 @@ // uint256 payoutScale = 10 ** uint256(lot_.payoutToken.decimals()); // uint256 quoteScale = 10 ** uint256(lot_.quoteToken.decimals()); - // // Calculate emissions rate // uint256 payoutCapacity = lot_.capacityInQuote ? lot_.capacity.mulDiv(payoutScale, equilibriumPrice_) : lot_.capacity; // SD59x18 emissionsRate = sd(int256(payoutCapacity.mulDiv(uUNIT, (lot_.conclusion - lot_.start) * payoutScale))); diff --git a/src/modules/auctions/OBB.sol b/src/modules/auctions/OBB.sol index b9cc6cc8..b6d1c4f1 100644 --- a/src/modules/auctions/OBB.sol +++ b/src/modules/auctions/OBB.sol @@ -1 +1,4 @@ -// open bid batch auction \ No newline at end of file +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +// open bid batch auction diff --git a/src/modules/auctions/OFDA.sol b/src/modules/auctions/OFDA.sol index b1f4706e..d38f8f13 100644 --- a/src/modules/auctions/OFDA.sol +++ b/src/modules/auctions/OFDA.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; @@ -30,8 +30,6 @@ // function marketPrice(uint256 id_) external view returns (uint256); // } - - // import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; // import {IFixedPriceAuctioneer} from "src/interfaces/IFixedPriceAuctioneer.sol"; // import {OracleHelper} from "src/lib/OracleHelper.sol"; diff --git a/src/modules/auctions/OSDA.sol b/src/modules/auctions/OSDA.sol index 4dd3c882..a18fc1df 100644 --- a/src/modules/auctions/OSDA.sol +++ b/src/modules/auctions/OSDA.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; @@ -49,7 +49,6 @@ // function marketPrice(uint256 id_) external view override returns (uint256); // } - // import {MaxPayoutAuctioneer, IAggregator, Authority} from "src/auctioneers/bases/MaxPayoutAuctioneer.sol"; // import {IFixedPriceAuctioneer} from "src/interfaces/IFixedPriceAuctioneer.sol"; // import {OracleHelper} from "src/lib/OracleHelper.sol"; diff --git a/src/modules/auctions/SBB.sol b/src/modules/auctions/SBB.sol index 9dad1994..8320f0e2 100644 --- a/src/modules/auctions/SBB.sol +++ b/src/modules/auctions/SBB.sol @@ -1 +1,4 @@ -// sealed bid batch auction \ No newline at end of file +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; + +// sealed bid batch auction diff --git a/src/modules/auctions/SDA.sol b/src/modules/auctions/SDA.sol index 214bd6bd..0d2a84b4 100644 --- a/src/modules/auctions/SDA.sol +++ b/src/modules/auctions/SDA.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import {IMaxPayoutAuctioneer} from "src/interfaces/IMaxPayoutAuctioneer.sol"; diff --git a/src/modules/auctions/TVGDA.sol b/src/modules/auctions/TVGDA.sol index 3e616e0b..19e68d6e 100644 --- a/src/modules/auctions/TVGDA.sol +++ b/src/modules/auctions/TVGDA.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import "src/modules/auctions/bases/AtomicAuction.sol"; // import {SD59x18, sd, convert, uUNIT} from "prb-math/SD59x18.sol"; @@ -118,5 +118,3 @@ // } // } - - diff --git a/src/modules/auctions/bases/AtomicAuction.sol b/src/modules/auctions/bases/AtomicAuction.sol index b6329fc4..1b301535 100644 --- a/src/modules/auctions/bases/AtomicAuction.sol +++ b/src/modules/auctions/bases/AtomicAuction.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import "src/modules/Auction.sol"; @@ -79,4 +79,4 @@ // // function maxPayout(uint256 id_) public view virtual returns (uint256); // // function maxAmountAccepted(uint256 id_) public view virtual returns (uint256); -// } \ No newline at end of file +// } diff --git a/src/modules/auctions/bases/BatchAuction.sol b/src/modules/auctions/bases/BatchAuction.sol index e17ba8e7..6c029897 100644 --- a/src/modules/auctions/bases/BatchAuction.sol +++ b/src/modules/auctions/bases/BatchAuction.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import "src/modules/Auction.sol"; @@ -26,7 +26,6 @@ // abstract contract BatchAuction { // error BatchAuction_NotConcluded(); - // // ========== STATE VARIABLES ========== // // mapping(uint256 lotId => Auction.Bid[] bids) public lotBids; @@ -36,7 +35,6 @@ // // TODO add batch auction specific getters // } - // abstract contract OnChainBatchAuctionModule is AuctionModule, BatchAuction { // function bid(address recipient_, address referrer_, uint256 id_, uint256 amount_, uint256 minAmountOut_, bytes calldata auctionData_, bytes calldata approval_) external override onlyParent { @@ -107,4 +105,4 @@ // function _settle(uint256 id_, Bid[] memory bids_) internal virtual returns (uint256[] memory amountsOut); -// } \ No newline at end of file +// } diff --git a/src/modules/auctions/bases/DiscreteAuction.sol b/src/modules/auctions/bases/DiscreteAuction.sol index 72067da5..4eca4e96 100644 --- a/src/modules/auctions/bases/DiscreteAuction.sol +++ b/src/modules/auctions/bases/DiscreteAuction.sol @@ -1,5 +1,5 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import "src/modules/auctions/bases/AtomicAuction.sol"; @@ -182,4 +182,4 @@ // // Cap max payout at the remaining capacity // return styleData[id_].maxPayout > capacity ? capacity : styleData[id_].maxPayout; // } -// } \ No newline at end of file +// } diff --git a/src/modules/derivatives/CliffVesting.sol b/src/modules/derivatives/CliffVesting.sol index 4e5a6b16..cedebd21 100644 --- a/src/modules/derivatives/CliffVesting.sol +++ b/src/modules/derivatives/CliffVesting.sol @@ -1,10 +1,9 @@ -// /// SPDX-License-Identifier: AGPL-3.0 -// pragma solidity 0.8.19; +/// SPDX-License-Identifier: AGPL-3.0 +pragma solidity 0.8.19; // import {ClonesWithImmutableArgs} from "src/lib/clones/ClonesWithImmutableArgs.sol"; // import "src/modules/Derivative.sol"; - // // TODO this only uses the ERC20 clones, need to convert to ERC6909 with optional ERC20 via wrapping the ERC6909 // contract CliffVesting is DerivativeModule { // using ClonesWithImmutableArgs for address; @@ -20,8 +19,6 @@ // uint48 expiry; // } - - // // ========== SUBMODULE SETUP ========== // // constructor(Module parent_) Submodule(parent_) {} @@ -70,12 +67,6 @@ // emit DerivativeCreated(dType, id, t.wrapped, base, expiry); // } - - - - - - // // // Get address of fixed expiry token using salt // // address feToken = ClonesWithImmutableArgs.addressOfClone3(salt); @@ -146,5 +137,4 @@ // return _computeId(base, expiry); // } - -// } \ No newline at end of file +// } From 439f369428c93de2db2cb1c310f307140922ee85 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 12:03:47 +0400 Subject: [PATCH 18/34] Add solhint:check script --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 4c619195..0621bae3 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "prettier": "prettier --write 'src/**/*.sol' '**/*.md'", "prettier:check": "prettier --check 'src/**/*.sol' '**/*.md'", "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix", + "solhint:check": "solhint --fix --config ./.solhint.json 'src/**/*.sol'", "lint": "npm run prettier && npm run solhint", - "lint:check": "pnpm run prettier:check && pnpm run solhint", + "lint:check": "pnpm run prettier:check && pnpm run solhint:check", "test": "forge test -vvv" }, "keywords": [], From 16eb1c07f20a65cdd564b4635c62a45c12441ae9 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 12:05:29 +0400 Subject: [PATCH 19/34] Update CI configuration --- .github/workflows/test.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09880b1d..89a53b40 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,10 @@ name: test -on: workflow_dispatch +on: + push: + branches: + - master + pull_request: env: FOUNDRY_PROFILE: ci @@ -17,10 +21,25 @@ jobs: with: submodules: recursive + - uses: actions/setup-node@v2 + + - uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Install Node dependencies + run: pnpm install + - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: nightly-ca67d15f4abd46394b324c50e21e66f306a1162d + + - name: Install Foundry dependencies + run: forge install + + - name: Run lint check + run: pnpm run lint:check - name: Run Forge build run: | @@ -30,5 +49,5 @@ jobs: - name: Run Forge tests run: | - forge test -vvv + pnpm run test id: test From ccb7f9e61c60a7563fdd5837def78bf1edbe119f Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 12:09:28 +0400 Subject: [PATCH 20/34] Fix linting --- .prettierignore | 6 ++++++ .solhintignore | 1 + package.json | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..064f0ddf --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +cache/ +coverage/ +docs/ +lib/ +node_modules/ +out/ diff --git a/.solhintignore b/.solhintignore index 82544125..5400ccf3 100644 --- a/.solhintignore +++ b/.solhintignore @@ -1,2 +1,3 @@ src/test/**/*.sol src/scripts/**/*.sol +lib/** diff --git a/package.json b/package.json index 0621bae3..261c09e3 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "build": "forge build", "prettier": "prettier --write 'src/**/*.sol' '**/*.md'", "prettier:check": "prettier --check 'src/**/*.sol' '**/*.md'", - "solhint": "solhint --config ./.solhint.json 'src/**/*.sol' --fix", - "solhint:check": "solhint --fix --config ./.solhint.json 'src/**/*.sol'", + "solhint": "solhint --fix --noPrompt --config ./.solhint.json 'src/**/*.sol'", + "solhint:check": "solhint --config ./.solhint.json 'src/**/*.sol'", "lint": "npm run prettier && npm run solhint", "lint:check": "pnpm run prettier:check && pnpm run solhint:check", "test": "forge test -vvv" From add0e1fd192d2a3d934cf17e5e9e32f2623fc316 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 12:45:04 +0400 Subject: [PATCH 21/34] Add tests for sunsetModule --- src/modules/Modules.sol | 19 ++++--- test/modules/Modules/installModule.t.sol | 48 ++++++++++++------ test/modules/Modules/sunsetModule.t.sol | 63 ++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 test/modules/Modules/sunsetModule.t.sol diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index e0bf51fa..da866696 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -126,10 +126,10 @@ abstract contract WithModules is Owned { /// @notice Mapping of Keycode to module status information mapping(Keycode => ModStatus) public getModuleStatus; - /// @notice Installs a module. Can be used to install a new module or upgrade an existing one. - /// @dev The version of the installed module must be one greater than the latest version. If it's a new module, then the version must be 1. - /// @dev Only one version of a module is active for creation functions at a time. Older versions continue to work for existing data. - /// @dev If a module is currently sunset, installing a new version will remove the sunset. + /// @notice Installs a module. Can be used to install a new module or upgrade an existing one. + /// @dev The version of the installed module must be one greater than the latest version. If it's a new module, then the version must be 1. + /// @dev Only one version of a module is active for creation functions at a time. Older versions continue to work for existing data. + /// @dev If a module is currently sunset, installing a new version will remove the sunset. /// /// @dev This function reverts if: /// @dev - The caller is not the owner @@ -162,8 +162,15 @@ abstract contract WithModules is Owned { emit ModuleInstalled(keycode, version, address(newModule_)); } - /// @notice Prevents future use of module, but functionality remains for existing users. Modules should implement functionality such that creation functions are disabled if sunset. - /// @dev Sunset is used to disable a module type without installing a new one. + /// @notice Sunsets a module + /// @notice Sunsetting a module prevents future deployments that use the module, but functionality remains for existing users. + /// @notice Modules should implement functionality such that creation functions are disabled if sunset. + /// @dev Sunset is used to disable a module type without installing a new one. + /// + /// @dev This function reverts if: + /// @dev - The caller is not the owner + /// @dev - The module is not installed + /// @dev - The module is already sunset function sunsetModule(Keycode keycode_) external onlyOwner { // Check that the module is installed if (!_moduleIsInstalled(keycode_)) revert ModuleNotInstalled(keycode_, 0); diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol index f87bb096..415b806a 100644 --- a/test/modules/Modules/installModule.t.sol +++ b/test/modules/Modules/installModule.t.sol @@ -20,10 +20,21 @@ contract InstallModuleTest is Test { mockModule = new MockModuleV1(address(withModules)); } - function testReverts_whenSameVersionIsInstalled() external { - // Install version 1 + modifier whenVersion1IsInstalled() { + withModules.installModule(mockModule); + _; + } + + function testReverts_whenUnauthorized() external { + address alice = address(0x1); + + vm.expectRevert("UNAUTHORIZED"); + + vm.prank(alice); withModules.installModule(mockModule); + } + function testReverts_whenSameVersionIsInstalled() external whenVersion1IsInstalled() { bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 1); vm.expectRevert(err); @@ -31,10 +42,7 @@ contract InstallModuleTest is Test { withModules.installModule(mockModule); } - function testReverts_whenNewerVersionIsInstalled() external { - // Install version 1 - withModules.installModule(mockModule); - + function testReverts_whenNewerVersionIsInstalled() external whenVersion1IsInstalled() { // Install version 2 Module upgradedModule = new MockModuleV2(address(withModules)); withModules.installModule(upgradedModule); @@ -66,10 +74,7 @@ contract InstallModuleTest is Test { withModules.installModule(upgradedModule); } - function testReverts_whenNewerVersionSkips() external { - // Install version 1 - withModules.installModule(mockModule); - + function testReverts_whenNewerVersionSkips() external whenVersion1IsInstalled() { Module upgradedModule = new MockModuleV3(address(withModules)); bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 3); @@ -106,10 +111,7 @@ contract InstallModuleTest is Test { assertEq(fromKeycode(modules[0]), "MOCK"); } - function test_whenPreviousVersionIsInstalled() external { - // Install version 1 - withModules.installModule(mockModule); - + function test_whenPreviousVersionIsInstalled() external whenVersion1IsInstalled() { Module upgradedModule = new MockModuleV2(address(withModules)); // Upgrade to version 2 @@ -133,4 +135,22 @@ contract InstallModuleTest is Test { assertEq(modules.length, 1); assertEq(fromKeycode(modules[0]), "MOCK"); } + + function test_whenModuleIsSunset() external whenVersion1IsInstalled() { + // Sunset version 1 + withModules.sunsetModule(toKeycode("MOCK")); + + // Install version 2 + Module upgradedModule = new MockModuleV2(address(withModules)); + withModules.installModule(upgradedModule); + + // Check that the module is installed + Module upgradedModule_ = withModules.getModuleForVeecode(upgradedModule.VEECODE()); + assertEq(address(upgradedModule_), address(upgradedModule)); + + // Check that the module is re-enabled + (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + assertEq(moduleLatestVersion, 2); + assertEq(moduleIsSunset, false); + } } diff --git a/test/modules/Modules/sunsetModule.t.sol b/test/modules/Modules/sunsetModule.t.sol new file mode 100644 index 00000000..3dde3845 --- /dev/null +++ b/test/modules/Modules/sunsetModule.t.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; + +// Contracts +import {WithModules, Module, toKeycode} from "src/modules/Modules.sol"; + +contract SunsetModuleTest is Test { + WithModules internal withModules; + MockModuleV1 internal mockModule; + + function setUp() external { + withModules = new MockWithModules(address(this)); + mockModule = new MockModuleV1(address(withModules)); + } + + modifier whenVersion1IsInstalled() { + withModules.installModule(mockModule); + _; + } + + function testReverts_whenUnauthorized() external whenVersion1IsInstalled() { + address alice = address(0x1); + + vm.expectRevert("UNAUTHORIZED"); + + vm.prank(alice); + withModules.sunsetModule(toKeycode("MOCK")); + } + + function testReverts_whenModuleIsNotInstalled() external { + bytes memory err = abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toKeycode("MOCK"), 0); + vm.expectRevert(err); + + withModules.sunsetModule(toKeycode("MOCK")); + } + + function testReverts_whenModuleAlreadySunset() external whenVersion1IsInstalled() { + // Sunset the module + withModules.sunsetModule(toKeycode("MOCK")); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadySunset.selector, toKeycode("MOCK")); + vm.expectRevert(err); + + // Sunset the module again + withModules.sunsetModule(toKeycode("MOCK")); + } + + function test_success() external whenVersion1IsInstalled() { + // Sunset the module + withModules.sunsetModule(toKeycode("MOCK")); + + // Assert that the status has been changed + ( , bool sunset) = withModules.getModuleStatus(toKeycode("MOCK")); + assertEq(sunset, true); + } +} From e07804aa2ad2aa8694a1f70e609555d6ebed17ea Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 12:57:37 +0400 Subject: [PATCH 22/34] Documentation --- src/modules/Modules.sol | 48 +++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index da866696..549e9e45 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -117,13 +117,13 @@ abstract contract WithModules is Owned { bool sunset; } - /// @notice Array of all modules currently installed. + /// @notice Array of the Keycodes corresponding to the currently installed modules. Keycode[] public modules; /// @notice Mapping of Veecode to Module address. mapping(Veecode => Module) public getModuleForVeecode; - /// @notice Mapping of Keycode to module status information + /// @notice Mapping of Keycode to module status information. mapping(Keycode => ModStatus) public getModuleStatus; /// @notice Installs a module. Can be used to install a new module or upgrade an existing one. @@ -137,6 +137,8 @@ abstract contract WithModules is Owned { /// @dev - The module has an invalid Veecode /// @dev - The module (or other versions) is already installed /// @dev - The module version is not one greater than the latest version + /// + /// @param newModule_ The new module function installModule(Module newModule_) external onlyOwner { // Validate new module is a contract, has correct parent, and has valid Keycode ensureContract(address(newModule_)); @@ -171,6 +173,8 @@ abstract contract WithModules is Owned { /// @dev - The caller is not the owner /// @dev - The module is not installed /// @dev - The module is already sunset + /// + /// @param keycode_ The module keycode function sunsetModule(Keycode keycode_) external onlyOwner { // Check that the module is installed if (!_moduleIsInstalled(keycode_)) revert ModuleNotInstalled(keycode_, 0); @@ -183,8 +187,16 @@ abstract contract WithModules is Owned { status.sunset = true; } - // Decide if we need this function, i.e. do we need to set any parameters or call permissioned functions on any modules? - // Answer: yes, e.g. when setting default values on an Auction module, like minimum duration or minimum deposit interval + /// @notice Performs a call on a module + /// @notice This can be used to perform administrative functions on a module, such as setting parameters or calling permissioned functions + /// @dev This function reverts if: + /// @dev - The caller is not the owner + /// @dev - The module is not installed + /// @dev - The call is made to a prohibited function + /// @dev - The call reverted + /// + /// @param veecode_ The module Veecode + /// @param callData_ The call data function execOnModule( Veecode veecode_, bytes memory callData_ @@ -195,12 +207,16 @@ abstract contract WithModules is Owned { return returnData; } - // Need to consider the implications of not having upgradable modules and the affect of this list growing over time + // TODO Need to consider the implications of not having upgradable modules and the affect of this list growing over time + /// @notice Returns the Keycodes of the installed modules function getModules() external view returns (Keycode[] memory) { return modules; } - // Checks whether any module is installed under the keycode + /// @notice Checks whether any module is installed under the keycode + /// + /// @param keycode_ The module keycode + /// @return True if the module is installed, false otherwise function _moduleIsInstalled(Keycode keycode_) internal view returns (bool) { // Any module that has been installed will have a latest version greater than 0 // We can check not equal here to save gas @@ -208,6 +224,13 @@ abstract contract WithModules is Owned { return latestVersion != uint8(0); } + /// @notice Returns the address of the latest version of a module + /// @dev This function reverts if: + /// @dev - The module is not installed + /// @dev - The module is sunset + /// + /// @param keycode_ The module keycode + /// @return The address of the latest version of the module function _getLatestModuleIfActive(Keycode keycode_) internal view returns (address) { // Check that the module is installed ModStatus memory status = getModuleStatus[keycode_]; @@ -222,6 +245,13 @@ abstract contract WithModules is Owned { return address(getModuleForVeecode[veecode]); } + /// @notice Returns the address of a module + /// @dev This function reverts if: + /// @dev - The specific module and version is not installed + /// + /// @param keycode_ The module keycode + /// @param version_ The module version + /// @return The address of the module function _getModuleIfInstalled( Keycode keycode_, uint8 version_ @@ -240,6 +270,12 @@ abstract contract WithModules is Owned { return address(getModuleForVeecode[veecode]); } + /// @notice Returns the address of a module + /// @dev This function reverts if: + /// @dev - The specific module and version is not installed + /// + /// @param veecode_ The module Veecode + /// @return The address of the module function _getModuleIfInstalled(Veecode veecode_) internal view returns (address) { // In this case, it's simpler to check that the stored address is not zero Module mod = getModuleForVeecode[veecode_]; From dbfb0bd889d9e78723f45848ea290a6a58f9b6af Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 13:55:04 +0400 Subject: [PATCH 23/34] Tests for execOnModule --- test/modules/Modules/MockModule.sol | 4 ++ test/modules/Modules/execOnModule.t.sol | 55 ++++++++++++++++++++++++ test/modules/Modules/installModule.t.sol | 2 +- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 test/modules/Modules/execOnModule.t.sol diff --git a/test/modules/Modules/MockModule.sol b/test/modules/Modules/MockModule.sol index bca35509..5f0299e6 100644 --- a/test/modules/Modules/MockModule.sol +++ b/test/modules/Modules/MockModule.sol @@ -10,6 +10,10 @@ contract MockModuleV1 is Module { function VEECODE() public pure override returns (Veecode) { return wrapVeecode(toKeycode("MOCK"), 1); } + + function mock() external pure returns (bool) { + return true; + } } contract MockModuleV2 is Module { diff --git a/test/modules/Modules/execOnModule.t.sol b/test/modules/Modules/execOnModule.t.sol new file mode 100644 index 00000000..59c3e067 --- /dev/null +++ b/test/modules/Modules/execOnModule.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; + +// Contracts +import {WithModules, Veecode, toKeycode} from "src/modules/Modules.sol"; + +contract ExecOnModule is Test { + WithModules internal withModules; + MockModuleV1 internal mockModule; + + function setUp() external { + withModules = new MockWithModules(address(this)); + mockModule = new MockModuleV1(address(withModules)); + } + + modifier whenVersion1IsInstalled() { + withModules.installModule(mockModule); + _; + } + + function testReverts_whenUnauthorized() external whenVersion1IsInstalled() { + address alice = address(0x1); + Veecode veecode = mockModule.VEECODE(); + + vm.expectRevert("UNAUTHORIZED"); + + vm.prank(alice); + withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.mock.selector)); + } + + function testReverts_whenModuleNotInstalled() external { + Veecode veecode = mockModule.VEECODE(); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toKeycode("MOCK"), 1); + vm.expectRevert(err); + + withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.mock.selector)); + } + + function test_success() external whenVersion1IsInstalled() { + bytes memory returnData = withModules.execOnModule(mockModule.VEECODE(), abi.encodeWithSelector(MockModuleV1.mock.selector)); + + // Decode the return data + (bool returnValue) = abi.decode(returnData, (bool)); + + assertEq(returnValue, true); + } +} \ No newline at end of file diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol index 415b806a..e58cbec9 100644 --- a/test/modules/Modules/installModule.t.sol +++ b/test/modules/Modules/installModule.t.sol @@ -9,7 +9,7 @@ import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; import {MockModuleV0, MockModuleV1, MockModuleV2, MockModuleV3, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; // Contracts -import {WithModules, Module, Keycode, fromKeycode, toKeycode, wrapVeecode, InvalidVeecode} from "src/modules/Modules.sol"; +import {WithModules, Module, Keycode, fromKeycode, toKeycode, InvalidVeecode} from "src/modules/Modules.sol"; contract InstallModuleTest is Test { WithModules internal withModules; From 220fc80b429d5890d675fa31cb1769d4383ff548 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 13:58:39 +0400 Subject: [PATCH 24/34] Add event for module sunset --- src/modules/Modules.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 549e9e45..af834944 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -96,7 +96,7 @@ abstract contract WithModules is Owned { error ModuleNotInstalled(Keycode keycode_, uint8 version_); error ModuleExecutionReverted(bytes error_); error ModuleAlreadySunset(Keycode keycode_); - error ModuleSunset(Keycode keycode_); + error ModuleIsSunset(Keycode keycode_); // ========= EVENTS ========= // @@ -106,6 +106,8 @@ abstract contract WithModules is Owned { address indexed address_ ); + event ModuleSunset(Keycode indexed keycode_); + // ========= CONSTRUCTOR ========= // constructor(address owner_) Owned(owner_) {} @@ -185,6 +187,8 @@ abstract contract WithModules is Owned { // Set the module to sunset status.sunset = true; + + emit ModuleSunset(keycode_); } /// @notice Performs a call on a module @@ -237,7 +241,7 @@ abstract contract WithModules is Owned { if (status.latestVersion == uint8(0)) revert ModuleNotInstalled(keycode_, 0); // Check that the module is not sunset - if (status.sunset) revert ModuleSunset(keycode_); + if (status.sunset) revert ModuleIsSunset(keycode_); // Wrap into a Veecode, get module address and return // We don't need to check that the Veecode is valid because we already checked that the module is installed and pulled the version from the contract From 4a968197a326d1ff1799dc8e72aaa9a604fdc949 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 15:37:01 +0400 Subject: [PATCH 25/34] Add blacklist of module functions --- src/modules/Modules.sol | 125 +++++++++++++++--- test/modules/Modules/MockModule.sol | 8 ++ .../Modules/addProhibitedModuleFunction.t.sol | 67 ++++++++++ test/modules/Modules/execOnModule.t.sol | 15 +++ .../removeProhibitedModuleFunction.t.sol | 58 ++++++++ 5 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 test/modules/Modules/addProhibitedModuleFunction.t.sol create mode 100644 test/modules/Modules/removeProhibitedModuleFunction.t.sol diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index af834944..83fe627e 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -95,6 +95,8 @@ abstract contract WithModules is Owned { error InvalidModuleInstall(Keycode keycode_, uint8 version_); error ModuleNotInstalled(Keycode keycode_, uint8 version_); error ModuleExecutionReverted(bytes error_); + error ModuleFunctionProhibited(Veecode veecode_, bytes4 function_); + error ModuleFunctionInvalid(bytes4 function_); error ModuleAlreadySunset(Keycode keycode_); error ModuleIsSunset(Keycode keycode_); @@ -108,17 +110,23 @@ abstract contract WithModules is Owned { event ModuleSunset(Keycode indexed keycode_); + event ModuleFunctionProhibitionAdded(bytes4 indexed function_); + + event ModuleFunctionProhibitionRemoved(bytes4 indexed function_); + // ========= CONSTRUCTOR ========= // constructor(address owner_) Owned(owner_) {} - // ========= MODULE MANAGEMENT ========= // + // ========= STRUCTS ========= // struct ModStatus { uint8 latestVersion; bool sunset; } + // ========= STATE VARIABLES ========= // + /// @notice Array of the Keycodes corresponding to the currently installed modules. Keycode[] public modules; @@ -128,6 +136,15 @@ abstract contract WithModules is Owned { /// @notice Mapping of Keycode to module status information. mapping(Keycode => ModStatus) public getModuleStatus; + /// @notice Functions that are prohibited from being called on a module + /// @dev This is used to prevent governance calling functions on modules that are not administrative. + bytes4[] public prohibitedModuleFunctions; + + /// @notice The number of prohibited module functions + uint256 public prohibitedModuleFunctionsCount; + + // ========= MODULE MANAGEMENT ========= // + /// @notice Installs a module. Can be used to install a new module or upgrade an existing one. /// @dev The version of the installed module must be one greater than the latest version. If it's a new module, then the version must be 1. /// @dev Only one version of a module is active for creation functions at a time. Older versions continue to work for existing data. @@ -191,26 +208,6 @@ abstract contract WithModules is Owned { emit ModuleSunset(keycode_); } - /// @notice Performs a call on a module - /// @notice This can be used to perform administrative functions on a module, such as setting parameters or calling permissioned functions - /// @dev This function reverts if: - /// @dev - The caller is not the owner - /// @dev - The module is not installed - /// @dev - The call is made to a prohibited function - /// @dev - The call reverted - /// - /// @param veecode_ The module Veecode - /// @param callData_ The call data - function execOnModule( - Veecode veecode_, - bytes memory callData_ - ) external onlyOwner returns (bytes memory) { - address module = _getModuleIfInstalled(veecode_); - (bool success, bytes memory returnData) = module.call(callData_); - if (!success) revert ModuleExecutionReverted(returnData); - return returnData; - } - // TODO Need to consider the implications of not having upgradable modules and the affect of this list growing over time /// @notice Returns the Keycodes of the installed modules function getModules() external view returns (Keycode[] memory) { @@ -289,6 +286,92 @@ abstract contract WithModules is Owned { } return address(mod); } + + // ========= MODULE FUNCTIONS ========= // + + /// @notice Performs a call on a module + /// @notice This can be used to perform administrative functions on a module, such as setting parameters or calling permissioned functions + /// @dev This function reverts if: + /// @dev - The caller is not the owner + /// @dev - The module is not installed + /// @dev - The call is made to a prohibited function + /// @dev - The call reverted + /// + /// @param veecode_ The module Veecode + /// @param callData_ The call data + function execOnModule( + Veecode veecode_, + bytes calldata callData_ + ) external onlyOwner returns (bytes memory) { + address module = _getModuleIfInstalled(veecode_); + + // Check that the function is not prohibited + bytes4 functionSelector = bytes4(callData_[:4]); + if (_isModuleFunctionProhibited(functionSelector)) + revert ModuleFunctionProhibited(veecode_, functionSelector); + + (bool success, bytes memory returnData) = module.call(callData_); + if (!success) revert ModuleExecutionReverted(returnData); + return returnData; + } + + function _isModuleFunctionProhibited(bytes4 function_) internal view returns (bool) { + uint256 length = prohibitedModuleFunctions.length; + for (uint256 i; i < length; i++) { + if (prohibitedModuleFunctions[i] == function_) return true; + } + return false; + } + + /// @notice Adds a function selector to the list of prohibited module functions + /// @notice This will prevent the function from being called on any module + /// @dev This function reverts if: + /// @dev - The caller is not the owner + /// @dev - The function selector is already prohibited + /// @dev - The function selector is zero + /// + /// @param function_ The function selector + function addProhibitedModuleFunction(bytes4 function_) external onlyOwner { + if (function_ == bytes4(0)) revert ModuleFunctionInvalid(function_); + + // Check for duplicates + if (_isModuleFunctionProhibited(function_)) revert ModuleFunctionInvalid(function_); + + prohibitedModuleFunctions.push(function_); + prohibitedModuleFunctionsCount++; + + emit ModuleFunctionProhibitionAdded(function_); + } + + /// @notice Removes a function selector from the list of prohibited module functions + /// @notice This will allow the function to be called on any module + /// @dev This function reverts if: + /// @dev - The caller is not the owner + /// @dev - The function selector is not prohibited + /// @dev - The function selector is zero + /// + /// @param function_ The function selector + function removeProhibitedModuleFunction(bytes4 function_) external onlyOwner { + if (function_ == bytes4(0)) revert ModuleFunctionInvalid(function_); + + uint256 length = prohibitedModuleFunctions.length; + bool functionFound; + + for (uint256 i; i < length; i++) { + if (prohibitedModuleFunctions[i] == function_) { + prohibitedModuleFunctions[i] = prohibitedModuleFunctions[length - 1]; + prohibitedModuleFunctions.pop(); + functionFound = true; + break; + } + } + + if (!functionFound) revert ModuleFunctionInvalid(function_); + + prohibitedModuleFunctionsCount--; + + emit ModuleFunctionProhibitionRemoved(function_); + } } /// @notice Modules are isolated components of a contract that can be upgraded independently. diff --git a/test/modules/Modules/MockModule.sol b/test/modules/Modules/MockModule.sol index 5f0299e6..67527651 100644 --- a/test/modules/Modules/MockModule.sol +++ b/test/modules/Modules/MockModule.sol @@ -14,6 +14,14 @@ contract MockModuleV1 is Module { function mock() external pure returns (bool) { return true; } + + function prohibited() external pure returns (bool) { + return true; + } + + function prohibitedTwo() external pure returns (bool) { + return true; + } } contract MockModuleV2 is Module { diff --git a/test/modules/Modules/addProhibitedModuleFunction.t.sol b/test/modules/Modules/addProhibitedModuleFunction.t.sol new file mode 100644 index 00000000..a599b75b --- /dev/null +++ b/test/modules/Modules/addProhibitedModuleFunction.t.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; + +// Contracts +import {WithModules, Veecode, toKeycode} from "src/modules/Modules.sol"; + +contract AddProhibitedModuleFunctionTest is Test { + WithModules internal withModules; + + function setUp() external { + withModules = new MockWithModules(address(this)); + } + + function testReverts_whenUnauthorized() external { + address alice = address(0x1); + + vm.expectRevert("UNAUTHORIZED"); + + vm.prank(alice); + withModules.addProhibitedModuleFunction(MockModuleV1.mock.selector); + } + + function testReverts_whenDuplicate() external { + // Set as prohibited + withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, MockModuleV1.prohibited.selector); + vm.expectRevert(err); + + // Set as prohibited again + withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); + } + + function testReverts_whenZero() external { + bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, bytes4(0)); + vm.expectRevert(err); + + // Set as prohibited again + withModules.addProhibitedModuleFunction(bytes4(0)); + } + + function test_success() external { + // Set as prohibited + withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); + + // Check that the selector is added + uint256 functionsCount = withModules.prohibitedModuleFunctionsCount(); + assertEq(functionsCount, 1); + assertEq(withModules.prohibitedModuleFunctions(0), MockModuleV1.prohibited.selector); + + // Add another + withModules.addProhibitedModuleFunction(MockModuleV1.prohibitedTwo.selector); + + // Check that the selector is added + functionsCount = withModules.prohibitedModuleFunctionsCount(); + assertEq(functionsCount, 2); + assertEq(withModules.prohibitedModuleFunctions(0), MockModuleV1.prohibited.selector); + assertEq(withModules.prohibitedModuleFunctions(1), MockModuleV1.prohibitedTwo.selector); + } +} \ No newline at end of file diff --git a/test/modules/Modules/execOnModule.t.sol b/test/modules/Modules/execOnModule.t.sol index 59c3e067..7710411c 100644 --- a/test/modules/Modules/execOnModule.t.sol +++ b/test/modules/Modules/execOnModule.t.sol @@ -44,6 +44,21 @@ contract ExecOnModule is Test { withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.mock.selector)); } + function testReverts_whenFunctionIsProhibited() external whenVersion1IsInstalled() { + Veecode veecode = mockModule.VEECODE(); + + // Call the function + withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.prohibited.selector)); + + // Set it as prohibited + withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); + + bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionProhibited.selector, veecode, MockModuleV1.prohibited.selector); + vm.expectRevert(err); + + withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.prohibited.selector)); + } + function test_success() external whenVersion1IsInstalled() { bytes memory returnData = withModules.execOnModule(mockModule.VEECODE(), abi.encodeWithSelector(MockModuleV1.mock.selector)); diff --git a/test/modules/Modules/removeProhibitedModuleFunction.t.sol b/test/modules/Modules/removeProhibitedModuleFunction.t.sol new file mode 100644 index 00000000..7363c886 --- /dev/null +++ b/test/modules/Modules/removeProhibitedModuleFunction.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.19; + +// Libraries +import {Test} from "forge-std/Test.sol"; + +// Mocks +import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; +import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; + +// Contracts +import {WithModules, Veecode, toKeycode} from "src/modules/Modules.sol"; + +contract RemoveProhibitedModuleFunctionTest is Test { + WithModules internal withModules; + + function setUp() external { + withModules = new MockWithModules(address(this)); + } + + modifier whenAModuleFunctionIsProhibited() { + withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); + _; + } + + function testReverts_whenUnauthorized() external whenAModuleFunctionIsProhibited() { + address alice = address(0x1); + + vm.expectRevert("UNAUTHORIZED"); + + vm.prank(alice); + withModules.removeProhibitedModuleFunction(MockModuleV1.prohibited.selector); + } + + function testReverts_whenNotProhibited() external { + bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, MockModuleV1.mock.selector); + vm.expectRevert(err); + + withModules.removeProhibitedModuleFunction(MockModuleV1.mock.selector); + } + + function testReverts_whenZero() external { + bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, bytes4(0)); + vm.expectRevert(err); + + // Set as prohibited again + withModules.removeProhibitedModuleFunction(bytes4(0)); + } + + function test_success() external whenAModuleFunctionIsProhibited() { + // Remove + withModules.removeProhibitedModuleFunction(MockModuleV1.prohibited.selector); + + // Check that the selector is removed + uint256 functionsCount = withModules.prohibitedModuleFunctionsCount(); + assertEq(functionsCount, 0); + } +} \ No newline at end of file From beae6fe1990c8e1d04d62afae2a42353aca1d467 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 15:37:28 +0400 Subject: [PATCH 26/34] Remove getModules() and add modulesCount --- src/modules/Modules.sol | 14 +++++++------- test/modules/Modules/installModule.t.sol | 17 +++++++++++------ test/modules/Modules/sunsetModule.t.sol | 7 ++++++- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 83fe627e..6f4ef1d6 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -130,6 +130,9 @@ abstract contract WithModules is Owned { /// @notice Array of the Keycodes corresponding to the currently installed modules. Keycode[] public modules; + /// @notice The number of modules installed. + uint256 public modulesCount; + /// @notice Mapping of Veecode to Module address. mapping(Veecode => Module) public getModuleForVeecode; @@ -175,7 +178,10 @@ abstract contract WithModules is Owned { getModuleForVeecode[veecode] = newModule_; // If the module is not already installed, add it to the list of modules - if (version == uint8(1)) modules.push(keycode); + if (version == uint8(1)) { + modules.push(keycode); + modulesCount++; + } // Initialize the module newModule_.INIT(); @@ -208,12 +214,6 @@ abstract contract WithModules is Owned { emit ModuleSunset(keycode_); } - // TODO Need to consider the implications of not having upgradable modules and the affect of this list growing over time - /// @notice Returns the Keycodes of the installed modules - function getModules() external view returns (Keycode[] memory) { - return modules; - } - /// @notice Checks whether any module is installed under the keycode /// /// @param keycode_ The module keycode diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol index e58cbec9..116296fa 100644 --- a/test/modules/Modules/installModule.t.sol +++ b/test/modules/Modules/installModule.t.sol @@ -106,9 +106,9 @@ contract InstallModuleTest is Test { assertEq(moduleLatestVersion, 1); // Check that the modules array is updated - Keycode[] memory modules = withModules.getModules(); - assertEq(modules.length, 1); - assertEq(fromKeycode(modules[0]), "MOCK"); + uint256 modulesCount = withModules.modulesCount(); + assertEq(modulesCount, 1); + assertEq(fromKeycode(withModules.modules(0)), "MOCK"); } function test_whenPreviousVersionIsInstalled() external whenVersion1IsInstalled() { @@ -131,9 +131,9 @@ contract InstallModuleTest is Test { assertEq(address(previousModule_), address(mockModule)); // Check that the modules array remains the same - Keycode[] memory modules = withModules.getModules(); - assertEq(modules.length, 1); - assertEq(fromKeycode(modules[0]), "MOCK"); + uint256 modulesCount = withModules.modulesCount(); + assertEq(modulesCount, 1); + assertEq(fromKeycode(withModules.modules(0)), "MOCK"); } function test_whenModuleIsSunset() external whenVersion1IsInstalled() { @@ -152,5 +152,10 @@ contract InstallModuleTest is Test { (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); assertEq(moduleLatestVersion, 2); assertEq(moduleIsSunset, false); + + // Check that the modules array remains the same + uint256 modulesCount = withModules.modulesCount(); + assertEq(modulesCount, 1); + assertEq(fromKeycode(withModules.modules(0)), "MOCK"); } } diff --git a/test/modules/Modules/sunsetModule.t.sol b/test/modules/Modules/sunsetModule.t.sol index 3dde3845..a28d4ca1 100644 --- a/test/modules/Modules/sunsetModule.t.sol +++ b/test/modules/Modules/sunsetModule.t.sol @@ -9,7 +9,7 @@ import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; // Contracts -import {WithModules, Module, toKeycode} from "src/modules/Modules.sol"; +import {WithModules, Module, toKeycode, fromKeycode} from "src/modules/Modules.sol"; contract SunsetModuleTest is Test { WithModules internal withModules; @@ -59,5 +59,10 @@ contract SunsetModuleTest is Test { // Assert that the status has been changed ( , bool sunset) = withModules.getModuleStatus(toKeycode("MOCK")); assertEq(sunset, true); + + // Check that the modules array remains the same + uint256 modulesCount = withModules.modulesCount(); + assertEq(modulesCount, 1); + assertEq(fromKeycode(withModules.modules(0)), "MOCK"); } } From 48d05064170bd321b1ba49bfc4cc2866434c179d Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Thu, 4 Jan 2024 15:44:07 +0400 Subject: [PATCH 27/34] Add TYPE() to Module contract. Natspec comments. --- src/modules/Modules.sol | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 6f4ef1d6..989556d2 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -90,6 +90,8 @@ function ensureValidVeecode(Veecode veecode_) pure { if (moduleVersion == 0) revert InvalidVeecode(veecode_); } +/// @notice Abstract contract that provides functionality for installing and interacting with modules. +/// @dev This contract is intended to be inherited by any contract that needs to install modules. abstract contract WithModules is Owned { // ========= ERRORS ========= // error InvalidModuleInstall(Keycode keycode_, uint8 version_); @@ -376,28 +378,41 @@ abstract contract WithModules is Owned { /// @notice Modules are isolated components of a contract that can be upgraded independently. /// @dev Two main patterns are considered for Modules: -/// 1. Directly calling modules from the parent contract to execute upgradable logic or having the option to add new sub-components to a contract -/// 2. Delegate calls to modules to execute upgradable logic, similar to a proxy, but only for specific functions and being able to add new sub-components to a contract +/// @dev 1. Directly calling modules from the parent contract to execute upgradable logic or having the option to add new sub-components to a contract +/// @dev 2. Delegate calls to modules to execute upgradable logic, similar to a proxy, but only for specific functions and being able to add new sub-components to a contract abstract contract Module { + // ========= ERRORS ========= // + error Module_OnlyParent(address caller_); error Module_InvalidParent(); + // ========= STATE VARIABLES ========= // + /// @notice The parent contract for this module. - // TODO should we use an Owner pattern here to be able to change the parent? - // May be useful if the parent contract needs to be replaced. - // On the otherhand, it may be better to just deploy a new module with the new parent to reduce the governance burden. - address public parent; + address public immutable parent; + + // ========= CONSTRUCTOR ========= // constructor(address parent_) { + if (parent_ == address(0)) revert Module_InvalidParent(); + parent = parent_; } + // ========= MODIFIERS ========= // + /// @notice Modifier to restrict functions to be called only by parent module. modifier onlyParent() { if (msg.sender != parent) revert Module_OnlyParent(msg.sender); _; } + // ========= FUNCTIONS ========= // + + /// @notice 2 byte identifier for the module type + /// @dev This enables the parent contract to check the module type + function TYPE() public pure virtual returns (bytes2) {} + /// @notice 7 byte, versioned identifier for the module. 2 characters from 0-9 that signify the version and 3-5 characters from A-Z. function VEECODE() public pure virtual returns (Veecode) {} From 26413e5ea4e51524fab8cfd6ff5f468412bc35f7 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 5 Jan 2024 10:29:41 +0400 Subject: [PATCH 28/34] Shift to using forge fmt for formatting --- foundry.toml | 13 +++++++++++++ package.json | 19 ++++--------------- pnpm-lock.yaml | 32 -------------------------------- 3 files changed, 17 insertions(+), 47 deletions(-) diff --git a/foundry.toml b/foundry.toml index b75591b1..b8b9e352 100644 --- a/foundry.toml +++ b/foundry.toml @@ -10,4 +10,17 @@ remappings = [ "solmate/=lib/solmate/src/" ] +[fmt] +line_length = 100 +tab_width = 4 +bracket_spacing = false +multiline_func_header = "params_first" +quote_style = "double" +number_underscore = "thousands" +wrap_comments = false +ignore = [ + "lib/**", + "src/lib/**", +] + # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/package.json b/package.json index 261c09e3..54e47edf 100644 --- a/package.json +++ b/package.json @@ -9,29 +9,18 @@ }, "scripts": { "build": "forge build", - "prettier": "prettier --write 'src/**/*.sol' '**/*.md'", - "prettier:check": "prettier --check 'src/**/*.sol' '**/*.md'", + "fmt": "forge fmt", + "fmt:check": "forge fmt --check", "solhint": "solhint --fix --noPrompt --config ./.solhint.json 'src/**/*.sol'", "solhint:check": "solhint --config ./.solhint.json 'src/**/*.sol'", - "lint": "npm run prettier && npm run solhint", - "lint:check": "pnpm run prettier:check && pnpm run solhint:check", + "lint": "npm run fmt && npm run solhint", + "lint:check": "pnpm run fmt:check && pnpm run solhint:check", "test": "forge test -vvv" }, "keywords": [], "author": "", "license": "ISC", - "prettier": { - "tabWidth": 4, - "singleQuote": false, - "bracketSpacing": false, - "printWidth": 100, - "plugins": [ - "prettier-plugin-solidity" - ] - }, "devDependencies": { - "prettier": "^3.1.1", - "prettier-plugin-solidity": "^1.3.1", "solhint": "^4.0.0" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da9eff40..51e8b155 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,12 +5,6 @@ settings: excludeLinksFromLockfile: false devDependencies: - prettier: - specifier: ^3.1.1 - version: 3.1.1 - prettier-plugin-solidity: - specifier: ^1.3.1 - version: 1.3.1(prettier@3.1.1) solhint: specifier: ^4.0.0 version: 4.0.0 @@ -71,10 +65,6 @@ packages: antlr4ts: 0.5.0-alpha.4 dev: true - /@solidity-parser/parser@0.17.0: - resolution: {integrity: sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw==} - dev: true - /@szmarczak/http-timer@5.0.1: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} @@ -530,18 +520,6 @@ packages: engines: {node: '>=4'} dev: true - /prettier-plugin-solidity@1.3.1(prettier@3.1.1): - resolution: {integrity: sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA==} - engines: {node: '>=16'} - peerDependencies: - prettier: '>=2.3.0' - dependencies: - '@solidity-parser/parser': 0.17.0 - prettier: 3.1.1 - semver: 7.5.4 - solidity-comments-extractor: 0.0.8 - dev: true - /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -550,12 +528,6 @@ packages: dev: true optional: true - /prettier@3.1.1: - resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} - engines: {node: '>=14'} - hasBin: true - dev: true - /proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} dev: true @@ -660,10 +632,6 @@ packages: - typescript dev: true - /solidity-comments-extractor@0.0.8: - resolution: {integrity: sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g==} - dev: true - /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} From 80e41ba9fb647c962c7cd1ea2b106f3445ffcde7 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 5 Jan 2024 10:29:53 +0400 Subject: [PATCH 29/34] chore: linting --- src/AuctionHouse.sol | 30 +++++------- src/bases/Auctioneer.sol | 5 +- src/bases/Derivatizer.sol | 5 +- src/modules/Auction.sol | 14 +++--- src/modules/Derivative.sol | 5 +- src/modules/Modules.sol | 12 ++--- test/modules/Modules/Keycode.t.sol | 48 +++++++++++-------- .../Modules/addProhibitedModuleFunction.t.sol | 9 ++-- test/modules/Modules/execOnModule.t.sol | 19 +++++--- test/modules/Modules/getModuleStatus.t.sol | 12 +++-- test/modules/Modules/installModule.t.sol | 47 ++++++++++++------ .../removeProhibitedModuleFunction.t.sol | 13 +++-- test/modules/Modules/sunsetModule.t.sol | 14 +++--- 13 files changed, 137 insertions(+), 96 deletions(-) diff --git a/src/AuctionHouse.sol b/src/AuctionHouse.sol index d8c7085b..8f3503d6 100644 --- a/src/AuctionHouse.sol +++ b/src/AuctionHouse.sol @@ -18,8 +18,8 @@ import {Auction, AuctionModule} from "src/modules/Auction.sol"; import {fromKeycode, WithModules} from "src/modules/Modules.sol"; abstract contract FeeManager { - // TODO write fee logic in separate contract to keep it organized - // Router can inherit +// TODO write fee logic in separate contract to keep it organized +// Router can inherit } abstract contract Router is FeeManager { @@ -129,23 +129,17 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { // 2. Calculate protocol fee as the total expected fee amount minus the referrer fee // to avoid issues with rounding from separate fee calculations // TODO think about how to reduce storage loads - uint256 toReferrer = referrer_ == address(0) - ? 0 - : (amount_ * referrerFees[referrer_]) / FEE_DECIMALS; - uint256 toProtocol = ((amount_ * (protocolFee + referrerFees[referrer_])) / FEE_DECIMALS) - - toReferrer; + uint256 toReferrer = + referrer_ == address(0) ? 0 : (amount_ * referrerFees[referrer_]) / FEE_DECIMALS; + uint256 toProtocol = + ((amount_ * (protocolFee + referrerFees[referrer_])) / FEE_DECIMALS) - toReferrer; // Load routing data for the lot Routing memory routing = lotRouting[id_]; // Send purchase to auction house and get payout plus any extra output (payout) = module.purchase( - recipient_, - referrer_, - amount_ - toReferrer - toProtocol, - id_, - auctionData_, - approval_ + recipient_, referrer_, amount_ - toReferrer - toProtocol, id_, auctionData_, approval_ ); // Check that payout is at least minimum amount out @@ -236,18 +230,16 @@ abstract contract AuctionHouse is Derivatizer, Auctioneer, Router { } else { // Get the module for the derivative type // We assume that the module type has been checked when the lot was created - DerivativeModule module = DerivativeModule( - _getLatestModuleIfActive(routing_.derivativeType) - ); + DerivativeModule module = + DerivativeModule(_getLatestModuleIfActive(routing_.derivativeType)); bytes memory derivativeParams = routing_.derivativeParams; // If condenser specified, condense auction output and derivative params before sending to derivative module if (fromKeycode(routing_.condenserType) != bytes6(0)) { // Get condenser module - CondenserModule condenser = CondenserModule( - _getLatestModuleIfActive(routing_.condenserType) - ); + CondenserModule condenser = + CondenserModule(_getLatestModuleIfActive(routing_.condenserType)); // Condense auction output and derivative params derivativeParams = condenser.condense(auctionOutput_, derivativeParams); diff --git a/src/bases/Auctioneer.sol b/src/bases/Auctioneer.sol index aa0017fc..9d810a85 100644 --- a/src/bases/Auctioneer.sol +++ b/src/bases/Auctioneer.sol @@ -107,9 +107,8 @@ abstract contract Auctioneer is WithModules { // If payout is a derivative, validate derivative data on the derivative module if (fromKeycode(routing_.derivativeType) != bytes6(0)) { // Load derivative module, this checks that it is installed. - DerivativeModule derivativeModule = DerivativeModule( - _getLatestModuleIfActive(routing_.derivativeType) - ); + DerivativeModule derivativeModule = + DerivativeModule(_getLatestModuleIfActive(routing_.derivativeType)); // Call module validate function to validate implementation-specific data derivativeModule.validate(routing_.derivativeParams); diff --git a/src/bases/Derivatizer.sol b/src/bases/Derivatizer.sol index f0bd0556..3a7e3192 100644 --- a/src/bases/Derivatizer.sol +++ b/src/bases/Derivatizer.sol @@ -59,7 +59,10 @@ abstract contract Derivatizer is WithModules { uint256 amount ) external view virtual returns (uint256); - function convertsTo(bytes memory data, uint256 amount) external view virtual returns (uint256); + function convertsTo( + bytes memory data, + uint256 amount + ) external view virtual returns (uint256); // Compute unique token ID for params on the submodule function computeId(bytes memory params_) external pure virtual returns (uint256); diff --git a/src/modules/Auction.sol b/src/modules/Auction.sol index 410cc2fd..7b81ad86 100644 --- a/src/modules/Auction.sol +++ b/src/modules/Auction.sol @@ -17,9 +17,7 @@ abstract contract Auction { /* ========== EVENTS ========== */ event AuctionCreated( - uint256 indexed id, - address indexed payoutToken, - address indexed quoteToken + uint256 indexed id, address indexed payoutToken, address indexed quoteToken ); event AuctionClosed(uint256 indexed id); @@ -120,8 +118,9 @@ abstract contract AuctionModule is Auction, Module { function auction(uint256 id_, AuctionParams memory params_) external override onlyParent { // Start time must be zero or in the future - if (params_.start > 0 && params_.start < uint48(block.timestamp)) + if (params_.start > 0 && params_.start < uint48(block.timestamp)) { revert Auction_InvalidParams(); + } // Duration must be at least min duration if (params_.duration < minAuctionDuration) revert Auction_InvalidParams(); @@ -163,9 +162,10 @@ abstract contract AuctionModule is Auction, Module { // TODO does this need to change for batch auctions? function isLive(uint256 id_) public view override returns (bool) { - return (lotData[id_].capacity != 0 && - lotData[id_].conclusion > uint48(block.timestamp) && - lotData[id_].start <= uint48(block.timestamp)); + return ( + lotData[id_].capacity != 0 && lotData[id_].conclusion > uint48(block.timestamp) + && lotData[id_].start <= uint48(block.timestamp) + ); } function remainingCapacity(uint256 id_) external view override returns (uint256) { diff --git a/src/modules/Derivative.sol b/src/modules/Derivative.sol index 529261d3..f80924f1 100644 --- a/src/modules/Derivative.sol +++ b/src/modules/Derivative.sol @@ -101,7 +101,10 @@ abstract contract Derivative { uint256 amount ) external view virtual returns (uint256); - function convertsTo(bytes memory data, uint256 amount) external view virtual returns (uint256); + function convertsTo( + bytes memory data, + uint256 amount + ) external view virtual returns (uint256); // Compute unique token ID for params on the submodule function computeId(bytes memory params_) external pure virtual returns (uint256); diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 989556d2..0e57c4b0 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -67,7 +67,7 @@ function ensureContract(address target_) view { // solhint-disable-next-line func-visibility function ensureValidVeecode(Veecode veecode_) pure { bytes7 unwrapped = Veecode.unwrap(veecode_); - for (uint256 i; i < 7; ) { + for (uint256 i; i < 7;) { bytes1 char = unwrapped[i]; if (i < 2) { // First 2 characters must be the version, each character is a number 0-9 @@ -105,9 +105,7 @@ abstract contract WithModules is Owned { // ========= EVENTS ========= // event ModuleInstalled( - Keycode indexed keycode_, - uint8 indexed version_, - address indexed address_ + Keycode indexed keycode_, uint8 indexed version_, address indexed address_ ); event ModuleSunset(Keycode indexed keycode_); @@ -264,8 +262,9 @@ abstract contract WithModules is Owned { if (status.latestVersion == uint8(0)) revert ModuleNotInstalled(keycode_, 0); // Check that the module version is less than or equal to the latest version and greater than 0 - if (version_ > status.latestVersion || version_ == 0) + if (version_ > status.latestVersion || version_ == 0) { revert ModuleNotInstalled(keycode_, version_); + } // Wrap into a Veecode, get module address and return // We don't need to check that the Veecode is valid because we already checked that the module is installed and pulled the version from the contract @@ -309,8 +308,9 @@ abstract contract WithModules is Owned { // Check that the function is not prohibited bytes4 functionSelector = bytes4(callData_[:4]); - if (_isModuleFunctionProhibited(functionSelector)) + if (_isModuleFunctionProhibited(functionSelector)) { revert ModuleFunctionProhibited(veecode_, functionSelector); + } (bool success, bytes memory returnData) = module.call(callData_); if (!success) revert ModuleExecutionReverted(returnData); diff --git a/test/modules/Modules/Keycode.t.sol b/test/modules/Modules/Keycode.t.sol index 1b793324..b138de24 100644 --- a/test/modules/Modules/Keycode.t.sol +++ b/test/modules/Modules/Keycode.t.sol @@ -4,7 +4,17 @@ pragma solidity 0.8.19; import {Test} from "forge-std/Test.sol"; import {console2} from "forge-std/console2.sol"; -import {Keycode, toKeycode, fromKeycode, Veecode, wrapVeecode, fromVeecode, unwrapVeecode, ensureValidVeecode, InvalidVeecode} from "src/modules/Modules.sol"; +import { + Keycode, + toKeycode, + fromKeycode, + Veecode, + wrapVeecode, + fromVeecode, + unwrapVeecode, + ensureValidVeecode, + InvalidVeecode +} from "src/modules/Modules.sol"; contract KeycodeTest is Test { function test_keycode() external { @@ -26,7 +36,11 @@ contract KeycodeTest is Test { ensureValidVeecode(t1_veecode); } - function _modifyKeycode(bytes5 keycode_, uint8 index_, uint8 character_) internal pure returns (bytes5) { + function _modifyKeycode( + bytes5 keycode_, + uint8 index_, + uint8 character_ + ) internal pure returns (bytes5) { bytes memory keycodeBytes = abi.encodePacked(keycode_); keycodeBytes[index_] = bytes1(character_); return bytes5(keycodeBytes); @@ -53,7 +67,10 @@ contract KeycodeTest is Test { assertFalse(fromVeecode(t3_veecode) == fromVeecode(t4_veecode)); } - function testRevert_ensureValidVeecode_invalidRequiredCharacter(uint8 character_, uint8 index_) external { + function testRevert_ensureValidVeecode_invalidRequiredCharacter( + uint8 character_, + uint8 index_ + ) external { // Only manipulating the first 3 characters vm.assume(index_ < 3); @@ -66,16 +83,16 @@ contract KeycodeTest is Test { Keycode keycode = toKeycode(keycodeInput); Veecode t1_veecode = wrapVeecode(keycode, 1); - bytes memory err = abi.encodeWithSelector( - InvalidVeecode.selector, - t1_veecode - ); + bytes memory err = abi.encodeWithSelector(InvalidVeecode.selector, t1_veecode); vm.expectRevert(err); ensureValidVeecode(t1_veecode); } - function testRevert_ensureValidVeecode_invalidOptionalCharacter(uint8 character_, uint8 index_) external { + function testRevert_ensureValidVeecode_invalidOptionalCharacter( + uint8 character_, + uint8 index_ + ) external { // Only manipulating the characters 4-5 vm.assume(index_ < 3); @@ -88,10 +105,7 @@ contract KeycodeTest is Test { Keycode keycode = toKeycode(keycodeInput); Veecode t1_veecode = wrapVeecode(keycode, 1); - bytes memory err = abi.encodeWithSelector( - InvalidVeecode.selector, - t1_veecode - ); + bytes memory err = abi.encodeWithSelector(InvalidVeecode.selector, t1_veecode); vm.expectRevert(err); ensureValidVeecode(t1_veecode); @@ -101,10 +115,7 @@ contract KeycodeTest is Test { Keycode keycode = toKeycode("TEST"); Veecode t1_veecode = wrapVeecode(keycode, 0); - bytes memory err = abi.encodeWithSelector( - InvalidVeecode.selector, - t1_veecode - ); + bytes memory err = abi.encodeWithSelector(InvalidVeecode.selector, t1_veecode); vm.expectRevert(err); ensureValidVeecode(t1_veecode); @@ -117,10 +128,7 @@ contract KeycodeTest is Test { Keycode keycode = toKeycode("TEST"); Veecode t1_veecode = wrapVeecode(keycode, version_); - bytes memory err = abi.encodeWithSelector( - InvalidVeecode.selector, - t1_veecode - ); + bytes memory err = abi.encodeWithSelector(InvalidVeecode.selector, t1_veecode); vm.expectRevert(err); ensureValidVeecode(t1_veecode); diff --git a/test/modules/Modules/addProhibitedModuleFunction.t.sol b/test/modules/Modules/addProhibitedModuleFunction.t.sol index a599b75b..33f7eb93 100644 --- a/test/modules/Modules/addProhibitedModuleFunction.t.sol +++ b/test/modules/Modules/addProhibitedModuleFunction.t.sol @@ -31,7 +31,9 @@ contract AddProhibitedModuleFunctionTest is Test { // Set as prohibited withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, MockModuleV1.prohibited.selector); + bytes memory err = abi.encodeWithSelector( + WithModules.ModuleFunctionInvalid.selector, MockModuleV1.prohibited.selector + ); vm.expectRevert(err); // Set as prohibited again @@ -39,7 +41,8 @@ contract AddProhibitedModuleFunctionTest is Test { } function testReverts_whenZero() external { - bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, bytes4(0)); + bytes memory err = + abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, bytes4(0)); vm.expectRevert(err); // Set as prohibited again @@ -64,4 +67,4 @@ contract AddProhibitedModuleFunctionTest is Test { assertEq(withModules.prohibitedModuleFunctions(0), MockModuleV1.prohibited.selector); assertEq(withModules.prohibitedModuleFunctions(1), MockModuleV1.prohibitedTwo.selector); } -} \ No newline at end of file +} diff --git a/test/modules/Modules/execOnModule.t.sol b/test/modules/Modules/execOnModule.t.sol index 7710411c..7db06bc1 100644 --- a/test/modules/Modules/execOnModule.t.sol +++ b/test/modules/Modules/execOnModule.t.sol @@ -25,7 +25,7 @@ contract ExecOnModule is Test { _; } - function testReverts_whenUnauthorized() external whenVersion1IsInstalled() { + function testReverts_whenUnauthorized() external whenVersion1IsInstalled { address alice = address(0x1); Veecode veecode = mockModule.VEECODE(); @@ -38,13 +38,14 @@ contract ExecOnModule is Test { function testReverts_whenModuleNotInstalled() external { Veecode veecode = mockModule.VEECODE(); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toKeycode("MOCK"), 1); + bytes memory err = + abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toKeycode("MOCK"), 1); vm.expectRevert(err); withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.mock.selector)); } - function testReverts_whenFunctionIsProhibited() external whenVersion1IsInstalled() { + function testReverts_whenFunctionIsProhibited() external whenVersion1IsInstalled { Veecode veecode = mockModule.VEECODE(); // Call the function @@ -53,18 +54,22 @@ contract ExecOnModule is Test { // Set it as prohibited withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionProhibited.selector, veecode, MockModuleV1.prohibited.selector); + bytes memory err = abi.encodeWithSelector( + WithModules.ModuleFunctionProhibited.selector, veecode, MockModuleV1.prohibited.selector + ); vm.expectRevert(err); withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.prohibited.selector)); } - function test_success() external whenVersion1IsInstalled() { - bytes memory returnData = withModules.execOnModule(mockModule.VEECODE(), abi.encodeWithSelector(MockModuleV1.mock.selector)); + function test_success() external whenVersion1IsInstalled { + bytes memory returnData = withModules.execOnModule( + mockModule.VEECODE(), abi.encodeWithSelector(MockModuleV1.mock.selector) + ); // Decode the return data (bool returnValue) = abi.decode(returnData, (bool)); assertEq(returnValue, true); } -} \ No newline at end of file +} diff --git a/test/modules/Modules/getModuleStatus.t.sol b/test/modules/Modules/getModuleStatus.t.sol index 065d4993..57691692 100644 --- a/test/modules/Modules/getModuleStatus.t.sol +++ b/test/modules/Modules/getModuleStatus.t.sol @@ -27,14 +27,16 @@ contract GetModuleStatusTest is Test { } function test_WhenAMatchingModuleCannotBeFound() external { - (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + (uint8 moduleLatestVersion, bool moduleIsSunset) = + withModules.getModuleStatus(toKeycode("MOCK")); assertEq(moduleLatestVersion, 0); assertFalse(moduleIsSunset); } function test_WhenAMatchingModuleIsFound() external whenAModuleIsInstalled { - (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + (uint8 moduleLatestVersion, bool moduleIsSunset) = + withModules.getModuleStatus(toKeycode("MOCK")); assertEq(moduleLatestVersion, 1); assertFalse(moduleIsSunset); @@ -45,7 +47,8 @@ contract GetModuleStatusTest is Test { MockModuleV2 upgradedMockModule = new MockModuleV2(address(withModules)); withModules.installModule(upgradedMockModule); - (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + (uint8 moduleLatestVersion, bool moduleIsSunset) = + withModules.getModuleStatus(toKeycode("MOCK")); assertEq(moduleLatestVersion, 2); assertFalse(moduleIsSunset); @@ -55,7 +58,8 @@ contract GetModuleStatusTest is Test { // Sunset the module withModules.sunsetModule(toKeycode("MOCK")); - (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + (uint8 moduleLatestVersion, bool moduleIsSunset) = + withModules.getModuleStatus(toKeycode("MOCK")); assertEq(moduleLatestVersion, 1); assertTrue(moduleIsSunset); diff --git a/test/modules/Modules/installModule.t.sol b/test/modules/Modules/installModule.t.sol index 116296fa..a2b15cb7 100644 --- a/test/modules/Modules/installModule.t.sol +++ b/test/modules/Modules/installModule.t.sol @@ -6,10 +6,23 @@ import {Test} from "forge-std/Test.sol"; // Mocks import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; -import {MockModuleV0, MockModuleV1, MockModuleV2, MockModuleV3, MockInvalidModule} from "test/modules/Modules/MockModule.sol"; +import { + MockModuleV0, + MockModuleV1, + MockModuleV2, + MockModuleV3, + MockInvalidModule +} from "test/modules/Modules/MockModule.sol"; // Contracts -import {WithModules, Module, Keycode, fromKeycode, toKeycode, InvalidVeecode} from "src/modules/Modules.sol"; +import { + WithModules, + Module, + Keycode, + fromKeycode, + toKeycode, + InvalidVeecode +} from "src/modules/Modules.sol"; contract InstallModuleTest is Test { WithModules internal withModules; @@ -34,20 +47,22 @@ contract InstallModuleTest is Test { withModules.installModule(mockModule); } - function testReverts_whenSameVersionIsInstalled() external whenVersion1IsInstalled() { - bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 1); + function testReverts_whenSameVersionIsInstalled() external whenVersion1IsInstalled { + bytes memory err = + abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 1); vm.expectRevert(err); // Install version 1 again withModules.installModule(mockModule); } - function testReverts_whenNewerVersionIsInstalled() external whenVersion1IsInstalled() { + function testReverts_whenNewerVersionIsInstalled() external whenVersion1IsInstalled { // Install version 2 Module upgradedModule = new MockModuleV2(address(withModules)); withModules.installModule(upgradedModule); - bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 1); + bytes memory err = + abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 1); vm.expectRevert(err); // Install version 1 @@ -67,17 +82,19 @@ contract InstallModuleTest is Test { function testReverts_whenInitialVersionIncorrect() external { Module upgradedModule = new MockModuleV2(address(withModules)); - bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 2); + bytes memory err = + abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 2); vm.expectRevert(err); // Install version 2 (skips version 1) withModules.installModule(upgradedModule); } - function testReverts_whenNewerVersionSkips() external whenVersion1IsInstalled() { + function testReverts_whenNewerVersionSkips() external whenVersion1IsInstalled { Module upgradedModule = new MockModuleV3(address(withModules)); - bytes memory err = abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 3); + bytes memory err = + abi.encodeWithSelector(WithModules.InvalidModuleInstall.selector, toKeycode("MOCK"), 3); vm.expectRevert(err); // Install version 3 @@ -102,7 +119,7 @@ contract InstallModuleTest is Test { assertEq(address(module), address(mockModule)); // Check that the latest version is recorded - (uint8 moduleLatestVersion, ) = withModules.getModuleStatus(toKeycode("MOCK")); + (uint8 moduleLatestVersion,) = withModules.getModuleStatus(toKeycode("MOCK")); assertEq(moduleLatestVersion, 1); // Check that the modules array is updated @@ -111,7 +128,7 @@ contract InstallModuleTest is Test { assertEq(fromKeycode(withModules.modules(0)), "MOCK"); } - function test_whenPreviousVersionIsInstalled() external whenVersion1IsInstalled() { + function test_whenPreviousVersionIsInstalled() external whenVersion1IsInstalled { Module upgradedModule = new MockModuleV2(address(withModules)); // Upgrade to version 2 @@ -122,7 +139,8 @@ contract InstallModuleTest is Test { assertEq(address(upgradedModule_), address(upgradedModule)); // Check that the version is correct - (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + (uint8 moduleLatestVersion, bool moduleIsSunset) = + withModules.getModuleStatus(toKeycode("MOCK")); assertEq(moduleLatestVersion, 2); assertEq(moduleIsSunset, false); @@ -136,7 +154,7 @@ contract InstallModuleTest is Test { assertEq(fromKeycode(withModules.modules(0)), "MOCK"); } - function test_whenModuleIsSunset() external whenVersion1IsInstalled() { + function test_whenModuleIsSunset() external whenVersion1IsInstalled { // Sunset version 1 withModules.sunsetModule(toKeycode("MOCK")); @@ -149,7 +167,8 @@ contract InstallModuleTest is Test { assertEq(address(upgradedModule_), address(upgradedModule)); // Check that the module is re-enabled - (uint8 moduleLatestVersion, bool moduleIsSunset) = withModules.getModuleStatus(toKeycode("MOCK")); + (uint8 moduleLatestVersion, bool moduleIsSunset) = + withModules.getModuleStatus(toKeycode("MOCK")); assertEq(moduleLatestVersion, 2); assertEq(moduleIsSunset, false); diff --git a/test/modules/Modules/removeProhibitedModuleFunction.t.sol b/test/modules/Modules/removeProhibitedModuleFunction.t.sol index 7363c886..94e3e9fa 100644 --- a/test/modules/Modules/removeProhibitedModuleFunction.t.sol +++ b/test/modules/Modules/removeProhibitedModuleFunction.t.sol @@ -23,7 +23,7 @@ contract RemoveProhibitedModuleFunctionTest is Test { _; } - function testReverts_whenUnauthorized() external whenAModuleFunctionIsProhibited() { + function testReverts_whenUnauthorized() external whenAModuleFunctionIsProhibited { address alice = address(0x1); vm.expectRevert("UNAUTHORIZED"); @@ -33,21 +33,24 @@ contract RemoveProhibitedModuleFunctionTest is Test { } function testReverts_whenNotProhibited() external { - bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, MockModuleV1.mock.selector); + bytes memory err = abi.encodeWithSelector( + WithModules.ModuleFunctionInvalid.selector, MockModuleV1.mock.selector + ); vm.expectRevert(err); withModules.removeProhibitedModuleFunction(MockModuleV1.mock.selector); } function testReverts_whenZero() external { - bytes memory err = abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, bytes4(0)); + bytes memory err = + abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, bytes4(0)); vm.expectRevert(err); // Set as prohibited again withModules.removeProhibitedModuleFunction(bytes4(0)); } - function test_success() external whenAModuleFunctionIsProhibited() { + function test_success() external whenAModuleFunctionIsProhibited { // Remove withModules.removeProhibitedModuleFunction(MockModuleV1.prohibited.selector); @@ -55,4 +58,4 @@ contract RemoveProhibitedModuleFunctionTest is Test { uint256 functionsCount = withModules.prohibitedModuleFunctionsCount(); assertEq(functionsCount, 0); } -} \ No newline at end of file +} diff --git a/test/modules/Modules/sunsetModule.t.sol b/test/modules/Modules/sunsetModule.t.sol index a28d4ca1..87ecad3a 100644 --- a/test/modules/Modules/sunsetModule.t.sol +++ b/test/modules/Modules/sunsetModule.t.sol @@ -25,7 +25,7 @@ contract SunsetModuleTest is Test { _; } - function testReverts_whenUnauthorized() external whenVersion1IsInstalled() { + function testReverts_whenUnauthorized() external whenVersion1IsInstalled { address alice = address(0x1); vm.expectRevert("UNAUTHORIZED"); @@ -35,29 +35,31 @@ contract SunsetModuleTest is Test { } function testReverts_whenModuleIsNotInstalled() external { - bytes memory err = abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toKeycode("MOCK"), 0); + bytes memory err = + abi.encodeWithSelector(WithModules.ModuleNotInstalled.selector, toKeycode("MOCK"), 0); vm.expectRevert(err); withModules.sunsetModule(toKeycode("MOCK")); } - function testReverts_whenModuleAlreadySunset() external whenVersion1IsInstalled() { + function testReverts_whenModuleAlreadySunset() external whenVersion1IsInstalled { // Sunset the module withModules.sunsetModule(toKeycode("MOCK")); - bytes memory err = abi.encodeWithSelector(WithModules.ModuleAlreadySunset.selector, toKeycode("MOCK")); + bytes memory err = + abi.encodeWithSelector(WithModules.ModuleAlreadySunset.selector, toKeycode("MOCK")); vm.expectRevert(err); // Sunset the module again withModules.sunsetModule(toKeycode("MOCK")); } - function test_success() external whenVersion1IsInstalled() { + function test_success() external whenVersion1IsInstalled { // Sunset the module withModules.sunsetModule(toKeycode("MOCK")); // Assert that the status has been changed - ( , bool sunset) = withModules.getModuleStatus(toKeycode("MOCK")); + (, bool sunset) = withModules.getModuleStatus(toKeycode("MOCK")); assertEq(sunset, true); // Check that the modules array remains the same From e622a8ae54490df58a61b037802cc75359261a25 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 5 Jan 2024 10:34:15 +0400 Subject: [PATCH 30/34] Remove .prettierignore --- .prettierignore | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 064f0ddf..00000000 --- a/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -cache/ -coverage/ -docs/ -lib/ -node_modules/ -out/ From f853b19425b3a741ca4e822fa16f9f972f5ff7e9 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 5 Jan 2024 11:09:28 +0400 Subject: [PATCH 31/34] Shift to module-level modifier to gate internal functions --- src/modules/Modules.sol | 119 ++++++++---------- test/modules/Modules/MockModule.sol | 6 +- test/modules/Modules/MockWithModules.sol | 9 +- .../Modules/addProhibitedModuleFunction.t.sol | 70 ----------- test/modules/Modules/execOnModule.t.sol | 31 +++-- .../removeProhibitedModuleFunction.t.sol | 61 --------- 6 files changed, 79 insertions(+), 217 deletions(-) delete mode 100644 test/modules/Modules/addProhibitedModuleFunction.t.sol delete mode 100644 test/modules/Modules/removeProhibitedModuleFunction.t.sol diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 0e57c4b0..5c5b3bec 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -94,10 +94,10 @@ function ensureValidVeecode(Veecode veecode_) pure { /// @dev This contract is intended to be inherited by any contract that needs to install modules. abstract contract WithModules is Owned { // ========= ERRORS ========= // + error InvalidModuleInstall(Keycode keycode_, uint8 version_); error ModuleNotInstalled(Keycode keycode_, uint8 version_); error ModuleExecutionReverted(bytes error_); - error ModuleFunctionProhibited(Veecode veecode_, bytes4 function_); error ModuleFunctionInvalid(bytes4 function_); error ModuleAlreadySunset(Keycode keycode_); error ModuleIsSunset(Keycode keycode_); @@ -110,10 +110,6 @@ abstract contract WithModules is Owned { event ModuleSunset(Keycode indexed keycode_); - event ModuleFunctionProhibitionAdded(bytes4 indexed function_); - - event ModuleFunctionProhibitionRemoved(bytes4 indexed function_); - // ========= CONSTRUCTOR ========= // constructor(address owner_) Owned(owner_) {} @@ -139,12 +135,7 @@ abstract contract WithModules is Owned { /// @notice Mapping of Keycode to module status information. mapping(Keycode => ModStatus) public getModuleStatus; - /// @notice Functions that are prohibited from being called on a module - /// @dev This is used to prevent governance calling functions on modules that are not administrative. - bytes4[] public prohibitedModuleFunctions; - - /// @notice The number of prohibited module functions - uint256 public prohibitedModuleFunctionsCount; + bool public isExecOnModuleInternal; // ========= MODULE MANAGEMENT ========= // @@ -300,79 +291,55 @@ abstract contract WithModules is Owned { /// /// @param veecode_ The module Veecode /// @param callData_ The call data + /// @return The return data from the call function execOnModule( Veecode veecode_, bytes calldata callData_ ) external onlyOwner returns (bytes memory) { - address module = _getModuleIfInstalled(veecode_); + // Set the internal flag to false + isExecOnModuleInternal = false; - // Check that the function is not prohibited - bytes4 functionSelector = bytes4(callData_[:4]); - if (_isModuleFunctionProhibited(functionSelector)) { - revert ModuleFunctionProhibited(veecode_, functionSelector); - } - - (bool success, bytes memory returnData) = module.call(callData_); - if (!success) revert ModuleExecutionReverted(returnData); - return returnData; + // Call the internal function + return _execOnModule(veecode_, callData_); } - function _isModuleFunctionProhibited(bytes4 function_) internal view returns (bool) { - uint256 length = prohibitedModuleFunctions.length; - for (uint256 i; i < length; i++) { - if (prohibitedModuleFunctions[i] == function_) return true; - } - return false; - } - - /// @notice Adds a function selector to the list of prohibited module functions - /// @notice This will prevent the function from being called on any module + /// @notice Performs a call on a module from an internal function + /// @notice This can be used to perform administrative functions on a module, such as setting parameters or calling permissioned functions /// @dev This function reverts if: - /// @dev - The caller is not the owner - /// @dev - The function selector is already prohibited - /// @dev - The function selector is zero + /// @dev - The module is not installed + /// @dev - The call is made to a prohibited function + /// @dev - The call reverted /// - /// @param function_ The function selector - function addProhibitedModuleFunction(bytes4 function_) external onlyOwner { - if (function_ == bytes4(0)) revert ModuleFunctionInvalid(function_); + /// @param veecode_ The module Veecode + /// @param callData_ The call data + /// @return The return data from the call + function execOnModuleInternal( + Veecode veecode_, + bytes calldata callData_ + ) internal returns (bytes memory) { + // Set the internal flag to true + isExecOnModuleInternal = true; - // Check for duplicates - if (_isModuleFunctionProhibited(function_)) revert ModuleFunctionInvalid(function_); + // Perform the call + bytes memory returnValue = _execOnModule(veecode_, callData_); - prohibitedModuleFunctions.push(function_); - prohibitedModuleFunctionsCount++; + // Reset the internal flag to false + isExecOnModuleInternal = false; - emit ModuleFunctionProhibitionAdded(function_); + return returnValue; } - /// @notice Removes a function selector from the list of prohibited module functions - /// @notice This will allow the function to be called on any module - /// @dev This function reverts if: - /// @dev - The caller is not the owner - /// @dev - The function selector is not prohibited - /// @dev - The function selector is zero - /// - /// @param function_ The function selector - function removeProhibitedModuleFunction(bytes4 function_) external onlyOwner { - if (function_ == bytes4(0)) revert ModuleFunctionInvalid(function_); - - uint256 length = prohibitedModuleFunctions.length; - bool functionFound; - - for (uint256 i; i < length; i++) { - if (prohibitedModuleFunctions[i] == function_) { - prohibitedModuleFunctions[i] = prohibitedModuleFunctions[length - 1]; - prohibitedModuleFunctions.pop(); - functionFound = true; - break; - } - } - - if (!functionFound) revert ModuleFunctionInvalid(function_); + function _execOnModule( + Veecode veecode_, + bytes calldata callData_ + ) internal returns (bytes memory) { + address module = _getModuleIfInstalled(veecode_); - prohibitedModuleFunctionsCount--; + // Call the module + (bool success, bytes memory returnData) = module.call(callData_); - emit ModuleFunctionProhibitionRemoved(function_); + if (!success) revert ModuleExecutionReverted(returnData); + return returnData; } } @@ -383,8 +350,14 @@ abstract contract WithModules is Owned { abstract contract Module { // ========= ERRORS ========= // + /// @notice Error when a module function is called by a non-parent contract error Module_OnlyParent(address caller_); - error Module_InvalidParent(); + + /// @notice Error when a module function is called by a non-internal contract + error Module_OnlyInternal(); + + /// @notice Error when the parent contract is invalid + error Module_InvalidParent(address parent_); // ========= STATE VARIABLES ========= // @@ -394,7 +367,7 @@ abstract contract Module { // ========= CONSTRUCTOR ========= // constructor(address parent_) { - if (parent_ == address(0)) revert Module_InvalidParent(); + if (parent_ == address(0)) revert Module_InvalidParent(parent_); parent = parent_; } @@ -407,6 +380,12 @@ abstract contract Module { _; } + /// @notice Modifier to restrict functions to be called only by internal module. + modifier onlyInternal() { + if (!WithModules(parent).isExecOnModuleInternal()) revert Module_OnlyInternal(); + _; + } + // ========= FUNCTIONS ========= // /// @notice 2 byte identifier for the module type diff --git a/test/modules/Modules/MockModule.sol b/test/modules/Modules/MockModule.sol index 67527651..5ad43948 100644 --- a/test/modules/Modules/MockModule.sol +++ b/test/modules/Modules/MockModule.sol @@ -15,11 +15,7 @@ contract MockModuleV1 is Module { return true; } - function prohibited() external pure returns (bool) { - return true; - } - - function prohibitedTwo() external pure returns (bool) { + function prohibited() external view onlyInternal returns (bool) { return true; } } diff --git a/test/modules/Modules/MockWithModules.sol b/test/modules/Modules/MockWithModules.sol index 59e65746..bc0b8498 100644 --- a/test/modules/Modules/MockWithModules.sol +++ b/test/modules/Modules/MockWithModules.sol @@ -2,8 +2,15 @@ pragma solidity 0.8.19; // Contracts -import {WithModules} from "src/modules/Modules.sol"; +import {WithModules, Veecode} from "src/modules/Modules.sol"; contract MockWithModules is WithModules { constructor(address _owner) WithModules(_owner) {} + + function execInternalFunction( + Veecode veecode_, + bytes calldata callData_ + ) external onlyOwner returns (bytes memory) { + return execOnModuleInternal(veecode_, callData_); + } } diff --git a/test/modules/Modules/addProhibitedModuleFunction.t.sol b/test/modules/Modules/addProhibitedModuleFunction.t.sol deleted file mode 100644 index 33f7eb93..00000000 --- a/test/modules/Modules/addProhibitedModuleFunction.t.sol +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -// Libraries -import {Test} from "forge-std/Test.sol"; - -// Mocks -import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; -import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; - -// Contracts -import {WithModules, Veecode, toKeycode} from "src/modules/Modules.sol"; - -contract AddProhibitedModuleFunctionTest is Test { - WithModules internal withModules; - - function setUp() external { - withModules = new MockWithModules(address(this)); - } - - function testReverts_whenUnauthorized() external { - address alice = address(0x1); - - vm.expectRevert("UNAUTHORIZED"); - - vm.prank(alice); - withModules.addProhibitedModuleFunction(MockModuleV1.mock.selector); - } - - function testReverts_whenDuplicate() external { - // Set as prohibited - withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); - - bytes memory err = abi.encodeWithSelector( - WithModules.ModuleFunctionInvalid.selector, MockModuleV1.prohibited.selector - ); - vm.expectRevert(err); - - // Set as prohibited again - withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); - } - - function testReverts_whenZero() external { - bytes memory err = - abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, bytes4(0)); - vm.expectRevert(err); - - // Set as prohibited again - withModules.addProhibitedModuleFunction(bytes4(0)); - } - - function test_success() external { - // Set as prohibited - withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); - - // Check that the selector is added - uint256 functionsCount = withModules.prohibitedModuleFunctionsCount(); - assertEq(functionsCount, 1); - assertEq(withModules.prohibitedModuleFunctions(0), MockModuleV1.prohibited.selector); - - // Add another - withModules.addProhibitedModuleFunction(MockModuleV1.prohibitedTwo.selector); - - // Check that the selector is added - functionsCount = withModules.prohibitedModuleFunctionsCount(); - assertEq(functionsCount, 2); - assertEq(withModules.prohibitedModuleFunctions(0), MockModuleV1.prohibited.selector); - assertEq(withModules.prohibitedModuleFunctions(1), MockModuleV1.prohibitedTwo.selector); - } -} diff --git a/test/modules/Modules/execOnModule.t.sol b/test/modules/Modules/execOnModule.t.sol index 7db06bc1..aaf64d99 100644 --- a/test/modules/Modules/execOnModule.t.sol +++ b/test/modules/Modules/execOnModule.t.sol @@ -9,10 +9,10 @@ import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; // Contracts -import {WithModules, Veecode, toKeycode} from "src/modules/Modules.sol"; +import {WithModules, Module, Veecode, toKeycode} from "src/modules/Modules.sol"; contract ExecOnModule is Test { - WithModules internal withModules; + MockWithModules internal withModules; MockModuleV1 internal mockModule; function setUp() external { @@ -45,23 +45,34 @@ contract ExecOnModule is Test { withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.mock.selector)); } - function testReverts_whenFunctionIsProhibited() external whenVersion1IsInstalled { + function testReverts_whenFunctionIsOnlyInternal() external whenVersion1IsInstalled { Veecode veecode = mockModule.VEECODE(); - // Call the function - withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.prohibited.selector)); - - // Set it as prohibited - withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); - bytes memory err = abi.encodeWithSelector( - WithModules.ModuleFunctionProhibited.selector, veecode, MockModuleV1.prohibited.selector + WithModules.ModuleExecutionReverted.selector, + abi.encodeWithSelector(Module.Module_OnlyInternal.selector) ); vm.expectRevert(err); withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.prohibited.selector)); } + function test_whenFunctionIsOnlyInternal() external whenVersion1IsInstalled { + Veecode veecode = mockModule.VEECODE(); + + bytes memory returnData = withModules.execInternalFunction( + veecode, abi.encodeWithSelector(MockModuleV1.prohibited.selector) + ); + + // Decode the return data + (bool returnValue) = abi.decode(returnData, (bool)); + + assertEq(returnValue, true); + + // Check that the internal flag was reset + assertEq(withModules.isExecOnModuleInternal(), false); + } + function test_success() external whenVersion1IsInstalled { bytes memory returnData = withModules.execOnModule( mockModule.VEECODE(), abi.encodeWithSelector(MockModuleV1.mock.selector) diff --git a/test/modules/Modules/removeProhibitedModuleFunction.t.sol b/test/modules/Modules/removeProhibitedModuleFunction.t.sol deleted file mode 100644 index 94e3e9fa..00000000 --- a/test/modules/Modules/removeProhibitedModuleFunction.t.sol +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity 0.8.19; - -// Libraries -import {Test} from "forge-std/Test.sol"; - -// Mocks -import {MockWithModules} from "test/modules/Modules/MockWithModules.sol"; -import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; - -// Contracts -import {WithModules, Veecode, toKeycode} from "src/modules/Modules.sol"; - -contract RemoveProhibitedModuleFunctionTest is Test { - WithModules internal withModules; - - function setUp() external { - withModules = new MockWithModules(address(this)); - } - - modifier whenAModuleFunctionIsProhibited() { - withModules.addProhibitedModuleFunction(MockModuleV1.prohibited.selector); - _; - } - - function testReverts_whenUnauthorized() external whenAModuleFunctionIsProhibited { - address alice = address(0x1); - - vm.expectRevert("UNAUTHORIZED"); - - vm.prank(alice); - withModules.removeProhibitedModuleFunction(MockModuleV1.prohibited.selector); - } - - function testReverts_whenNotProhibited() external { - bytes memory err = abi.encodeWithSelector( - WithModules.ModuleFunctionInvalid.selector, MockModuleV1.mock.selector - ); - vm.expectRevert(err); - - withModules.removeProhibitedModuleFunction(MockModuleV1.mock.selector); - } - - function testReverts_whenZero() external { - bytes memory err = - abi.encodeWithSelector(WithModules.ModuleFunctionInvalid.selector, bytes4(0)); - vm.expectRevert(err); - - // Set as prohibited again - withModules.removeProhibitedModuleFunction(bytes4(0)); - } - - function test_success() external whenAModuleFunctionIsProhibited { - // Remove - withModules.removeProhibitedModuleFunction(MockModuleV1.prohibited.selector); - - // Check that the selector is removed - uint256 functionsCount = withModules.prohibitedModuleFunctionsCount(); - assertEq(functionsCount, 0); - } -} From cba5b2928d421d6a76ff9e592b26b469870be576 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Fri, 5 Jan 2024 11:13:25 +0400 Subject: [PATCH 32/34] Add enum for Module type --- src/modules/Modules.sol | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index 5c5b3bec..b2ec2dbc 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -359,6 +359,16 @@ abstract contract Module { /// @notice Error when the parent contract is invalid error Module_InvalidParent(address parent_); + // ========= DATA TYPES ========= // + + /// @notice Enum of module types + enum Type { + Auction, + Derivative, + Condenser, + Transformer + } + // ========= STATE VARIABLES ========= // /// @notice The parent contract for this module. @@ -389,8 +399,9 @@ abstract contract Module { // ========= FUNCTIONS ========= // /// @notice 2 byte identifier for the module type - /// @dev This enables the parent contract to check the module type - function TYPE() public pure virtual returns (bytes2) {} + /// @dev This enables the parent contract to check that the module Keycode specified + /// @dev is of the correct type + function TYPE() public pure virtual returns (Type) {} /// @notice 7 byte, versioned identifier for the module. 2 characters from 0-9 that signify the version and 3-5 characters from A-Z. function VEECODE() public pure virtual returns (Veecode) {} From f702ef64c9ab6e9fb067595a2568e5e5a4fda429 Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 8 Jan 2024 13:49:20 +0400 Subject: [PATCH 33/34] Integrate feedback on execOnModule --- src/modules/Modules.sol | 56 +++++++----------------- test/modules/Modules/MockModule.sol | 2 +- test/modules/Modules/MockWithModules.sol | 11 ++--- test/modules/Modules/execOnModule.t.sol | 27 +++++++----- 4 files changed, 39 insertions(+), 57 deletions(-) diff --git a/src/modules/Modules.sol b/src/modules/Modules.sol index b2ec2dbc..de3124fc 100644 --- a/src/modules/Modules.sol +++ b/src/modules/Modules.sol @@ -135,7 +135,7 @@ abstract contract WithModules is Owned { /// @notice Mapping of Keycode to module status information. mapping(Keycode => ModStatus) public getModuleStatus; - bool public isExecOnModuleInternal; + bool public isExecOnModule; // ========= MODULE MANAGEMENT ========= // @@ -284,7 +284,7 @@ abstract contract WithModules is Owned { /// @notice Performs a call on a module /// @notice This can be used to perform administrative functions on a module, such as setting parameters or calling permissioned functions /// @dev This function reverts if: - /// @dev - The caller is not the owner + /// @dev - The caller is not the parent /// @dev - The module is not installed /// @dev - The call is made to a prohibited function /// @dev - The call reverted @@ -296,49 +296,19 @@ abstract contract WithModules is Owned { Veecode veecode_, bytes calldata callData_ ) external onlyOwner returns (bytes memory) { - // Set the internal flag to false - isExecOnModuleInternal = false; - - // Call the internal function - return _execOnModule(veecode_, callData_); - } - - /// @notice Performs a call on a module from an internal function - /// @notice This can be used to perform administrative functions on a module, such as setting parameters or calling permissioned functions - /// @dev This function reverts if: - /// @dev - The module is not installed - /// @dev - The call is made to a prohibited function - /// @dev - The call reverted - /// - /// @param veecode_ The module Veecode - /// @param callData_ The call data - /// @return The return data from the call - function execOnModuleInternal( - Veecode veecode_, - bytes calldata callData_ - ) internal returns (bytes memory) { - // Set the internal flag to true - isExecOnModuleInternal = true; - - // Perform the call - bytes memory returnValue = _execOnModule(veecode_, callData_); - - // Reset the internal flag to false - isExecOnModuleInternal = false; + // Set the flag to true + isExecOnModule = true; - return returnValue; - } - - function _execOnModule( - Veecode veecode_, - bytes calldata callData_ - ) internal returns (bytes memory) { + // Check that the module is installed (or revert) address module = _getModuleIfInstalled(veecode_); // Call the module (bool success, bytes memory returnData) = module.call(callData_); - if (!success) revert ModuleExecutionReverted(returnData); + + // Reset the flag to false + isExecOnModule = false; + return returnData; } } @@ -384,15 +354,19 @@ abstract contract Module { // ========= MODIFIERS ========= // - /// @notice Modifier to restrict functions to be called only by parent module. + /// @notice Modifier to restrict functions to be called only by the parent module. modifier onlyParent() { if (msg.sender != parent) revert Module_OnlyParent(msg.sender); _; } /// @notice Modifier to restrict functions to be called only by internal module. + /// @notice If a function is called through `execOnModule()` on the parent contract, this modifier will revert. + /// @dev This modifier can be used to prevent functions from being called by governance or other external actors through `execOnModule()`. modifier onlyInternal() { - if (!WithModules(parent).isExecOnModuleInternal()) revert Module_OnlyInternal(); + if (msg.sender != parent) revert Module_OnlyParent(msg.sender); + + if (WithModules(parent).isExecOnModule()) revert Module_OnlyInternal(); _; } diff --git a/test/modules/Modules/MockModule.sol b/test/modules/Modules/MockModule.sol index 5ad43948..bb10e8d5 100644 --- a/test/modules/Modules/MockModule.sol +++ b/test/modules/Modules/MockModule.sol @@ -11,7 +11,7 @@ contract MockModuleV1 is Module { return wrapVeecode(toKeycode("MOCK"), 1); } - function mock() external pure returns (bool) { + function mock() external view onlyParent returns (bool) { return true; } diff --git a/test/modules/Modules/MockWithModules.sol b/test/modules/Modules/MockWithModules.sol index bc0b8498..47489888 100644 --- a/test/modules/Modules/MockWithModules.sol +++ b/test/modules/Modules/MockWithModules.sol @@ -4,13 +4,14 @@ pragma solidity 0.8.19; // Contracts import {WithModules, Veecode} from "src/modules/Modules.sol"; +import {MockModuleV1} from "test/modules/Modules/MockModule.sol"; + contract MockWithModules is WithModules { constructor(address _owner) WithModules(_owner) {} - function execInternalFunction( - Veecode veecode_, - bytes calldata callData_ - ) external onlyOwner returns (bytes memory) { - return execOnModuleInternal(veecode_, callData_); + function callProhibited(Veecode veecode_) external view returns (bool) { + MockModuleV1 module = MockModuleV1(_getModuleIfInstalled(veecode_)); + + return module.prohibited(); } } diff --git a/test/modules/Modules/execOnModule.t.sol b/test/modules/Modules/execOnModule.t.sol index aaf64d99..400a91da 100644 --- a/test/modules/Modules/execOnModule.t.sol +++ b/test/modules/Modules/execOnModule.t.sol @@ -48,6 +48,7 @@ contract ExecOnModule is Test { function testReverts_whenFunctionIsOnlyInternal() external whenVersion1IsInstalled { Veecode veecode = mockModule.VEECODE(); + // This mimics that the function was called from the outside (e.g. governance) via execOnModule bytes memory err = abi.encodeWithSelector( WithModules.ModuleExecutionReverted.selector, abi.encodeWithSelector(Module.Module_OnlyInternal.selector) @@ -57,20 +58,26 @@ contract ExecOnModule is Test { withModules.execOnModule(veecode, abi.encodeWithSelector(MockModuleV1.prohibited.selector)); } - function test_whenFunctionIsOnlyInternal() external whenVersion1IsInstalled { + function test_whenFunctionIsOnlyInternal_whenParentIsCalling() + external + whenVersion1IsInstalled + { Veecode veecode = mockModule.VEECODE(); - bytes memory returnData = withModules.execInternalFunction( - veecode, abi.encodeWithSelector(MockModuleV1.prohibited.selector) - ); - - // Decode the return data - (bool returnValue) = abi.decode(returnData, (bool)); - + // Mimic the parent contract calling a protected function directly + bool returnValue = withModules.callProhibited(veecode); assertEq(returnValue, true); + } + + function testReverts_whenFunctionIsOnlyInternal_whenExternalIsCalling() + external + whenVersion1IsInstalled + { + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); - // Check that the internal flag was reset - assertEq(withModules.isExecOnModuleInternal(), false); + // Mimic the parent contract calling a protected function directly + mockModule.prohibited(); } function test_success() external whenVersion1IsInstalled { From f49499f9b4cf705dd6b3e05c55128c39c9f544bb Mon Sep 17 00:00:00 2001 From: Jem <0x0xjem@gmail.com> Date: Mon, 8 Jan 2024 13:52:09 +0400 Subject: [PATCH 34/34] Additional test case --- test/modules/Modules/MockModule.sol | 2 ++ test/modules/Modules/execOnModule.t.sol | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/test/modules/Modules/MockModule.sol b/test/modules/Modules/MockModule.sol index bb10e8d5..92d2b371 100644 --- a/test/modules/Modules/MockModule.sol +++ b/test/modules/Modules/MockModule.sol @@ -11,10 +11,12 @@ contract MockModuleV1 is Module { return wrapVeecode(toKeycode("MOCK"), 1); } + /// @notice Mock function that can be called by execOnModule or from the parent contract function mock() external view onlyParent returns (bool) { return true; } + /// @notice Mock function that can only be called from the parent contract and not execOnModule function prohibited() external view onlyInternal returns (bool) { return true; } diff --git a/test/modules/Modules/execOnModule.t.sol b/test/modules/Modules/execOnModule.t.sol index 400a91da..65a09931 100644 --- a/test/modules/Modules/execOnModule.t.sol +++ b/test/modules/Modules/execOnModule.t.sol @@ -69,6 +69,17 @@ contract ExecOnModule is Test { assertEq(returnValue, true); } + function testReverts_whenFunctionIsOnlyParent_whenExternalIsCalling() + external + whenVersion1IsInstalled + { + bytes memory err = abi.encodeWithSelector(Module.Module_OnlyParent.selector, address(this)); + vm.expectRevert(err); + + // Mimic the parent contract calling a protected function directly + mockModule.mock(); + } + function testReverts_whenFunctionIsOnlyInternal_whenExternalIsCalling() external whenVersion1IsInstalled