diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index adc0d2f..049beb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,13 +30,10 @@ jobs: - name: "Install the Node.js dependencies" run: "bun install --frozen-lockfile" - - name: "Run Forge build" - run: | - forge --version - forge build + - name: "Build the contracts" + run: "FOUNDRY_PROFILE=optimized forge build" id: build - - name: "Run Forge tests" - run: | - forge test -vvv + - name: "Run the tests" + run: "FOUNDRY_PROFILE=optimized forge test -vvv" id: test diff --git a/.gitignore b/.gitignore index ff52966..19c68d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Directories cache/ out/ +out-optimized/ node_modules # Coverage diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9e88c82 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/openzeppelin-foundry-upgrades"] + path = lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades diff --git a/Makefile b/Makefile index 05d321c..8a0eec1 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ tests-coverage :; ./script/coverage.sh deploy-invoice-collection: forge script script/DeployInvoiceCollection.s.sol:DeployInvoiceCollection \ $(CREATE2SALT) {RELAYER} {NAME} {SYMBOL} \ - --sig "run(address,string,string)" --rpc-url {RPC_URL} --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) + --sig "run(string,address,string,string)" --rpc-url {RPC_URL} --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) --broadcast --verify # Deploys the {ModuleKeeper} contract deterministically @@ -40,7 +40,7 @@ deploy-deterministic-module-keeper: # Deploys the {StationRegistry} contract deterministically # Update the following configs before running the script: # - {INITIAL_OWNER} with the address of the initial owner -# - {ENTRYPOINT} with the address of the {Entrypoiny} contract (currently v6) +# - {ENTRYPOINT} with the address of the {Entrypoint} contract (currently v6) # - {MODULE_KEEPER} with the address of the {ModuleKeeper} deployment # - {RPC_URL} with the network RPC used for deployment deploy-deterministic-dock-registry: @@ -48,4 +48,35 @@ deploy-deterministic-dock-registry: $(CREATE2SALT) {INITIAL_OWNER} {ENTRYPOINT} {MODULE_KEEPER} \ --sig "run(string,address,address)" --rpc-url {RPC_URL} \ --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) \ - --broadcast --verify \ No newline at end of file + --broadcast --verify + +# Deploys the {PaymentModule} contract deterministically +# +# Update the following configs before running the script: +# - {SABLIER_LOCKUP_LINEAR} with the according {SablierV2LockupLinear} deployment address +# - {SABLIER_LOCKUP_TRANCHED} with the according {SablierV2LockupTranched} deployment address +# - {INITIAL_OWNER} with the address of the initial admin of the {PaymentModule} +# - {BROKER_ACCOUNT} with the address of the account responsible for collecting the broker fees (multisig vault) +# - {RPC_URL} with the network RPC used for deployment +deploy-payment-module: + forge script script/DeployDeterministicPaymentModule.s.sol:DeployDeterministicPaymentModule \ + $(CREATE2SALT) {SABLIER_LOCKUP_LINEAR} {SABLIER_LOCKUP_TRANCHED} {INITIAL_OWNER} {BROKER_ACCOUNT} \ + --sig "run(string,address,address,address,address)" --rpc-url {RPC_URL} --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) + --broadcast --verify + + # Deploys the {PaymentModule} contract deterministically + +# Deploys the core contracts deterministically +# +# Update the following configs before running the script: +# - {SABLIER_LOCKUP_LINEAR} with the according {SablierV2LockupLinear} deployment address +# - {SABLIER_LOCKUP_TRANCHED} with the according {SablierV2LockupTranched} deployment address +# - {INITIAL_OWNER} with the address of the initial admin of the {StationRegistry} and {PaymentModule} +# - {BROKER_ACCOUNT} with the address of the account responsible for collecting the broker fees (multisig vault) +# - {ENTRYPOINT} with the address of the {Entrypoint} contract (currently v6) +# - {RPC_URL} with the network RPC used for deployment +deploy-core: + forge script script/DeployDeterministicCore.s.sol:DeployDeterministicCore \ + $(CREATE2SALT) {SABLIER_LOCKUP_LINEAR} {SABLIER_LOCKUP_TRANCHED} {INITIAL_OWNER} {BROKER_ACCOUNT} {ENTRYPOINT}\ + --sig "run(string,address,address,address,address,address)" --rpc-url {RPC_URL} --private-key $(PRIVATE_KEY) --etherscan-api-key $(ETHERSCAN_API_KEY) + --broadcast --verify \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 2a82ff7..ba0af76 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/foundry.toml b/foundry.toml index 9c740d2..b2a9547 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,6 +14,10 @@ extra_output = ["storageLayout"] max_test_rejects = 500_000 runs = 10_000 +[profile.optimized] +out = "out-optimized" +via_ir = true + [fmt] bracket_spacing = true int_types = "long" diff --git a/lib/openzeppelin-foundry-upgrades b/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..16e0ae2 --- /dev/null +++ b/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit 16e0ae21e0e39049f619f2396fa28c57fad07368 diff --git a/package.json b/package.json index c534718..bb82017 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,22 @@ { - "scripts": { - "build": "forge build", - "lint": "bun run lint:sol && bun run prettier:check", - "lint:sol": "forge fmt --check && bun solhint \"{precompiles,script,src,test}/**/*.sol\"", - "prettier:check": "prettier --check --plugin=prettier-plugin-solidity \"**/*.{json,md,svg,yml}\"", - "prettier:write": "prettier --write --plugin=prettier-plugin-solidity \"**/*.{json,md,svg,yml,sol}\"" - }, - "devDependencies": { - "forge-std": "github:foundry-rs/forge-std#v1.9.4", - "prettier": "^3.3.3", - "prettier-plugin-solidity": "^1.4.1", - "solhint": "^5.0.3" - }, - "dependencies": { - "@openzeppelin/contracts": "^5.1.0", - "@prb/math": "^4.1.0", - "@sablier/v2-core": "^1.2.0", - "@thirdweb-dev/contracts": "^3.15.0" - } + "scripts": { + "build": "forge build", + "lint": "bun run lint:sol && bun run prettier:check", + "lint:sol": "forge fmt --check && bun solhint \"{precompiles,script,src,test}/**/*.sol\"", + "prettier:check": "prettier --check --plugin=prettier-plugin-solidity \"**/*.{json,md,svg,yml}\"", + "prettier:write": "prettier --write --plugin=prettier-plugin-solidity \"**/*.{json,md,svg,yml,sol}\"" + }, + "devDependencies": { + "forge-std": "github:foundry-rs/forge-std#v1.9.4", + "prettier": "^3.3.3", + "prettier-plugin-solidity": "^1.4.1", + "solhint": "^5.0.3" + }, + "dependencies": { + "@openzeppelin/contracts": "^5.1.0", + "@openzeppelin/contracts-upgradeable": "^5.1.0", + "@prb/math": "^4.1.0", + "@sablier/v2-core": "^1.2.0", + "@thirdweb-dev/contracts": "^3.15.0" + } } diff --git a/remappings.txt b/remappings.txt index 07ab4b1..5212548 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,5 @@ @openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +@openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ @sablier/v2-core/=node_modules/@sablier/v2-core/ @prb/math/=node_modules/@prb/math/ @thirdweb/contracts/=node_modules/@thirdweb-dev/contracts/ diff --git a/script/DeployDeterministicCore.s.sol b/script/DeployDeterministicCore.s.sol new file mode 100644 index 0000000..99fc48c --- /dev/null +++ b/script/DeployDeterministicCore.s.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { BaseScript } from "./Base.s.sol"; +import { PaymentModule } from "./../src/modules/payment-module/PaymentModule.sol"; +import { StationRegistry } from "./../src/StationRegistry.sol"; +import { ModuleKeeper } from "./../src/ModuleKeeper.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Options } from "./../lib/openzeppelin-foundry-upgrades/src/Options.sol"; +import { Core } from "./../lib/openzeppelin-foundry-upgrades/src/internal/Core.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { IEntryPoint } from "@thirdweb/contracts/prebuilts/account/interface/IEntrypoint.sol"; +import { ud } from "@prb/math/src/UD60x18.sol"; + +/// @notice Deploys at deterministic addresses across chains the core contracts of the Werk Protocol +/// @dev Reverts if any contract has already been deployed +contract DeployDeterministicCore is BaseScript { + /// @dev By using a salt, Forge will deploy the contract via a deterministic CREATE2 factory + /// https://book.getfoundry.sh/tutorials/create2-tutorial?highlight=deter#deterministic-deployment-using-create2 + function run( + string memory create2Salt, + ISablierV2LockupLinear sablierLockupLinear, + ISablierV2LockupTranched sablierLockupTranched, + address initialOwner, + address brokerAccount, + IEntryPoint entrypoint + ) + public + virtual + broadcast + returns (ModuleKeeper moduleKeeper, StationRegistry stationRegistry, PaymentModule paymentModule) + { + bytes32 salt = bytes32(abi.encodePacked(create2Salt)); + + // Deterministically deploy the {ModuleKeeper} contract + moduleKeeper = new ModuleKeeper{ salt: salt }(initialOwner); + + // Deterministically deploy the {StationRegistry} contract + stationRegistry = new StationRegistry{ salt: salt }(initialOwner, entrypoint, moduleKeeper); + + // Deterministically deploy the {PaymentModule} module + paymentModule = PaymentModule( + deployDetermisticUUPSProxy( + salt, + abi.encode(sablierLockupLinear, sablierLockupTranched), + "PaymentModule.sol", + abi.encodeCall(PaymentModule.initialize, (initialOwner, brokerAccount, ud(0))) + ) + ); + + // Add the {PaymentModule} module to the allowlist of the {ModuleKeeper} + moduleKeeper.addToAllowlist(address(paymentModule)); + } + + /// @dev Deploys a UUPS proxy at deterministic addresses across chains based on a provided salt + /// @param salt Salt to use for deterministic deployment + /// @param contractName The name of the implementation contract + /// @param initializerData The ABI encoded call to be made to the initialize method + function deployDetermisticUUPSProxy( + bytes32 salt, + bytes memory constructorData, + string memory contractName, + bytes memory initializerData + ) + internal + returns (address) + { + Options memory opts; + opts.constructorData = constructorData; + + address impl = Core.deployImplementation(contractName, opts); + + return address(new ERC1967Proxy{ salt: salt }(impl, initializerData)); + } +} diff --git a/script/DeployDeterministicPaymentModule.s.sol b/script/DeployDeterministicPaymentModule.s.sol new file mode 100644 index 0000000..ecbd8df --- /dev/null +++ b/script/DeployDeterministicPaymentModule.s.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.26; + +import { BaseScript } from "./Base.s.sol"; +import { PaymentModule } from "./../src/modules/payment-module/PaymentModule.sol"; + +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { Options } from "./../lib/openzeppelin-foundry-upgrades/src/Options.sol"; +import { Core } from "./../lib/openzeppelin-foundry-upgrades/src/internal/Core.sol"; +import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; +import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; +import { ud } from "@prb/math/src/UD60x18.sol"; + +/// @notice Deploys at deterministic addresses across chains an instance of {PaymentModule} +/// @dev Reverts if any contract has already been deployed +contract DeployDeterministicPaymentModule is BaseScript { + /// @dev By using a salt, Forge will deploy the contract via a deterministic CREATE2 factory + /// https://book.getfoundry.sh/tutorials/create2-tutorial?highlight=deter#deterministic-deployment-using-create2 + function run( + string memory create2Salt, + ISablierV2LockupLinear sablierLockupLinear, + ISablierV2LockupTranched sablierLockupTranched, + address initialOwner, + address brokerAccount + ) + public + virtual + broadcast + returns (PaymentModule paymentModule) + { + bytes32 salt = bytes32(abi.encodePacked(create2Salt)); + + // Deterministically deploy the {PaymentModule} module + paymentModule = PaymentModule( + deployDetermisticUUPSProxy( + salt, + abi.encode(sablierLockupLinear, sablierLockupTranched), + "PaymentModule.sol", + abi.encodeCall(PaymentModule.initialize, (initialOwner, brokerAccount, ud(0))) + ) + ); + } + + /// @dev Deploys a UUPS proxy at deterministic addresses across chains based on a provided salt + /// @param salt Salt to use for deterministic deployment + /// @param contractName The name of the implementation contract + /// @param initializerData The ABI encoded call to be made to the initialize method + function deployDetermisticUUPSProxy( + bytes32 salt, + bytes memory constructorData, + string memory contractName, + bytes memory initializerData + ) + internal + returns (address) + { + Options memory opts; + opts.constructorData = constructorData; + + address impl = Core.deployImplementation(contractName, opts); + + return address(new ERC1967Proxy{ salt: salt }(impl, initializerData)); + } +} diff --git a/src/StationRegistry.sol b/src/StationRegistry.sol index cbb0c30..b762e6f 100644 --- a/src/StationRegistry.sol +++ b/src/StationRegistry.sol @@ -41,7 +41,9 @@ contract StationRegistry is IStationRegistry, BaseAccountFactory, PermissionsEnu address _initialAdmin, IEntryPoint _entrypoint, ModuleKeeper _moduleKeeper - ) BaseAccountFactory(address(new Space(_entrypoint, address(this))), address(_entrypoint)) { + ) + BaseAccountFactory(address(new Space(_entrypoint, address(this))), address(_entrypoint)) + { _setupRole(DEFAULT_ADMIN_ROLE, _initialAdmin); _stationNextId = 1; @@ -56,7 +58,11 @@ contract StationRegistry is IStationRegistry, BaseAccountFactory, PermissionsEnu function createAccount( address _admin, bytes calldata _data - ) public override(BaseAccountFactory, IStationRegistry) returns (address) { + ) + public + override(BaseAccountFactory, IStationRegistry) + returns (address) + { // Get the station ID and initial modules array from the calldata // Note: calldata contains a salt (usually the number of accounts created by an admin), // station ID and an array with the initial enabled modules on the account diff --git a/src/modules/payment-module/PaymentModule.sol b/src/modules/payment-module/PaymentModule.sol index db0f5b0..72fe335 100644 --- a/src/modules/payment-module/PaymentModule.sol +++ b/src/modules/payment-module/PaymentModule.sol @@ -4,49 +4,81 @@ pragma solidity ^0.8.26; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { Lockup } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { Lockup } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { UD60x18 } from "@prb/math/src/ud60x18/ValueType.sol"; +import { StreamManager } from "./sablier-v2/StreamManager.sol"; import { Types } from "./libraries/Types.sol"; import { Errors } from "./libraries/Errors.sol"; import { IPaymentModule } from "./interfaces/IPaymentModule.sol"; import { ISpace } from "./../../interfaces/ISpace.sol"; -import { StreamManager } from "./sablier-v2/StreamManager.sol"; import { Helpers } from "./libraries/Helpers.sol"; /// @title PaymentModule /// @notice See the documentation in {IPaymentModule} -contract PaymentModule is IPaymentModule, StreamManager { +contract PaymentModule is IPaymentModule, StreamManager, UUPSUpgradeable { using SafeERC20 for IERC20; using Strings for uint256; + /// @dev Version identifier for the current implementation of the contract + string public constant VERSION = "1.0.0"; + /*////////////////////////////////////////////////////////////////////////// - PRIVATE STORAGE + NAMESPACED STORAGE LAYOUT //////////////////////////////////////////////////////////////////////////*/ - /// @dev Payment requests details mapped by the `id` payment request ID - mapping(uint256 id => Types.PaymentRequest) private _requests; + /// @custom:storage-location erc7201:werk.storage.PaymentModule + struct PaymentModuleStorage { + /// @notice Payment requests details mapped by the `id` payment request ID + mapping(uint256 id => Types.PaymentRequest) requests; + /// @notice Counter to keep track of the next ID used to create a new payment request + uint256 nextRequestId; + } - /// @dev Counter to keep track of the next ID used to create a new payment request - uint256 private _nextRequestId; + // keccak256(abi.encode(uint256(keccak256("werk.storage.PaymentModule")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant PAYMENT_MODULE_STORAGE_LOCATION = + 0x69242e762af97d314866e2398c5d39d67197520146b0e3b1471c97ebda768e00; + + /// @dev Retrieves the storage of the {PaymentModule} contract + function _getPaymentModuleStorage() internal pure returns (PaymentModuleStorage storage $) { + assembly { + $.slot := PAYMENT_MODULE_STORAGE_LOCATION + } + } /*////////////////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////////////////*/ - /// @dev Initializes the {StreamManager} contract and first request ID + /// @dev Deploys and locks the implementation contract + /// @custom:oz-upgrades-unsafe-allow constructor constructor( ISablierV2LockupLinear _sablierLockupLinear, - ISablierV2LockupTranched _sablierLockupTranched, - address _brokerAdmin + ISablierV2LockupTranched _sablierLockupTranched ) - StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) + StreamManager(_sablierLockupLinear, _sablierLockupTranched) { + _disableInitializers(); + } + + /// @dev Initializes the proxy and the {Ownable} contract + function initialize(address _initialOwner, address _brokerAccount, UD60x18 _brokerFee) public initializer { + __StreamManager_init(_initialOwner, _brokerAccount, _brokerFee); + __UUPSUpgradeable_init(); + + // Retrieve the contract storage + PaymentModuleStorage storage $ = _getPaymentModuleStorage(); + // Start the first payment request ID from 1 - _nextRequestId = 1; + $.nextRequestId = 1; } + /// @dev Allows only the owner to upgrade the contract + function _authorizeUpgrade(address newImplementation) internal override onlyOwner { } + /*////////////////////////////////////////////////////////////////////////// MODIFIERS //////////////////////////////////////////////////////////////////////////*/ @@ -70,7 +102,10 @@ contract PaymentModule is IPaymentModule, StreamManager { /// @inheritdoc IPaymentModule function getRequest(uint256 requestId) external view returns (Types.PaymentRequest memory request) { - return _requests[requestId]; + // Retrieve the contract storage + PaymentModuleStorage storage $ = _getPaymentModuleStorage(); + + return $.requests[requestId]; } /// @inheritdoc IPaymentModule @@ -143,11 +178,14 @@ contract PaymentModule is IPaymentModule, StreamManager { } } + // Retrieve the contract storage + PaymentModuleStorage storage $ = _getPaymentModuleStorage(); + // Get the next payment request ID - requestId = _nextRequestId; + requestId = $.nextRequestId; // Effects: create the payment request - _requests[requestId] = Types.PaymentRequest({ + $.requests[requestId] = Types.PaymentRequest({ wasCanceled: false, wasAccepted: false, startTime: request.startTime, @@ -166,7 +204,7 @@ contract PaymentModule is IPaymentModule, StreamManager { // Effects: increment the next payment request ID // Use unchecked because the request id cannot realistically overflow unchecked { - ++_nextRequestId; + ++$.nextRequestId; } // Log the payment request creation @@ -181,8 +219,11 @@ contract PaymentModule is IPaymentModule, StreamManager { /// @inheritdoc IPaymentModule function payRequest(uint256 requestId) external payable { + // Retrieve the contract storage + PaymentModuleStorage storage $ = _getPaymentModuleStorage(); + // Load the payment request state from storage - Types.PaymentRequest memory request = _requests[requestId]; + Types.PaymentRequest memory request = $.requests[requestId]; // Checks: the payment request is not null if (request.recipient == address(0)) { @@ -215,7 +256,7 @@ contract PaymentModule is IPaymentModule, StreamManager { } // Effects: set the stream ID of the payment request - _requests[requestId].config.streamId = streamId; + $.requests[requestId].config.streamId = streamId; } // Effects: decrease the number of payments left @@ -225,20 +266,23 @@ contract PaymentModule is IPaymentModule, StreamManager { uint40 paymentsLeft; unchecked { paymentsLeft = request.config.paymentsLeft - 1; - _requests[requestId].config.paymentsLeft = paymentsLeft; + $.requests[requestId].config.paymentsLeft = paymentsLeft; } // Effects: mark the payment request as accepted - _requests[requestId].wasAccepted = true; + $.requests[requestId].wasAccepted = true; // Log the payment transaction - emit RequestPaid({ requestId: requestId, payer: msg.sender, config: _requests[requestId].config }); + emit RequestPaid({ requestId: requestId, payer: msg.sender, config: $.requests[requestId].config }); } /// @inheritdoc IPaymentModule function cancelRequest(uint256 requestId) external { + // Retrieve the contract storage + PaymentModuleStorage storage $ = _getPaymentModuleStorage(); + // Load the payment request state from storage - Types.PaymentRequest memory request = _requests[requestId]; + Types.PaymentRequest memory request = $.requests[requestId]; // Retrieve the request status Types.Status requestStatus = _statusOf(requestId); @@ -268,11 +312,11 @@ contract PaymentModule is IPaymentModule, StreamManager { // - A linear or tranched stream MUST be canceled by calling the `cancel` method on the according // {ISablierV2Lockup} contract else if (request.config.method != Types.Method.Transfer) { - _cancelStream({ streamType: request.config.method, streamId: request.config.streamId }); + cancelStream({ streamType: request.config.method, streamId: request.config.streamId }); } // Effects: mark the payment request as canceled - _requests[requestId].wasCanceled = true; + $.requests[requestId].wasCanceled = true; // Log the payment request cancelation emit RequestCanceled(requestId); @@ -280,11 +324,14 @@ contract PaymentModule is IPaymentModule, StreamManager { /// @inheritdoc IPaymentModule function withdrawRequestStream(uint256 requestId) public returns (uint128 withdrawnAmount) { + // Retrieve the contract storage + PaymentModuleStorage storage $ = _getPaymentModuleStorage(); + // Load the payment request state from storage - Types.PaymentRequest memory request = _requests[requestId]; + Types.PaymentRequest memory request = $.requests[requestId]; // Check, Effects, Interactions: withdraw from the stream - return _withdrawStream({ + return withdrawMaxStream({ streamType: request.config.method, streamId: request.config.streamId, to: request.recipient @@ -319,7 +366,7 @@ contract PaymentModule is IPaymentModule, StreamManager { /// @dev Create the linear stream payment function _payByLinearStream(Types.PaymentRequest memory request) internal returns (uint256 streamId) { - streamId = StreamManager.createLinearStream({ + streamId = createLinearStream({ asset: IERC20(request.config.asset), totalAmount: request.config.amount, startTime: request.startTime, @@ -333,7 +380,7 @@ contract PaymentModule is IPaymentModule, StreamManager { uint40 numberOfTranches = Helpers.computeNumberOfPayments(request.config.recurrence, request.endTime - request.startTime); - streamId = StreamManager.createTranchedStream({ + streamId = createTranchedStream({ asset: IERC20(request.config.asset), totalAmount: request.config.amount, startTime: request.startTime, @@ -376,8 +423,11 @@ contract PaymentModule is IPaymentModule, StreamManager { /// - For a stream-based payment request, by the status of the underlying stream; /// - For a transfer-based payment request, by the number of payments left; function _statusOf(uint256 requestId) internal view returns (Types.Status status) { + // Retrieve the contract storage + PaymentModuleStorage storage $ = _getPaymentModuleStorage(); + // Load the payment request state from storage - Types.PaymentRequest memory request = _requests[requestId]; + Types.PaymentRequest memory request = $.requests[requestId]; if (!request.wasAccepted && !request.wasCanceled) { return Types.Status.Pending; @@ -385,7 +435,7 @@ contract PaymentModule is IPaymentModule, StreamManager { // Check if dealing with a stream-based payment request if (request.config.streamId != 0) { - Lockup.Status statusOfStream = StreamManager.statusOfStream(request.config.method, request.config.streamId); + Lockup.Status statusOfStream = statusOfStream(request.config.method, request.config.streamId); if (statusOfStream == Lockup.Status.SETTLED) { return Types.Status.Paid; diff --git a/src/modules/payment-module/libraries/Errors.sol b/src/modules/payment-module/libraries/Errors.sol index e4cefdf..1f6abe3 100644 --- a/src/modules/payment-module/libraries/Errors.sol +++ b/src/modules/payment-module/libraries/Errors.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.26; /// @title Errors -/// @notice Library containing all custom errors the {PaymentModule} and {StreamManager} may revert with +/// @notice Library containing all custom errors the {PaymentModule} contract may revert with library Errors { /*////////////////////////////////////////////////////////////////////////// PAYMENT-MODULE diff --git a/src/modules/payment-module/libraries/Types.sol b/src/modules/payment-module/libraries/Types.sol index 2f41757..f43aef1 100644 --- a/src/modules/payment-module/libraries/Types.sol +++ b/src/modules/payment-module/libraries/Types.sol @@ -28,7 +28,7 @@ library Types { /// @notice Struct encapsulating the different values describing a payment config /// @param method The payment method /// @param recurrence The payment recurrence - /// @param paymentsLeft The number of payments required to fully settle the payment request (only for transfer or tranched stream based paymentRequests) + /// @param paymentsLeft The number of payments required to fully settle the payment request (only for transfer or tranched stream based payment requests) /// @param asset The address of the payment asset /// @param amount The amount that must be paid /// @param streamId The ID of the linear or tranched stream if payment method is either `LinearStream` or `TranchedStream`, otherwise 0 diff --git a/src/modules/payment-module/sablier-v2/StreamManager.sol b/src/modules/payment-module/sablier-v2/StreamManager.sol index 209af16..9c26be4 100644 --- a/src/modules/payment-module/sablier-v2/StreamManager.sol +++ b/src/modules/payment-module/sablier-v2/StreamManager.sol @@ -4,66 +4,95 @@ pragma solidity >=0.8.22; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; -import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; -import { Broker, LockupLinear, Lockup } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { Broker, Lockup, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { ud60x18, UD60x18, ud, intoUint128 } from "@prb/math/src/UD60x18.sol"; - +import { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; import { IStreamManager } from "./interfaces/IStreamManager.sol"; import { Errors } from "./../libraries/Errors.sol"; import { Types } from "./../libraries/Types.sol"; /// @title StreamManager /// @dev See the documentation in {IStreamManager} -abstract contract StreamManager is IStreamManager { +abstract contract StreamManager is IStreamManager, Initializable, OwnableUpgradeable { using SafeERC20 for IERC20; - /*////////////////////////////////////////////////////////////////////////// - PUBLIC STORAGE - //////////////////////////////////////////////////////////////////////////*/ - /// @inheritdoc IStreamManager ISablierV2LockupLinear public immutable override LOCKUP_LINEAR; /// @inheritdoc IStreamManager ISablierV2LockupTranched public immutable override LOCKUP_TRANCHED; - /// @inheritdoc IStreamManager - address public override brokerAdmin; - - /// @inheritdoc IStreamManager - UD60x18 public override brokerFee; - /*////////////////////////////////////////////////////////////////////////// - PRIVATE STORAGE + NAMESPACED STORAGE LAYOUT //////////////////////////////////////////////////////////////////////////*/ - /// @dev Stores the initial address of the account that started the stream - /// By default, each stream will be created by this contract (the sender address of each stream will be address(this)) - /// therefore this mapping is used to allow only authorized senders to execute management-related actions i.e. cancellations - mapping(uint256 streamId => address initialSender) private _initialStreamSender; + /// @custom:storage-location erc7201:werk.storage.StreamManager + struct StreamManagerStorage { + /// @notice Stores the initial address of the account that started the stream + /// By default, each stream will be created by this contract (the sender address of each stream will be address(this)) + /// therefore this mapping is used to allow only authorized senders to execute management-related actions i.e. cancellations + mapping(uint256 streamId => address initialSender) initialStreamSender; + /// @notice The broker parameters charged to create Sablier V2 stream + Broker broker; + } + + // keccak256(abi.encode(uint256(keccak256("werk.storage.StreamManager")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant STREAM_MANAGER_STORAGE_LOCATION = + 0x37eb5ed31cc419f1937b308ec5ab43829484edc140a0a162efda74d20d290400; + + /// @dev Retrieves the storage of the {StreamManager} contract + function _getStreamManagerStorage() internal pure returns (StreamManagerStorage storage $) { + assembly { + $.slot := STREAM_MANAGER_STORAGE_LOCATION + } + } /*////////////////////////////////////////////////////////////////////////// - CONSTRUCTOR + CONSTRUCTOR & INITIALIZER //////////////////////////////////////////////////////////////////////////*/ /// @dev Initializes the address of the {SablierV2LockupLinear} and {SablierV2LockupTranched} contracts - /// and the address of the broker admin account or contract - constructor( - ISablierV2LockupLinear _sablierLockupLinear, - ISablierV2LockupTranched _sablierLockupTranched, - address _brokerAdmin - ) { + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(ISablierV2LockupLinear _sablierLockupLinear, ISablierV2LockupTranched _sablierLockupTranched) { LOCKUP_LINEAR = _sablierLockupLinear; LOCKUP_TRANCHED = _sablierLockupTranched; - brokerAdmin = _brokerAdmin; + + _disableInitializers(); + } + + function __StreamManager_init( + address _initialAdmin, + address _brokerAccount, + UD60x18 _brokerFee + ) + internal + onlyInitializing + { + __Ownable_init(_initialAdmin); + + // Retrieve the storage of the {StreamManager} contract + StreamManagerStorage storage $ = _getStreamManagerStorage(); + + // Set the broker account and fee + $.broker = Broker({ account: _brokerAccount, fee: _brokerFee }); } /*////////////////////////////////////////////////////////////////////////// CONSTANT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ + /// @inheritdoc IStreamManager + function broker() public view returns (Broker memory brokerConfig) { + // Retrieve the storage of the {StreamManager} contract + StreamManagerStorage storage $ = _getStreamManagerStorage(); + + // Return the broker fee + brokerConfig = $.broker; + } + /// @inheritdoc IStreamManager function getLinearStream(uint256 streamId) public view returns (LockupLinear.StreamLL memory stream) { stream = LOCKUP_LINEAR.getStream(streamId); @@ -117,8 +146,11 @@ abstract contract StreamManager is IStreamManager { // Create the Lockup Linear stream streamId = _createLinearStream(asset, totalAmount, startTime, endTime, recipient); + // Retrieve the storage of the {StreamManager} contract + StreamManagerStorage storage $ = _getStreamManagerStorage(); + // Set `msg.sender` as the initial stream sender to allow authenticated stream management - _initialStreamSender[streamId] = msg.sender; + $.initialStreamSender[streamId] = msg.sender; } /// @inheritdoc IStreamManager @@ -139,27 +171,47 @@ abstract contract StreamManager is IStreamManager { // Create the Lockup Linear stream streamId = _createTranchedStream(asset, totalAmount, startTime, recipient, numberOfTranches, recurrence); + // Retrieve the storage of the {StreamManager} contract + StreamManagerStorage storage $ = _getStreamManagerStorage(); + // Set `msg.sender` as the initial stream sender to allow authenticated stream management - _initialStreamSender[streamId] = msg.sender; + $.initialStreamSender[streamId] = msg.sender; } /// @inheritdoc IStreamManager - function updateStreamBrokerFee(UD60x18 newBrokerFee) public { - // Checks: the `msg.sender` is the broker admin - if (msg.sender != brokerAdmin) revert Errors.OnlyBrokerAdmin(); + function updateStreamBrokerFee(UD60x18 newBrokerFee) public onlyOwner { + // Retrieve the storage of the {StreamManager} contract + StreamManagerStorage storage $ = _getStreamManagerStorage(); // Log the broker fee update - emit BrokerFeeUpdated({ oldFee: brokerFee, newFee: newBrokerFee }); + emit BrokerFeeUpdated({ oldFee: $.broker.fee, newFee: newBrokerFee }); // Update the fee charged by the broker - brokerFee = newBrokerFee; + $.broker.fee = newBrokerFee; + } + + /// @inheritdoc IStreamManager + function withdrawMaxStream( + Types.Method streamType, + uint256 streamId, + address to + ) + public + returns (uint128 withdrawnAmount) + { + withdrawnAmount = _withdrawMaxStream({ streamType: streamType, streamId: streamId, to: to }); + } + + /// @inheritdoc IStreamManager + function cancelStream(Types.Method streamType, uint256 streamId) public { + _cancelStream({ streamType: streamType, streamId: streamId }); } /*////////////////////////////////////////////////////////////////////////// INTERNAL MANAGEMENT FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Creates a Lockup Linear stream + /// @dev Creates a Lockup Linear streams /// See https://docs.sablier.com/concepts/protocol/stream-types#lockup-linear function _createLinearStream( IERC20 asset, @@ -171,6 +223,9 @@ abstract contract StreamManager is IStreamManager { internal returns (uint256 streamId) { + // Retrieve the storage of the {StreamManager} contract + StreamManagerStorage storage $ = _getStreamManagerStorage(); + // Declare the params struct LockupLinear.CreateWithTimestamps memory params; @@ -180,9 +235,9 @@ abstract contract StreamManager is IStreamManager { params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees params.asset = asset; // The streaming asset params.cancelable = true; // Whether the stream will be cancelable or not - params.transferable = true; // Whether the stream will be transferable or not + params.transferable = false; // Whether the stream will be transferable or not params.timestamps = LockupLinear.Timestamps({ start: startTime, cliff: 0, end: endTime }); - params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); // Optional parameter for charging a fee + params.broker = Broker({ account: $.broker.account, fee: $.broker.fee }); // Optional parameter for charging a fee // Create the LockupLinear stream using a function that sets the start time to `block.timestamp` streamId = LOCKUP_LINEAR.createWithTimestamps(params); @@ -201,6 +256,9 @@ abstract contract StreamManager is IStreamManager { internal returns (uint256 streamId) { + // Retrieve the storage of the {StreamManager} contract + StreamManagerStorage storage $ = _getStreamManagerStorage(); + // Declare the params struct LockupTranched.CreateWithTimestamps memory params; @@ -210,14 +268,14 @@ abstract contract StreamManager is IStreamManager { params.totalAmount = totalAmount; // Total amount is the amount inclusive of all fees params.asset = asset; // The streaming asset params.cancelable = true; // Whether the stream will be cancelable or not - params.transferable = true; // Whether the stream will be transferable or not + params.transferable = false; // Whether the stream will be transferable or not params.startTime = startTime; // The timestamp when to start streaming // Calculate the duration of each tranche based on the payment recurrence uint40 durationPerTranche = _getDurationPerTrache(recurrence); // Calculate the broker fee amount - uint128 brokerFeeAmount = ud(totalAmount).mul(brokerFee).intoUint128(); + uint128 brokerFeeAmount = ud(totalAmount).mul($.broker.fee).intoUint128(); // Calculate the remaining amount to be streamed after substracting the broker fee uint128 deposit = totalAmount - brokerFeeAmount; @@ -243,7 +301,7 @@ abstract contract StreamManager is IStreamManager { params.tranches[numberOfTranches - 1].amount += deposit - estimatedDepositAmount; // Optional parameter for charging a fee - params.broker = Broker({ account: brokerAdmin, fee: brokerFee }); + params.broker = Broker({ account: $.broker.account, fee: $.broker.fee }); // Create the LockupTranched stream streamId = LOCKUP_TRANCHED.createWithTimestamps(params); @@ -252,7 +310,7 @@ abstract contract StreamManager is IStreamManager { /// @dev See the documentation in {ISablierV2Lockup-withdrawMax} /// Notes: /// - `streamType` parameter has been added to withdraw from the according {ISablierV2Lockup} contract - function _withdrawStream( + function _withdrawMaxStream( Types.Method streamType, uint256 streamId, address to @@ -267,40 +325,19 @@ abstract contract StreamManager is IStreamManager { return sablier.withdrawMax(streamId, to); } - /// @dev Withdraws the maximum withdrawable amount and transfers the stream NFT to the new recipient - /// Notes: - /// - `streamType` parameter has been added to withdraw from the according {ISablierV2Lockup} contract - function _withdrawMaxAndTransferStream( - Types.Method streamType, - uint256 streamId, - address newRecipient - ) - internal - returns (uint128 withdrawnAmount) - { - // Set the according {ISablierV2Lockup} based on the stream type - ISablierV2Lockup sablier = _getISablierV2Lockup(streamType); - - // Checks: the caller is the current recipient. This also checks that the NFT was not burned. - address currentRecipient = sablier.ownerOf(streamId); - - // Checks, Effects and Interactions: withdraw the maximum withdrawable amount - withdrawnAmount = sablier.withdrawMax(streamId, currentRecipient); - - // Interactions: transfer the stream to the new recipient - sablier.transferFrom({ from: msg.sender, to: newRecipient, tokenId: streamId }); - } - /// @dev See the documentation in {ISablierV2Lockup-cancel} /// /// Notes: /// - `msg.sender` must be the initial stream creator function _cancelStream(Types.Method streamType, uint256 streamId) internal { + // Retrieve the storage of the {StreamManager} contract + StreamManagerStorage storage $ = _getStreamManagerStorage(); + // Set the according {ISablierV2Lockup} based on the stream type ISablierV2Lockup sablier = _getISablierV2Lockup(streamType); // Checks: the `msg.sender` is the initial stream creator - address initialSender = _initialStreamSender[streamId]; + address initialSender = $.initialStreamSender[streamId]; if (msg.sender != initialSender) revert Errors.OnlyInitialStreamSender(initialSender); // Checks, Effect, Interactions: cancel the stream diff --git a/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol index 36a739f..89dde4d 100644 --- a/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol +++ b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol @@ -3,10 +3,9 @@ pragma solidity >=0.8.22; import { ISablierV2LockupLinear } from "@sablier/v2-core/src/interfaces/ISablierV2LockupLinear.sol"; import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISablierV2LockupTranched.sol"; -import { LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; +import { Broker, Lockup, LockupLinear, LockupTranched } from "@sablier/v2-core/src/types/DataTypes.sol"; import { ISablierV2Lockup } from "@sablier/v2-core/src/interfaces/ISablierV2Lockup.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { Lockup } from "@sablier/v2-core/src/types/DataTypes.sol"; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; import { Types } from "./../../libraries/Types.sol"; @@ -37,12 +36,8 @@ interface IStreamManager { /// See https://docs.sablier.com/contracts/v2/deployments function LOCKUP_TRANCHED() external view returns (ISablierV2LockupTranched); - /// @notice The address of the broker admin account or contract managing the broker fee - function brokerAdmin() external view returns (address); - - /// @notice The broker fee charged to create Sablier V2 stream - /// @dev See the `UD60x18` type definition in the `@prb/math/src/ud60x18/ValueType.sol file` - function brokerFee() external view returns (UD60x18); + /// @notice The broker account andfee charged to create Sablier V2 stream + function broker() external view returns (Broker memory brokerConfig); /// @notice Retrieves a linear stream details according to the {LockupLinear.StreamLL} struct /// @param streamId The ID of the stream to be retrieved @@ -54,7 +49,7 @@ interface IStreamManager { /// @notice See the documentation in {ISablierV2Lockup-withdrawableAmountOf} /// Notes: - /// - `streamType` parameter has been added to retrieve from the according {ISablierV2Lockup} contract + /// - `streamType` parameter has been added to get the correct {ISablierV2Lockup} implementation function withdrawableAmountOf( Types.Method streamType, uint256 streamId @@ -65,7 +60,7 @@ interface IStreamManager { /// @notice See the documentation in {ISablierV2Lockup-streamedAmountOf} /// Notes: - /// - `streamType` parameter has been added to retrieve from the according {ISablierV2Lockup} contract + /// - `streamType` parameter has been added to get the correct {ISablierV2Lockup} implementation function streamedAmountOf( Types.Method streamType, uint256 streamId @@ -76,7 +71,7 @@ interface IStreamManager { /// @notice See the documentation in {ISablierV2Lockup-statusOf} /// Notes: - /// - `streamType` parameter has been added to retrieve from the according {ISablierV2Lockup} contract + /// - `streamType` parameter has been added to get the correct {ISablierV2Lockup} implementation function statusOfStream(Types.Method streamType, uint256 streamId) external view returns (Lockup.Status status); /*////////////////////////////////////////////////////////////////////////// @@ -125,4 +120,20 @@ interface IStreamManager { /// /// @param newBrokerFee The new broker fee function updateStreamBrokerFee(UD60x18 newBrokerFee) external; + + /// @notice See the documentation in {ISablierV2Lockup-withdrawMax} + /// Notes: + /// - `streamType` parameter has been added to get the correct {ISablierV2Lockup} implementation + function withdrawMaxStream( + Types.Method streamType, + uint256 streamId, + address to + ) + external + returns (uint128 withdrawnAmount); + + /// @notice See the documentation in {ISablierV2Lockup-cancel} + /// Notes: + /// - `streamType` parameter has been added to get the correct {ISablierV2Lockup} implementation + function cancelStream(Types.Method streamType, uint256 streamId) external; } diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol index 1e08bc8..c50bdc2 100644 --- a/test/integration/Integration.t.sol +++ b/test/integration/Integration.t.sol @@ -6,9 +6,11 @@ import { PaymentModule } from "./../../src/modules/payment-module/PaymentModule. import { InvoiceCollection } from "./../../src/peripherals/invoice-collection/InvoiceCollection.sol"; import { SablierV2LockupLinear } from "@sablier/v2-core/src/SablierV2LockupLinear.sol"; import { SablierV2LockupTranched } from "@sablier/v2-core/src/SablierV2LockupTranched.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { MockNFTDescriptor } from "../mocks/MockNFTDescriptor.sol"; import { MockStreamManager } from "../mocks/MockStreamManager.sol"; import { MockBadSpace } from "../mocks/MockBadSpace.sol"; +import { ud } from "@prb/math/src/UD60x18.sol"; import { Space } from "./../../src/Space.sol"; abstract contract Integration_Test is Base_Test { @@ -32,11 +34,8 @@ abstract contract Integration_Test is Base_Test { function setUp() public virtual override { Base_Test.setUp(); - // Deploy the {PaymentModule} module - deployPaymentModule(); - - // Deploy the {InvoiceCollection} module - deployInvoiceCollection(); + // Deploy corect contracts + deployCoreContracts(); // Enable the {PaymentModule} module on the {Space} contract address[] memory modules = new address[](1); @@ -48,9 +47,6 @@ abstract contract Integration_Test is Base_Test { // Deploy a "bad" {Space} with the `mockBadReceiver` as the owner badSpace = deployBadSpace({ _owner: address(mockBadReceiver), _stationId: 0, _initialModules: modules }); - // Deploy the mock {StreamManager} - mockStreamManager = new MockStreamManager(sablierV2LockupLinear, sablierV2LockupTranched, users.admin); - // Label the test contracts so we can easily track them vm.label({ account: address(paymentModule), newLabel: "PaymentModule" }); vm.label({ account: address(sablierV2LockupLinear), newLabel: "SablierV2LockupLinear" }); @@ -63,15 +59,19 @@ abstract contract Integration_Test is Base_Test { DEPLOYMENT-RELATED FUNCTIONS //////////////////////////////////////////////////////////////////////////*/ - /// @dev Deploys the {PaymentModule} module by initializing the Sablier v2-required contracts first + /// @dev Deploys the core contracts of the Werk Protocol + function deployCoreContracts() internal { + deployPaymentModule(); + deployInvoiceCollection(); + } + + /// @dev Deploys the {PaymentModule} module function deployPaymentModule() internal { deploySablierContracts(); - paymentModule = new PaymentModule({ - _sablierLockupLinear: sablierV2LockupLinear, - _sablierLockupTranched: sablierV2LockupTranched, - _brokerAdmin: users.admin - }); + address implementation = address(new PaymentModule(sablierV2LockupLinear, sablierV2LockupTranched)); + bytes memory data = abi.encodeWithSelector(PaymentModule.initialize.selector, users.admin, users.admin, ud(0)); + paymentModule = PaymentModule(address(new ERC1967Proxy(implementation, data))); } /// @dev Deploys the {InvoiceCollection} peripheral diff --git a/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol b/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol index 986bc67..5492fcd 100644 --- a/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol +++ b/test/integration/concrete/payment-module/cancel-request/cancelRequest.t.sol @@ -89,7 +89,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Run the test paymentModule.cancelRequest({ requestId: paymentRequestId }); - // Assert the actual and expected paymentRequest status + // Assert the actual and expected payment request status Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); } @@ -101,7 +101,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha givenPaymentMethodLinearStream givenRequestStatusPending { - // Set current paymentRequest as a linear stream-based one + // Set the current payment request as a linear stream-based one uint256 paymentRequestId = 5; // Make Bob the caller who IS NOT the recipient of the payment request @@ -122,7 +122,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha givenRequestStatusPending whenRequestSenderRecipient { - // Set current paymentRequest as a linear stream-based one + // Set the current payment request as a linear stream-based one uint256 paymentRequestId = 5; // Make Eve's space the caller which is the recipient of the payment request @@ -135,7 +135,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Run the test paymentModule.cancelRequest({ requestId: paymentRequestId }); - // Assert the actual and expected paymentRequest status + // Assert the actual and expected payment request status Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); } @@ -147,7 +147,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha givenPaymentMethodLinearStream givenRequestStatusPending { - // Set current paymentRequest as a linear stream-based one + // Set the current payment request as a linear stream-based one uint256 paymentRequestId = 5; // The payment request must be paid for its status to be updated to `Accepted` @@ -180,7 +180,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha givenRequestStatusPending whenSenderInitialStreamSender { - // Set current paymentRequest as a linear stream-based one + // Set the current payment request as a linear stream-based one uint256 paymentRequestId = 5; // The payment request must be paid for its status to be updated to `Accepted` @@ -205,7 +205,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Run the test paymentModule.cancelRequest({ requestId: paymentRequestId }); - // Assert the actual and expected paymentRequest status + // Assert the actual and expected payment request status Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); } @@ -217,7 +217,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha givenPaymentMethodTranchedStream givenRequestStatusPending { - // Set current paymentRequest as a tranched stream-based one + // Set the current payment request as a tranched stream-based one uint256 paymentRequestId = 5; // Make Bob the caller who IS NOT the recipient of the payment request @@ -238,7 +238,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha givenRequestStatusPending whenRequestSenderRecipient { - // Set current paymentRequest as a tranched stream-based one + // Set the current payment request as a tranched stream-based one uint256 paymentRequestId = 5; // Make Eve's space the caller which is the recipient of the payment request @@ -251,7 +251,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Run the test paymentModule.cancelRequest({ requestId: paymentRequestId }); - // Assert the actual and expected paymentRequest status + // Assert the actual and expected payment request status Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); } @@ -263,7 +263,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha givenPaymentMethodTranchedStream givenRequestStatusPending { - // Set current paymentRequest as a tranched stream-based one + // Set the current payment request as a tranched stream-based one uint256 paymentRequestId = 5; // The payment request must be paid for its status to be updated to `Accepted` @@ -296,7 +296,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha givenRequestStatusPending whenSenderInitialStreamSender { - // Set current paymentRequest as a tranched stream-based one + // Set the current payment request as a tranched stream-based one uint256 paymentRequestId = 5; // The payment request must be paid for its status to be updated to `Accepted` @@ -318,7 +318,7 @@ contract CancelRequest_Integration_Concret_Test is CancelRequest_Integration_Sha // Run the test paymentModule.cancelRequest({ requestId: paymentRequestId }); - // Assert the actual and expected paymentRequest status + // Assert the actual and expected payment request status Types.Status paymentRequestStatus = paymentModule.statusOf({ requestId: paymentRequestId }); assertEq(uint8(paymentRequestStatus), uint8(Types.Status.Canceled)); } diff --git a/test/integration/concrete/stream-manager/constructor.t.sol b/test/integration/concrete/stream-manager/constructor.t.sol index b59ccc6..f3ef8b7 100644 --- a/test/integration/concrete/stream-manager/constructor.t.sol +++ b/test/integration/concrete/stream-manager/constructor.t.sol @@ -3,20 +3,16 @@ pragma solidity ^0.8.26; import { Integration_Test } from "../../Integration.t.sol"; import { UD60x18 } from "@prb/math/src/UD60x18.sol"; -import { MockStreamManager } from "../../../mocks/MockStreamManager.sol"; contract Constructor_StreamManager_Integration_Concret_Test is Integration_Test { function setUp() public virtual override { Integration_Test.setUp(); } - function test_Constructor() external { - // Run the test - mockStreamManager = new MockStreamManager(sablierV2LockupLinear, sablierV2LockupTranched, users.admin); - - assertEq(UD60x18.unwrap(mockStreamManager.brokerFee()), 0); - assertEq(mockStreamManager.brokerAdmin(), users.admin); - assertEq(address(mockStreamManager.LOCKUP_TRANCHED()), address(sablierV2LockupTranched)); - assertEq(address(mockStreamManager.LOCKUP_LINEAR()), address(sablierV2LockupLinear)); + function test_Constructor() external view { + assertEq(UD60x18.unwrap(paymentModule.broker().fee), 0); + assertEq(paymentModule.broker().account, users.admin); + assertEq(address(paymentModule.LOCKUP_TRANCHED()), address(sablierV2LockupTranched)); + assertEq(address(paymentModule.LOCKUP_LINEAR()), address(sablierV2LockupLinear)); } } diff --git a/test/integration/concrete/stream-manager/update-stream-broker-fee/updateStreamBrokerFee.t.sol b/test/integration/concrete/stream-manager/update-stream-broker-fee/updateStreamBrokerFee.t.sol index 776087d..88bfbd5 100644 --- a/test/integration/concrete/stream-manager/update-stream-broker-fee/updateStreamBrokerFee.t.sol +++ b/test/integration/concrete/stream-manager/update-stream-broker-fee/updateStreamBrokerFee.t.sol @@ -19,10 +19,10 @@ contract UpdateStreamBrokerFee_Integration_Concret_Test is Integration_Test { vm.startPrank({ msgSender: users.bob }); // Expect the call to revert with the {OnlyBrokerAdmin} error - vm.expectRevert(Errors.OnlyBrokerAdmin.selector); + vm.expectRevert(abi.encodeWithSelector(Errors.OwnableUnauthorizedAccount.selector, users.bob)); // Run the test - mockStreamManager.updateStreamBrokerFee({ newBrokerFee: ud(0.05e18) }); + paymentModule.updateStreamBrokerFee({ newBrokerFee: ud(0.05e18) }); } modifier whenCallerBrokerAdmin() { @@ -40,10 +40,10 @@ contract UpdateStreamBrokerFee_Integration_Concret_Test is Integration_Test { emit Events.BrokerFeeUpdated({ oldFee: ud(0), newFee: newBrokerFee }); // Run the test - mockStreamManager.updateStreamBrokerFee(newBrokerFee); + paymentModule.updateStreamBrokerFee(newBrokerFee); // Assert the actual and expected broker fee - UD60x18 actualBrokerFee = mockStreamManager.brokerFee(); + UD60x18 actualBrokerFee = paymentModule.broker().fee; assertEq(UD60x18.unwrap(actualBrokerFee), UD60x18.unwrap(newBrokerFee)); } } diff --git a/test/integration/fuzz/createRequest.t.sol b/test/integration/fuzz/createRequest.t.sol index 8872b7e..82bc896 100644 --- a/test/integration/fuzz/createRequest.t.sol +++ b/test/integration/fuzz/createRequest.t.sol @@ -35,7 +35,7 @@ contract CreateRequest_Integration_Fuzz_Test is CreateRequest_Integration_Shared // Discard bad fuzz inputs // Assume recurrence is within Types.Recurrence enum values (OneOff, Weekly, Monthly, Yearly) (0, 1, 2, 3) vm.assume(recurrence < 4); - // Assume recurrence is within Types.Method enum values (Transfer, LinearStream, TranchedStream) (0, 1, 2) + // Assume the payment method is within Types.Method enum values (Transfer, LinearStream, TranchedStream) (0, 1, 2) vm.assume(paymentMethod < 3); vm.assume(recipient != address(0) && recipient != address(this)); vm.assume(startTime >= uint40(block.timestamp) && startTime < endTime); diff --git a/test/mocks/MockStreamManager.sol b/test/mocks/MockStreamManager.sol index 839eaff..1f8ee68 100644 --- a/test/mocks/MockStreamManager.sol +++ b/test/mocks/MockStreamManager.sol @@ -10,9 +10,8 @@ import { ISablierV2LockupTranched } from "@sablier/v2-core/src/interfaces/ISabli contract MockStreamManager is StreamManager { constructor( ISablierV2LockupLinear _sablierLockupLinear, - ISablierV2LockupTranched _sablierLockupTranched, - address _brokerAdmin + ISablierV2LockupTranched _sablierLockupTranched ) - StreamManager(_sablierLockupLinear, _sablierLockupTranched, _brokerAdmin) + StreamManager(_sablierLockupLinear, _sablierLockupTranched) { } }