From 93003465541cbfe3b95a25c0763db2b68382be88 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 21 Nov 2024 16:07:16 +0200 Subject: [PATCH 01/12] build: install @openzeppelin/contracts-upgradeable --- bun.lockb | Bin 42895 -> 43319 bytes package.json | 39 ++++++++++++++++++++------------------- remappings.txt | 1 + 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/bun.lockb b/bun.lockb index 2a82ff7fbaa77db375c208b95b1e029157221144..ba0af76b8ebb3c30374bac12fbd2bd4e730eacf4 100755 GIT binary patch delta 7235 zcmeHMYgANMmcFL|sS8v9#XtpufboGq$-5|!Djtg9m4`|!6f?SbWGGFHr+|E^JvrQbhIBC6IVuKXF9H49rNfoI+IC)&-wPLd(-raGe2gn zS-sY*xr?v%{r1`CvG+dba_+egy={8&9n*Jh$xrM**zj@tm9I`V)+MfQ_`UhHLZ9ck z%SFe>ym+qs?|)IV^Ql9Myt=;Up5VKu(@*~L`v`8M6{UX(${Zpj{2@HUvquO7lDcrf{I<9RW0>sF?eNG>0OT{xs??R{unQK~RX zb|4?p7loS!PiI%!#*XF`DA=&GzV*KP4n?_ufv`s{ZEHN8O^Wg^_#NOK=!FM#3X&U2 z0b%`T4$2y#5+F^q}_*bN>7`D&=*lNCBvOMB%Fw89OxqJ`Gx&aPo_ zuF%-lB@LD$&fL&TkR0*~{ILDb){n2T_X$5RC@$*B`9QKf{h2_$fxi9v^|^NS&c!)0Df`AWK}U4wAJO zAyJ`!MhsmwP4=DA!}=N|n_ag0%|3$U&Y!x=@5N5YyTR)_I@&tY>U$2r6}I>4<+X8s zdkvDiNrp^`%bpdLVFmc5WX*zxhwE~)Ie#rZu#98V}Qn zfat_4nDppLP7@9uCP>=aPDN?oxZbc};En)sbZ8`a zt0`zD%!8;n#ID{5P?Yj-ST#D%mF$FSH(7${YKT*vh3Pp1N`(egyBHvEs7sus;ZT=?q{9C~|#%9_6b6}henK1QV zbxVKxvZCmCs8h7jaI{N(2D6M4g(3{=V=x}1jG&s11(pvML?X&A*3xi{OFaP{H;m-O z*i~_-VZj3`0Q;uXpa;QnsW7BKeGPYZsD^odfJWk6 zY5*1(>-=#m1oO{#tc0MKz~<16$bujf7XB(Kj4V*s;Ep>JRD73ReF}_+s&D|rpJ+JV zr7n;2w}Ig!4Ew>pXIx{FT>`r zp^b4+z~z2A798ipYUM%&&li5Y$?J9!|KIuXEH9QO0P|j6x07s-*)K1an9<54KOHT7 zvg2te;0h_a0ZTj+6`RgSOV(%TdX`)-QdLp3oykzf`bcpKh>O zH@KbTfo;*tM@w#atF9j{d4St>y&;*(#m4^sjaHlN*Y~$uZ89Xj|7JP*bM?Qr+GPLu z7ytjO4UzaizS?ev-{j>v^mNLZfFfmJ($8;}(8bg}r=9V$f8^V9Y}fvvKPqjn&RBZ> zC(YN+zVCed_0;Wq0^f{3v!HYE?C#TV+J3&K!Sm$o{mq{rpL*#6y)r$9`lf5bLMNxY zsVCh+Y3Z5>CvUo&re;{^1F%R+&T!LNu-zG&7)R&8c4S(pFjEub>A_4lgT_%{Rofn`wg4EP7Odxj>m=p5LNLiksxi5z;c z5dJygADEpSPWT7*m{SuD`UGsR3;ww@Q9%1#@UIB|ffZ6k5&Q!?TBHdVjes30hJVGH zD5j^2;a>^-E73$LEiQq7rSK1I7OAE153IFR6SHX;tZ63vo2iKkYMu%IX2Cx&jjXfa zA6VZkO;plJu%0sbSEh+-@|MBB+3*i+9wpC)e_*?3YhnSN1KUv!|H?ISFFjZe|0>`g zn426G@DJ><3Qa7gPr&xhfq!!}QAhjcz&{QCfi0s74gP^0)ikk!M!*iug@1E3;i0GJ z!oN!RSE-5Bw73%fRlz^7dQz+4A6RRZ##=zJrfT?Ct%+u8u7-a#@DFSqS!>`QSYM4M zTInQM&ph}yPs1yNcOLwk5C6b6Q1X2EH$RMa&7UhesknA7y%jo@YHI&LmxBW-uGSnk z3r~d*1zS%n9eMVGqnDe|@tgV3g@sK*S{}LAZ0>^@zc3FK*9J=M&>IUQOy*5^cfkIU zjW3F!np%e}`~Fh1`9z}scwp$le7hLByzuP*;0I%UD^xUf$_KgfMJHu+IH{{6%7@oi zdGWS_$IdWIjiR*boc}8)>AE&Zemw9)5RS-;w;-(Jf6lyBW%~^P>&5`{0KPf^)+OkB zls|7&=weqQ#dId9+$Zk@w$ZH4=|1fK`BY3v1i0sk0Pg{~M;tmRyw~9EKtC`5@FwMc zfW6>RuLnH9DquCR2B-&C0=#9N222K~08@c20Pl%;PlJuoLdA<4c8lHMVRPWt0u2EF zm&I!jzq9b01)n)6(}8p#72r)Z@1A(absx|Tu*Z#5*)_$-!I%$}0=z3;4y*upL%Il9 z3@ica0N%vT1ZDwc0R98Z9kdhD1@IQN1SkUd-68@A1u(s2A~2tn9H176;h}JD(tGHm zt|SY4#GbK-7K+?x_i zCIg%RP6j7|c?Q6VDFVs?4jqS+!^ffJG&q1fo=>^B;f!+3c$yRgMF3YS1UO%ub&0akxELdiMMuS_2R3RtPaW`B&|W<=J@2MwXW-bf70w^wHZr z*4R>*VS4m07*k$j?Y^`xLgd&oxEE!R`nFlg<{Cv%~ z_xgq6B`eTzo-Lg#o}z=^M)58M_hyL;RDj=4sSUqh&=b9tVu-HvT5*^Vy2%=A9E{&+ z5zn_L&3qqiWWz-ak!x6EjRW*0;=uW`uMW1OJO_PX!pU;6pLU}a<5>MjTgFdXF0FnX zR$2a$?4lE!tm08RzsVZ+Ut1-bDdPT^IODYa^rC0}sp5-+e=>>98HfZTnWECn`!jR+ zxIGGyI=5r;O=sN>75yMk?6GkmUl;qU7kYATeu@$#%7z8fL5J_J%*8pmJji{osAzI@ z`$NZ2p6jn^oWMWd6nE=o_dDlMqI;MUMz+3~SmUTYH|lJ+W8PEovQwF#hse`s6;IM8 z{9d7l`?AC_I*H$5y3toD-XLwWRh**JJ}Zume7OIRKJ!^)jf3yGsgsAEn4Fj?hYCN_ zl@a>wW^1l-K;D?|tUA+qy<84R&N|}=ec%TlKKgId{^c4RmA#>={P3p{Fu;Q2X=8j(*jl3|^*r9q zt0mi?DCvEy@wuQiIeyhMdsbDO1kWUXz#XA~=(mcW($D$_Om)%Z9EgcEz7hmod^mUg z-e<$mm_8pa(uM)63116#4K#@N>E8#kVvTPDO*t!mG(Gk6hhd!KU(J`Pc#BnxP#u1~ zv~f#Tj`7`~VCN5ipP4vv0=8HbJkg9V3%%zjMEquTnpbw2VZ%f90DZ8chdgtVYFrNF#UKiZm4DZ-vf+_H z*bRh7QW?QdEmSL`omv64wP}lX##W}|XkdJxj)IR`+iFD%_MsKjLVw@AcWXL>`bWqA zdNaR#_xGLG_c-UCv*+YXmfK&j9L&zVH1zlCYng-lfARU<2k$JJQnNH=X2EnV@$`>g zJbv5dtfzw49+dPaaLAupKfTK!U;Yo^b)qDNtU{8~A>RW}g`8*VUxpkH9@yB{u&zgv z)`E`%UkNFXs;BcqZ#jZ@MoLl)3c4E_+nP{m$mBn_8u~U!8_E~lU<_OYIR?DZEO$p4 z<+C7Je+tqLIWF30zXbJS!5dn)pm#~S1U(!0I%I+*1w-o_0^M1gyIRgb!4(5d?OU4Q zcRvhai`zD?YX~$;QUH7`csxd7gIXcEqcf1KpARMLTRK|wfewMNW3wR>A?sij8#oa> z3383zPf!YVag6Y+LoeK6J9=nu33RUj=LQ=#cI)L|z&h?|IpWD7y#UVjuY#}v=fLA2 zpD|@ryd*im{{baD{ZGC3z&S!8)Uz$Z%MrYA2d$87@vTigdaP8mVgpV>&jbG0l(#{0 z2TNfMoCz(0a7ed zGYweV+|YI_0^5{gc;DO*$Xeglut75oYv|~1>zX4;;fZqy(;Q=mCr$`cCaX0dNu8TJ zde&}S*OWmba_ev?a({#v8kVDXTdb9+ZKb*xxBO9rB<1Vutn9Wt5=m8KJo393s02!h z4#{pYg+|7xVjIaeRsJ>RESFd*&gQm!W~CvUN35a|6#Wd---DuPI%{*we+A=;kvf}) zsb2gVd_?(3q6=}GZ^Q^!l9La3dSAWp!;dV z!eGNA^qkd#Rp~YB#=6D#NuHpJspOrY$~{sh|q6D{yAz4x72CO#D zGQy;|<@>>m8nDIs2AA=8)=aFveDcK<%k_BU?CBnh@6lkgDxZNa2fAq8(^#xfwm{Fe z@R8S{%6CJzFx*Q10F15IQzK{N^JVl{H^D8|lXsFT-v=EQi&4Y!QWBk>dJD{$=~x5S<;e&+`QnSM58$nl4#XA9kyzI}n4m++Zn*-CO_kVr zv4`YTRlW)x_kmH6j-pA%n0o)Bn?_Pq>+8^A-pIwW6JG%io-uj=jFW|ym<+!LV{47{ zUj{1%i{UciB<~be4x|`ih~gD1p9EveBB^d-K{SYI7^-KHcd9BHX>h74pTKux6{=Y{ z)*_1JbXE4J8PUQFN_Sho3xHuzlFPZ+Z zWWL0p|0VIAmR6ejuan&VX0u*cTKPrLfoZA2LeEW04RWP=v(jkE`C4b{S#rH5Q?57l zqb1LeW>e3S>$jMkCHL21a+VzBE}jqk;6;B}a>IZ@b;;r1V)E-r9(bEs&XU{rn0&P4 zdfUzN9cK9sbA0Jbg<)C9tT0+~$Ni>$v}6MZOnq1~!cDe`_M}>KQD#)|b5Ark|t}cMR59V`f zB9V?b{dCxAr+B<~zQRnD=~BXcyN&=s)DV5?o4aL_TApB{4A$)2x?$+RNhPs{V| z^dwj+$!apEgntX+-$D(?%JX37z)CALQA2|j@UH^?fz?rACH$*|f0de8LKnc^ z2lII~v6PN@;hz`&f%(Z>1^=qxUzH|S&=s)DV5>Dv+)T$b_@}|YMVeSmD;B}OMeq-- zo@5{V^T9u#CK~7ou*bnBS8HM|wN%5uYWN4%L`n_(tAT$tn%F=iV9$bO)oP-JdTQZc zE&Kz!l``w#Umg6b(?mO+2RjE=x>yq%X>c+8TMYleHc{ac__xG1eD9Ju;*RLZ8*x3- z+|to4y^F6ipXzx3>g;Oj?rv%7x(~Z2|6m?|Y(+Cymcus^lZX44rA7$H@ck>C5z!4( zj9=vxH>-jiwv6GN)yEUZh)#36eZPyc1D<4kU0%IrJWTi}FYie=P=8?jGCpna(+;qf zH;XiYAKu7W$D4UNz-1c&*73Gyu6e1`)Fqi4`=-tO7K zRJ^4DQvlvSWq^-JHh{k#eZWqDw@%*RdjURSu-R<@KIzgeKm*VStOeEpyc5j;ZUi!b zX+Q{=4e-H$4;NecWW`>w7i{$=pcz;P@Zk`rd8rV{18}sKvVd&hCV)?;d;(!7yMfIB zhq?*i^Iiq{Hct_V-df54K9=&y zAr`+3d{oq|un}M47E(&pIh%i#y1^wgMbP_Lc2rf0qK^ z0W_crs07l1sQ@P@4HyUT=xhZaavT89s6-$h;Brna%LHINfM1Hz1oJ(K^QeGu)>8od zrs5wfdY1Aa(*O=~IAeTX;b2b(_;AA+oe6Lfa!t;Om<^}^e%I?E<1li#IHVj}PNW-f z@jT*~aW*-_WdJwiICH!??wl{qI(NYN<7{$Hya4Bq^HmG@fJHzxPy=uo=Ynwyz!Bs; zbG~`K@f=vmlYo=X$>+q@18V@*Hv$d7T7bjGC+-bEJCF+SeCK)J0i*$(N=|I$R{X>B z(*Y{lc59TyVD#{|qC0vfIJh??B$U6e{PVvZd})?NtjW#J%SPA&l-uJJFH=*GGygr* z!Wf}F^Ul5Tc6;OrvlikMzS+I}a^-L1`#$*QaznTn^yX;cY_GflI>2>Py6tHh<>v}p;xvi zi#zF~?LK__OLr)a@ZD~Cy;>1@;+~Im6Sy0xjW+F2j8=~D4e?5G?;8t0J=%#{E)0fw zqW9_u-xU94-O8nJl{_^c{qZ03%ElDET0y9VVDVUbEz7AW zTGf{{DSVxLcKPuay&oTa*CKZ2X6Nz`@H5eLurF_td7m6Qzkk|Q&&~T~^pk7!b19mB z*Ow&X>C--+!?^X8La&yUO`F(x`(vn{J}#`{zx2ryqGn`u&q;d2X(Bh~C-h#3q{w~+b{e5;rN@E_u1(Ga6-NkbAA^RRpMJ3FC*q+f^yoBfy^XE~mHhC% z@zcqjuYKZCIKXCk_@;P6k*E5Vz!#M$aT#;)WGq#OlJdiM)b}OWyT<$ z@GbT&@%u6!y`y&kC3y%mz7`i^>E2M1BYX`Wt6a=>9rhRKee1R_rx!ws z21oM0+J*2f_@zkc;b$)SA4NCj>_7)?{Yj4SwfVI3cj?lo=^MU5xBmIA{Eowq z+Ay*?mG;q-yA%s<($DQ$FM3Jc?R13i&6{2I56+%-@h&|a`f@)=2X-rtBl!Me_Xr)$ z2+f+gr>QYPcuILayCj89p?&zCy?E$>H}j@kIfeFl*|3*)wAcY9KYZ{0&yKryCB!_K zj&e>Eyb0g9zgOEE;ahs-Bud=bd9Lh2=|cyt8Ax)3ujm7Vl~;fMqZ@aig6T&x9U4$@ zI6g6;ILwRr(1*4(Wz4ROJk&C0(JXo!wVLS4fX||@vcwb&d!2=rla0(snCIK~`0!yG z+2f4)Y{2*`el|ci>@AC#vB#jFK1j8D)9}%fB Date: Thu, 21 Nov 2024 16:09:25 +0200 Subject: [PATCH 02/12] chore: style and fix doc typo --- src/StationRegistry.sol | 10 ++++++++-- src/modules/payment-module/libraries/Types.sol | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) 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/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 From 52d7e8575d266883e8e6d778d6de1ac5c8c702ea Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 21 Nov 2024 16:10:17 +0200 Subject: [PATCH 03/12] feat: make 'PaymentModule' UUPSUpgradeable compatible --- src/modules/payment-module/PaymentModule.sol | 112 +++++++---- .../sablier-v2/StreamManager.sol | 175 +++++++++++------- .../sablier-v2/interfaces/IStreamManager.sol | 33 ++-- 3 files changed, 209 insertions(+), 111 deletions(-) diff --git a/src/modules/payment-module/PaymentModule.sol b/src/modules/payment-module/PaymentModule.sol index db0f5b0..944edf4 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 {StreamManager} 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({ sender: msg.sender, 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/sablier-v2/StreamManager.sol b/src/modules/payment-module/sablier-v2/StreamManager.sol index 209af16..c25934e 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(address sender, Types.Method streamType, uint256 streamId) public { + _cancelStream({ sender: sender, 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,41 +325,20 @@ 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 { + function _cancelStream(address sender, 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]; - if (msg.sender != initialSender) revert Errors.OnlyInitialStreamSender(initialSender); + // Checks: the `sender` is the initial stream creator + address initialSender = $.initialStreamSender[streamId]; + if (sender != initialSender) revert Errors.OnlyInitialStreamSender(initialSender); // Checks, Effect, Interactions: cancel the stream sablier.cancel(streamId); diff --git a/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol index 36a739f..ffd646b 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(address sender, Types.Method streamType, uint256 streamId) external; } From 91c3a3bc80aa0d1b605899857dbe37629825e7cb Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 21 Nov 2024 16:16:17 +0200 Subject: [PATCH 04/12] docs: fix typos --- src/modules/payment-module/PaymentModule.sol | 2 +- src/modules/payment-module/libraries/Errors.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/payment-module/PaymentModule.sol b/src/modules/payment-module/PaymentModule.sol index 944edf4..427625d 100644 --- a/src/modules/payment-module/PaymentModule.sol +++ b/src/modules/payment-module/PaymentModule.sol @@ -42,7 +42,7 @@ contract PaymentModule is IPaymentModule, StreamManager, UUPSUpgradeable { bytes32 private constant PAYMENT_MODULE_STORAGE_LOCATION = 0x69242e762af97d314866e2398c5d39d67197520146b0e3b1471c97ebda768e00; - /// @dev Retrieves the storage of the {StreamManager} contract + /// @dev Retrieves the storage of the {PaymentModule} contract function _getPaymentModuleStorage() internal pure returns (PaymentModuleStorage storage $) { assembly { $.slot := PAYMENT_MODULE_STORAGE_LOCATION 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 From c7957feed4ae07f000972a0ca29c4dd362dac7c6 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 21 Nov 2024 16:20:01 +0200 Subject: [PATCH 05/12] test: fix docs and improve testing --- test/integration/Integration.t.sol | 28 +++++++++---------- .../cancel-request/cancelRequest.t.sol | 26 ++++++++--------- .../concrete/stream-manager/constructor.t.sol | 14 ++++------ .../updateStreamBrokerFee.t.sol | 8 +++--- test/integration/fuzz/createRequest.t.sol | 2 +- test/mocks/MockStreamManager.sol | 5 ++-- 6 files changed, 39 insertions(+), 44 deletions(-) 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) { } } From c0e90752bde352ff99b1d79cc0611a9c4b4d4711 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 21 Nov 2024 16:28:10 +0200 Subject: [PATCH 06/12] perf: remove unecessary 'sender' from 'cancelStream' method --- src/modules/payment-module/PaymentModule.sol | 2 +- .../payment-module/sablier-v2/StreamManager.sol | 10 +++++----- .../sablier-v2/interfaces/IStreamManager.sol | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/payment-module/PaymentModule.sol b/src/modules/payment-module/PaymentModule.sol index 427625d..72fe335 100644 --- a/src/modules/payment-module/PaymentModule.sol +++ b/src/modules/payment-module/PaymentModule.sol @@ -312,7 +312,7 @@ contract PaymentModule is IPaymentModule, StreamManager, UUPSUpgradeable { // - 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({ sender: msg.sender, streamType: request.config.method, streamId: request.config.streamId }); + cancelStream({ streamType: request.config.method, streamId: request.config.streamId }); } // Effects: mark the payment request as canceled diff --git a/src/modules/payment-module/sablier-v2/StreamManager.sol b/src/modules/payment-module/sablier-v2/StreamManager.sol index c25934e..9c26be4 100644 --- a/src/modules/payment-module/sablier-v2/StreamManager.sol +++ b/src/modules/payment-module/sablier-v2/StreamManager.sol @@ -203,8 +203,8 @@ abstract contract StreamManager is IStreamManager, Initializable, OwnableUpgrade } /// @inheritdoc IStreamManager - function cancelStream(address sender, Types.Method streamType, uint256 streamId) public { - _cancelStream({ sender: sender, streamType: streamType, streamId: streamId }); + function cancelStream(Types.Method streamType, uint256 streamId) public { + _cancelStream({ streamType: streamType, streamId: streamId }); } /*////////////////////////////////////////////////////////////////////////// @@ -329,16 +329,16 @@ abstract contract StreamManager is IStreamManager, Initializable, OwnableUpgrade /// /// Notes: /// - `msg.sender` must be the initial stream creator - function _cancelStream(address sender, Types.Method streamType, uint256 streamId) internal { + 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 `sender` is the initial stream creator + // Checks: the `msg.sender` is the initial stream creator address initialSender = $.initialStreamSender[streamId]; - if (sender != initialSender) revert Errors.OnlyInitialStreamSender(initialSender); + if (msg.sender != initialSender) revert Errors.OnlyInitialStreamSender(initialSender); // Checks, Effect, Interactions: cancel the stream sablier.cancel(streamId); diff --git a/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol index ffd646b..89dde4d 100644 --- a/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol +++ b/src/modules/payment-module/sablier-v2/interfaces/IStreamManager.sol @@ -135,5 +135,5 @@ interface IStreamManager { /// @notice See the documentation in {ISablierV2Lockup-cancel} /// Notes: /// - `streamType` parameter has been added to get the correct {ISablierV2Lockup} implementation - function cancelStream(address sender, Types.Method streamType, uint256 streamId) external; + function cancelStream(Types.Method streamType, uint256 streamId) external; } From df0a7bf0311a68fb51cf8934764242fc066f6113 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 21 Nov 2024 16:41:21 +0200 Subject: [PATCH 07/12] build: add optimized foundry profile and update test action --- .github/workflows/test.yml | 11 ++++------- .gitignore | 1 + foundry.toml | 4 ++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index adc0d2f..7f7d675 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: "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/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" From 977495e0faf652e393c93e7af5b5b9f457586a8f Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Thu, 21 Nov 2024 16:47:54 +0200 Subject: [PATCH 08/12] build(tesy.yml): test the optimized contracts without re-compiling them --- .github/workflows/test.yml | 4 ++++ foundry.toml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f7d675..2c2feb4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,10 @@ jobs: run: "FOUNDRY_PROFILE=optimized forge build" id: build + - name: "Build the test contracts" + run: "FOUNDRY_PROFILE=test-optimized forge build" + id: build-test + - name: "Run the tests" run: "forge test -vvv" id: test diff --git a/foundry.toml b/foundry.toml index b2a9547..3c360c4 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,6 +18,10 @@ runs = 10_000 out = "out-optimized" via_ir = true +# Test the optimized contracts without re-compiling them +[profile.test-optimized] +src = "test" + [fmt] bracket_spacing = true int_types = "long" From ff1a9f465dcbbb6d12c600e05d2ea5c2075d3c2c Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 22 Nov 2024 09:19:56 +0200 Subject: [PATCH 09/12] ci: run forge test via ir --- .github/workflows/test.yml | 6 +----- foundry.toml | 4 ---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c2feb4..049beb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,10 +34,6 @@ jobs: run: "FOUNDRY_PROFILE=optimized forge build" id: build - - name: "Build the test contracts" - run: "FOUNDRY_PROFILE=test-optimized forge build" - id: build-test - - name: "Run the tests" - run: "forge test -vvv" + run: "FOUNDRY_PROFILE=optimized forge test -vvv" id: test diff --git a/foundry.toml b/foundry.toml index 3c360c4..b2a9547 100644 --- a/foundry.toml +++ b/foundry.toml @@ -18,10 +18,6 @@ runs = 10_000 out = "out-optimized" via_ir = true -# Test the optimized contracts without re-compiling them -[profile.test-optimized] -src = "test" - [fmt] bracket_spacing = true int_types = "long" From e53b8fb4de5798d018455facc3e7bc360ccc4806 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 22 Nov 2024 12:14:14 +0200 Subject: [PATCH 10/12] build: install openzeppelin-foundry-upgrades lib --- .gitmodules | 3 +++ lib/openzeppelin-foundry-upgrades | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 lib/openzeppelin-foundry-upgrades 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/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 From 03719dd221e69bc27dd8d1839fee57fdf7fb2614 Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 22 Nov 2024 12:41:13 +0200 Subject: [PATCH 11/12] chore: add deployment script for 'PaymentModule' and Makefile shortcut --- Makefile | 18 +++++- script/DeployDeterministicPaymentModule.s.sol | 64 +++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 script/DeployDeterministicPaymentModule.s.sol diff --git a/Makefile b/Makefile index 05d321c..7301159 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 @@ -48,4 +48,18 @@ 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 \ No newline at end of file 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)); + } +} From 5526e3ee060b6d040962f2a20b652fb6f636cb0a Mon Sep 17 00:00:00 2001 From: gabrielstoica Date: Fri, 22 Nov 2024 13:12:16 +0200 Subject: [PATCH 12/12] chore: add core deployment script and Makefile shortcut --- Makefile | 19 ++++++- script/DeployDeterministicCore.s.sol | 77 ++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 script/DeployDeterministicCore.s.sol diff --git a/Makefile b/Makefile index 7301159..8a0eec1 100644 --- a/Makefile +++ b/Makefile @@ -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: @@ -62,4 +62,21 @@ 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/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)); + } +}