From f2d640e4234e6846152e24560f863b8b4257962b Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Fri, 29 Nov 2024 13:49:24 +0700 Subject: [PATCH 01/16] chore: remove ccip docs --- ensips/ccip-write/ccip-write.md | 366 ----------------------- ensips/ccip-write/images/db.register.png | Bin 35456 -> 0 bytes ensips/ccip-write/images/l2.register.png | Bin 15430 -> 0 bytes 3 files changed, 366 deletions(-) delete mode 100644 ensips/ccip-write/ccip-write.md delete mode 100644 ensips/ccip-write/images/db.register.png delete mode 100644 ensips/ccip-write/images/l2.register.png diff --git a/ensips/ccip-write/ccip-write.md b/ensips/ccip-write/ccip-write.md deleted file mode 100644 index a4b4db2a..00000000 --- a/ensips/ccip-write/ccip-write.md +++ /dev/null @@ -1,366 +0,0 @@ ---- - -> ensip: -> -> -> title: Wildcard Writing -> description: A standardized implementation for managing offchain domains using an External Resolver -> author: Lucas Picollo (@pikonha), Alex T. Netto (@alextnetto) -> discussions-to: -> status: Draft -> type: Standards Track -> category: ERC -> created: 2024-08-14 -> requires: 5559 -> - -## Abstract - -This ENSIP proposes a standardized Offchain Resolver interface for managing offchain domains within the Ethereum Name Service (ENS) ecosystem. It addresses the growing trend of storing domains off the Ethereum blockchain to reduce transaction fees while maintaining compatibility with existing ENS components. The proposal outlines methods for domain registration, transferring, and setting records, ensuring a consistent approach to offchain domain management. - -## Motivation - -With the acceptance of CCIP-Read by the Ethereum community, there has been a notable shift towards storing domains in locations other than the Ethereum blockchain to avoid high transaction fees. This shift has revealed a significant gap: the lack of standardized methods for managing offchain domains. By establishing a standardized offchain resolver implementation and user flow, we can ensure a consistent approach to offchain domain management, enabling applications that support this EIP flow to integrate this feature and enhance user experience seamlessly, increasing scalability and providing cost-effective solutions. - -## Specification - -### Database Implementation Considerations - -All supported offchain writing calls MUST rely on EIP-5559, specifying the custom error `StorageHandledByOffChainDatabase`. This error MUST return all required values to implement the EIP-712 signature, ensuring domain ownership. - -### L2 Implementation Considerations - -Following EIP-5559, this specification MUST support implementation on Layer 2 solutions. The specification describes the writing strategy for L2 relying on the custom error `StorageHandledByL2` with the following arguments: - -1. **Chain ID**: the chain ID of the L2 network where the contract is deployed. -2. **Contract Address**: the address of the contract on the L2 network. - -### Subdomain registering - -**Register parameters** - -As the initial step in registering an offchain subdomain, the `registerParams` function has been implemented to support a wide variety of use cases. This function plays a crucial role in creating a flexible and extensible offchain subdomain registration system. - -Given that this step relies on the CCIP-Read standard, this interface SHALL only be implemented by the resolver deployed to Ethereum since it won't be gathering the data through the function call on layer 2, but by directly accessing the storage layout of the given contract. - -The function has the following signature: - -```solidity -function registerParams( - bytes calldata name, - uint256 duration -) - external - view - returns (uint256 price, uint256 commitTime, bytes extraData); -``` - -Parameters: - -- `name`: DNS-encoded name to be registered -- `duration`: The duration in seconds for the registration - -Return: - -- `price`: the amount of ETH charger per second -- `commitTime`: the amount of time the commit should wait before being revealed -- `extraData`: any given structure in an ABI encoded format - -Since that CCIP-Read relies on storage direct access, the L2 contract is unable to run any function before sending the data. Therefore, any required logic has to be run on the function callback using the data gathered from the L2. - -Sample of callback function used to validate and handle the response returned by the L2 contract: - -```solidity -function registerParamsCallback( - bytes[] memory values, - bytes memory extraData -) - public - pure - returns (uint256, uint256, bytes) -{ - return abi.decode(values, (uint256, uint256, bytes)); -} -``` - -**Register subdomain** - -Aiming to integrate with the already existing interface of domain registration, the register function MUST have the following signature: - -```solidity -function register( - bytes32 parentNode, - string calldata label, - address owner, - uint256 duration, - bytes32 secret, - address resolver, - bytes[] calldata data, - bool reverseRecord, - uint16 fuses, - bytes memory extraData -) external payable; -``` - -Parameters: - -- `parentNode`: namehash of the parent domain -- `label`: DNS-encoded name to be registered -- `owner`: subdomain owner's address -- `duration`: the duration in miliseconds of the registration -- `secret`: random seed to be used for commit/reveal -- `resolver`: the address of the resolver to set for this name. -- `data`: multicallable data bytes for setting records in the associated resolver upon registration -- `fuses`: the nameWrapper fuses to set for this name -- `extraData`: any additional data (e.g. signatures from an external source) - -Behavior: - -- When implemented by the resolver deployed to the Ethereum network it MUST revert with the respective error, providing necessary data for offchain processing. -- When implemented by the contract responsible for issuing subdomains on the given layer 2 it MUST register the subdomain. -- Both implementations should include appropriate access controls and emit events for transparency. - -### Transfer Domain - -The interface for enabling domain transfers MUST be implemented by both the resolver deployed to Ethereum and the contract responsible for managing domains deployed on the layer 2. The implementation differs between L1 and L2 as follows: - -1. **L1 Resolver**: MUST revert with the respective error described by EIP-5559, indicating that the actual transfer should occur elsewhere. -2. **L2 Contract**: MUST handle the actual domain transfer operation. - -The transfer function MUST have the following signature: - -```solidity -function transfer(bytes32 node, address to) external; -``` - -With the arguments being: - -1. `node`: a valid ENS node (namehash) -2. `to`: the Ethereum address to receive the domain - -Security Considerations - -- The function SHOULD include appropriate access controls to ensure only authorized parties (e.g., the current owner) can initiate transfers. -- Implementations should consider emitting events to log transfer operations for transparency and off-chain tracking. - -### Set records `multicall` - -This function MUST be implemented to support batch operations on ENS records, providing a gas-efficient and convenient way to update multiple aspects of a domain or subdomain simultaneously. - -The `multicall` function MUST have the following signature: - -```solidity -function multicall(bytes[] calldata data) external returns (bytes[] memory); -``` - -Parameters - -- `data`: An array of bytes, where each element represents an encoded function call. - -Behavior: - -- The L1 contract should use EIP-5559 to redirect the `multicall` operation to the appropriate off-chain or L2 system. -- The off-chain or L2 implementation should process the batch operations and update the records accordingly. -- Events should be emitted to allow off-chain indexers to track changes and maintain consistency with on-chain state. - -## Rationale - -The proposed Offchain Resolver standardizes the management of offchain domains within the ENS ecosystem. By leveraging EIP-5559 for offchain writing and maintaining compatibility with existing ENS components, this proposal ensures a seamless integration of offchain domain management into current ENS workflows. - -The use of reverts with custom errors allows for a consistent handling of offchain requests, while the implementation of standard ENS resolver functions ensures compatibility with existing ENS tools and interfaces. - -### Architecture - -The proposed flow is designed to make the Resolver deployed on the L1 responsible for redirecting the requests to the given offchain storage, enabling the communication of any dapps (e.g. ENS dapp) in a standard way. - -![L2 subdomain registering](./images/l2.register.png) - -L2 subdomain registering - -![Database subdomain registering](./images/db.register.png) - -Database subdomain registering - -## Backwards Compatibility - -This ENSIP introduces new functionality relying on an mechanism similar to what is being used on the CCIP-Read standard. However, it requires the EIP-5559 to change its behavior from having a intermediary structure to revert with the full `msg.data` provided by the client. This change is described on Appendix 1 and it's being discussed in the [following issue](https://ethereum-magicians.org/t/eip-5559-cross-chain-write-deferral-protocol/10576/13). - -## Reference Implementation - -A reference implementation of the Offchain Resolver is provided in Appendix 1. This implementation includes the core functions `registerParams`, `register`, `transfer`, and `multicall`, along with the necessary error handling for offchain storage. - -For L2 implementation, a separate reference implementation will be provided, showcasing: - -1. Integration with a popular L2 solution (e.g., Optimism or Arbitrum) -2. Implementation of `StorageHandledByL2` error handling -3. Integration with existing L2 domain issuers such as Base, Linea and Namespace. - -These reference implementations will be made available in a separate repository. - -## Security Considerations - -### Database - -1. The authentication logic for domain ownership is shifted entirely to the signing step performed by the Client. Implementations MUST ensure robust signature verification to prevent unauthorized access or modifications. -2. The Gateway that receives redirected calls is responsible for ownership validation. Proper security measures MUST be implemented in the Gateway to prevent unauthorized actions. -3. The use of EIP-712 signatures for authentication provides a secure method for verifying domain ownership. However, implementers SHOULD be aware of potential signature replay attacks and implement appropriate mitigations. -4. The offchain storage of domain information introduces potential risks related to data availability and integrity. Implementers SHOULD consider redundancy and data verification mechanisms to mitigate these risks. -5. The `multicall` function allows for batch operations, which could potentially be used for denial-of-service attacks if not properly rate-limited or gas-optimized. Implementations SHOULD include appropriate safeguards against such attacks. - -### L2 - -1. **L2 Security Model**: Implementations on L2 MUST consider the security model of the chosen L2 solution. This includes understanding the dispute resolution mechanisms, data availability guarantees, and potential vulnerabilities specific to the L2 platform. -2. **Cross-layer Attacks**: Implementers MUST be aware of potential attack vectors that could arise from cross-layer interactions. Proper validation and synchronization mechanisms SHOULD be implemented to prevent exploitation of differences between L1 and L2 states. -3. **L2 Exits**: For L2 implementations, secure exit mechanisms MUST be provided to allow users to withdraw their domain ownership and data back to L1 in case of L2 failure or other emergencies. -4. **Data Availability Challenges**: L2 implementations MUST have robust mechanisms to ensure that crucial ENS data remains available and verifiable on L1, even in scenarios where the L2 network faces challenges or downtime. - -Further security analysis and auditing are RECOMMENDED before deploying this system in a production environment, with special attention given to the unique security considerations of both database and L2 implementations. - -## Appendix - -### Appendix 1: Offchain Storage Implementation with EIP-5559 changes - -Although EIP-5559 is still under community discussion, it would be a huge improvement if the parameters were encoded as native bytes format, changing the `parameters` struct to the encoded calldata in `bytes` format and removing the type cast library from the contract. The `StorageHandledByOffChainDatabase` revert implementation as specified by EIP-5559: - -```solidity -// current IWriteDeferral implementation -struct messageData { - bytes4 functionSelector; - address sender; - parameter[] parameters; - uint256 expirationTimestamp; -} - -// proposed IWriteDeferral implementation -struct messageData { - bytes callData; // encoded version of function signature and its arguments - address sender; - uint256 expirationTimestamp; -} -``` - -```solidity -/** - * @notice Builds a StorageHandledByOffChainDatabase error. - * @param params The offChainDatabaseParamters used to build the corresponding mutation action. - */ -function _offChainStorage(IWriteDeferral.parameter[] memory params) - private - view -{ - revert StorageHandledByOffChainDatabase( - IWriteDeferral.domainData({ - name: _WRITE_DEFERRAL_DOMAIN_NAME, - version: _WRITE_DEFERRAL_DOMAIN_VERSION, - chainId: _CHAIN_ID, - verifyingContract: address(this) - }), - gatewayUrl, - IWriteDeferral.messageData({ - callData: msg.data, - sender: msg.sender, - expirationTimestamp: block.timestamp - + gatewayDatabaseTimeoutDuration - }) - ); - -``` - -### Appendix 2: CCIP-Store - -Even though the recently proposed offchain writing strategy specified in [EIP-7700](https://eips.ethereum.org/EIPS/eip-7700#database-router-storageroutedtodatabase) handles the signature step differently, the current `register` signature is still a fully compliant way of registering a new domain. - -### Appendix 3: IOffchainResolver interface - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - -interface OffchainRegister { - - /** - * Forwards the registering of a domain to the L2 contracts - * @param parentNode namehash of the parent domain - * @param label The DNS-encoded name to resolve. - * @param owner Owner of the domain - * @param duration duration The duration in seconds of the registration. - * @param resolver The address of the resolver to set for this name. - * @param data Multicallable data bytes for setting records in the associated resolver upon reigstration. - * @param fuses The fuses to set for this name. - * @param extraData any encoded additional data - */ - function register( - bytes32 parentNode, - string calldata label, - address owner, - uint256 duration, - bytes32 secret, - address resolver, - bytes[] calldata data, - bool reverseRecord, - uint16 fuses, - bytes memory extraData - ) - external - payable; - -} - -interface OffchainRegisterParams { - - /** - * @notice Returns the registration parameters for a given name and duration - * @param name The DNS-encoded name to query - * @param duration The duration in seconds for the registration - * @return price The price of the registration in wei per second - * @return commitTime the amount of time the commit should wait before being revealed - * @return extraData any given structure in an ABI encoded format - */ - function registerParams( - bytes memory name, - uint256 duration - ) - external - view - returns (uint256 price, uint256 commitTime, bytes memory extraData); - -} - -interface OffchainMulticallable { - - /** - * @notice Executes multiple calls in a single transaction. - * @param data An array of encoded function call data. - */ - function multicall(bytes[] calldata data) - external - returns (bytes[] memory); - -} - -interface OffchainCommitable { - - /** - * @notice produces the commit hash from the register calldata - * @returns the hash of the commit to be used - */ - function makeCommitment( - string calldata name, - address owner, - uint256 duration, - bytes32 secret, - address resolver, - bytes[] calldata data, - bool reverseRecord, - uint16 fuses, - bytes memory extraData - ) external pure returns (bytes32); - - /** - * @notice Commits the register callData to prevent frontrunning. - * @param commitment hash of the register callData - */ - function commit(bytes32 commitment) external; - -} -``` diff --git a/ensips/ccip-write/images/db.register.png b/ensips/ccip-write/images/db.register.png deleted file mode 100644 index 2e2620cc7c092f4695e8092bb6cc3e51ae9b6879..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35456 zcmcfp1yEe;(mf6j5?n$cxVr`o?(Xgm!8HU4uEE`dOM(V>_u%gC?(XspvWv<^mupF{9g~v96N^} zxr_R-Xi|$2x+-tf&eCCRlX{fgoa&mIHR7+X5BZ9{78kcZ;Z-nO>)U6@+t(-54D-xb zWG6i_UL!vGJT3G5{pIQ5YRTjOgToUS+$!%=hwpeNU#XMD=2%2tNR#3MwoWN=wyuXF z!z;v0Zi}`9{-bv{s$-iKU~u8ag28l z`PAxhyu=;7Se?u&cn4eAMK2qre@O}>6!nUSTMWTwUy0;ab<3bfk%}W?ostt`bcV&C z_Z#g926sc*g9ps7&;PN^{}}iG2b2F7SpDbad4SfcWl9@(D2OA|@JR`YEIp>=v(_GJ zkO_#lArM!>&FUbH*jkEtxvf*Dd&i|J&Nye(o$tXbn!Kd^E0Ax47 zQa~sHP`%*Xmdx>D#DqB+_~p=!$MAusR*$-z?%E#=q$c8B6Ai$m59d4wAB`*CEsrJas-jJ*`}!-Sa>$)p?XX#9o6wYCQx$4qY7$cx0t*KAk*8Jz3sa9|WHb zoi<-S^*`l4WZyvnukm?Ayp}m@egMAp^mKML_4G8~e5CXAboYdiRs($N+_M&^q@EOk zG2RS$gcL3fLdJ)zHF~+{*x;JM6@&ZFH#!~s2;dS@$GEaxBrg*V7-x+uhD`sD)9-t! z^ql&(G7z8qzMHdOEvbL9&!?e?XK4||Eqc{@2d9JggGX80U{`98Bl$Dyq0RhSZI^A> z7S4Y@)lQxS-a4*6(&`Q7sy3)i#BBkYJm@m<{jxuP(cG7@>mB!p4nVgl(z{b;OQO)P zA>RAKv1A}Ocs`G7shlkW2c2jWQ9fy-37&i7pz;Zx|I0mr7GW3wvYFdwO<@Ba)BzUH zR)&v{GxSniJZ#fBax=|4G`DRscXiEocf->6 z6*XE{=_xn==3|EuU!#DLaO{wy{4Wmv%jn;tkp{!v;^ukTt@-cbQ*M_x4f~JHu-FFg zPyR<_@IZh)yRLNRPv|gzFk(aSOQKfzZ7Y<9BX@+3a!gK&2k^}(?Tr5k5TDZTKrX>{ z+mwJ8bdH@c#ZKt0is+DW@&7ovEk8Sxw|d!uw%Lqp&?nU}wl{JSrwYM?e$%)9TzTvTmMtAQyViZmTW^n=03;r~bKy_|pAiuDe`F9O%F5&wgbXoJ4zSL(1Xot~_5Z}Io= zwi0@cmVzyXL~DB@8Zn#~!J}|j6AC|3+3Mq81=1lZdS5`7s`Wx=Y#GDzKj1%-Kiv6J z(-jfNrU(SbfO|hm>%!j7qT8nyxj*%GO-85Octuz81ulL~s`BHcslI??QRl<$?76WWoFxxX9pFN@YR@`GO1JEQ2Swc37!(T4r@tA z+3ny){#BPves4xd%nfFpq9@;*%o^tWNKdIn-&!Z@j|4-SppjNzixElFM0~1og%n$K z5d-jJ&33tRcXW8;v^#iSR~O63&uUbPU1u*6(-_XVHbTRbHJTgHwGhCozV z5m1EL6XEEb)+tb*q$0U*NtGmq;pCL=f|i@%BtltQricifxjob>^@geLnkboEC)+0I zIUHNsGfC1sGD|&}PkQT*qez&FN*}6bTVfp(u zybdhd9m$rz=xWUr%kc^Z1$OYb={fu0_!;TD*IEo4w&Y<}l-Y?o(HXOu@q9pl+}5eO zf0?j9)8j>p%JSpla^O4kU+EkIn(JQ&2yS>PUN3pZ1_dW!6 zZP=UPsly3R_c;fj5x);PjcC63e9TMk8r(`lWSJ)X#T zz|0Nln1{%qKhw8k$4THCawi$}3tP^vk#WiM?Hj5_t`-+BTiGnUSOftd2V_F_vycIx zX=6-;RXsi-8?!nlR8@QeqJ?F;Yt%LOfhxB?brR7Z#yPWff2aG+m8%yb?b4B=Fijn2 zevIjdw0VF4lE}=Uud91Oz0W@6)Z+9NaR0i|EV^`JNhN>xMF~1F@U|s*I(pG}x*Vx? z4Rj=bl`j_u%Jj8tw8j5GmOZwpgMBD}XVqGEJ93Htn`TekI~ z4{00L$h->t*tIV|M%~@itpXi}dOr|#d;LqJUny~Vjh>nGH)eEqd^znRcv!(I!JkP3 z7FJj8ZD4Rf`^g+ttKTpMbSqa0Xv1^B9nLr2 zXS>MG0dI@bb&gR%ZiD+^02QxffWqWU%QtN5hzCBo+B*kS3~^?GD(CW+UZEwIwi(a$ zoEu1&S5;ik0r`ECa42*ya3a*GVpX7{eEDqwi3*Ax)62%m-@gUM9YVRB?iofb=h-m+ zpZ>Wz_Sh5@O=9mlHlqbibBR&za<#*z!p)C$4Rvh+jkAZMjqeqLI}_* z+^|8dF_wA$KML8)gOVW z`@>gxKBzx@`yYifHq{BWI=VvXnq_?Xe02Ufdi9pRRm zTM7q{Y^STqrUe?1<62WU+mKwI!+Es5Eka>Z_%XU}j^*M>1ctTrhA?t31}YZG=qt1; z0kCFXNHXG0r2TledAwbR<ZoB2==1+|m!)S+7WO+;H}=LC#=Aok@%G_BG%2>zKd9Z79U_+C zPJ?Lw#zrreAwPR@A98};sN%ixo=F3cSdxM$1#C1Mo{pn-95T4x=>Dl;ZFs^bkbt`N zJi(!T*0?;`9OOc0N93Pi`UlUrxrO?dgigP<7y`P38_(`9-UWh(PPP?DiVZFco|i2! zzo_vqHwC1P0*j|EcV6)Oj2}E2LK}=A^Ye1@r@v#_snh70(V9mSjDI2e_dK5G&wyJ1 z(!5=@#m8)2>2N&+$V|z9NbqW7@1-9BIxhb~>yeD6d;LTFE%15@WA|~rC$VP?BZp#Z zUpI=0MHRLFFACD_lsd=zZsA{5WrXX-?3Z(~7hdf&mgiSi&G`?ia{-UtR7Zl~4`s?t z2Zr^mVTZ5U)BmI0e@NS|PRTw3w?7pxdj2q4%8J_&^{ch3=ib1B?{8hlpdHoPh4I#s zE_c&qSN$Jl@z31$mV9>N#Z~cnww~`ZY+VfA{U&IWKmQ_=8n<4<`#(JXtFXST;G+Fp zZQMLtNv&FZjNA(z)Q<1IJr~@+*7t*6L;kr2tin|QmA}IOQYb?NZ(gKlr!wJV%uc{_ zu$MUgrWrzTg6Tt(wo3jFu8VYf))>!hpV0Zj(LT>2=NIAIm+WPld*7n`5S&Hax#bnmC6{ycTf3LF7jY^XL{7dP@11{eFFOoidN@7rwRAHsY;2N%Q z1-ILze3zUWhG{XS&bf{iq~b#n1JK>eFO}ulBhLZ@n5Jp@dgRX%;3do3VQBCn{m+Kn zzw%wWf%BtaD9Op44rFUiw_2kO_B|Jqm(H$_BpJj<#;;*_10X4i*SQx4vAismcqQbj zYJwvFiRTA54$SpmHvm>L_k#PoU+z}|x_06Ac3ncHMr0-cTPe3F;I1J;Ux~~Nw9ZbJ zBZyM_A2-<>?v&KiH53aq#F@H84bz)sObb%KlwaCcN8DZS{I+f0>HhPnR*0)9<;;64 zSH-J!`NomuhOIZOM4657ZbhoSL;_#JkSS8_t#}py64c-AT}c`uZ)7we#thIb{?i=^ z&lZ&DuD9sjTAPyR?cE&i-|zth69n{2!Ffq^KNRQI0WLcdw~#nbN`IO6R!Fsv5UnUp z$Dqza8+{TO0P0)#KUHt?$vVDuUGXcQ%W}Fq|K$-HU}`uV(3X;cNIwd7hy4+viFt`M;2(%Cp2j`S>F2 z7fo*#WroI>yONxne&>rS>nld;XNXslprkrOV0WB^_W$96u^f-wUff>?y~&aeoESJu z*6aP*_Wc?l>~CSdK>Gm5rFSdO5&yGm10o%r12xmDin%D{7S8_;P*7|PT{Y6qvRSs0 zHCFpLniA0^TBxj5P78Wj=0IJ?34LN27lhqJAp=H-g3Wbz@SJeX0g43wEEazW=NHNN zuk;R^OLiplW~a@W=xh70>4&vbNpf{MP+k@{mLC3HEB$gJkS5&r9w@%*J(m*PnZI25 zJE^bL+@JiW)n1DIpRyO2t8{J8MT4sh?cdKpY;M;6c_cE>e)t0P@a~5HKyZI9wf6rn z5&VgU=Ph(?h5Xg({F-*KAlAP0KRkq|314=Uan_jlAO&9L8kNx7QOooEcmMOa;taAt z$MV0-Gp_HxqSvC{{Z$womL~@*g$G^S#6y!WZ)3684lXEJ2wsr>b?WqmdjG+cf13XB z4%k)vEhM!yKxO@0H^39>|JE=4-e_s@0UcMYH;JxZ%5Rlu^@wyf*4*U|>5*vV zoLC3S@zP0)a|a%Dt*M*ael!Yt)*5X|H2-EehGjknh45=&hlK#e9?)%qEcWZ-pez9A(`ura$K9n-#yc}ZkOSy?0tJLO=PP{92~lnn1^wo!5TZ(s)7P7}uGVR374CUqC;#44 zJd23uYVU_;5rK5h-NXe=;aZ;z_0AEsnU09U*;)I2Q_4U>WuNK*mrq)p*M+pzg8!GF z`aGjFH?rPpa4oNUY*Zzn(&%X`JDZ>sy%}`~E+(Z@UP_**ZVpNIYc6TQXkXd)dXe5FP?t5cUHBmBpN(G?%`UOhGjI?d6Pz61 zc<^qnyh{69qJoHYAK)^{Ff(IBsT$(90t;YI#RrC)reolZm;Ng)hQP zk8Q?{tC%4V_k6^67cQE9vD5LNbjb7RqPiGDjjGSl(UFwEXtg9Wth_aKv~WHa?aHDJ zyJG?LQar*Ns>WrI&JB7PQLdtwQPI>(sOHzukPs4XE2x5>v)|eBC$MllC*5yCEu5Zz zUMsmKIrJ=^F6{@O?04it-?XY~VX|kDNsIg6bm=qxev_$x=(1naDnBMIQd98cF`WOR zZU3(|Y{1CzAwP|cV9*Q=Ut98@u)nZHGy;#Z&q3E~2nFD98(0LQfe#QcTx?4|cUA|Z z{O?ohzmH*>aUYnL~2IPUVdk@55NyBhu#zw3@$$bR9={+>nYKl$R1 z)4wK2e>89}I(`Z9SnRvM9xz&>Fb?u`*}vhkv;5$paDHkT2?1FR1_nCCUSZ|0{}kqDRNYqfXj*5V44&>T|^OQd3vq=b7q1>B`?v*)As1 z^THs$37bZcRi1TU?^d#jez`!u61anq&&wKXmEngcSsiwuRy@%h0zO+QWEDGq%3$Qw z+T_|&aw1!*ffz+jETXL4?D~cv4B~_y^hz9ZuH8$Zj&zdwf=AR9PQM$$ks`v{_6G+- zy7e)$jV$mnOf?u(a6K@w+d^1PgGV3x%~_)UgR=VrYlfj}D{4^E<5Dehn*9#2&Rw!s zlyAPTN>YJjx1+oJCq+O_PU`nJ4JQzhzVcW4di|O2J$p!03m$*g=%YqEl6$uGubl5F z2DVBM3YgD5=igZ1Z>K(n52x4+hTG5lTNc%xJ6~&-`03pe_>A&{Xw#J9;xEAdKG^*o z!hb^Jc@}4DUXS65H?X|4Q}X<%5dF?O7YfC|x%|Q;`|Vyl3;Cl2XEHfLlQ-aQ~UGfbbkAJ&b*V_6F%MyFcN5ZUPv>QO@45YKp$g#DF1JdD*t>0 zbTftiYp4_Y?mf>lP5*nH@^?4-XGL-Hy_y^) z4XvRpvG#aiV_@)mI#ziSa<}t-c-jFh6{j6TKM@H#++Sh4f%QrDA6AZAU8(P~xD?uB zHBQ!rF79awAK4du`&>6ASHEDMyYSyGm~%u9^cOSCBX!lp8?2JY3mYfvS(ox*8#ql& zrR04YHD#N_I0gXT5|Gv4Z+eypxD ztQ{>Z_B$ONe~Kzk@7{r)Ds9Fn=#j3STy9*9K&Ux)9S}#XU3XUyA06v{vrA)dz`NfL zmRfC%r4hKgS!X75>DQS?ms%2$k4VQEcnM+OINA1m zf7r&L1~0^qmm-pdIHTJ3uPjl$SJM81jfWL(Ew%yLODMjNpk2C((`8dE z($DKOyO#C#4caD+pYSZ>!n@>BG;IMp;%6eWWm^wh&Dfhr?~OEFcygc-?HEkY;So6ApmTyGix+aBkl}PBPIi2y74QXcsFV z)|N~ohH1So&m7koUnLX!AkqZYF0Zz|Aj?RC>OYm!$QJ+TaOBL6QV!`3=`ki3@VbPB zS=j{o71%qm5i7zbJv`w!y5)GNftYSttTR^Bs@ENHQ^fw-<(h$0LGLF`MBo?V|W@Y4JnLCv@$lWpDye z5gZkI3`X)#jU`+{r|(}uor3gX(C#};*5IZ}JGYGlDtfDc>};EkabJc4oDo3%3I*~% zg*2gj3YQCR!9ZG<&&2CWnu$x2{gj4BZots*K`4medLy?|;ic)h&M@A}rAJ#dfSvcj zE0|0Jl!1&)*+lF!mnj4 z@x;dIZxRMW0g_c+VXE)FMW``biQ?dOBl{pqL^r&HmRSU8#^m?-w3M&d?<`!d+&(fa z*d#?XwR^Vk5SNAF-SnzsRy=-Cq)uY^*MIc*0czbz!Ug#w316S-dw9e*N;xvaT3H&Z4 z@Y|F{4f!jyw4U%0?WqE(QUhV4?wqX?4MyN7_Ol*l)vYrb%r zD3_dwKC*Yb{ID_=uR13`DVa|PaUIy(O`<@Bfr(#~QVhM_(^p8Pn(WBoR4ELUb}ZI- zlYF9GMZ&h)Xd!O<(}l6k@kbc+7**P<%MRDI2a_%k7nv0Z4hc0ATQ>*3!OP0n-DPxD zgLK)UBU7*d>Bd3P&K&d*sM;(=8_9hal83&joi)V4Hdqlmkl-B85n=ckV#c&%Tyg^$&l$3ymTJ*} zcOkZ~55>)VG$(SK>53*#+KLvHvBY^%8}VfVBxJ|6{@cDNGYA@0?`cf8D3m*$dXzT~g zXDK!3l@kvuHQ2X>$xwOa0>Mo8?~QNcRW8k-EXa^HAS9|7L39c`Oy3c$W}2HPO+vZh zZv^$A?$VRIV^2W#S_(}Y8sGCck5JanX1t~CZ4?| zF;PgBzIs)^PKdniPE;hn9*gF)wH&nL3l~&c%%`NDn#Q5{`9uyQVek7YmPKMt51m0~ zi}|3xcn_(nvkl5kG-teeELd%xDvs^o_tYv4qSyTP)OUPGaj+x{;k@0XTtXE3uS-Rd za*OmQe^Rzcju@CqhEy<05NL7i)bV4^i@hogY$5Hllh-G~eXzOQz^HG*#F8_%Q6xKG zyr+#RQ|H*-=_^n9j$2?W*B1|V*5hB7TJgm&vCqZoD8^HB;S7Sz6jBEMO-#$@J5Y+o ze2RRvm{%!%A?!5TM^yshG^AJUGTrM6d{aU=WNb`P9~ufrnlq{07>MB)DsvyAqK}Ck z26LA%4fSgT(Hvm32iIJl08JDF!ig>gjgO;mS0VGydA)3q*ANow7UTuFzN^Xo(u3*d+X@~grh4lAU^~r8VCZb8p@Pn{=HurbdIKHl0Eow^(&N@7=#XJxOPN+)k zOQk~)S5B@nNIl8?iL5MOhd(b5oq2qpoh#4q9$MuH`;ls=cwRy}c)XN>19?O&H{hGj z^kde}_$Z!6=|;=#?Tq#@u1GJlBBT{*%FN}k3fr}KFLfSSFfSRN<6Gl)ifIpY>=b4u zGUkEPST>j)!Ef`|&{Lo`ob8Uh-J#$G@A~`%a}4-AS0JSuGPGS1VUZFQ%IfVmwY^uV zpSGMU3*FIjqH$(WvtV16mlMNpvpW0xkh`a*nWk%_ZtUeZxXJJG0oYE_-y>Msi%-MZ zxd&|D=Xoz&E}1#RLmeY{Gl)Dnw+ah3ZT(=`C6bGnC+2^zBq+FNI!w|F>ETxy`?a$D zb9g1%I=@YQO#vV`&0S# zE=;04FU%>rXo(}=!&-}?GX8r8u#{CXl*%krE)pnG;VVT7tha7#SHSOBLMmY&?kM0d z&B}bQ-#hM~K81_xq3B<%jFeA;mh+Xy2>aBj zWJ4@FIoTU22-vCj6$^s(CFQ3FLj$MkmK`=OXJs9iUpt;?yf_%8V{T##qZCW(L>s+b zdW%|ESz%&yu{QMNC2=e!_@dnXuIMw89lYICev&JYAg`Zq!L!)3&1|;%uZk()ICu2Y zvHF~M3JMWtlcLmR^M25^wE(*JQ|M7bLE0IK3Rv8!g={Z#fUCZ;q=NY8MN`s)i&=FG z4&LygFJ+jQEBT1-$wrvE6}QMZ-yyf6_Hf4bMbMq40`4C)MqwtW+zS)N-w3JCyi+o9CZF*Tz^VlQu1obTnh~3lhw&gqQFNA7C zDl|AOl^qIM#}eIPgL>^ z4wljSIzhI4D!C+NA!xgkEzM!qpAeW%KITB!7iy`&5YGkQiSk&?xi9LU35|2$c`cv% zroww2R)z}cd}r0osi;@zJ-&p!9JUM&c;$fz%79maWVrcZ?U*`NwF`nF6xrIRt*au{ z9a?$j!0kPMjHy;URt{>mbN~)TC|ex7UEV%&gvYzykku}YPrdo3-`${lqb76wc@>>A zo6dX^=}z4Ix7wVwcD4yAbFf+|*USOmschE((>Lc{XS0rxh8zBV%u5-?HQc=|-{2HK zq~ES8SYb{K>g*^61n!7j$rw^@78ItPxb1$gV?LkkTxeNOJ>7-+x@^mYt&&)PE}_;V zvzmcUR1j&OK{X?jda2vH45cqWr(RFs!}2(fA!K|;*4W(MA?yveRWq{FZJZAlV`fyD zOeYztH;3({OgmS2)u+WmFIRrjx%uIZDzZc`hDbb8;@0#Ag%R3ePt599QbKnH_#(`e z*A-zFH7(T0?Wa(LIK0>^zDrJ@WDcDs{FbS``ps@J=~V^yXr~|&@{4^RDe;A0!|H0A&Ke`>NZ&GKKu4cYFMhmU2oAv9 z{#tlg_kCYWX7Z=`*<|*@IN^GK_hO2SHmE4>wbKAz^fTYc}3imnQJ)8@^*az(=2z_gbT)FOyWsV;nMI*yh6 z*}C&!DQXx(Q?55u>8ekf0-DCCVZnLh-bCrggGBBXgGAL%;mvR!O){jHn*b_4mbLM? zT3B#w-6VE;<-IFxqN|k0#P)!U+{sK?6h4$`Q9WLPYVY^+$URMiY=<}y^J7EEMBcin z-G|NBWB6K=Yr_nc1{Im0DI*yns=8m~IG?#%GJ?RrHr^d+GXys@ zMzbLo7XnqqYIzg)W5F}sv!RED?{4<%ECA}BqjK@)6n&({dM$tg0U(`4wz(2V4dx-ob0HmrAm%>&jXjNvS|jRexct*_`)a zJONeNQESE~S9#m0;vEHu-9dtOO{m3x6$GJV|0PuyAB%EpiU$OgA+Ve4k@AbAR~PBc ztc}jeDOPW?e;ILB4u!&E@rvKMcoKvX_|E-f?Jb1SJ28oEB=EEDM1LQ`SN$B4R%Skp zZ_mVWc^Htm5 zkPy9Bk<2`~cO7W&Q~~?hQ{yr8=;6iL^I%$=?a_aY$KN;!&MbvzPN2mVQWA;DEbPiS zp$`j{G-I}93A6TALwr%8NKOu#LQhVkb;I1ATR*wL-zrPu;H^M(G4>Nqfo&(u$Ahft zwgpwp@F068rOH+zNvQW${iA5~qi#dTD6E8A1JlO*H9?^p$#OBn5smvI=G`W8pSE;i z@(HmgX$!34WJ;SdA@W*NcB=|PhY;G$`3Zf}jesbC~AC{HK$ z!Y#rvA>PS-CaTVn8|T`ymr}O`Z~queCr@D!AFk;Icw-Ois3_aFO}EhSL%2qt;Wgt) z;#y>`TOxyKf-RiV=GiOGnWs`^nA2IzgijN&WV3jvYvLdKM#eSIAR84T;;az~v1nBT zD68QhdiI4q`NJ(Qt>8kvsUSi;OacS~Uhx3br{D>bM3=R%PwzxP!3vQcasyutU+=67B1!`TssA55IHZ{?A|dUIN8vZ()Qy@)21*K{8}=so$~&gdqIgXS zeJPbqa<+NJG8H5C^6#VuG$U<$PgqS=6}{E`9SAd6TbB?*9Z`@5EHw`nYhb&+3tO=0 z+xp6q3NsKaZ^WTB$ z^DSYw;tt!6n$3z>A1LZ36W?fh$bl$3oogFt;)Jr<(nD^4-Nmr)+qI6-ly`jBJW~0T z7JwA~$%|c5M)QJ0+K#hE8Pdw3m0x5}xH}r_09MbtuYwsuCAhRE#==}NM%xDZJ)$`k zNf)I5(Mnu@lahcJCQ`SR9qMO@Ir%cPRe(xXER`=*>wg@&qA- z)_N#5{ERY7Zjzk$6=)&&m{WfxWkQ?eBl?YSXPzmp%1Qg~Td=b6cLiFOoDtA2baBcL zH!7$;x2xpRu2%=;VCr^{=_6)IX$?|cr<`kuTx6wvIrPM~wd(0Mpd`Y>4rc(ZtRN{t zhajqvnP!OUxp>Hw4rZrDb6){ z>IAP&V(z6m zx`QD{29mCgQGK@PZrE?8QXyf7oKN7w-(&rR|M{o*i~u=G^0A!4CsH+6&l^d4WSM&8 z>(_b<&04?)zKvdPYL|pH~V%W zE8ZmQd>de&E{h|*13#Q?d`w4qSSA8v7_UFn_$)wRO`_0V`TA^S(KeO1bw`xA38ZakzaMojNAqu;a$FhyNXvuZq?p&l_KKSqW@q>jhs(&y;c~Lhf}te zoit0mzo8iN4Els`T?{?x3QOD;1j=Hk#VUz{5NJ|I=NtOD!0!ixE}nH2yjp!~a%+De zINWw}lPjkRKFES#9`{r!A3_v0`9^r&Po3Hh!7m3g_yRuzLGgxHkqRp@~ho%cxee?D$U0rA1ple=b zM(B%mC#>YWk?*OiH+qnZp_ASZH|Mc>n#ZGeXy@_Sl@gNEk*fXnAS+#6CYwb%&HXw( z6(qk-RiS>R*Yg`j zEh_1b@qEy{!FB0g-Mg%bhRm)F{VU1Q-$7`Cy!PAHIk*phyh?UFzYHQdxqdaZRzV)$ zb$}wVwr4$wg+Jixu)!!iyPk-RpLc<6%&#O#wW>jsTWnV}y&Hh7Gd~zQJ*;qQe@XU* zlkzk(Ysqw;UN!UB?XgvM=dIfY9noi)vz^oNqS_T0nk?LfgA=Cm9i1gX*0_pk+#s_e*>KGZ|TR=f_dTB!tc zh}!qDY#(RhJS!KM^1QSG$!ID)Dt9h6FdG3y(DZn-*wAvI`)KEsHS@E1ze1BSY|Oc| z%Gtsbj4~`MKRXWEjt^zkVet-D;HcLrwQS5}3+wBs_#q|=h?pt>K;AZ`pna;-e7-Q%g`MBsK}1s)OJ+Pm6km}R52Sbr2P z?Z3Bm&GWJH6gwQpL;G4mL#Wr(tFe87)!YX&st1vw-}5}kN4dg8qYWLVeA>kjPaL%2z?cZ}ZapENZOvWnC!035nU!Csva_WmROHmlMrY=#$NWM@kQ4n5)0fA*;NjnF(pEbTcQ5qQZ8!sTzXs+0*gfo-Qxsx|JXxfyV;>7=$ z=oFc$jPpA|TX(E?+Hom-i`<;o0!r-P4WHCCa+H#m<$mV2GuNzu3kZ-VS==dnl+iFC}A4Dk)YSxt^*LzPw4jhUR8W|2BuAKJc4XUUM< z)DpCD5+SgUQ*OBe=?!E1x9;Pd7g;R`zUTFbj%DX1`ph#+M{y}NOtq8`aTekWqnv?X zlULH>+HXKbEM0+KP!Fm*l0HvwF(dZughwuM+(HnpcvCi_^M1so=i5pN#R41z`<+2b z(djmZ+W^ZoQy1Qy$V-pj4s6YZ^Ze#M`?-d%E8{!E>vU}zn3v|qlWasCW1xssus1=563Y3ByF1ltpF^K~L=ZE{`MM^ULQw9$zW zY)h+Q=M^)mM|-}YxdF^;+I8LAFUuGknI&Zsnj=VZV zp{4$pUYLtqDKz(s(xsN@UL)+4h|e?Msmo6ziqB~)^@`E~K) z^)lbRpfPM)cf?$lI4Uo05Yns~`9X&exP6XNeB2Fy`Dtwv9MrEr1p7$0S4YdFtJs5t z@CSL+-6%ETBPNThQy^!k%2CMO=Mbo+n`|d?cB>Q+KktVS_MM^mR_AjeKl}fFi1YR&N&KtwyC7c1ZkE6>j~1W({COg)OMLEZZ4*>_iO;s z5O!L}L~<7-a>H79wswiIRyWaE`rx5E!mZ}W`f46Htv{|Sf|M6$Er2-qmsu-d7+aPfc{=GOs1O>J*~Nif}ar;U~hR7oCP~ z^EKj7qUO{Yk(FB)NJ+Zvo5|mD+>%dzF^C*mCHu}XmesPzH*2hYzpgV`%Y+(NBCU8K0szt(9L#aiJ--W zYOb26+lvnOvaXe_l-TZZn2*R<_j^(Wx?*NWZy7^to6LLgrKRv~&76k1owv>g-_ls# zlv@E%*J$)3WGYY_PsHY8bo!uQyF76SZ~3NYznhy%sWbXEmt$Dfoy+2Oe{O+uN~UAX zZ|>w!!x-&6F;Nq{th`ZWW3)V*usfs)OS9FsmI^-ogtyTX%x@hbo$I0b)lI`U!MZ*o zMJR~IlMaK$Q9(yiFjgn((fmV)wpW*Zi7#_Mv4oP;+2j<|UYLZ~J)Xd|Up;DFe|Q#s zy&~>r*n!WDr+?M-y6WPXI|r+qwkY|6r89EUl5l!gGj$7{I1=2~y%oqgItAq0iot^B zZ*1M1Zl!|HpX&|OtbVwAV>|UP$RmQ^tc}oF~oh|$)3`>BkCxO&_7qAhGF9k%c)W&2 zApKLhP7b=U8~#UIGOFu$i-MR{2*uF!6G|qr<>7JK#lq!lML`Htljej2>qq;J zj%B*>-s2O%!1NtUtkPV;3LkMEcfq>)A)V@1*!;6nOd+l9>racvA^M-7iDrpUPpFK$ zKG6!A-b#txL}biwF=RP5-l!WQdlp0OS+M8gf`9VxNY6H7%X4rzZu-6mpeeXlj{+~> zPd+|C%OX@omdha~AxsR5FYIT{QcI=k*L6;(Z;Z6}_E-jCE(|s*ORAV`(z0v!z23bxzzK|g(9E!4c z={N{bVeM=!hthRWb^r7vXJ3a? zFtRQH0+Rd!ZeMN(nVPDE?QTAfybb2H(Y$MbF~g9N;d(ipxZXuWz|n5V&TXx5`asJJ zD1)kq7Dy^p$Wh#i^C>hUolI7XsVqlZ$VSWj=%R~rcKzGlzFB8k5zDjeTn|Z zHRL^n3$c<-#?n5uqG={UjP}Yg<>q4rnz1BpdgQZWF3LSAf-ToVFiiuMP1m}ED*Z6? zM|pT!7H|%%JKjzxZZ#;r^09Gz;T!^!B-n#~2+a$qmn{zQvh`riO}};A zeWeUjrN}&O4aYrhQqx&pBs8n#Z3OBLkf?#Ly-Ckn{ySAX zYldhy!RISD^BS{Q8^Py@t)5tI4sf0O& zJcSC$MI*pm0+6$??%1uTp~m&$L(599>3}3-jJ`maeMoJSlDKFnmWbRcY5|TxN885D z2MJ165z!Ag)~o?Z-vT5;nryfv-L-*@5dT>3=CgbLyIVB;I_aWAo&W%-cpabbj#-VG z&P8p=H)-y>^q=Ec_{tNiv-feq9gu8|(Rib@A5*@;)*oEn)OmIeV8AB!oGmwEGCpiy zIKcIwBsA9Ani2B80WJ!l=j45Ud9Q;HJR)a$^PZ32n{B1!gNV*x z$v6Xk=`E6|$7M;2`b9G`s7m&hycI|cra~#6uOjBi%a@Un`^m>JB6K4FuaT%3o5czA zJgY$oX1lXzo7-mkD@ND0&8w=$SoBPLay)Lfsgf}TArxTK{awjia=bdm(U461i4Voc z-6p%j>!uI*rRDNfu1lst_J<6UD2G1;v7*F0`Ja&Ik#`k%q7Ez^x?s_7h|F6qV>o3h zdH!hV{O*IVuvQSBg2cp3mtO_2X>w?HYbZzI36hOry@CHY7#4q$=?wt$seV;L`n+NM z-RC3NPfiHG6D#YVG}^X~V{f8Ve6kbGAby(jmBv=r3uiT`nRC&rI4THXoFu zl{!Z7dF0zXHN^?yQQWJqvTRr#Fe7hGf{OPA#4=sV1O{Yt>UmK!alUUR3aA~D zw7@h(002a(U48XLh@mUUv3|RQ{T&c%V}N3PLJXoB13hJ zB5Qo~b6$MfCo%6-DQOg6nWrNb-Xh+zn0qH?ll+|*0eGRzCDCv! z>LXRPj_An?JkX1)KU7NzhLnn1V_{c?-UM7qEly-Y6LryDnN}!PhOcj}a}0RkwE+Hq zMSTNsE9S)`6LFLZRIkHS?)=9#uls}eh9Q%2l} zln`2+wPxX}pIbB%4Im~}vudr_j5>y|kc6BvqnKE1;`86+z7jOmC;(ecv0>lukHTZj zTtR1bUrhhUpeojFoDv19!Xg@o4fQQ=Zi-Wi9oy-NRz@hHW#gwFZPdL2+N9n|@-r#4 zhoDRSVi7s}1Gdqla@$W&)xb0F>qZH0Ou|#`f`u(7BO|p5 zGW>H%wxXcc0w=BMo_h^DR;*kQYZ~G>s2CSOhh88alEHihHP9L>%D4bg=`Jl;9>@|?{`%pC zlo*fR`Q(e)Viile{^-K;qP-StVbcIdv$8b4a8i}+l^AhW5t3-~Qo6v|_r2a>A zn22poWCkrH^$h>yO8>7_n^XA9j(8Fz3E@&d8q!y@k}|k5cA!cba7gupmQ|Vh3O%hd z*Kik>N?txo#faKS*9(Ano`C&mAuKVEcv3=I^c`1R+rAMkP0bj`rDSGBOb%g~FZ3D) z(o(CfViKPp0H87eViR)Z5yirsjTclk&X8dR2m8plJUmj6Fg`Vbv%kwP$xUR^2QkSg zx}Y3rI{7G`Wed}Y>^5;nC-1p16vNxXLctfqG?LOJiz7Hb8j0M21{O^{*8Y6|Q?#($ zZ)T!vt-|P0?f{v}K0XW#J*G=BKshpWg9iB%53R1kR@8jk`2G_vlGc7kdVJCfm^LR- zDVc)r!W7m78`vo-u$WL#tF9$V4|c>|LGq4ocxSf7a;i``?epf@ULqYWFO{hu=s*Pl$Dd{bT|CCTP26DsVJg zRqE6_bqO1@nzF{`g#&SKXHKA*3H&wG*l;*o=b;R4#X<0X$vz})g#%t({Uja?tKO_f ze&fKm-gnP#*1KZISkzq@s^JHkSEV91zmE%C#>&P<^gG{Yo3fb2O`w{TXkKh8yj-!v zw)x^%4BAT4fwBNh%4f&l7|1y+hqp@7?!+l+KP!Wu0$uu2Ms@-?JH%f)Tl?)oF7$wz z#|nVXRB!2gUa||j0WqTzQWZvdb-_R6+ml4kT`#+m+JSf)bPCQDg9bi`J z{;$4AUdN#7tfb5=xh|FgbOh?vD?>@e4f;0NSe$QP;7J?=N#+eX?Q1gEiHmsv8n->P z(drIgthwJIw07wN@|v`RMEVoQw5|-mo3x6CtWV}^EO7=^`T4x-svs3^%Hr^{W5Yds z@qVHQ1+pLGGj<{SSo`O`xk>zBV~z)xE%O8JLikFz(W~(snekBUHh9Ggho)h|O<@W{ z%ih@f{z@mFy`!9r*$*%Wa$V0OuQ^@xg}b$v0HvXRq(&D;0pxxJePsR^iDqr5O53lQ zAVIa2iM70k!&CQu-fG;V!CCVIKEN&D`2x^k2Dov5cL5LoEqYI>$Kg}kLeWaPXft$4 zaXds0bbIsp61Vsbr?Wk^vEQkANf{E%D9l7Z4E4<%oeDo6i60VMZY2)_W2Iy}`Q-qk z-RC-M*#_A9HO;{JFWo5!eedTn|BVCB`ods}XOfLN?c@pxyUQD3TkK;E5JsK|f6#>E z>`%?FK6JXpHU>f=dsUp0rsMVV{A0;=+4=29?+U&a%1T0b4jz*SoFEp|Q;Zea%gx;5 zYs%ux)BMaUY+C2s)eq|C;7#~#iA6dgz6;VCpX>qOpepqf1DgtM3a?iD;+_Dmix?`f zJ{k7sRl(14Fq(+3K~x??^ZFh8M)_5(HP9kXuJPo&+2~s%!akP#8A<&GKW~mgo{aF}l)geNh&dS@o?*Mz(%!#?@I38#E z2vY*NuNnYL9NICt4Ud;}br}w&>mia;p63`jj+0RH&tC16{f>-|v&*08(;*m_mgEVK zX&Tz6Xwx0ha_|U2*4lyGu)(Bcs6vsc2!gLix-TGaEd$?Y22{+XRahn`C`ZsxoAM!g zGRgGl1X0$T+C6;H(azHo3+3cKQ1?=77hO2>bXSbTAOhjTZQ6u0T2+VZFI zIKYW7nM(q(SX(PlLxd{P3?o+cU|B!xDpJ)Mc&;O3f91~9Zz*YMOefPw1d$g}gbYr| z_Tezl(H5m*)Nx_+)zNzaKy~xR5{eCOwi`W@@`Hz26avdJ>J@X<6Hyh2N=IllrAC$k zrIVo*6e$uD$2#l4ME6m<&JXSaZN|ZA;V2z-9PTDQRW({ty~4FgC94QG5H5X5@<#vs z1BF&?PomT6Fm(?mAdRP1>g4%ed?iNfVk2shMRMXS=zf?`i!h{{bN^_DiykF{@VQwc?Kbfc%x<+{8rc4s#SBn|Fb8BV>KySsna) zRMybf8YW9RP>bb}B1&e_lG`2eZt?2hB{Fwq8?PUzxrVUiFsT;ODttxArw`+$bBLYR zZsC-6+Xe^AY|qg_5sHa~NF{o7xtW=C^y<7~eE8Rg#;+v9?BICxFNBUeH9^D7QHhZr z+P~Z|Nglfje~=M4c^71r6U~jOdvSZ2-?m9WA7jfV095%c2$QA+BamOF0aMooFz^$d z2We*`gVqv!upjN*Wll#o7lf_)K7a3Qz0$)3m!xV&d#Z1Wk9uEY&De2WkRk2OlSDCx zVY$=Gmpcb3fhGo5#6K*JEBiA*W(=zO$qEV_pd=>oc;6xKs00U`p zk?^I~sL*LIgG4JSvWQ&QEA7YO=d+mP=#l~bmUS`|#|VUdNciSl920VOP&FT*Oo$Tu zd&3sRMI*mUlDzb%WSbvmV_#NN@x_1CQeWx6zF zf}s7{UJvi>1ZWi=GLmtKhaTMY$T3tsw`P*(IneYveJ(3hF)-5%o6OFu@E_(yr~dhA z`?1e!qWwY;9k*4qxpmLEzh6K-!j(PC0WeUq?OwFraqIa{ioijKh^~q&CX&*YmtAmZ z@sujlem_7Q4k3d&IO&R)Q&2#b;~wb?mHcM-+S!(m=fw>3VyIE z3C`S#1>&_CBQBSuRbus#>WDdfq?uIV#Y9jD*v1PqZZ=tSdNj)dNODbj)X(w+8CtIS zBtXP^=Qix}=(H%N%dK@FH?K`*sY^f9Y3<$fc++~AF zlMibt2BS5#R7_K17C{5Sfvok_IGL~%`{nWw=mH$lxqcz;L5`wRMEY8*C`;zIQG7|e zikZ5fj2X^6=0`eg^Zv~H;kLgrabP&eYEUYN@b7FoI*5C9hIyaO(yfMYk3}x;C)p7a zrMKQIeg%(QVyWvt_t&*GeH`mt*>TFSJH3xuf@8m#zmUkkDcH)vd%=7>*v^_-I6e=X zAj~H$s=!?*R&lSEX3Hjkj7nfe5d2P87GPV03baxQq-5 zjOXcRBPB^7Bt=U>3dQ%h5ma!X&9Zpym=K6Q`1V51< z^;g+SJQSfp`?vUb{ZvAFkyp2&&Jo^B%uVn=WJs4 z)k#?3VMk*afwIOq+9=Pl@4{PZ1IGVGf~9cif&)+zN&C#@Au*$*&zU6pM%=NgDr~EY zalAHVs3gkN6~sKM6SddqT}BrN8u!?IYZ1Jz3KZu$#+w}VR}_8w4`wk{;bDZZkz;OR z>EoyjPsmV3CI?@v9PP?-TPs%(5xSM5F7N_kNoGA6U;>nml55sA{!J((dk$!DEKA}F zW1+JsI%J4E!Ow1dD3@mJG1dKDCY+YXUW7Zg_Up?{0N>LJg?&puh=ne(Dp1i169glI@-0azetGOfUCl z(H_(bAS`kH_H?KBc`Pi=WWW3XpR=^RqE7V;j+)P~e0jK{f`TV6l+oKR0eEdY&I z{>0>VvM+g)Z%otB4HOK>R@C?k6aEJ3w;nCEi4Xjgltg2h*jLbHc;g=XOeGWOJTcVq$Vz(8=NPgswrjY_fLybc!To&K9r=|+5Q(tjQ zjjFJlrY1Svgb+Ef_ZEtW)|~$cer`JYogb)PdBL-fjvh1+kw+GfSx> z{sYw9d&Q}UxI?+RHB}vpbvhgfEBL-!d{pt#M)-(j8UgGosAq1tYsu&7c5PwC@;|-0 zOL+sq8H@)#M3nZ0EI7sJ~fxyt}*Cp_(@>kNJG1ZP999 zkk`eNx~uSh4n?0?XaqWM_r&zI0vU*+Iur@45xa=G)!K#66z%*vkvcV%b}#-39?e(ieI4TQE{Lgc8uA)vF5~ z>;_|rgW3E%)x*%6Pf(yOa7dbyw~zGk0je%PmAENfyim=jYMTF@=oqhjP92y}CeFHm zrX0m+Lyr{n6|HZQZGrC&z$ol_&M=2AFI`2Em0|><^;!mXV^Vry-~bl6`Y<{}B2PaYO>DSA7Sd7+xi6`N-g9_lPc8L0|< zMonIIYEJeU>ik)(N#_4~D**5)H~;BkO8;*FZRHeuc!VWje9o1~`vA7-&NjP|EgU%A z4{jFKD5^o-PWK`ly0WsbDdV8Zby{g;`L}$osc~PoDKjIgH-0{y| zW8n5}iVok#8^QzW)==4P+7??Ht02_+t_;7W)N%KHN+zVVUnhQJAp3IPDHBp%@3X1&$ZU-!sEGwOTnA7gsF<)ouS{p)qS zEBip@6{~mCJty&6n-)Z`+WEwM467L-&FQ_6B-``)g64h!p=^Yp=CLONvDw?fSl=}q z`n~pH+@5GVNC`(w3i(ocNtJ({Jkr1-$`>1W5+xhg4^{LgB#c3=YdH98!mp{EvtpJS`H&j&^veJ(h{z{(H5c zav42rb{QS8%qMoBoGsju;Xb{t(7XYNLant@0$p#hsXcPx)?`*0Hsi)cLM8lH;w24A z?O{-2bvLF(gu!l@v2F93VG@2z-o8|hz-RuJxE>c>;^G8>AntXws*(g<=;jji}V;f4{}B}IwCGBnJzXtPaqoYC8R$rQ`$Rc!zbOF5?qVH7Sh%lOHg zuz_hOV@rM$g7ha%AID5zt%(m7t10d3A>|0n7ATqy1?=9AB=X#s@f!iP*|itP$Us`z zPN{jB+608|MRwg9Tp~DSkWZFjE!+{TONy&Bj0*v#gG=b-6G<-IZ1>yexxBE${q2uFY#Bm5;yd1D>MuX8XcnavGWA?fWpZ~`EEWZn1qks`ev%ED63 zeBJsQhO|5=a|9c^#622gYbySMHM=#C2xRw%vl7quOvR0x>!hEQ6Y_SGtc?-;?bG8r z(TstL2nj+7$F(UTMJ{`}raFVBO)U)Hfv;djt1SPq%}{$+a+;b6*EW7HihS^Xp{KN8 zleTTJdnYB_5oCSyI*{USs_C@z&|K`z?e<(RL#H8rG$>PEQeGmgUYO(L zyMGo_v83<_%Gl+P%M^wIHeE*T z&oET(d+iF3?6ltB9brBpx1?C2Va!MXtlQ!zfZKWrJ}e%0m30F#a6DP0NEkV z5Bjv0*aM3|6TAfaumPJ3dIt*p@MQ_7!ovP=s}?Q?o2F0Ip-@M>`_f=^ummjZnys{q zpVi25T@ZVBKLY~A@0w>0cDvRGu!mWC+V}Jh#}bbDPR`*MnX~C2Royo%K~p2xPh=o3 zMZsjnM!S3dujG;_(sB7!VWScH|)?GgP6Iie2qC~T^* z04-E0GDk*IoR@~xg?gG==|K^coZ^c8YHTY5hGYaS7r*744q0nZcYx!PjQ+tpf%8Z_ z*ia)Zmj$Wb&rB>1o%ZBeBJlS+@pFpW2N?Cf44MUQvuJZVs+;BeWRz=tqkZ8n3Y~Qr zCmU4!rtj3!c|j1ySV@G?rY>_KU={d4<66({dcEerqiM)wCCZI9PT<6?*a#hMcljEt z{r;3w^dZEJU$S{$BUH8_R9e+KifbiSQK!bu*>Y&%L_giyp*M_Aq#+Y*JVMvKE%US4 zwKNp`D@yYP7)ZXPRrUrG2*S%y8ywIzwN|}m1}6N~7l(2B3( z5@gmfkKAJm>6rWKe54|c>^PwCb8k#^k?!V}^^_N|<5B5yjda}o2Po?LjcZd-a>NbI z-#WwVS+I20a?i|#@%^^Zm0w_SIlRY*_w%)JAMNc%_b?Xe6w>-UOn#^nLoy8MdWl6b1gg4Um8%(8 zAOl&JXkwy0mT8-!7(n7XS+xc=h5EC(e;=uPM;H{S^NocCfE zC5gh~NXLzc4HkQf%9F8yHaM~#@->0URy4gJZ6Zppo9FeM)qQ^51Yq<)D2{UpCI{0i z?e+p2bO-5NL&56T%#C;m2Pa|8QR8->l(=LYb6JhJk86P#M1c5peFwWVo!a}R(83-U zY;9LMt5fRKC0AyM)5sR}-z>4uixKAe?;@l;WvzYsIyt~j$7D10k;3VM_2%F2qYfn=hUO^j^T3lxs6laOr6)b>sRX~)9_Ivnc0-! zJwaNXWuMc&a>tNh%=Pm{Am|tnA-8YupR9rKq%5-Vtav(kiL91Ooqk{YeM_?pL~+ku zz@b`R0Wyu#t?$XTWr^)8y@qK{TIWpW01xMOmV2UF^Kov6 zh*QQspSN^(&sO1{Z(eEwDmDj$p#|Knc(HH zK|8CAOv*_syN+kI`7;Lg%|8Z@+GiuDW;Ku(2?rnATEycW!&tZ5PZ->$4ykRDYaTRZ z;Gf3ggKU76Lr9|Z%Nplv2~*JM7d>-*C3S(0A+Y+fF-zDuC*KU%0T{CWtPOI(GTj4idZHtQ^-)=?-F({a3c1 z&7;op#7*=3OF8@A<8SS2ANkVx9gYwIU$f|02$2$ zB;D+NN1{A9X%Sac_4+U@F{eF2xMzF*4!>*3Fsa#!_NL;gw2KKVa;McpxuORt9r^ay zZY%e@r!mnKWE<4GKWzsE%}b?G8n9^DjiyKNs#0)D^uu+cnL%b-2 zjmul~jX<`6Zhxu(z8Xk9QixD3%Z78R!2}d{&q%I;*805bf=G$iZVRzjICXyG*mZHVW6qE#w7^ zVPku)R}!#pCZc2`z>|+yLj=d%VFno+=Ga0jxAy8f{1T@n3wmAp1tlg+AsEuu=wk3`s1F$!bd3b8R0oH4XN)iL{nq7d zWSx+u<0ODo!xs3MVhq~1C)^WDaz2___psH~NE4vGZAhztGo<-z(byl=&FI$VkTdV9G&|?rf{J3^bq?MFhux1?AnFq? zrlmg)WkvBUi|;OC_l-FF-l_&Fl%9>gY>207Is;t8AQ zV?Z&VK^m{r^;xoLqhVGdtq1su#e_DY)83NKpBpM4AWc5|ZLvts`P(P|z!=PE>)h8@ zrEX;IMcARHeiThI^~K6I3r~KkjYAv!OvH4XUY%!lMJ2=IpV@DtYe=_WJL_oUuu52K zl8W|WsGueGTy<0f#Xdemm+qBs11Yg~q6w6qfBBllnD0a!cVMB1Vub<;V=C@7(cJ#r zA!_)Ru@=gP5#XA?*P)MN!nr@8QplYZF_Z?jCimRQ!8mpy}m(qk1>NTn3guFW2hP*B$N*N!7!Oe=JO!hUBKPfq>BJxLc@PwUoc#V9A+5y``=6vUd;^+`VA zI*i{f<6;4m-|;DRj!oAq*g0z(vv}rskpf0kGCA4+TE(sgDPTrK1N4lc040Ur#R}X& zWR)|gEicV_Pens#vd69DpSonNTP#}&i{q>l zlafxF3@%Ep89y;_<>A9bxy}bEq8PVgr}#uja3|nAz8z>7Etbn}q_z~gleTpD(pOZp z{{B(g2BlfD#7S}f=EQa>HL{Z;y=(n9nHRAqWYoX#F=iDaE!rPk-N27VM{_ zb|XM(3WtanAVMRn?20cu8;qn`a119WI7~P{I3{E*Z3QvB2Jz?YhcF#Khhb?`b(Llo~yP4zo# z6E&O-IG#EV1C=1L<1X2~B z!dY1`m&T*$74`O|5i=3z3%||i((tO$dOs1!QT=@vwFJ_0Mt;7kMn!}fMSD7|(mjdfIYcew@KP)R+AN>ZmM~5I*@N|0gj$(I=KA%s9K+ys_Px5zDc)%@# z0geLkMPe`N{!ABRJ} zH_C4y7|n0vSQqEN;xq{&n#(3c*w>gTqMPFhax)jYWoFkr5`{Lm`SIu~SN@PTHvZF< zj%N?=Sd@Org*6hy9CXf!iJQ{%TPmH&{Mx=tzh6<8tRuTpb2?`FlVSPPAKooTHJcmh zeE1S#OcK>Q%<5)EsLo3~YS$ zbngC6uFL{=x~1(-$%<3XVUzEwmG1SrF*Fz|1kiA;!2LF1Jz89r$=|tf>F;S$OFc&r z0Mkbm-tkB_3Y1XxuUYBeK^pz#9oneiPx2@9CXF<^SYf0cDI!*6r>zZr2$>h&QRVtm zD_1}mj7GZl`iSuopMKi^*g>CD!>yJ_Xp%~UPZW_jvluQ@gAremiI0$D-!M?*ta5cs z)Z6@&XUY@2Q_ypRQlP|=IY(u{Q7+D3JpxX=X}JBoU4jpZ`9HB5DPb&yv6^(S>wUO% zP|A31u5}I4oVsBQ!|4M(_`qj+xrUl{vkc1x`U`t$!vhZC4jMJG_;@M~sjss1__$bv z1LuWznSQn?&6~stqE&eQYZlDCi7&ZYEL^%L@=FzFpEl=NA_*T!q!+kv%MY=B8!fw6 zO2Rp-#x56oEPbB#UnW_JbMZqoTfcG{H!NaR<0T<+*Qi205^>u3<=^y*Z?^gnPN@#=r{%l!foqc97GNy`K59 zeG3oD%@@zv`i2Y&(bFU59xihp#gCG`AOYRP)8bW{=R5k<>7ZP>3r)K?Y%ixUFJv9S z-)J7xGBY7FvUczuWL_oDRu$WaMCo(h2MX59X~$Q$2lTdyF&=;#WrRE!YsaKa3iLIj z@5vJb<1g3w?ogMMDlutm>xM=Wk{Q+VNDI6NL9UJEf<`;wd&S?-TA*`}0LM;TPb49( z`7gx;nxR%!)9knvGKw(m+0jTBC#rssv;PLTHt`=9Z!?Nf8D75@=u{;0e!dm*W;MR` z(v6+jn@@yId|}Bmu`y2L_quXy5EOI@?-7F1QG*cMnr3+HPOKb``L;r-N8d~m z0%SzSNn>~?D?!a>Sft;T%lPqh3R;_%?k}ObbH3^RCfs?j<72D6LH-NREKIRH)?Dhv zIvF6YRU}N!kpi;6zYVe6q}I+q#zEI8S#D?FUm>jnk8YYmZ~8l&xFO6x-`(LMYK%1} zP=#NQ8R7?`kQ>O~mvKt!)(+P@j!Mxq{zzKt2_V`IH4W=oC&+0>R}q{x&#^ypZGCe8 zL}UItLu5ni-x~II9Rp3p-DFJzb5pQU%2kbP8wdVT6S1OKx&_;MeshM~j(h3Mam^H# zQ*;G9M1zty;HQU@dJw~e3oMBPY5&%a-{QrfYzcs4cz4!B9f!=^8{QO{Y3f(O8hKxf z0&2)6BR_WV!+oVem-O5Z86D7-xgZc9x((-}mdt#O_;{WWA%{3X!C-ZUujUp69;>&d z24|W!&6?rJzT;ymhp-xextgb}EsQkNpQ3TBXBhphH{qqAO~Mat0{jE-1A$ThG{Zh9 zs6PW}b`%1JuBbi5^;zWMfI<*mwI>ar%~x7*BaK_`lqr}BCQ~*T1JDXF>1urFk#+z8 z!YGba%-DpaQ=#r@9+xe(d_)^~xjv$@GKUb$ZVFmEVnG+a)cpTAPH#o+C-d+kj}w&0WxoF8O)v1k|6Lb0au+)FQv9R_1{!c=Rb6yy3#OE5&xeZSNz&IJX9S4QvMh+7zLND2gc-2+z&|Qx92!-Hy8zPhz7$T@hz_;WMs4A;-<8?ZkDf!Z@dHq1 zmjVuTkYe%vv%UhO=m+>^VC?JwSt1DCBrFJMowsw-ZD-`UPdm_$1)qVyg^Tk)&ZoYVR$-odqIxCK_Y+ky=09r$9Gf3GBPomc%d6#cgvGtx&~3 zvZ8tVO2j1AJiniIkQTktwG9p*h69arquhceF+F^Q2tnk&(GTWv)=;1O9{%k(8k1Cf zO#KSnhKDY%Uxu*8fr^6AO3D^d5nf1*Kw8x8b`6Oh%=B`Xg()VuA*PR3q1tf@hS1CI zaE#!b^E~_Z4a>1WG0&(^avblYckW2REDQ+rD+BWyXn_vq8Fn!l?I&m&`=e*3;k~N_ zj_O5lqoHbYb-GId8#>?QQ066VK2knm4FpBLT9!uV!cA^Bu7qA=Xs3F-EhgIn0Nw(` zs9N8p=Fp|tyn*;aXYicVw8`D|roeWs_hq06(!Febs^s9h!VuDpzbxBXm`ph(Wp!^NYT}?DP}7$R<&f-cSXu|0PA2_!X~#l2nV<{_Rm}j)H>(2#u+T4GXkQk zbbIMxb40W#}$OIB35a>_JLo<|>->Uop(3uqf~?MbMA9 z!bL>G8+{HuG7*{{1gpXcbsV1dB&va51zQ~^>2u!r+SX!Bphr~VU=FV^zFdjDbRuH~ z9$2mbLC6XXbRgcsd+H2~7UgL2y7mRk1uR*Df$B%uORg;V=gWT}On;)gSDw%%u4z`* z0v%SqJ(;ZpowaJOy6PFV9SIW3`oLoST8i0Jb3&Q zqN2^qC3_5Pw%?ku;@X%vK?(u))bj_~pzv!Avu50^e zapj)9aB(C1%81t%7h8)(Zgep}G>A$!+dp@~v|2t;Y{qnTN@LYsSfZIr=mu_bz5pZY zJ6Gb7U?M~q)3$*rLPE^HXG%mta|Sa*%m{_-!;xHik6$O4k;E@L`5E`1O>dtTn~-!z zol;SRpO<*+s9%|A5MW$;T$jEQ2$DGAFp5;Bz-qtQGIm4M$ferso;YvP{8>j|#j9Qv7C>`X_A8{3_}P$RDTaIfHt1vyp5#%Vs{7_b7PlTn`GYrFN> zc|(&Ywu|@YT0U$xa7&kSZKv}ww@Ck9N2$E_Va34!{f42pW+Kt3{Jhm=8^U3dc{6>0 z5z$57&?ek`rt6g29fn`+R<_hTb{@%4(&&%~?=pS)h;JV+BRyWkRn7JDe*i(j(MNwV zi8AnZNOM>a5H)_OJy}E+Jwdn^=|z}Wy>UFmC-H1-}H?2JYmQRVbzaY%n)&_yOehMaR~HzAS ztg7r_uH3SNuiD+z`aYkk+ohxS1&J153yZI+o+xFhy03rH5yiM@wJ2(`*^U(8+vt`_7! za0UQr4s?ZHJ@6Y(kaFBCrI(qZR*b{9s2mq3m<(Mf_GtH zHfp;rYMzB>lO$*1$a_}Bu{!X9?2)V;#QW8^vX0x>f2kR1Q{z}z_=TfL`^&rc*6X(( z1`8cC!Xul|Em#K{g1*0jkPl#CP3yyl>)X4a>@UZWoux*}^Cv z!`diCgT}S6tL{t%o6@#zf4PQ~Y@x(1CO8PWM0;w1YVB=VYReFw_&T8qbm0r(N{Bd4 z*^m%O>Ec3BE55Lb|HApkEthCO?7qKA1(F*|A*DPThVc#b58i$K<3DH8#b+^wW<5>2 zzV_LjK9Efso3z>6$(3b@rKaQz)V7x1IQH9PuV><`o%+miwUvm`rhQS7FmCShwXxfR z5a#mpA1IX2DBYwcl<}HKw5HTxQg`0j^FKw1`3#8DV&-bphja8U7`L4-g+9)VWX9?~ ze|h&mTdjB@Zom9Tm!6&9S$iOVl|r)yV6llWInCd37}S=@`D67MuA#C)tiE7_T$s$I zz(KpDiC;X>-emx9`|6z`ML$!S;#w~rSzOP~8gd(8b5Bxhf^NeqEe2}nnES(t$*fVN zTCnHl#Tqx;m%C;ea~ZAWCnFZ@@R~UHes_A&6>zo|cqz&vmT}!2=GE+HhUHzV7(!); zHXUQ|C99{|d+nZUqqAHFQjzPo>2^PTri&-)^S}w`$)!J))A=+Qe7|yP$UQg$o5r-N z%$z61j0qQ|TUM)@EI35n+2&vV>i|*O1@zkUCPXm$S&ushL|EQ@sYZ!x{T#m>Ziu?Fi3;hF%p zT2^6LQYv99#Q**(qlI;zSUvJAN}KF`=3ab#?+qUE1#u+69MrW@r5VUy+N*tLlgbKs z6~!$qG`+aFVju}Nw5&=(J>{%(m1XuFEN)kG4K44>lsQAYvWuC%VYp#%sXrfjC@JZA z=pEi+8j90u3oeI9uFQN@KSu{{?W-QO>Wzq@r(1N2eeb5r#7qu3FnbAPM4EE*-{=zG zUdl_1#vs!hD_*YF+AgkJLAgs=juCdmx+#r>4cjA@EiLIhI#+wcS!P6OQT!p2I?di* zK9@nUO3N`hqyAyLR#qC7whHyjD)yji`Xz{8icb!NxM zM>GtzAliL2@FtKMY87ZFCvk--LWJ6#!(9L~M9`J?Mox`uF(?^CG(K{c| zeI+zM59?Z@I9GKsceVY2!85LFLri-$sk96f{I+Ky^2Fv9xYgtL<{nBb3 zaaK?!Ia-bVji~jIDR;i}hs1hD8ViMj6r24Bz|EYdsddKt!(Dr5i7)wTnE zT-g+IwYj9q)uH*94*cKoYKVy&5yjsF{M4n(=UXH`%+`fzdXpx2ZJrd796j^{FrG^$0w2ZKu;%8*n%x&0Y3=|m&dxwiRbr@1~1rrJhEXm-y#G4 z6T(@|ZI^L1Fmqy2dtJ}X%0-UEi(nH#hO6ty1GZG9$Vqhco;vb@WfPXylcikWUdm*7 zNS4be$4YPA4k!F2&B?EDwjll8BdAg zF=D}H`?JXdfY>Sg&UAV)Y>*ezq>mGO<-E#9!(Mnmqu$2s3u?6(lhwgGVL^9xA{EtU6&w(b-dJMKMewQjvslJE%?5Hmo9Usx zevIlhbt9O_j6SJsvWzB6;8CI#*_y#9Xdi2hoIhboFFi|=+pa|KE699uhl3|jR~!Z! zWqR{);ab+Y@R=GzYl9%@>P2ujf(OyjXt5S)trUARWaXdxh%SNCxlrlz4ub)mo_PQO zLViCoY{YgRXTzG_luc{9Se`0hH8vdX5M?};mM1OD(fQXy92>>7=K?#8P8G!+ifPMjhezgA_m_VTa z?NxtttpC+@esuExwfPV1f(-nB_U54=|F!>d_+R-?V(x$Z{Qn*d0Ehzz{AVKrdPW9% sp!omMfqpyy^8WWc13f)Z-v4;~{{AUt&-)*b|GBwe|ErJx$H9;MKWC|^LI3~& diff --git a/ensips/ccip-write/images/l2.register.png b/ensips/ccip-write/images/l2.register.png deleted file mode 100644 index 05af0159c2bcd68f9419e0884900c59183269cf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15430 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 Dq&@nQ From badd345639a2b3bc96dc6f7ef31bae39296816e2 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Fri, 29 Nov 2024 13:56:00 +0700 Subject: [PATCH 02/16] feat: support writeParams on L1 and DB resolvers --- packages/contracts/src/DatabaseResolver.sol | 63 ++-------- packages/contracts/src/L1Resolver.sol | 118 +++++------------- .../{IWriteDeferral.sol => WriteDeferral.sol} | 52 ++++++-- 3 files changed, 84 insertions(+), 149 deletions(-) rename packages/contracts/src/interfaces/{IWriteDeferral.sol => WriteDeferral.sol} (75%) diff --git a/packages/contracts/src/DatabaseResolver.sol b/packages/contracts/src/DatabaseResolver.sol index 70cc38ae..290fc547 100644 --- a/packages/contracts/src/DatabaseResolver.sol +++ b/packages/contracts/src/DatabaseResolver.sol @@ -18,12 +18,8 @@ import {ContentHashResolver} from import {ENSIP16} from "./ENSIP16.sol"; import {SignatureVerifier} from "./SignatureVerifier.sol"; -import {IWriteDeferral} from "./interfaces/IWriteDeferral.sol"; +import {WriteDeferral, DBWriteDeferral} from "./interfaces/WriteDeferral.sol"; import {EnumerableSetUpgradeable} from "./utils/EnumerableSetUpgradeable.sol"; -import { - OffchainRegister, - OffchainMulticallable -} from "./interfaces/OffchainResolver.sol"; /** * Implements an ENS resolver that directs all queries to a CCIP read gateway. @@ -33,15 +29,13 @@ contract DatabaseResolver is ERC165, ENSIP16, IExtendedResolver, - IWriteDeferral, + DBWriteDeferral, AddrResolver, ABIResolver, PubkeyResolver, TextResolver, ContentHashResolver, NameResolver, - OffchainRegister, - OffchainMulticallable, Ownable { @@ -109,53 +103,20 @@ contract DatabaseResolver is return true; } - //////// OFFCHAIN STORAGE REGISTER DOMAIN //////// + //////// ENSIP Wildcard Writing //////// /** - * Forwards the registering of a domain to the L2 contracts - * @param -name DNS-encoded name to be registered. - * @param -owner Owner of the domain - * @param -duration duration The duration in seconds of the registration. - * @param -secret The secret to be used for the registration based on commit/reveal - * @param -resolver The address of the resolver to set for this name. - * @param -data Multicallable data bytes for setting records in the associated resolver upon reigstration. - * @param -reverseRecord Whether this name is the primary name - * @param -fuses The fuses to set for this name. - * @param -extraData any encoded additional data + * @notice Validates and processes write parameters for deferred storage mutations + * @param -name The encoded name or identifier of the write operation + * @param -data The encoded data to be written + * @dev This function reverts with StorageHandledByL2 error to indicate L2 deferral */ - function register( + function writeParams( bytes calldata, /* name */ - address, /* owner */ - uint256, /* duration */ - bytes32, /* secret */ - address, /* resolver */ - bytes[] calldata, /* data */ - bool, /* reverseRecord */ - uint16, /* fuses */ - bytes memory /* extraData */ + bytes calldata /* data */ ) - external - payable - override - { - _offChainStorage(); - } - - //////// OFFCHAIN STORAGE TRANSFER DOMAIN //////// - - /** - * Transfer a domain to a new owner - * @param -node The DNS-encoded name to resolve. - * @param -owner The address of the new owner - */ - function transfer(bytes32, /* node */ address /* owner */ ) external view { - _offChainStorage(); - } - - function multicall(bytes[] calldata /* datas */ ) external view - returns (bytes[] memory /* results */ ) { _offChainStorage(); } @@ -439,14 +400,14 @@ contract DatabaseResolver is */ function _offChainStorage() private view { revert StorageHandledByOffChainDatabase( - IWriteDeferral.domainData({ + DBWriteDeferral.domainData({ name: _WRITE_DEFERRAL_DOMAIN_NAME, version: _WRITE_DEFERRAL_DOMAIN_VERSION, chainId: _CHAIN_ID, verifyingContract: address(this) }), gatewayUrl, - IWriteDeferral.messageData({ + DBWriteDeferral.messageData({ callData: msg.data, sender: msg.sender, expirationTimestamp: block.timestamp @@ -585,7 +546,7 @@ contract DatabaseResolver is ) returns (bool) { - return interfaceID == type(IWriteDeferral).interfaceId + return interfaceID == type(WriteDeferral).interfaceId || interfaceID == type(IExtendedResolver).interfaceId || super.supportsInterface(interfaceID); } diff --git a/packages/contracts/src/L1Resolver.sol b/packages/contracts/src/L1Resolver.sol index 732c5162..237034d2 100644 --- a/packages/contracts/src/L1Resolver.sol +++ b/packages/contracts/src/L1Resolver.sol @@ -18,22 +18,15 @@ import {ENSIP16} from "./ENSIP16.sol"; import {EVMFetcher} from "./evmgateway/EVMFetcher.sol"; import {IEVMVerifier} from "./evmgateway/IEVMVerifier.sol"; import {EVMFetchTarget} from "./evmgateway/EVMFetchTarget.sol"; -import {IWriteDeferral} from "./interfaces/IWriteDeferral.sol"; -import { - OffchainRegister, - OffchainMulticallable, - OffchainRegisterParams -} from "./interfaces/OffchainResolver.sol"; +import {L2WriteDeferral} from "./interfaces/WriteDeferral.sol"; +import {OffchainRegister} from "./interfaces/OffchainResolver.sol"; contract L1Resolver is EVMFetchTarget, IExtendedResolver, IERC165, - IWriteDeferral, + L2WriteDeferral, Ownable, - OffchainRegister, - OffchainMulticallable, - OffchainRegisterParams, ENSIP16 { @@ -47,7 +40,7 @@ contract L1Resolver is //////// CONTRACT IMMUTABLE STATE //////// // id of chain that is storing the domains - uint256 immutable chainId; + uint256 public immutable chainId; // EVM Verifier to handle data validation based on Merkle Proof IEVMVerifier immutable verifier; @@ -64,8 +57,8 @@ contract L1Resolver is uint256 constant EXTRA_DATA_SLOT = 2; /// Contract targets - bytes32 constant TARGET_RESOLVER = keccak256("resolver"); - bytes32 constant TARGET_REGISTRAR = keccak256("registrar"); + bytes32 public constant TARGET_RESOLVER = keccak256("resolver"); + bytes32 public constant TARGET_REGISTRAR = keccak256("registrar"); //////// INITIALIZER //////// @@ -99,94 +92,41 @@ contract L1Resolver is setTarget(TARGET_REGISTRAR, _target_registrar); } - //////// OFFCHAIN STORAGE REGISTER SUBDOMAIN //////// + //////// ENSIP Wildcard Writing //////// /** - * Forwards the registering of a subdomain to the L2 contracts - * @param -name The DNS-encoded name to be registered. - * @param -owner Owner of the domain - * @param -duration duration The duration in seconds of the registration. - * @param -secret The secret to be used for the registration based on commit/reveal - * @param -resolver The address of the resolver to set for this name. - * @param -data Multicallable data bytes for setting records in the associated resolver upon reigstration. - * @param -reverseRecord Whether this name is the primary name - * @param -fuses The fuses to set for this name. - * @param -extraData any encoded additional data + * @notice Validates and processes write parameters for deferred storage mutations + * @param -name The encoded name or identifier of the write operation + * @param data The encoded data to be written + * @dev This function reverts with StorageHandledByL2 error to indicate L2 deferral */ - function register( + function writeParams( bytes calldata, /* name */ - address, /* owner */ - uint256, /* duration */ - bytes32, /* secret */ - address, /* resolver */ - bytes[] calldata, /* data */ - bool, /* reverseRecord */ - uint16, /* fuses */ - bytes memory /* extraData */ - ) - external - payable - { - _offChainStorage(targets[TARGET_REGISTRAR]); - } - - /** - * @notice Returns the registration parameters for a given name and duration - * @param -name The DNS-encoded name to query - * @param -duration The duration in seconds for the registration - * @return price The price of the registration in wei per second - * @return commitTime the amount of time the commit should wait before being revealed - * @return extraData any given structure in an ABI encoded format - */ - function registerParams( - bytes calldata, /* name */ - uint256 /* duration */ + bytes calldata data ) external view - override - returns ( - uint256, /* price */ - uint256, /* commitTime */ - bytes memory /* extraData */ - ) { - EVMFetcher.newFetchRequest(verifier, targets[TARGET_REGISTRAR]) - .getStatic(PRICE_SLOT).getStatic(COMMIT_SLOT).fetch( - this.registerParamsCallback.selector, "" - ); - } + bytes4 selector = bytes4(data); - function registerParamsCallback( - bytes[] memory values, - bytes memory - ) - public - pure - returns (uint256 price, uint256 commitTime, bytes memory extraData) - { - price = abi.decode(values[0], (uint256)); - commitTime = abi.decode(values[1], (uint256)); - return (price, commitTime, abi.encode("")); - } + if (selector == OffchainRegister.register.selector) { + _offChainStorage(targets[TARGET_REGISTRAR]); + } - /** - * @notice Executes multiple calls in a single transaction. - * @param -data An array of encoded function call data. - */ - function multicall(bytes[] calldata /* data */ ) - external - view - override - returns (bytes[] memory) - { - _offChainStorage(targets[TARGET_RESOLVER]); + if ( + selector == bytes4(keccak256("setAddr(bytes32,address)")) + || selector == bytes4(keccak256("setAddr(bytes32,uint256,bytes)")) + || selector == TextResolver.setText.selector + || selector == ContentHashResolver.setContenthash.selector + ) _offChainStorage(targets[TARGET_RESOLVER]); + + revert FunctionNotSupported(); } //////// ENSIP 10 //////// /** - * @dev Resolve and verify a record stored in l2 target address. It supports subname by fetching target recursively to the nearlest parent. + * @dev Resolve and verify a record stored in l2 target address. It supports subdomain by fetching target recursively to the nearest parent. * @param -name DNS encoded ENS name to query * @param data The actual calldata * @return result result of the call @@ -427,12 +367,10 @@ contract L1Resolver is returns (bool) { return interfaceID == type(IExtendedResolver).interfaceId - || interfaceID == type(IWriteDeferral).interfaceId + || interfaceID == type(L2WriteDeferral).interfaceId || interfaceID == type(EVMFetchTarget).interfaceId - || interfaceID == type(OffchainRegister).interfaceId - || interfaceID == type(OffchainMulticallable).interfaceId - || interfaceID == type(OffchainRegisterParams).interfaceId || interfaceID == type(IERC165).interfaceId + || interfaceID == type(ENSIP16).interfaceId || interfaceID == type(AddrResolver).interfaceId || interfaceID == type(TextResolver).interfaceId || interfaceID == type(ContentHashResolver).interfaceId diff --git a/packages/contracts/src/interfaces/IWriteDeferral.sol b/packages/contracts/src/interfaces/WriteDeferral.sol similarity index 75% rename from packages/contracts/src/interfaces/IWriteDeferral.sol rename to packages/contracts/src/interfaces/WriteDeferral.sol index 08f7a5e8..3ac7913b 100644 --- a/packages/contracts/src/interfaces/IWriteDeferral.sol +++ b/packages/contracts/src/interfaces/WriteDeferral.sol @@ -1,7 +1,30 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -interface IWriteDeferral { +interface WriteDeferral { + + /// @notice Validates and processes write parameters for deferred storage mutations + /// @param name The encoded name or identifier of the write operation + /// @param data The encoded data to be written + /// @dev This function should revert with appropriate errors when write operations need to be deferred + function writeParams( + bytes calldata name, + bytes calldata data + ) + external + view; + + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /// @notice Error thrown when an unsupported function is called + /// @dev Used to indicate when a function call is not implemented or allowed + error FunctionNotSupported(); + +} + +interface L2WriteDeferral is WriteDeferral { /*////////////////////////////////////////////////////////////// EVENTS @@ -11,6 +34,7 @@ interface IWriteDeferral { event L2HandlerDefaultChainIdChanged( uint256 indexed previousChainId, uint256 indexed newChainId ); + /// @notice Event raised when the contractAddress is changed for the L2 handler corresponding to chainId. event L2HandlerContractAddressChanged( uint256 indexed chainId, @@ -18,6 +42,25 @@ interface IWriteDeferral { address indexed newContractAddress ); + /*////////////////////////////////////////////////////////////// + ERRORS + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Error to raise when mutations are being deferred to an L2. + * @param chainId Chain ID to perform the deferred mutation to. + * @param contractAddress Contract Address at which the deferred mutation should transact with. + */ + error StorageHandledByL2(uint256 chainId, address contractAddress); + +} + +interface DBWriteDeferral is WriteDeferral { + + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + /// @notice Event raised when the url is changed for the corresponding Off-Chain Database handler. event OffChainDatabaseHandlerURLChanged( string indexed previousUrl, string indexed newUrl @@ -57,13 +100,6 @@ interface IWriteDeferral { ERRORS //////////////////////////////////////////////////////////////*/ - /** - * @dev Error to raise when mutations are being deferred to an L2. - * @param chainId Chain ID to perform the deferred mutation to. - * @param contractAddress Contract Address at which the deferred mutation should transact with. - */ - error StorageHandledByL2(uint256 chainId, address contractAddress); - /** * @dev Error to raise when mutations are being deferred to an Off-Chain Database. * @param sender the EIP-712 domain definition of the corresponding contract performing the off-chain database, write From cb962985330e2505c1a4a3923172b4523346f099 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Fri, 29 Nov 2024 13:56:13 +0700 Subject: [PATCH 03/16] test: l1resolver writeparams test --- packages/contracts/test/L1Resolver.t.sol | 659 ++++++++++------------- 1 file changed, 295 insertions(+), 364 deletions(-) diff --git a/packages/contracts/test/L1Resolver.t.sol b/packages/contracts/test/L1Resolver.t.sol index 06962812..29fef030 100644 --- a/packages/contracts/test/L1Resolver.t.sol +++ b/packages/contracts/test/L1Resolver.t.sol @@ -3,367 +3,298 @@ pragma solidity ^0.8.17; import {Test} from "forge-std/Test.sol"; -// import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; -// import {ENSRegistry} from "@ens-contracts/registry/ENSRegistry.sol"; -// import {INameWrapper} from "@ens-contracts/wrapper/INameWrapper.sol"; -// import {BytesUtils} from "@ens-contracts/utils/BytesUtils.sol"; -// import {HexUtils} from "@ens-contracts/utils/HexUtils.sol"; -// import {NameEncoder} from "@ens-contracts/utils/NameEncoder.sol"; -// import {IAddrResolver} from -// "@ens-contracts/resolvers/profiles/IAddrResolver.sol"; -// import {IAddressResolver} from -// "@ens-contracts/resolvers/profiles/IAddressResolver.sol"; -// import {ITextResolver} from -// "@ens-contracts/resolvers/profiles/ITextResolver.sol"; -// import {IContentHashResolver} from -// "@ens-contracts/resolvers/profiles/IContentHashResolver.sol"; -// import {NameWrapper} from "@ens-contracts/wrapper/NameWrapper.sol"; -// import {IBaseRegistrar} from "@ens-contracts/ethregistrar/IBaseRegistrar.sol"; -// import {IMetadataService} from "@ens-contracts/wrapper/IMetadataService.sol"; -// import {ReverseRegistrar} from -// "@ens-contracts/reverseRegistrar/ReverseRegistrar.sol"; - -// import {L1Resolver} from "../src/L1Resolver.sol"; -// import {L1Verifier} from "../src/evmgateway/L1Verifier.sol"; -// import {IEVMVerifier} from "../src/evmgateway/IEVMVerifier.sol"; -// import {EVMFetcher} from "../src/evmgateway/EVMFetcher.sol"; -// import {IWriteDeferral} from "../src/interfaces/IWriteDeferral.sol"; -// import {ENSHelper} from "../script/ENSHelper.sol"; - -// contract L1ResolverTest is Test, ENSHelper, IWriteDeferral { -// ENSRegistry registry; -// IEVMVerifier verifier; -// L1Resolver l1Resolver; -// uint32 chainId; - -// bytes dnsName; -// bytes32 testNode; - -// function setUp() public { -// chainId = 31337; -// (dnsName, testNode) = NameEncoder.dnsEncodeName("test.eth"); - -// registry = new ENSRegistry(); -// string[] memory urls = new string[](1); -// urls[0] = "http://localhost:3000/{sender}/{data}.json"; - -// ReverseRegistrar registrar = new ReverseRegistrar(registry); -// // .reverse -// registry.setSubnodeOwner( -// rootNode, labelhash("reverse"), address(registrar) -// ); -// // addr.reverse -// vm.prank(address(registrar)); -// registry.setSubnodeOwner( -// namehash("reverse"), labelhash("addr"), address(registrar) -// ); - -// NameWrapper nameWrap = new NameWrapper( -// registry, -// IBaseRegistrar(address(registrar)), -// IMetadataService(msg.sender) -// ); - -// verifier = new L1Verifier(urls); -// l1Resolver = new L1Resolver(chainId, verifier, registry, nameWrap); - -// registry.setSubnodeOwner(rootNode, labelhash("eth"), address(this)); -// registry.setSubnodeOwner( -// namehash("eth"), labelhash("test"), address(this) -// ); -// } - -// function test_ConstructorChainId() public { -// assertEq(l1Resolver.chainId(), chainId); -// } - -// function test_SetChainId() public { -// uint32 newChainId = 137; -// l1Resolver.setChainId(newChainId); -// assertEq(l1Resolver.chainId(), newChainId); -// } - -// function test_OwnerCanSetTarget() public { -// address target = address(0x123); -// l1Resolver.setTarget(testNode, target); -// (, address actual,) = l1Resolver.getTarget(dnsName); -// assertEq(actual, target); -// } - -// function test_EmitEventOnSetTarget() public { -// address target = address(0x123); -// vm.expectEmit(true, true, true, true); -// emit L2HandlerContractAddressChanged(chainId, address(0), target); -// l1Resolver.setTarget(testNode, target); -// } - -// function test_RevertWhen_UnauthorizedSetTarget() public { -// address target = address(0x123); -// vm.prank(address(0x2024)); -// vm.expectRevert( -// abi.encodeWithSelector( -// L1Resolver.L1Resolver__ForbiddenAction.selector, testNode -// ) -// ); -// l1Resolver.setTarget(testNode, target); -// } - -// function test_EmitEventOnSetChainId() public { -// uint32 newChainId = 137; -// vm.expectEmit(true, true, true, true); -// emit L2HandlerDefaultChainIdChanged(chainId, newChainId); -// l1Resolver.setChainId(newChainId); -// } - -// function test_SetTarget() public { -// address target = address(0x123); -// vm.expectEmit(true, true, true, true); -// emit L2HandlerContractAddressChanged(chainId, address(0), target); -// l1Resolver.setTarget(testNode, target); -// } - -// function test_RevertWhen_SetTargetUnauthorizedOwner() public { -// address target = address(0x123); -// vm.expectRevert(); -// vm.prank(address(0x2024)); -// l1Resolver.setTarget(testNode, target); -// } - -// function test_GetExistingTarget() public { -// address target = address(0x123); -// l1Resolver.setTarget(testNode, target); -// (, address actual,) = l1Resolver.getTarget(dnsName); -// assertEq(actual, target); -// } - -// function test_GetExistingTargetSubdomain() public { -// address expected = address(0x123); - -// bytes32 ethNode = namehash("eth"); -// l1Resolver.setTarget(ethNode, address(0x456)); - -// bytes32 blockfulNode = namehash("blockful.eth"); -// l1Resolver.setTarget(blockfulNode, expected); - -// (bytes memory dnsBlockful,) = NameEncoder.dnsEncodeName("blockful.eth"); -// (, address actual,) = l1Resolver.getTarget(dnsBlockful); -// assertEq(actual, expected); -// } - -// function test_GetExistingTargetSubdomainFromParent() public { -// address expected = address(0x123); - -// bytes32 ethNode = namehash("eth"); -// l1Resolver.setTarget(ethNode, expected); - -// (bytes memory blockfulNode,) = NameEncoder.dnsEncodeName("blockful.eth"); -// (, address actual,) = l1Resolver.getTarget(blockfulNode); -// assertEq(actual, expected); -// } - -// function test_GetExistingTargetSubdomainFromParentMultiplesLevels() -// public -// { -// address expected = address(0x123); - -// bytes32 ethNode = namehash("eth"); -// l1Resolver.setTarget(ethNode, expected); - -// (bytes memory blockfulNode,) = NameEncoder.dnsEncodeName( -// "optimizing.human.cordination.blockful.eth" -// ); -// (, address actual,) = l1Resolver.getTarget(blockfulNode); -// assertEq(actual, expected); -// } - -// function test_GetNotExistingTarget() public { -// (, address actual,) = l1Resolver.getTarget(dnsName); -// assertEq(actual, address(0)); -// } - -// function test_RevertWhen_SetAddr() public { -// address target = address(0x456); -// l1Resolver.setTarget(testNode, target); -// vm.expectRevert( -// abi.encodeWithSelector( -// IWriteDeferral.StorageHandledByL2.selector, 31337, target -// ) -// ); -// l1Resolver.setAddr(dnsName, address(0x123)); -// } - -// function test_RevertWhen_GetAddr() public { -// vm.expectRevert(); -// l1Resolver.resolve( -// dnsName, -// abi.encodeWithSelector(IAddrResolver.addr.selector, address(0)) -// ); -// } - -// function test_RevertWhen_SetText() public { -// address target = address(0x456); -// l1Resolver.setTarget(testNode, target); - -// vm.expectRevert( -// abi.encodeWithSelector( -// IWriteDeferral.StorageHandledByL2.selector, 31337, target -// ) -// ); -// l1Resolver.setText(dnsName, "com.twitter", "@blockful"); -// } - -// function test_RevertWhen_GetText() public { -// vm.expectRevert(); -// l1Resolver.resolve( -// dnsName, -// abi.encodeWithSelector( -// ITextResolver.text.selector, testNode, "com.twitter" -// ) -// ); -// } - -// function test_RevertWhen_SetContentHash() public { -// address target = address(0x456); -// l1Resolver.setTarget(testNode, target); - -// vm.expectRevert( -// abi.encodeWithSelector( -// IWriteDeferral.StorageHandledByL2.selector, 31337, target -// ) -// ); -// l1Resolver.setContenthash(dnsName, "contenthash"); -// } - -// function test_RevertWhen_GetContentHash() public { -// vm.expectRevert(); -// l1Resolver.resolve( -// dnsName, -// abi.encodeWithSelector( -// IContentHashResolver.contenthash.selector, testNode -// ) -// ); -// } - -// function test_registerDomain() public { -// address expected = address(0x42); -// l1Resolver.register(dnsName, expected); -// (, address actual,) = l1Resolver.getTarget(dnsName); -// assertEq(expected, actual); -// } - -// function test_registerDomainOwnedOnChainByOwner() public { -// registry.setSubnodeOwner( -// namehash("eth"), labelhash("owned"), address(this) -// ); - -// (bytes memory owned, bytes32 expectedNode) = -// NameEncoder.dnsEncodeName("owned.eth"); -// address expected = address(0x281); -// l1Resolver.register(owned, expected); - -// (bytes32 actualNode, address actual,) = l1Resolver.getTarget(owned); -// assertEq(expected, actual); -// assertEq(expectedNode, actualNode); -// } - -// function test_registerDomainOwnedOnChainBySomeoneElse() public { -// registry.setSubnodeOwner( -// namehash("eth"), labelhash("owned"), address(0x999) -// ); - -// (bytes memory owned, bytes32 node) = -// NameEncoder.dnsEncodeName("owned.eth"); -// vm.expectRevert( -// abi.encodeWithSelector( -// L1Resolver.L1Resolver__UnavailableDomain.selector, node -// ) -// ); -// l1Resolver.register(owned, address(0x281)); -// } - -// function test_registerSubdomain() public { -// address expected = address(0x42); -// l1Resolver.register(dnsName, expected); -// (bytes memory subdomain,) = -// NameEncoder.dnsEncodeName("subdomain.test.eth"); -// l1Resolver.register(subdomain, expected); - -// (bytes32 node, address actual,) = l1Resolver.getTarget(subdomain); -// assertEq(expected, actual); -// assertEq(node, namehash("subdomain.test.eth")); -// } - -// function test_RevertIf_registerDomainDuplicated() public { -// address expected = address(0x42); -// l1Resolver.register(dnsName, expected); -// (bytes32 node,,) = l1Resolver.getTarget(dnsName); - -// vm.expectRevert( -// abi.encodeWithSelector( -// L1Resolver.L1Resolver__UnavailableDomain.selector, node -// ) -// ); -// l1Resolver.register(dnsName, address(0x24)); - -// (, address actual,) = l1Resolver.getTarget(dnsName); -// assertEq(expected, actual); -// } - -// function test_setOwnerRegisteredDomain() public { -// address expected = address(0x42); -// l1Resolver.register(dnsName, expected); - -// vm.expectRevert( -// abi.encodeWithSelector( -// IWriteDeferral.StorageHandledByL2.selector, chainId, expected -// ) -// ); -// l1Resolver.setOwner(dnsName, address(this)); -// } - -// function test_ReverIf_setOwnerUnregisteredDomain() public { -// vm.expectRevert( -// abi.encodeWithSelector( -// L1Resolver.L1Resolver__DomainNotFound.selector, testNode -// ) -// ); -// l1Resolver.setOwner(dnsName, address(this)); -// } - -// function test_OwnerCanRegisterDomain() public { -// address expected = address(0x42); -// l1Resolver.register(dnsName, expected); -// (, address actual,) = l1Resolver.getTarget(dnsName); -// assertEq(expected, actual); -// } - -// function test_RevertWhen_NonOwnerRegisterDomain() public { -// address expected = address(0x42); -// vm.prank(address(0x2024)); -// vm.expectRevert(); -// l1Resolver.register(dnsName, expected); -// } - -// function test_OwnerCanSetAddr() public { -// address target = address(0x123); -// l1Resolver.setTarget(testNode, target); -// vm.expectRevert( -// abi.encodeWithSelector( -// IWriteDeferral.StorageHandledByL2.selector, 31337, target -// ) -// ); -// l1Resolver.setAddr(dnsName, address(0x456)); -// } - -// function test_RevertWhen_NonOwnerSetAddr() public { -// address target = address(0x123); -// l1Resolver.setTarget(testNode, target); -// vm.prank(address(0x2024)); -// vm.expectRevert( -// abi.encodeWithSelector( -// IWriteDeferral.StorageHandledByL2.selector, 31337, target -// ) -// ); -// l1Resolver.setAddr(dnsName, address(0x456)); -// } -// } +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IAddrResolver} from + "@ens-contracts/resolvers/profiles/IAddrResolver.sol"; +import {AddrResolver} from "@ens-contracts/resolvers/profiles/AddrResolver.sol"; +import {TextResolver} from "@ens-contracts/resolvers/profiles/TextResolver.sol"; +import {ContentHashResolver} from + "@ens-contracts/resolvers/profiles/ContentHashResolver.sol"; +import {IAddressResolver} from + "@ens-contracts/resolvers/profiles/IAddressResolver.sol"; +import {IExtendedResolver} from + "@ens-contracts/resolvers/profiles/IExtendedResolver.sol"; + +import {OffchainRegister} from "../src/interfaces/OffchainResolver.sol"; +import {IEVMVerifier} from "../src/evmgateway/IEVMVerifier.sol"; +import {L1Verifier} from "../src/evmgateway/L1Verifier.sol"; +import {L1Resolver} from "../src/L1Resolver.sol"; +import { + WriteDeferral, L2WriteDeferral +} from "../src/interfaces/WriteDeferral.sol"; +import {ENSHelper} from "../script/ENSHelper.sol"; +import {ENSIP16} from "../src/ENSIP16.sol"; + +contract L1ResolverTest is Test, ENSHelper { + + L1Resolver l1Resolver; + uint32 constant chainId = 31337; + address constant TARGET_RESOLVER = address(2); + address constant TARGET_REGISTRAR = address(3); + + function setUp() public { + string[] memory urls = new string[](1); + urls[0] = "http://localhost:3000/{sender}/{data}.json"; + + IEVMVerifier verifier = new L1Verifier(urls); + l1Resolver = new L1Resolver( + chainId, TARGET_RESOLVER, TARGET_REGISTRAR, verifier, urls[0] + ); + } + + function test_ConstructorChainId() public view { + assertEq(l1Resolver.chainId(), chainId); + } + + function test_OwnerCanSetTarget() public { + address expected = address(0x123); + l1Resolver.setTarget(l1Resolver.TARGET_RESOLVER(), expected); + address actual = l1Resolver.targets(l1Resolver.TARGET_RESOLVER()); + assertEq(actual, expected); + } + + function test_EmitEventOnSetTarget() public { + address target = address(0x123); + vm.expectEmit(true, true, true, false); + emit L2WriteDeferral.L2HandlerContractAddressChanged( + chainId, TARGET_RESOLVER, target + ); + l1Resolver.setTarget(l1Resolver.TARGET_RESOLVER(), target); + } + + function test_RevertWhen_UnauthorizedSetTarget() public { + address target = address(0x123); + + bytes32 node = namehash("test.eth"); + vm.prank(address(0x2024)); + vm.expectRevert(bytes("Ownable: caller is not the owner")); + l1Resolver.setTarget(node, target); + } + + //////// WRITE PARAMS TESTS //////// + + function test_WriteParamsRegister() public { + bytes memory name = "test.eth"; + bytes memory data = abi.encodeWithSelector( + OffchainRegister.register.selector, + name, + address(0), + 0, + bytes32(0), + address(0), + new bytes[](0), + false, + 0 + ); + + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, + chainId, + TARGET_REGISTRAR + ) + ); + l1Resolver.writeParams(name, data); + } + + function test_WriteParamsSetAddr() public { + bytes memory name = "test.eth"; + bytes memory data = abi.encodeWithSelector( + bytes4(keccak256("setAddr(bytes32,address)")), + bytes32(0), + address(0) + ); + + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, + chainId, + TARGET_RESOLVER + ) + ); + l1Resolver.writeParams(name, data); + } + + function test_WriteParamsSetAddrWithCoinType() public { + bytes memory name = "test.eth"; + bytes memory data = abi.encodeWithSelector( + bytes4(keccak256("setAddr(bytes32,uint256,bytes)")), + bytes32(0), + uint256(0), + new bytes(0) + ); + + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, + chainId, + TARGET_RESOLVER + ) + ); + l1Resolver.writeParams(name, data); + } + + function test_WriteParamsSetText() public { + bytes memory name = "test.eth"; + bytes memory data = abi.encodeWithSelector( + bytes4(keccak256("setText(bytes32,string,string)")), + bytes32(0), + "", + "" + ); + + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, + chainId, + TARGET_RESOLVER + ) + ); + l1Resolver.writeParams(name, data); + } + + function test_WriteParamsSetContenthash() public { + bytes memory name = "test.eth"; + bytes memory data = abi.encodeWithSelector( + bytes4(keccak256("setContenthash(bytes32,bytes)")), + bytes32(0), + new bytes(0) + ); + + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, + chainId, + TARGET_RESOLVER + ) + ); + l1Resolver.writeParams(name, data); + } + + function test_WriteParamsUnsupportedFunction() public { + bytes memory name = "test.eth"; + bytes memory data = abi.encodeWithSelector( + bytes4(keccak256("unsupportedFunction()")), bytes32(0) + ); + + vm.expectRevert(WriteDeferral.FunctionNotSupported.selector); + l1Resolver.writeParams(name, data); + } + + function test_RevertWhen_GetAddr() public { + vm.expectRevert(); + l1Resolver.resolve( + bytes("test.eth"), + abi.encodeWithSelector(IAddrResolver.addr.selector, bytes32(0)) + ); + } + + function test_RevertWhen_SetText() public { + address target = address(0x456); + l1Resolver.setTarget(l1Resolver.TARGET_RESOLVER(), target); + + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, chainId, target + ) + ); + l1Resolver.setText(bytes32(0), "com.twitter", "@blockful"); + } + + function test_RevertWhen_GetText() public { + vm.expectRevert(); + l1Resolver.resolve( + bytes("test.eth"), + abi.encodeWithSelector( + L1Resolver.text.selector, bytes32(0), "com.twitter" + ) + ); + } + + function test_RevertWhen_SetContentHash() public { + address target = address(0x456); + l1Resolver.setTarget(l1Resolver.TARGET_RESOLVER(), target); + + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, chainId, target + ) + ); + l1Resolver.setContenthash(bytes32(0), bytes("contenthash")); + } + + function test_RevertWhen_GetContentHash() public { + vm.expectRevert(); + l1Resolver.resolve( + bytes("test.eth"), + abi.encodeWithSelector(L1Resolver.contenthash.selector, bytes32(0)) + ); + } + + function test_OwnerCanSetAddr() public { + address target = address(0x123); + l1Resolver.setTarget(l1Resolver.TARGET_RESOLVER(), target); + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, chainId, target + ) + ); + l1Resolver.setAddr(bytes32(0), address(0x456)); + } + + function test_RevertWhen_NonOwnerSetAddr() public { + address target = address(0x123); + l1Resolver.setTarget(l1Resolver.TARGET_RESOLVER(), target); + vm.prank(address(0x2024)); + vm.expectRevert( + abi.encodeWithSelector( + L2WriteDeferral.StorageHandledByL2.selector, chainId, target + ) + ); + l1Resolver.setAddr(bytes32(0), address(0x456)); + } + + function test_OwnerCanSetMetadataUrl() public { + string memory newUrl = "https://newurl.com/{sender}/{data}.json"; + l1Resolver.setMetadataUrl(newUrl); + } + + function test_RevertWhen_NonOwnerSetMetadataUrl() public { + string memory newUrl = "https://newurl.com/{sender}/{data}.json"; + vm.prank(address(0x2024)); + vm.expectRevert("Ownable: caller is not the owner"); + l1Resolver.setMetadataUrl(newUrl); + } + + function test_OwnerCanSetRandomTarget() public { + bytes32 key = keccak256("test"); + address newTarget = address(0x789); + l1Resolver.setTarget(key, newTarget); + assertEq(l1Resolver.targets(key), newTarget); + } + + function test_RevertWhen_NonOwnerSetRandomTarget() public { + bytes32 key = keccak256("test"); + address newTarget = address(0x789); + vm.prank(address(0x2024)); + vm.expectRevert("Ownable: caller is not the owner"); + l1Resolver.setTarget(key, newTarget); + } + + function test_SupportsInterface() public view { + assertTrue( + l1Resolver.supportsInterface(type(IExtendedResolver).interfaceId) + ); + assertTrue( + l1Resolver.supportsInterface(type(L2WriteDeferral).interfaceId) + ); + assertTrue(l1Resolver.supportsInterface(type(IERC165).interfaceId)); + assertTrue(l1Resolver.supportsInterface(type(ENSIP16).interfaceId)); + assertTrue(l1Resolver.supportsInterface(type(AddrResolver).interfaceId)); + assertTrue(l1Resolver.supportsInterface(type(TextResolver).interfaceId)); + assertTrue( + l1Resolver.supportsInterface(type(ContentHashResolver).interfaceId) + ); + } + +} From 3d4eeb8a5542af5a8391e9382b8842d02c97379f Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Fri, 29 Nov 2024 17:15:16 +0700 Subject: [PATCH 04/16] feat: register with registerParams --- packages/client/src/write.ts | 80 +++++++++++-------- packages/contracts/package.json | 1 + packages/contracts/src/L1Resolver.sol | 7 +- .../contracts/src/SubdomainController.sol | 30 ++++++- 4 files changed, 83 insertions(+), 35 deletions(-) diff --git a/packages/client/src/write.ts b/packages/client/src/write.ts index a00181f0..820bf87f 100644 --- a/packages/client/src/write.ts +++ b/packages/client/src/write.ts @@ -7,6 +7,7 @@ import { config } from 'dotenv' import { Hex, createPublicClient, + decodeErrorResult, encodeFunctionData, http, namehash, @@ -19,6 +20,8 @@ import { normalize, packetToBytes } from 'viem/ens' import { privateKeyToAccount } from 'viem/accounts' import { abi as l1Abi } from '@blockful/contracts/out/L1Resolver.sol/L1Resolver.json' +import { abi as universalResolverResolveAbi } from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' +import { abi as scAbi } from '@blockful/contracts/out/SubdomainController.sol/SubdomainController.json' import { MessageData, DomainData } from '@blockful/gateway/src/types' import { getRevertErrorData, getChain, handleDBStorage } from './client' @@ -57,31 +60,6 @@ const _ = (async () => { const node = namehash(name) const signer = privateKeyToAccount(privateKey as Hex) - const duration = 31556952000n - - const resolverAddr = await client.getEnsResolver({ - name, - universalResolverAddress: universalResolver as Hex, - }) - - // SUBDOMAIN PRICING - - let value = 0n - try { - const [_value /* commitTime */ /* extraData */, ,] = - (await client.readContract({ - address: resolverAddr, - abi: l1Abi, - functionName: 'registerParams', - args: [toHex(name), duration], - })) as [bigint, bigint, Hex] - value = _value - } catch { - // interface not implemented by the resolver - } - - // REGISTER NEW SUBDOMAIN - const data: Hex[] = [ encodeFunctionData({ functionName: 'setText', @@ -110,9 +88,10 @@ const _ = (async () => { }), ] + const duration = 31556952000n const calldata = { functionName: 'register', - abi: l1Abi, + abi: scAbi, args: [ encodedName, signer.address, // owner @@ -124,18 +103,35 @@ const _ = (async () => { 0, // fuses zeroHash, ], - address: resolverAddr, account: signer, - value, } try { - await client.simulateContract(calldata) + await client.readContract({ + address: universalResolver as Hex, + abi: universalResolverResolveAbi, + functionName: 'resolve', + args: [ + encodedName, + encodeFunctionData({ + functionName: 'writeParams', + abi: l1Abi, + args: [encodedName, encodeFunctionData(calldata)], + }), + ], + }) } catch (err) { const data = getRevertErrorData(err) - switch (data?.errorName) { + if (!data || data.args.length === 0) return + + const [params] = data.args + const errorResult = decodeErrorResult({ + abi: l1Abi, + data: params as Hex, + }) + switch (errorResult?.errorName) { case 'StorageHandledByOffChainDatabase': { - const [domain, url, message] = data.args as [ + const [domain, url, message] = errorResult.args as [ DomainData, string, MessageData, @@ -144,17 +140,37 @@ const _ = (async () => { return } case 'StorageHandledByL2': { - const [chainId, contractAddress] = data.args as [bigint, `0x${string}`] + const [chainId, contractAddress] = errorResult.args as [ + bigint, + `0x${string}`, + ] const l2Client = createPublicClient({ chain: getChain(Number(chainId)), transport: http(providerL2), }).extend(walletActions) + // SUBDOMAIN PRICING + + let value = 0n + try { + const [_value /* commitTime */ /* extraData */, ,] = + (await client.readContract({ + address: contractAddress, + abi: scAbi, + functionName: 'registerParams', + args: [encodedName, duration], + })) as [bigint, bigint, Hex] + value = _value + } catch { + // interface not implemented by the resolver + } + try { const { request } = await l2Client.simulateContract({ ...calldata, address: contractAddress, + value, }) await l2Client.writeContract(request) } catch (err) { diff --git a/packages/contracts/package.json b/packages/contracts/package.json index dd90e5b3..5fd58cd9 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -9,6 +9,7 @@ "lint": "forge fmt", "build": "forge build", "dev:db": "forge script script/local/DatabaseResolver.sol --rpc-url http://localhost:8545 --broadcast -vvv --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "dev:arb": "npm-run-all --sequential dev:arb:l2 dev:arb:l1", "dev:arb:l1": "forge script script/local/L1ArbitrumResolver.sol --rpc-url http://localhost:8545 --broadcast -vvv --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", "dev:arb:l2": "forge script script/local/L2ArbitrumResolver.sol --rpc-url http://localhost:8547 --broadcast -vvv --private-key 0xb6b15c8cb491557369f3c7d2c287b053eb229daa9c22138887752191c9520659" }, diff --git a/packages/contracts/src/L1Resolver.sol b/packages/contracts/src/L1Resolver.sol index 237034d2..38adec74 100644 --- a/packages/contracts/src/L1Resolver.sol +++ b/packages/contracts/src/L1Resolver.sol @@ -104,7 +104,7 @@ contract L1Resolver is bytes calldata, /* name */ bytes calldata data ) - external + public view { bytes4 selector = bytes4(data); @@ -159,6 +159,11 @@ contract L1Resolver is bytes32 node = abi.decode(data[4:], (bytes32)); return _contenthash(node); } + if (selector == this.writeParams.selector) { + (bytes memory name, bytes memory _data) = + abi.decode(data[4:], (bytes, bytes)); + this.writeParams(name, _data); + } } //////// ENS ERC-137 //////// diff --git a/packages/contracts/src/SubdomainController.sol b/packages/contracts/src/SubdomainController.sol index c6bd0a50..2d91fa9f 100644 --- a/packages/contracts/src/SubdomainController.sol +++ b/packages/contracts/src/SubdomainController.sol @@ -4,11 +4,20 @@ pragma solidity ^0.8.17; import {INameWrapper} from "@ens-contracts/wrapper/INameWrapper.sol"; import {Resolver} from "@ens-contracts/resolvers/Resolver.sol"; import {BytesUtils} from "@ens-contracts/utils/BytesUtils.sol"; +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import {ENSHelper} from "../script/ENSHelper.sol"; -import {OffchainRegister} from "./interfaces/OffchainResolver.sol"; +import { + OffchainRegister, + OffchainRegisterParams +} from "./interfaces/OffchainResolver.sol"; -contract SubdomainController is OffchainRegister, ENSHelper { +contract SubdomainController is + IERC165, + OffchainRegister, + OffchainRegisterParams, + ENSHelper +{ using BytesUtils for bytes; @@ -26,6 +35,17 @@ contract SubdomainController is OffchainRegister, ENSHelper { nameWrapper = INameWrapper(_nameWrapperAddress); } + function registerParams( + bytes calldata, /* name */ + uint256 /* duration */ + ) + external + view + returns (uint256, uint256, bytes memory) + { + return (price, commitTime, ""); + } + function register( bytes calldata name, address owner, @@ -72,4 +92,10 @@ contract SubdomainController is OffchainRegister, ENSHelper { return string(name[1:labelLength + 1]); } + function supportsInterface(bytes4 interfaceId) public pure returns (bool) { + return interfaceId == type(IERC165).interfaceId + || interfaceId == type(OffchainRegister).interfaceId + || interfaceId == type(OffchainRegisterParams).interfaceId; + } + } From cc73677596d2e5eafb865ebde6e87bd6fffcb690 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Fri, 29 Nov 2024 17:30:51 +0700 Subject: [PATCH 05/16] chore: remove contenthash from read --- packages/client/src/read.ts | 33 +++------------------------------ 1 file changed, 3 insertions(+), 30 deletions(-) diff --git a/packages/client/src/read.ts b/packages/client/src/read.ts index 140f7fdb..29a60653 100644 --- a/packages/client/src/read.ts +++ b/packages/client/src/read.ts @@ -4,17 +4,10 @@ */ import { config } from 'dotenv' -import { - Hex, - createPublicClient, - http, - namehash, - decodeFunctionResult, - hexToString, -} from 'viem' +import { Hex, createPublicClient, http } from 'viem' import { normalize } from 'viem/ens' import { getChain } from './client' -import { abi as l1Abi } from '@blockful/contracts/out/L1Resolver.sol/L1Resolver.json' + config({ path: process.env.ENV_FILE || '../.env', }) @@ -36,7 +29,7 @@ const client = createPublicClient({ // eslint-disable-next-line const _ = (async () => { - const name = normalize('lucas.arb.eth') + const name = normalize('gibi.arb.eth') const twitter = await client.getEnsText({ name, @@ -67,31 +60,11 @@ const _ = (async () => { gatewayUrls: [gateway], }) - const resolver = await client.getEnsResolver({ - name, - universalResolverAddress: universalResolverAddress as Hex, - }) - const encodedContentHash = (await client.readContract({ - address: resolver, - functionName: 'contenthash', - abi: l1Abi, - args: [namehash(name)], - })) as Hex - - const contentHash = hexToString( - decodeFunctionResult({ - abi: l1Abi, - functionName: 'contenthash', - data: encodedContentHash, - }) as Hex, - ) - console.log({ twitter, avatar, address, addressBtc, name: domainName, - contentHash, }) })() From da756a243bf139608f4886a84cab9277c8c4215f Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Tue, 3 Dec 2024 18:17:10 +0700 Subject: [PATCH 06/16] feat(db): writeParams db returning calldata from args --- packages/contracts/src/DatabaseResolver.sol | 47 ++++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/packages/contracts/src/DatabaseResolver.sol b/packages/contracts/src/DatabaseResolver.sol index 290fc547..d7315bc2 100644 --- a/packages/contracts/src/DatabaseResolver.sol +++ b/packages/contracts/src/DatabaseResolver.sol @@ -106,19 +106,21 @@ contract DatabaseResolver is //////// ENSIP Wildcard Writing //////// /** - * @notice Validates and processes write parameters for deferred storage mutations + * @notice Read call for fetching the required parameters for the offchain call + * @notice avoiding multiple transactions * @param -name The encoded name or identifier of the write operation - * @param -data The encoded data to be written + * @param data The encoded data to be written * @dev This function reverts with StorageHandledByL2 error to indicate L2 deferral */ function writeParams( bytes calldata, /* name */ - bytes calldata /* data */ + bytes calldata data ) - external + public view + override { - _offChainStorage(); + _offChainStorage(data); } //////// ENSIP 10 //////// @@ -139,10 +141,17 @@ contract DatabaseResolver is override returns (bytes memory) { - if (bytes4(data[:4]) == this.name.selector) { - // name(bytes32) should be handled on-chain - (, bytes memory result) = address(this).staticcall(data); - return result; + // EIP-181 `name(bytes32)` should be handled on-chain because the ENS + // contracts call it during + if (bytes4(data[:4]) == NameResolver.name.selector) { + (bytes32 node) = abi.decode(data[4:], (bytes32)); + return bytes(this.name(node)); + } + + if (bytes4(data[:4]) == this.writeParams.selector) { + (bytes memory name, bytes memory _data) = + abi.decode(data[4:], (bytes, bytes)); + this.writeParams(name, _data); } _offChainLookup(data); @@ -152,7 +161,6 @@ contract DatabaseResolver is /** * Sets the address associated with an ENS node. - * May only be called by the owner of that node in the ENS registry. * @param -node The node to update. * @param -a The address to set. */ @@ -164,7 +172,7 @@ contract DatabaseResolver is view override { - _offChainStorage(); + _offChainStorage(msg.data); } /** @@ -186,7 +194,6 @@ contract DatabaseResolver is /** * Sets the address associated with an ENS node. - * May only be called by the owner of that node in the ENS registry. * @param -node The node to update. * @param -coinType The constant used to define the coin type of the corresponding address. * @param -a The address to set. @@ -200,7 +207,7 @@ contract DatabaseResolver is view override { - _offChainStorage(); + _offChainStorage(msg.data); } /** @@ -225,7 +232,6 @@ contract DatabaseResolver is /** * Sets the text data associated with an ENS node and key. - * May only be called by the owner of that node in the ENS registry. * @param -node The node to update. * @param -key The key to set. * @param -value The text data value to set. @@ -239,7 +245,7 @@ contract DatabaseResolver is view override { - _offChainStorage(); + _offChainStorage(msg.data); } /** @@ -264,7 +270,6 @@ contract DatabaseResolver is /** * Sets the contenthash associated with an ENS node. - * May only be called by the owner of that node in the ENS registry. * @param -node The node to update. * @param -hash The contenthash to set */ @@ -276,7 +281,7 @@ contract DatabaseResolver is view override { - _offChainStorage(); + _offChainStorage(msg.data); } /** @@ -322,7 +327,7 @@ contract DatabaseResolver is view override { - _offChainStorage(); + _offChainStorage(msg.data); } //////// ENS ERC-619 LOGIC //////// @@ -345,7 +350,7 @@ contract DatabaseResolver is view override { - _offChainStorage(); + _offChainStorage(msg.data); } //////// CCIP READ (EIP-3668) //////// @@ -398,7 +403,7 @@ contract DatabaseResolver is /** * @notice Builds an StorageHandledByOffChainDatabase error. */ - function _offChainStorage() private view { + function _offChainStorage(bytes calldata callData) private view { revert StorageHandledByOffChainDatabase( DBWriteDeferral.domainData({ name: _WRITE_DEFERRAL_DOMAIN_NAME, @@ -408,7 +413,7 @@ contract DatabaseResolver is }), gatewayUrl, DBWriteDeferral.messageData({ - callData: msg.data, + callData: callData, sender: msg.sender, expirationTimestamp: block.timestamp + gatewayDatabaseTimeoutDuration From b78bd358b3fef905d6e77fc6c3a92dcd8c65fafb Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Tue, 3 Dec 2024 20:34:28 +0700 Subject: [PATCH 07/16] chore: revert changes to write deferral interface --- packages/client/src/write.ts | 16 +++--- packages/contracts/src/DatabaseResolver.sol | 13 +++-- packages/contracts/src/L1Resolver.sol | 17 ++++-- .../contracts/src/SubdomainController.sol | 2 +- .../{WriteDeferral.sol => IWriteDeferral.sol} | 52 +++---------------- ...fchainResolver.sol => WildcardWriting.sol} | 15 ++++++ packages/contracts/test/L1Resolver.t.sol | 6 +-- 7 files changed, 55 insertions(+), 66 deletions(-) rename packages/contracts/src/interfaces/{WriteDeferral.sol => IWriteDeferral.sol} (75%) rename packages/contracts/src/interfaces/{OffchainResolver.sol => WildcardWriting.sol} (85%) diff --git a/packages/client/src/write.ts b/packages/client/src/write.ts index 820bf87f..40e1ad92 100644 --- a/packages/client/src/write.ts +++ b/packages/client/src/write.ts @@ -19,7 +19,7 @@ import { import { normalize, packetToBytes } from 'viem/ens' import { privateKeyToAccount } from 'viem/accounts' -import { abi as l1Abi } from '@blockful/contracts/out/L1Resolver.sol/L1Resolver.json' +import { abi } from '@blockful/contracts/out/DatabaseResolver.sol/DatabaseResolver.json' import { abi as universalResolverResolveAbi } from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' import { abi as scAbi } from '@blockful/contracts/out/SubdomainController.sol/SubdomainController.json' import { MessageData, DomainData } from '@blockful/gateway/src/types' @@ -63,22 +63,22 @@ const _ = (async () => { const data: Hex[] = [ encodeFunctionData({ functionName: 'setText', - abi: l1Abi, + abi, args: [node, 'com.twitter', `@${name}`], }), encodeFunctionData({ functionName: 'setAddr', - abi: l1Abi, + abi, args: [node, '0x3a872f8FED4421E7d5BE5c98Ab5Ea0e0245169A0'], }), encodeFunctionData({ functionName: 'setAddr', - abi: l1Abi, + abi, args: [node, 1n, '0x3a872f8FED4421E7d5BE5c98Ab5Ea0e0245169A0'], }), encodeFunctionData({ functionName: 'setContenthash', - abi: l1Abi, + abi, args: [ node, stringToHex( @@ -115,18 +115,18 @@ const _ = (async () => { encodedName, encodeFunctionData({ functionName: 'writeParams', - abi: l1Abi, + abi, args: [encodedName, encodeFunctionData(calldata)], }), ], }) } catch (err) { const data = getRevertErrorData(err) - if (!data || data.args.length === 0) return + if (!data || !data.args || data.args?.length === 0) return const [params] = data.args const errorResult = decodeErrorResult({ - abi: l1Abi, + abi, data: params as Hex, }) switch (errorResult?.errorName) { diff --git a/packages/contracts/src/DatabaseResolver.sol b/packages/contracts/src/DatabaseResolver.sol index d7315bc2..e8beef7a 100644 --- a/packages/contracts/src/DatabaseResolver.sol +++ b/packages/contracts/src/DatabaseResolver.sol @@ -18,7 +18,8 @@ import {ContentHashResolver} from import {ENSIP16} from "./ENSIP16.sol"; import {SignatureVerifier} from "./SignatureVerifier.sol"; -import {WriteDeferral, DBWriteDeferral} from "./interfaces/WriteDeferral.sol"; +import {IWriteDeferral} from "./interfaces/IWriteDeferral.sol"; +import {WildcardWriting} from "./interfaces/WildcardWriting.sol"; import {EnumerableSetUpgradeable} from "./utils/EnumerableSetUpgradeable.sol"; /** @@ -29,7 +30,8 @@ contract DatabaseResolver is ERC165, ENSIP16, IExtendedResolver, - DBWriteDeferral, + IWriteDeferral, + WildcardWriting, AddrResolver, ABIResolver, PubkeyResolver, @@ -405,14 +407,14 @@ contract DatabaseResolver is */ function _offChainStorage(bytes calldata callData) private view { revert StorageHandledByOffChainDatabase( - DBWriteDeferral.domainData({ + IWriteDeferral.domainData({ name: _WRITE_DEFERRAL_DOMAIN_NAME, version: _WRITE_DEFERRAL_DOMAIN_VERSION, chainId: _CHAIN_ID, verifyingContract: address(this) }), gatewayUrl, - DBWriteDeferral.messageData({ + IWriteDeferral.messageData({ callData: callData, sender: msg.sender, expirationTimestamp: block.timestamp @@ -551,8 +553,9 @@ contract DatabaseResolver is ) returns (bool) { - return interfaceID == type(WriteDeferral).interfaceId + return interfaceID == type(IWriteDeferral).interfaceId || interfaceID == type(IExtendedResolver).interfaceId + || interfaceID == type(WildcardWriting).interfaceId || super.supportsInterface(interfaceID); } diff --git a/packages/contracts/src/L1Resolver.sol b/packages/contracts/src/L1Resolver.sol index 38adec74..c65d3087 100644 --- a/packages/contracts/src/L1Resolver.sol +++ b/packages/contracts/src/L1Resolver.sol @@ -18,20 +18,28 @@ import {ENSIP16} from "./ENSIP16.sol"; import {EVMFetcher} from "./evmgateway/EVMFetcher.sol"; import {IEVMVerifier} from "./evmgateway/IEVMVerifier.sol"; import {EVMFetchTarget} from "./evmgateway/EVMFetchTarget.sol"; -import {L2WriteDeferral} from "./interfaces/WriteDeferral.sol"; -import {OffchainRegister} from "./interfaces/OffchainResolver.sol"; +import {IWriteDeferral} from "./interfaces/IWriteDeferral.sol"; +import { + WildcardWriting, OffchainRegister +} from "./interfaces/WildcardWriting.sol"; contract L1Resolver is EVMFetchTarget, IExtendedResolver, IERC165, - L2WriteDeferral, + IWriteDeferral, Ownable, ENSIP16 { using EVMFetcher for EVMFetcher.EVMFetchRequest; + //////// ERRORS //////// + + /// @notice Error thrown when an unsupported function is called + /// @dev Used to indicate when a function call is not implemented or allowed + error FunctionNotSupported(); + //////// CONTRACT VARIABLE STATE //////// // address of each target contract @@ -372,7 +380,8 @@ contract L1Resolver is returns (bool) { return interfaceID == type(IExtendedResolver).interfaceId - || interfaceID == type(L2WriteDeferral).interfaceId + || interfaceID == type(IWriteDeferral).interfaceId + || interfaceID == type(WildcardWriting).interfaceId || interfaceID == type(EVMFetchTarget).interfaceId || interfaceID == type(IERC165).interfaceId || interfaceID == type(ENSIP16).interfaceId diff --git a/packages/contracts/src/SubdomainController.sol b/packages/contracts/src/SubdomainController.sol index 2d91fa9f..398e5621 100644 --- a/packages/contracts/src/SubdomainController.sol +++ b/packages/contracts/src/SubdomainController.sol @@ -10,7 +10,7 @@ import {ENSHelper} from "../script/ENSHelper.sol"; import { OffchainRegister, OffchainRegisterParams -} from "./interfaces/OffchainResolver.sol"; +} from "./interfaces/WildcardWriting.sol"; contract SubdomainController is IERC165, diff --git a/packages/contracts/src/interfaces/WriteDeferral.sol b/packages/contracts/src/interfaces/IWriteDeferral.sol similarity index 75% rename from packages/contracts/src/interfaces/WriteDeferral.sol rename to packages/contracts/src/interfaces/IWriteDeferral.sol index 3ac7913b..08f7a5e8 100644 --- a/packages/contracts/src/interfaces/WriteDeferral.sol +++ b/packages/contracts/src/interfaces/IWriteDeferral.sol @@ -1,30 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -interface WriteDeferral { - - /// @notice Validates and processes write parameters for deferred storage mutations - /// @param name The encoded name or identifier of the write operation - /// @param data The encoded data to be written - /// @dev This function should revert with appropriate errors when write operations need to be deferred - function writeParams( - bytes calldata name, - bytes calldata data - ) - external - view; - - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - /// @notice Error thrown when an unsupported function is called - /// @dev Used to indicate when a function call is not implemented or allowed - error FunctionNotSupported(); - -} - -interface L2WriteDeferral is WriteDeferral { +interface IWriteDeferral { /*////////////////////////////////////////////////////////////// EVENTS @@ -34,7 +11,6 @@ interface L2WriteDeferral is WriteDeferral { event L2HandlerDefaultChainIdChanged( uint256 indexed previousChainId, uint256 indexed newChainId ); - /// @notice Event raised when the contractAddress is changed for the L2 handler corresponding to chainId. event L2HandlerContractAddressChanged( uint256 indexed chainId, @@ -42,25 +18,6 @@ interface L2WriteDeferral is WriteDeferral { address indexed newContractAddress ); - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - /** - * @dev Error to raise when mutations are being deferred to an L2. - * @param chainId Chain ID to perform the deferred mutation to. - * @param contractAddress Contract Address at which the deferred mutation should transact with. - */ - error StorageHandledByL2(uint256 chainId, address contractAddress); - -} - -interface DBWriteDeferral is WriteDeferral { - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - /// @notice Event raised when the url is changed for the corresponding Off-Chain Database handler. event OffChainDatabaseHandlerURLChanged( string indexed previousUrl, string indexed newUrl @@ -100,6 +57,13 @@ interface DBWriteDeferral is WriteDeferral { ERRORS //////////////////////////////////////////////////////////////*/ + /** + * @dev Error to raise when mutations are being deferred to an L2. + * @param chainId Chain ID to perform the deferred mutation to. + * @param contractAddress Contract Address at which the deferred mutation should transact with. + */ + error StorageHandledByL2(uint256 chainId, address contractAddress); + /** * @dev Error to raise when mutations are being deferred to an Off-Chain Database. * @param sender the EIP-712 domain definition of the corresponding contract performing the off-chain database, write diff --git a/packages/contracts/src/interfaces/OffchainResolver.sol b/packages/contracts/src/interfaces/WildcardWriting.sol similarity index 85% rename from packages/contracts/src/interfaces/OffchainResolver.sol rename to packages/contracts/src/interfaces/WildcardWriting.sol index ad25f463..f74aba57 100644 --- a/packages/contracts/src/interfaces/OffchainResolver.sol +++ b/packages/contracts/src/interfaces/WildcardWriting.sol @@ -1,6 +1,21 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; +interface WildcardWriting { + + /// @notice Validates and processes write parameters for deferred storage mutations + /// @param name The encoded name or identifier of the write operation + /// @param data The encoded data to be written + /// @dev This function should revert with appropriate errors when write operations need to be deferred + function writeParams( + bytes calldata name, + bytes calldata data + ) + external + view; + +} + interface OffchainRegister { /** diff --git a/packages/contracts/test/L1Resolver.t.sol b/packages/contracts/test/L1Resolver.t.sol index 29fef030..908e913f 100644 --- a/packages/contracts/test/L1Resolver.t.sol +++ b/packages/contracts/test/L1Resolver.t.sol @@ -15,13 +15,11 @@ import {IAddressResolver} from import {IExtendedResolver} from "@ens-contracts/resolvers/profiles/IExtendedResolver.sol"; -import {OffchainRegister} from "../src/interfaces/OffchainResolver.sol"; +import {OffchainRegister} from "../src/interfaces/WildcardWriting.sol"; import {IEVMVerifier} from "../src/evmgateway/IEVMVerifier.sol"; import {L1Verifier} from "../src/evmgateway/L1Verifier.sol"; import {L1Resolver} from "../src/L1Resolver.sol"; -import { - WriteDeferral, L2WriteDeferral -} from "../src/interfaces/WriteDeferral.sol"; +import {IWriteDeferral} from "../src/interfaces/IWriteDeferral.sol"; import {ENSHelper} from "../script/ENSHelper.sol"; import {ENSIP16} from "../src/ENSIP16.sol"; From e93666c5aca2f3acdc825f1ac6760646d7cb54e8 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Wed, 4 Dec 2024 12:12:47 +0700 Subject: [PATCH 08/16] test: l1resolver writeparams test --- packages/contracts/test/L1Resolver.t.sol | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/contracts/test/L1Resolver.t.sol b/packages/contracts/test/L1Resolver.t.sol index 908e913f..626fd963 100644 --- a/packages/contracts/test/L1Resolver.t.sol +++ b/packages/contracts/test/L1Resolver.t.sol @@ -54,7 +54,7 @@ contract L1ResolverTest is Test, ENSHelper { function test_EmitEventOnSetTarget() public { address target = address(0x123); vm.expectEmit(true, true, true, false); - emit L2WriteDeferral.L2HandlerContractAddressChanged( + emit IWriteDeferral.L2HandlerContractAddressChanged( chainId, TARGET_RESOLVER, target ); l1Resolver.setTarget(l1Resolver.TARGET_RESOLVER(), target); @@ -87,7 +87,7 @@ contract L1ResolverTest is Test, ENSHelper { vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, + IWriteDeferral.StorageHandledByL2.selector, chainId, TARGET_REGISTRAR ) @@ -105,7 +105,7 @@ contract L1ResolverTest is Test, ENSHelper { vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, + IWriteDeferral.StorageHandledByL2.selector, chainId, TARGET_RESOLVER ) @@ -124,7 +124,7 @@ contract L1ResolverTest is Test, ENSHelper { vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, + IWriteDeferral.StorageHandledByL2.selector, chainId, TARGET_RESOLVER ) @@ -143,7 +143,7 @@ contract L1ResolverTest is Test, ENSHelper { vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, + IWriteDeferral.StorageHandledByL2.selector, chainId, TARGET_RESOLVER ) @@ -161,7 +161,7 @@ contract L1ResolverTest is Test, ENSHelper { vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, + IWriteDeferral.StorageHandledByL2.selector, chainId, TARGET_RESOLVER ) @@ -175,7 +175,7 @@ contract L1ResolverTest is Test, ENSHelper { bytes4(keccak256("unsupportedFunction()")), bytes32(0) ); - vm.expectRevert(WriteDeferral.FunctionNotSupported.selector); + vm.expectRevert(L1Resolver.FunctionNotSupported.selector); l1Resolver.writeParams(name, data); } @@ -193,7 +193,7 @@ contract L1ResolverTest is Test, ENSHelper { vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, chainId, target + IWriteDeferral.StorageHandledByL2.selector, chainId, target ) ); l1Resolver.setText(bytes32(0), "com.twitter", "@blockful"); @@ -215,7 +215,7 @@ contract L1ResolverTest is Test, ENSHelper { vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, chainId, target + IWriteDeferral.StorageHandledByL2.selector, chainId, target ) ); l1Resolver.setContenthash(bytes32(0), bytes("contenthash")); @@ -234,7 +234,7 @@ contract L1ResolverTest is Test, ENSHelper { l1Resolver.setTarget(l1Resolver.TARGET_RESOLVER(), target); vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, chainId, target + IWriteDeferral.StorageHandledByL2.selector, chainId, target ) ); l1Resolver.setAddr(bytes32(0), address(0x456)); @@ -246,7 +246,7 @@ contract L1ResolverTest is Test, ENSHelper { vm.prank(address(0x2024)); vm.expectRevert( abi.encodeWithSelector( - L2WriteDeferral.StorageHandledByL2.selector, chainId, target + IWriteDeferral.StorageHandledByL2.selector, chainId, target ) ); l1Resolver.setAddr(bytes32(0), address(0x456)); @@ -284,7 +284,7 @@ contract L1ResolverTest is Test, ENSHelper { l1Resolver.supportsInterface(type(IExtendedResolver).interfaceId) ); assertTrue( - l1Resolver.supportsInterface(type(L2WriteDeferral).interfaceId) + l1Resolver.supportsInterface(type(IWriteDeferral).interfaceId) ); assertTrue(l1Resolver.supportsInterface(type(IERC165).interfaceId)); assertTrue(l1Resolver.supportsInterface(type(ENSIP16).interfaceId)); From 867acbbe8ba8c84066526ed0c8582cb518314e59 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Wed, 4 Dec 2024 12:13:11 +0700 Subject: [PATCH 09/16] fix: get domain w/ contenthash table --- packages/gateway/src/repositories/postgres.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/gateway/src/repositories/postgres.ts b/packages/gateway/src/repositories/postgres.ts index b1da6c2b..a5be6dde 100644 --- a/packages/gateway/src/repositories/postgres.ts +++ b/packages/gateway/src/repositories/postgres.ts @@ -85,15 +85,15 @@ export class PostgresRepository { .getRepository(Domain) .createQueryBuilder('domain') .where('domain.node = :node', { node }) - .leftJoinAndMapOne( - 'domain.contenthash', - Contenthash, - 'contenthash', - 'contenthash.domain = domain.node', - ) if (includeRelations) { query + .leftJoinAndMapOne( + 'domain.contenthash', + Contenthash, + 'contenthash', + 'contenthash.domain = domain.node', + ) .leftJoinAndMapMany( 'domain.addresses', Address, From a4ffb3031721e6558f6a7fff95264763fc8c5b4e Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Wed, 4 Dec 2024 15:41:39 +0700 Subject: [PATCH 10/16] feat: l1 and db resolver implement imulticallable --- packages/contracts/src/DatabaseResolver.sol | 24 ++++++++++++++++++ packages/contracts/src/L1Resolver.sol | 25 +++++++++++++++++++ .../src/interfaces/WildcardWriting.sol | 12 --------- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/packages/contracts/src/DatabaseResolver.sol b/packages/contracts/src/DatabaseResolver.sol index e8beef7a..c84b5eb4 100644 --- a/packages/contracts/src/DatabaseResolver.sol +++ b/packages/contracts/src/DatabaseResolver.sol @@ -15,6 +15,7 @@ import {PubkeyResolver} from import {TextResolver} from "@ens-contracts/resolvers/profiles/TextResolver.sol"; import {ContentHashResolver} from "@ens-contracts/resolvers/profiles/ContentHashResolver.sol"; +import {IMulticallable} from "@ens-contracts/resolvers/IMulticallable.sol"; import {ENSIP16} from "./ENSIP16.sol"; import {SignatureVerifier} from "./SignatureVerifier.sol"; @@ -38,6 +39,7 @@ contract DatabaseResolver is TextResolver, ContentHashResolver, NameResolver, + IMulticallable, Ownable { @@ -556,7 +558,29 @@ contract DatabaseResolver is return interfaceID == type(IWriteDeferral).interfaceId || interfaceID == type(IExtendedResolver).interfaceId || interfaceID == type(WildcardWriting).interfaceId + || interfaceID == type(IMulticallable).interfaceId || super.supportsInterface(interfaceID); } + function multicall(bytes[] calldata /* data */ ) + external + view + override + returns (bytes[] memory) + { + _offChainStorage(msg.data); + } + + function multicallWithNodeCheck( + bytes32, + bytes[] calldata /* data */ + ) + external + view + override + returns (bytes[] memory) + { + _offChainStorage(msg.data); + } + } diff --git a/packages/contracts/src/L1Resolver.sol b/packages/contracts/src/L1Resolver.sol index c65d3087..0c8dfd44 100644 --- a/packages/contracts/src/L1Resolver.sol +++ b/packages/contracts/src/L1Resolver.sol @@ -13,6 +13,7 @@ import {TextResolver} from "@ens-contracts/resolvers/profiles/TextResolver.sol"; import {ContentHashResolver} from "@ens-contracts/resolvers/profiles/ContentHashResolver.sol"; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IMulticallable} from "@ens-contracts/resolvers/IMulticallable.sol"; import {ENSIP16} from "./ENSIP16.sol"; import {EVMFetcher} from "./evmgateway/EVMFetcher.sol"; @@ -27,7 +28,9 @@ contract L1Resolver is EVMFetchTarget, IExtendedResolver, IERC165, + WildcardWriting, IWriteDeferral, + IMulticallable, Ownable, ENSIP16 { @@ -388,6 +391,7 @@ contract L1Resolver is || interfaceID == type(AddrResolver).interfaceId || interfaceID == type(TextResolver).interfaceId || interfaceID == type(ContentHashResolver).interfaceId + || interfaceID == type(IMulticallable).interfaceId || super.supportsInterface(interfaceID); } @@ -411,4 +415,25 @@ contract L1Resolver is emit L2HandlerContractAddressChanged(chainId, prevAddr, target); } + function multicall(bytes[] calldata /* data */ ) + external + view + override + returns (bytes[] memory) + { + _offChainStorage(targets[TARGET_RESOLVER]); + } + + function multicallWithNodeCheck( + bytes32, + bytes[] calldata /* data */ + ) + external + view + override + returns (bytes[] memory) + { + _offChainStorage(targets[TARGET_RESOLVER]); + } + } diff --git a/packages/contracts/src/interfaces/WildcardWriting.sol b/packages/contracts/src/interfaces/WildcardWriting.sol index f74aba57..a2f15c79 100644 --- a/packages/contracts/src/interfaces/WildcardWriting.sol +++ b/packages/contracts/src/interfaces/WildcardWriting.sol @@ -66,18 +66,6 @@ interface OffchainRegisterParams { } -interface OffchainMulticallable { - - /** - * @notice Executes multiple calls in a single transaction. - * @param data An array of encoded function call data. - */ - function multicall(bytes[] calldata data) - external - returns (bytes[] memory); - -} - interface OffchainCommitable { /** From b4a2a4211909a642782f7d6a88d6e482a1d6835e Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Wed, 4 Dec 2024 15:57:52 +0700 Subject: [PATCH 11/16] test: db e2e writeParams --- packages/client/test/db.e2e.spec.ts | 163 +++++++++++++---------- packages/gateway/src/resolvers/domain.ts | 8 +- 2 files changed, 102 insertions(+), 69 deletions(-) diff --git a/packages/client/test/db.e2e.spec.ts b/packages/client/test/db.e2e.spec.ts index 692a3081..86276690 100644 --- a/packages/client/test/db.e2e.spec.ts +++ b/packages/client/test/db.e2e.spec.ts @@ -13,6 +13,9 @@ import { abi as abiDBResolver, bytecode as bytecodeDBResolver, } from '@blockful/contracts/out/DatabaseResolver.sol/DatabaseResolver.json' +import { abi as abiOffchainRegister } from '@blockful/contracts/out/WildcardWriting.sol/OffchainRegister.json' +import { abi as abiWriteDeferral } from '@blockful/contracts/out/IWriteDeferral.sol/IWriteDeferral.json' +import { abi as abiWildcardWriting } from '@blockful/contracts/out/WildcardWriting.sol/WildcardWriting.json' import { abi as abiRegistry, bytecode as bytecodeRegistry, @@ -25,8 +28,8 @@ import { abi as abiUniversalResolver, bytecode as bytecodeUniversalResolver, } from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' + import { DataSource } from 'typeorm' -import { abi } from '@blockful/gateway/src/abi' import { ChildProcess, spawn } from 'child_process' import { normalize, labelhash, namehash, packetToBytes } from 'viem/ens' import { anvil, sepolia } from 'viem/chains' @@ -45,6 +48,7 @@ import { toHex, stringToHex, decodeFunctionResult, + decodeErrorResult, } from 'viem' import { assert, expect } from 'chai' import { ApolloServer } from '@apollo/server' @@ -54,6 +58,7 @@ import { privateKeyToAddress, } from 'viem/accounts' +import { abi } from '@blockful/gateway/src/abi' import * as ccip from '@blockful/ccip-server' import { withGetAddr, @@ -185,10 +190,8 @@ function setupGateway( ethClient, repo, ]) - const server = new ccip.Server() server.app.use(withSigner(privateKey)) - server.add( abi, withQuery(), @@ -204,7 +207,7 @@ function setupGateway( } async function offchainWriting({ - name, + encodedName, functionName, args, signer, @@ -212,7 +215,7 @@ async function offchainWriting({ universalResolverAddress, chainId, }: { - name: string + encodedName: string functionName: string signer: PrivateKeyAccount abi: unknown[] @@ -220,29 +223,44 @@ async function offchainWriting({ universalResolverAddress: Hex chainId?: number }): Promise { - const [resolverAddr] = (await client.readContract({ - address: universalResolverAddress, - functionName: 'findResolver', - abi: abiUniversalResolver, - args: [toHex(packetToBytes(name))], - })) as Hash[] + const calldata = { + abi, + functionName, + args, + account: signer, + } try { - await client.simulateContract({ - address: resolverAddr, - abi, - functionName, - args, + await client.readContract({ + address: universalResolverAddress, + abi: abiUniversalResolver, + functionName: 'resolve', + args: [ + encodedName, + encodeFunctionData({ + functionName: 'writeParams', + abi: abiWildcardWriting, + args: [encodedName, encodeFunctionData(calldata)], + }), + ], }) } catch (err) { const data = getRevertErrorData(err) - if (data?.errorName === 'StorageHandledByOffChainDatabase') { - const [domain, url, message] = data?.args as [ + if (!data || !data.args || data.args?.length === 0) return + + const [params] = data.args + const errorResult = decodeErrorResult({ + abi: abiWriteDeferral, + data: params as Hex, + }) + if (errorResult?.errorName === 'StorageHandledByOffChainDatabase') { + const [domain, url, message] = errorResult?.args as [ DomainData, string, MessageData, ] + // using for testing the chainId validation if (chainId) { domain.chainId = chainId } @@ -288,29 +306,25 @@ describe('DatabaseResolver', () => { describe('Subdomain created on database', async () => { const name = normalize('database.eth') + const encodedName = toHex(packetToBytes(name)) const node = namehash(name) const resolver = '0x6AEBB4AdC056F3B01d225fE34c20b1FdC21323A2' - beforeEach(async () => { - let domain = new Domain() - domain.node = node - domain.name = name - domain.parent = namehash('eth') - domain.resolver = resolver - domain.resolverVersion = '1' - domain.owner = owner.address - domain.ttl = '300' - domain = await datasource.manager.save(domain) - }) + // used for testing with a domain already in the db + let domain = new Domain() + domain.node = node + domain.name = name + domain.parent = namehash('eth') + domain.resolver = resolver + domain.resolverVersion = '1' + domain.owner = owner.address + domain.ttl = '300' it('should register new domain', async () => { - const name = normalize('newdomain.eth') - const encodedName = toHex(packetToBytes(name)) - const node = namehash(name) const response = await offchainWriting({ - name, + encodedName, functionName: 'register', - abi: abiDBResolver, + abi: abiOffchainRegister, args: [ encodedName, owner.address, @@ -336,9 +350,6 @@ describe('DatabaseResolver', () => { }) it('should register new domain with records', async () => { - const name = normalize('newdomain.eth') - const encodedName = toHex(packetToBytes(name)) - const node = namehash(name) const calldata = [ encodeFunctionData({ abi: abiDBResolver, @@ -357,9 +368,9 @@ describe('DatabaseResolver', () => { }), ] const response = await offchainWriting({ - name, + encodedName, functionName: 'register', - abi: abiDBResolver, + abi: abiOffchainRegister, args: [ encodedName, owner.address, @@ -405,11 +416,12 @@ describe('DatabaseResolver', () => { }) it('should block register of duplicated domain with same owner', async () => { - const encodedName = toHex(packetToBytes(name)) + domain = await datasource.manager.save(domain) + const response = await offchainWriting({ - name, + encodedName, functionName: 'register', - abi: abiDBResolver, + abi: abiOffchainRegister, args: [ encodedName, owner.address, @@ -435,12 +447,13 @@ describe('DatabaseResolver', () => { }) it('should block register of duplicated domain with different owner', async () => { + domain = await datasource.manager.save(domain) + const newOwner = privateKeyToAccount(generatePrivateKey()) - const encodedName = toHex(packetToBytes(name)) const response = await offchainWriting({ - name, + encodedName, functionName: 'register', - abi: abiDBResolver, + abi: abiOffchainRegister, args: [ encodedName, newOwner.address, @@ -478,13 +491,10 @@ describe('DatabaseResolver', () => { it('should allow register a domain with different owner', async () => { const newOwner = privateKeyToAddress(generatePrivateKey()) - const name = normalize('newdomain.eth') - const encodedName = toHex(packetToBytes(name)) - const node = namehash(name) const response = await offchainWriting({ - name, + encodedName, functionName: 'register', - abi: abiDBResolver, + abi: abiOffchainRegister, args: [ encodedName, newOwner, @@ -546,8 +556,10 @@ describe('DatabaseResolver', () => { }) it('should write valid text record onto the database', async () => { + domain = await datasource.manager.save(domain) + const response = await offchainWriting({ - name, + encodedName, functionName: 'setText', abi: abiDBResolver, args: [node, 'com.twitter', '@blockful'], @@ -568,7 +580,7 @@ describe('DatabaseResolver', () => { it('should block unauthorized text change', async () => { const response = await offchainWriting({ - name, + encodedName, functionName: 'setText', abi: abiDBResolver, args: [node, 'com.twitter', '@unauthorized'], @@ -589,7 +601,7 @@ describe('DatabaseResolver', () => { it('should block writing text record with different chain ID', async () => { const response = await offchainWriting({ - name, + encodedName, functionName: 'setText', abi: abiDBResolver, args: [node, 'com.twitter', '@blockful'], @@ -656,8 +668,10 @@ describe('DatabaseResolver', () => { }) it('should write valid address record onto the database', async () => { + domain = await datasource.manager.save(domain) + const response = await offchainWriting({ - name, + encodedName, functionName: 'setAddr', abi: abiDBResolver, args: [node, '0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5'], @@ -677,7 +691,7 @@ describe('DatabaseResolver', () => { it('should block unauthorized text change', async () => { const response = await offchainWriting({ - name, + encodedName, functionName: 'setAddr', abi: abiDBResolver, args: [node, '0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5'], @@ -696,6 +710,8 @@ describe('DatabaseResolver', () => { }) it('should handle multicall valid write calls', async () => { + domain = await datasource.manager.save(domain) + const calls = [ encodeFunctionData({ abi: abiDBResolver, @@ -710,7 +726,7 @@ describe('DatabaseResolver', () => { ] const response = await offchainWriting({ - name, + encodedName, functionName: 'multicall', abi: abiDBResolver, args: [calls], @@ -735,6 +751,8 @@ describe('DatabaseResolver', () => { }) it('should handle multicall invalid write calls', async () => { + domain = await datasource.manager.save(domain) + const calls = [ encodeFunctionData({ abi: abiDBResolver, @@ -752,7 +770,7 @@ describe('DatabaseResolver', () => { ] const response = await offchainWriting({ - name, + encodedName, functionName: 'multicall', abi: abiDBResolver, args: [calls], @@ -778,7 +796,8 @@ describe('DatabaseResolver', () => { }) describe('2nd level domain on L1', () => { - const name = normalize('l1domain.eth') + const name = 'l1domain.eth' + const encodedName = toHex(packetToBytes(name)) const node = namehash(name) it('should set and read contenthash from database', async () => { @@ -786,7 +805,7 @@ describe('DatabaseResolver', () => { 'ipns://k51qzi5uqu5dgccx524mfjv7znyfsa6g013o6v4yvis9dxnrjbwojc62pt0450' const response = await offchainWriting({ - name, + encodedName, functionName: 'setContenthash', abi: abiDBResolver, args: [node, stringToHex(contentHash)], @@ -858,7 +877,7 @@ describe('DatabaseResolver', () => { it('should write valid text record onto the database', async () => { const response = await offchainWriting({ - name, + encodedName, functionName: 'setText', abi: abiDBResolver, args: [node, 'com.twitter', '@blockful'], @@ -879,7 +898,7 @@ describe('DatabaseResolver', () => { it('should block unauthorized text change', async () => { const response = await offchainWriting({ - name, + encodedName, functionName: 'setText', abi: abiDBResolver, args: [node, 'com.twitter', '@unauthorized'], @@ -936,7 +955,7 @@ describe('DatabaseResolver', () => { it('should block writing text record with different chain ID', async () => { const response = await offchainWriting({ - name, + encodedName, functionName: 'setText', abi: abiDBResolver, args: [node, 'com.twitter', '@blockful'], @@ -968,7 +987,7 @@ describe('DatabaseResolver', () => { it('should write valid address record onto the database', async () => { const response = await offchainWriting({ - name, + encodedName, functionName: 'setAddr', abi: abiDBResolver, args: [node, '0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5'], @@ -988,7 +1007,7 @@ describe('DatabaseResolver', () => { it('should block unauthorized text change', async () => { const response = await offchainWriting({ - name, + encodedName, functionName: 'setAddr', abi: abiDBResolver, args: [node, '0x95222290dd7278aa3ddd389cc1e1d165cc4bafe5'], @@ -1021,7 +1040,7 @@ describe('DatabaseResolver', () => { ] const response = await offchainWriting({ - name, + encodedName, functionName: 'multicall', abi: abiDBResolver, args: [calls], @@ -1063,7 +1082,7 @@ describe('DatabaseResolver', () => { ] const response = await offchainWriting({ - name, + encodedName, functionName: 'multicall', abi: abiDBResolver, args: [calls], @@ -1145,6 +1164,14 @@ describe('DatabaseResolver', () => { a2.resolverVersion = '2' await datasource.manager.save(a2) + const ch = new Contenthash() + ch.domain = node + ch.contenthash = + 'ipfs://QmYwWkU8H6x5xYz1234567890abcdefghijklmnopqrstuvwxyz' + ch.resolver = '0x2resolver' + ch.resolverVersion = '2' + await datasource.manager.save(ch) + const response = await server.executeOperation({ query: `query Domain($name: String!) { domain(name: $name) { @@ -1201,7 +1228,9 @@ describe('DatabaseResolver', () => { expect(actual.resolver.context).equal(owner.address) expect(actual.resolver.address).equal(dbResolverAddr) expect(actual.resolver.addr).equal('0x2') - expect(actual.resolver.contentHash).equal(null) + expect(actual.resolver.contentHash).equal( + 'ipfs://QmYwWkU8H6x5xYz1234567890abcdefghijklmnopqrstuvwxyz', + ) expect(actual.resolver.texts).eql([ { key: '1key', diff --git a/packages/gateway/src/resolvers/domain.ts b/packages/gateway/src/resolvers/domain.ts index 3eabaf77..b15578f2 100644 --- a/packages/gateway/src/resolvers/domain.ts +++ b/packages/gateway/src/resolvers/domain.ts @@ -1,11 +1,12 @@ import { Hex, labelhash, namehash, zeroAddress } from 'viem' import { normalize } from 'viem/ens' -import { DomainMetadata, NodeProps, GetDomainProps } from '../types' +import { DomainMetadata, NodeProps, GetDomainProps, Response } from '../types' import { Address, Domain, Text } from '../entities' import { extractLabelFromName, extractParentFromName } from '../utils' interface ReadRepository { + getContentHash(params: GetDomainProps): Promise getDomain(params: GetDomainProps): Promise getSubdomains({ node }: NodeProps): Promise getTexts({ node }: NodeProps): Promise @@ -80,6 +81,9 @@ export async function domainResolver({ ? domain.addresses : await repo.getAddresses({ node }) const ethAddr = addresses.find((addr) => addr.coin === '60')?.address + const contentHash = domain + ? domain.contenthash?.contenthash + : (await repo.getContentHash({ node }))?.value const owner = domain?.owner || (await client.getOwner(node)) return { @@ -103,7 +107,7 @@ export async function domainResolver({ context: owner, address: resolver, addr: ethAddr, - contentHash: domain?.contenthash?.contenthash, + contentHash, texts: texts.map((t) => ({ key: t.key, value: t.value })), addresses: addresses.map((addr) => ({ address: addr.address, From 368f3dd8df9420c0302c3b16cb6fedd0e29b8121 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Wed, 4 Dec 2024 17:04:46 +0700 Subject: [PATCH 12/16] test: decouple metadata e2e test --- packages/client/package.json | 1 + packages/client/test/db.e2e.spec.ts | 440 ++-------------------- packages/client/test/helpers.ts | 167 ++++++++ packages/client/test/metadata.e2e.spec.ts | 334 ++++++++++++++++ 4 files changed, 523 insertions(+), 419 deletions(-) create mode 100644 packages/client/test/helpers.ts create mode 100644 packages/client/test/metadata.e2e.spec.ts diff --git a/packages/client/package.json b/packages/client/package.json index e3097f4d..c9ab9f57 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -4,6 +4,7 @@ "test": "npm-run-all test:**", "test:l1": "hardhat test ./test/arb.e2e.spec.ts --network localhost", "test:db": "hardhat test ./test/db.e2e.spec.ts --network localhost", + "test:metadata": "hardhat test ./test/metadata.e2e.spec.ts --network localhost", "start:read": "ts-node src/read.ts", "start:write": "ts-node src/write.ts", "lint": "eslint . --ext .ts --fix" diff --git a/packages/client/test/db.e2e.spec.ts b/packages/client/test/db.e2e.spec.ts index 86276690..bcec5a58 100644 --- a/packages/client/test/db.e2e.spec.ts +++ b/packages/client/test/db.e2e.spec.ts @@ -9,39 +9,23 @@ import 'reflect-metadata' // Importing abi and bytecode from contracts folder -import { - abi as abiDBResolver, - bytecode as bytecodeDBResolver, -} from '@blockful/contracts/out/DatabaseResolver.sol/DatabaseResolver.json' +import { abi as abiDBResolver } from '@blockful/contracts/out/DatabaseResolver.sol/DatabaseResolver.json' import { abi as abiOffchainRegister } from '@blockful/contracts/out/WildcardWriting.sol/OffchainRegister.json' import { abi as abiWriteDeferral } from '@blockful/contracts/out/IWriteDeferral.sol/IWriteDeferral.json' import { abi as abiWildcardWriting } from '@blockful/contracts/out/WildcardWriting.sol/WildcardWriting.json' -import { - abi as abiRegistry, - bytecode as bytecodeRegistry, -} from '@blockful/contracts/out/ENSRegistry.sol/ENSRegistry.json' -import { - abi as abiRegistrar, - bytecode as bytecodeRegistrar, -} from '@blockful/contracts/out/BaseRegistrarImplementation.sol/BaseRegistrarImplementation.json' -import { - abi as abiUniversalResolver, - bytecode as bytecodeUniversalResolver, -} from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' + +import { abi as abiUniversalResolver } from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' import { DataSource } from 'typeorm' import { ChildProcess, spawn } from 'child_process' -import { normalize, labelhash, namehash, packetToBytes } from 'viem/ens' +import { normalize, namehash, packetToBytes } from 'viem/ens' import { anvil, sepolia } from 'viem/chains' import { createTestClient, http, publicActions, - Hash, - getContractAddress, walletActions, zeroHash, - getContract, encodeFunctionData, Hex, PrivateKeyAccount, @@ -50,33 +34,14 @@ import { decodeFunctionResult, decodeErrorResult, } from 'viem' -import { assert, expect } from 'chai' -import { ApolloServer } from '@apollo/server' +import { expect } from 'chai' import { generatePrivateKey, privateKeyToAccount, privateKeyToAddress, } from 'viem/accounts' -import { abi } from '@blockful/gateway/src/abi' -import * as ccip from '@blockful/ccip-server' -import { - withGetAddr, - withGetContentHash, - withGetText, - withQuery, - withSetAddr, - withSetText, - withRegisterDomain, - withSetContentHash, -} from '@blockful/gateway/src/handlers' -import { - DomainData, - MessageData, - typeDefs, - DomainMetadata, -} from '@blockful/gateway/src/types' -import { domainResolver } from '@blockful/gateway/src/resolvers' +import { DomainData, MessageData } from '@blockful/gateway/src/types' import { PostgresRepository } from '@blockful/gateway/src/repositories' import { Text, @@ -84,21 +49,8 @@ import { Address, Contenthash, } from '@blockful/gateway/src/entities' -import { withSigner } from '@blockful/gateway/src/middlewares' -import { - EthereumClient, - OwnershipValidator, - SignatureRecover, -} from '@blockful/gateway/src/services' import { getRevertErrorData, handleDBStorage } from '../src/client' - -const GATEWAY_URL = 'http://127.0.0.1:3000/{sender}/{data}.json' -const GRAPHQL_URL = 'http://127.0.0.1:4000' - -let universalResolverAddress: Hash, - registryAddr: Hash, - dbResolverAddr: Hash, - registrarAddr: Hash +import { deployContracts, setupGateway } from './helpers' const client = createTestClient({ chain: anvil, @@ -108,104 +60,6 @@ const client = createTestClient({ .extend(publicActions) .extend(walletActions) -async function deployContract({ - abi, - bytecode, - account, - args, -}: { - abi: unknown[] - bytecode: Hash - account: Hash - args?: unknown[] -}): Promise { - const txHash = await client.deployContract({ - abi, - bytecode, - account, - args, - }) - - const { nonce } = await client.getTransaction({ - hash: txHash, - }) - - return await getContractAddress({ - from: account, - nonce: BigInt(nonce), - }) -} - -async function deployContracts(signer: Hash) { - registryAddr = await deployContract({ - abi: abiRegistry, - bytecode: bytecodeRegistry.object as Hash, - account: signer, - }) - - const registry = await getContract({ - abi: abiRegistry, - address: registryAddr, - client, - }) - - universalResolverAddress = await deployContract({ - abi: abiUniversalResolver, - bytecode: bytecodeUniversalResolver.object as Hash, - account: signer, - args: [registryAddr, [GATEWAY_URL]], - }) - - registrarAddr = await deployContract({ - abi: abiRegistrar, - bytecode: bytecodeRegistrar.object as Hash, - account: signer, - args: [registryAddr, namehash('eth')], - }) - - dbResolverAddr = await deployContract({ - abi: abiDBResolver, - bytecode: bytecodeDBResolver.object as Hash, - account: signer, - args: [GATEWAY_URL, GRAPHQL_URL, 600, [signer]], - }) - - await registry.write.setSubnodeRecord( - [zeroHash, labelhash('eth'), signer, dbResolverAddr, 10000000], - { account: signer }, - ) - await registry.write.setSubnodeRecord( - [namehash('eth'), labelhash('l1domain'), signer, dbResolverAddr, 10000000], - { account: signer }, - ) -} - -function setupGateway( - privateKey: `0x${string}`, - { repo }: { repo: PostgresRepository }, -) { - const signatureRecover = new SignatureRecover() - const ethClient = new EthereumClient(client, registryAddr, registrarAddr) - const validator = new OwnershipValidator(anvil.id, signatureRecover, [ - ethClient, - repo, - ]) - const server = new ccip.Server() - server.app.use(withSigner(privateKey)) - server.add( - abi, - withQuery(), - withGetText(repo), - withRegisterDomain(repo), - withSetText(repo, validator), - withGetAddr(repo), - withSetAddr(repo, validator), - withGetContentHash(repo), - withSetContentHash(repo, validator), - ) - server.makeApp('/').listen('3000') -} - async function offchainWriting({ encodedName, functionName, @@ -269,17 +123,27 @@ async function offchainWriting({ } } -describe('DatabaseResolver', () => { +describe('DatabaseResolver', async () => { let repo: PostgresRepository, datasource: DataSource const owner = privateKeyToAccount( '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', ) let localNode: ChildProcess + let registryAddr: Hex, universalResolverAddress: Hex, registrarAddr: Hex before(async () => { localNode = spawn('anvil') - await deployContracts(owner.address) + const { + registryAddr: _registryAddr, + universalResolverAddr: _universalResolverAddress, + registrarAddr: _registrarAddr, + } = await deployContracts(owner.address) + + registryAddr = _registryAddr + universalResolverAddress = _universalResolverAddress + registrarAddr = _registrarAddr + datasource = new DataSource({ type: 'better-sqlite3', database: './test.db', @@ -290,6 +154,8 @@ describe('DatabaseResolver', () => { setupGateway( '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', { repo }, + registryAddr, + registrarAddr, ) }) @@ -1105,269 +971,5 @@ describe('DatabaseResolver', () => { }) expect(address).eq(null) }) - - describe('Metadata API', async () => { - let server: ApolloServer - - before(async () => { - const ethClient = new EthereumClient( - client, - registryAddr, - registrarAddr, - ) - server = new ApolloServer({ - typeDefs, - resolvers: { - Query: { - domain: async (_, name) => - await domainResolver({ - name, - repo, - client: ethClient, - resolverAddress: dbResolverAddr, - }), - }, - }, - }) - }) - - it('should fetch 2LD properties with no subdomains', async () => { - const t1 = new Text() - t1.domain = node - t1.key = '1key' - t1.value = '1value' - t1.resolver = '0x1resolver' - t1.resolverVersion = '1' - await datasource.manager.save(t1) - - const t2 = new Text() - t2.domain = node - t2.key = '2key' - t2.value = '2value' - t2.resolver = '0x2resolver' - t2.resolverVersion = '2' - await datasource.manager.save(t2) - - const a1 = new Address() - a1.domain = node - a1.address = '0x1' - a1.coin = '1' - a1.resolver = '0x1resolver' - a1.resolverVersion = '1' - await datasource.manager.save(a1) - - const a2 = new Address() - a2.domain = node - a2.address = '0x2' - a2.coin = '60' - a2.resolver = '0x2resolver' - a2.resolverVersion = '2' - await datasource.manager.save(a2) - - const ch = new Contenthash() - ch.domain = node - ch.contenthash = - 'ipfs://QmYwWkU8H6x5xYz1234567890abcdefghijklmnopqrstuvwxyz' - ch.resolver = '0x2resolver' - ch.resolverVersion = '2' - await datasource.manager.save(ch) - - const response = await server.executeOperation({ - query: `query Domain($name: String!) { - domain(name: $name) { - id - context - owner - label - labelhash - parent - parentNode - name - node - resolvedAddress - subdomainCount - resolver { - id - node - addr - address - contentHash - context - texts { - key - value - } - addresses { - address - coin - } - } - } - }`, - variables: { - name, - }, - }) - assert(response.body.kind === 'single') - const actual = response.body.singleResult.data?.domain as DomainMetadata - - assert(actual !== null) - expect(actual.id).equal(`${owner.address}-${node}`) - expect(actual.context).equal(owner.address) - expect(actual.owner).equal(owner.address) - expect(actual.label).equal('l1domain') - expect(actual.labelhash).equal(labelhash('l1domain')) - expect(actual.parent).equal('eth') - expect(actual.parentNode).equal(namehash('eth')) - expect(actual.name).equal(name) - expect(actual.node).equal(node) - expect(actual.resolvedAddress).equal('0x2') - expect(actual.subdomainCount).equal(0) - expect(actual.resolver.id).equal(`${owner.address}-${node}`) - expect(actual.resolver.node).equal(node) - expect(actual.resolver.context).equal(owner.address) - expect(actual.resolver.address).equal(dbResolverAddr) - expect(actual.resolver.addr).equal('0x2') - expect(actual.resolver.contentHash).equal( - 'ipfs://QmYwWkU8H6x5xYz1234567890abcdefghijklmnopqrstuvwxyz', - ) - expect(actual.resolver.texts).eql([ - { - key: '1key', - value: '1value', - }, - { - key: '2key', - value: '2value', - }, - ]) - expect(actual.resolver.addresses).eql([ - { - address: '0x1', - coin: '1', - }, - { - address: '0x2', - coin: '60', - }, - ]) - }) - - it('should fetch 2LD properties with subdomains', async () => { - const d = new Domain() - d.name = 'd1.public.eth' - d.node = namehash('d1') - d.ttl = '300' - d.parent = node - d.resolver = '0xresolver' - d.resolverVersion = '1' - d.owner = privateKeyToAddress(generatePrivateKey()) - await datasource.manager.save(d) - - const t = new Text() - t.key = '1key' - t.value = '1value' - t.domain = d.node - t.resolver = '0x1resolver' - t.resolverVersion = '1' - t.createdAt = new Date() - t.updatedAt = new Date() - await datasource.manager.save(t) - - const a = new Address() - a.address = '0x1' - a.coin = '60' - a.domain = d.node - a.resolver = '0x1resolver' - a.resolverVersion = '1' - a.createdAt = new Date() - a.updatedAt = new Date() - await datasource.manager.save(a) - - const ch = new Contenthash() - ch.domain = d.node - ch.contenthash = - 'ipns://k51qzi5uqu5dgccx524mfjv7znyfsa6g013o6v4yvis9dxnrjbwojc62pt0450' - ch.resolver = '0x1resolver' - ch.resolverVersion = '1' - await datasource.manager.save(ch) - - const response = await server.executeOperation({ - query: `query Domain($name: String!) { - domain(name: $name) { - subdomains { - id - context - owner - name - node - label - labelhash - parent - parentNode - resolvedAddress - resolver { - id - node - context - address - addr - contentHash - texts { - key - value - } - addresses { - address - coin - } - } - expiryDate - registerDate - } - subdomainCount - } - }`, - variables: { - name, - }, - }) - assert(response.body.kind === 'single') - const actual = response.body.singleResult.data?.domain as DomainMetadata - - assert(actual !== null) - expect(actual.subdomainCount).equal(1) - assert(actual.subdomains != null) - const subdomain = actual.subdomains[0] - expect(subdomain).to.have.property('id', `${d.owner}-${d.node}`) - expect(subdomain).to.have.property('context', d.owner) - expect(subdomain).to.have.property('owner', d.owner) - expect(subdomain).to.have.property('name', d.name) - expect(subdomain).to.have.property('label', 'd1') - expect(subdomain).to.have.property('labelhash', labelhash('d1')) - expect(subdomain).to.have.property('parent', 'public.eth') - expect(subdomain).to.have.property('parentNode', namehash('public.eth')) - expect(subdomain).to.have.property('node', d.node) - expect(subdomain).to.have.property('resolvedAddress', '0x1') - expect(subdomain.resolver).to.have.property( - 'id', - `${d.owner}-${d.node}`, - ) - expect(subdomain.resolver).to.have.property('node', d.node) - expect(subdomain.resolver).to.have.property('context', d.owner) - expect(subdomain.resolver).to.have.property('address', d.resolver) - expect(subdomain.resolver).to.have.property('addr', '0x1') - expect(subdomain.resolver).to.have.property( - 'contentHash', - ch.contenthash, - ) - expect(subdomain.resolver.texts).to.eql([ - { key: t.key, value: t.value }, - ]) - expect(subdomain.resolver.addresses).to.eql([ - { address: a.address, coin: a.coin }, - ]) - }) - }) }) }) diff --git a/packages/client/test/helpers.ts b/packages/client/test/helpers.ts new file mode 100644 index 00000000..e4902c4d --- /dev/null +++ b/packages/client/test/helpers.ts @@ -0,0 +1,167 @@ +import { + createTestClient, + getContract, + getContractAddress, + Hash, + Hex, + http, + labelhash, + namehash, + publicActions, + walletActions, + zeroHash, +} from 'viem' +import { + abi as abiRegistry, + bytecode as bytecodeRegistry, +} from '@blockful/contracts/out/ENSRegistry.sol/ENSRegistry.json' +import { + abi as abiRegistrar, + bytecode as bytecodeRegistrar, +} from '@blockful/contracts/out/BaseRegistrarImplementation.sol/BaseRegistrarImplementation.json' +import { + abi as abiDBResolver, + bytecode as bytecodeDBResolver, +} from '@blockful/contracts/out/DatabaseResolver.sol/DatabaseResolver.json' +import { + abi as abiUniversalResolver, + bytecode as bytecodeUniversalResolver, +} from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' +import { anvil } from 'viem/chains' +import { + EthereumClient, + OwnershipValidator, + SignatureRecover, +} from '@blockful/gateway/src/services' +import { withSigner } from '@blockful/gateway/src/middlewares' +import { abi } from '@blockful/gateway/src/abi' +import { + withQuery, + withGetText, + withRegisterDomain, + withSetText, + withGetAddr, + withSetAddr, + withGetContentHash, + withSetContentHash, +} from '@blockful/gateway/src/handlers' +import { PostgresRepository } from '@blockful/gateway/src/repositories' +import * as ccip from '@blockful/ccip-server' + +const GATEWAY_URL = 'http://127.0.0.1:3000/{sender}/{data}.json' +const GRAPHQL_URL = 'http://127.0.0.1:4000' + +const client = createTestClient({ + chain: anvil, + mode: 'anvil', + transport: http(), +}) + .extend(publicActions) + .extend(walletActions) + +async function deployContract({ + abi, + bytecode, + account, + args, +}: { + abi: unknown[] + bytecode: Hash + account: Hash + args?: unknown[] +}): Promise { + const txHash = await client.deployContract({ + abi, + bytecode, + account, + args, + }) + + const { nonce } = await client.getTransaction({ + hash: txHash, + }) + + return await getContractAddress({ + from: account, + nonce: BigInt(nonce), + }) +} + +export async function deployContracts(signer: Hash) { + const registryAddr = await deployContract({ + abi: abiRegistry, + bytecode: bytecodeRegistry.object as Hash, + account: signer, + }) + + const registry = await getContract({ + abi: abiRegistry, + address: registryAddr, + client, + }) + + const universalResolverAddr = await deployContract({ + abi: abiUniversalResolver, + bytecode: bytecodeUniversalResolver.object as Hash, + account: signer, + args: [registryAddr, [GATEWAY_URL]], + }) + + const registrarAddr = await deployContract({ + abi: abiRegistrar, + bytecode: bytecodeRegistrar.object as Hash, + account: signer, + args: [registryAddr, namehash('eth')], + }) + + const dbResolverAddr = await deployContract({ + abi: abiDBResolver, + bytecode: bytecodeDBResolver.object as Hash, + account: signer, + args: [GATEWAY_URL, GRAPHQL_URL, 600, [signer]], + }) + + await registry.write.setSubnodeRecord( + [zeroHash, labelhash('eth'), signer, dbResolverAddr, 10000000], + { account: signer }, + ) + await registry.write.setSubnodeRecord( + [namehash('eth'), labelhash('l1domain'), signer, dbResolverAddr, 10000000], + { account: signer }, + ) + + return { + registryAddr, + universalResolverAddr, + registrarAddr, + dbResolverAddr, + } +} + +export function setupGateway( + privateKey: Hex, + { repo }: { repo: PostgresRepository }, + registryAddr: Hex, + registrarAddr: Hex, +) { + const signatureRecover = new SignatureRecover() + const ethClient = new EthereumClient(client, registryAddr, registrarAddr) + const validator = new OwnershipValidator(anvil.id, signatureRecover, [ + ethClient, + repo, + ]) + const server = new ccip.Server() + server.app.use(withSigner(privateKey)) + server.add( + abi, + withQuery(), + withGetText(repo), + withRegisterDomain(repo), + withSetText(repo, validator), + withGetAddr(repo), + withSetAddr(repo, validator), + withGetContentHash(repo), + withSetContentHash(repo, validator), + ) + server.makeApp('/').listen('3000') +} diff --git a/packages/client/test/metadata.e2e.spec.ts b/packages/client/test/metadata.e2e.spec.ts new file mode 100644 index 00000000..a9596e87 --- /dev/null +++ b/packages/client/test/metadata.e2e.spec.ts @@ -0,0 +1,334 @@ +import 'reflect-metadata' +import { assert, expect } from 'chai' +import { ApolloServer } from '@apollo/server' +import { DataSource } from 'typeorm' +import { ChildProcess, spawn } from 'child_process' +import { labelhash, namehash, normalize } from 'viem/ens' +import { anvil } from 'viem/chains' +import { createTestClient, http, publicActions, walletActions, Hex } from 'viem' +import { + generatePrivateKey, + privateKeyToAccount, + privateKeyToAddress, +} from 'viem/accounts' + +import { typeDefs, DomainMetadata } from '@blockful/gateway/src/types' +import { domainResolver } from '@blockful/gateway/src/resolvers' +import { PostgresRepository } from '@blockful/gateway/src/repositories' +import { + Text, + Domain, + Address, + Contenthash, +} from '@blockful/gateway/src/entities' +import { EthereumClient } from '@blockful/gateway/src/services' + +import { deployContracts, setupGateway } from './helpers' + +describe('Metadata API', () => { + let repo: PostgresRepository, + datasource: DataSource, + server: ApolloServer, + localNode: ChildProcess, + dbResolver: Hex + + const owner = privateKeyToAccount( + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + ) + + const client = createTestClient({ + chain: anvil, + mode: 'anvil', + transport: http(), + }) + .extend(publicActions) + .extend(walletActions) + + before(async () => { + localNode = spawn('anvil') + + const { registryAddr, registrarAddr, dbResolverAddr } = + await deployContracts(owner.address) + + dbResolver = dbResolverAddr + + datasource = new DataSource({ + type: 'better-sqlite3', + database: './metadata.test.db', + entities: [Text, Domain, Address, Contenthash], + synchronize: true, + }) + + repo = new PostgresRepository(await datasource.initialize()) + + const ethClient = new EthereumClient(client, registryAddr, registrarAddr) + + server = new ApolloServer({ + typeDefs, + resolvers: { + Query: { + domain: async (_, name) => + await domainResolver({ + name, + repo, + client: ethClient, + resolverAddress: dbResolverAddr, + }), + }, + }, + }) + + setupGateway( + '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + { repo }, + registryAddr, + registrarAddr, + ) + }) + + beforeEach(async () => { + for (const entity of ['Text', 'Address', 'Domain', 'Contenthash']) { + await datasource.getRepository(entity).clear() + } + }) + + after(async () => { + localNode.kill() + await datasource.destroy() + }) + + describe('2LD properties', async () => { + const name = normalize('l1domain.eth') + const node = namehash(name) + + it('should fetch 2LD properties with no subdomains', async () => { + const t1 = new Text() + t1.domain = node + t1.key = '1key' + t1.value = '1value' + t1.resolver = '0x1resolver' + t1.resolverVersion = '1' + await datasource.manager.save(t1) + + const t2 = new Text() + t2.domain = node + t2.key = '2key' + t2.value = '2value' + t2.resolver = '0x2resolver' + t2.resolverVersion = '2' + await datasource.manager.save(t2) + + const a1 = new Address() + a1.domain = node + a1.address = '0x1' + a1.coin = '1' + a1.resolver = '0x1resolver' + a1.resolverVersion = '1' + await datasource.manager.save(a1) + + const a2 = new Address() + a2.domain = node + a2.address = '0x2' + a2.coin = '60' + a2.resolver = '0x2resolver' + a2.resolverVersion = '2' + await datasource.manager.save(a2) + + const ch = new Contenthash() + ch.domain = node + ch.contenthash = + 'ipfs://QmYwWkU8H6x5xYz1234567890abcdefghijklmnopqrstuvwxyz' + ch.resolver = '0x2resolver' + ch.resolverVersion = '2' + await datasource.manager.save(ch) + + const response = await server.executeOperation({ + query: `query Domain($name: String!) { + domain(name: $name) { + id + context + owner + label + labelhash + parent + parentNode + name + node + resolvedAddress + subdomainCount + resolver { + id + node + addr + address + contentHash + context + texts { + key + value + } + addresses { + address + coin + } + } + } + }`, + variables: { + name, + }, + }) + assert(response.body.kind === 'single') + const actual = response.body.singleResult.data?.domain as DomainMetadata + + assert(actual !== null) + expect(actual.id).equal(`${owner.address}-${node}`) + expect(actual.context).equal(owner.address) + expect(actual.owner).equal(owner.address) + expect(actual.label).equal('l1domain') + expect(actual.labelhash).equal(labelhash('l1domain')) + expect(actual.parent).equal('eth') + expect(actual.parentNode).equal(namehash('eth')) + expect(actual.name).equal(name) + expect(actual.node).equal(node) + expect(actual.resolvedAddress).equal('0x2') + expect(actual.subdomainCount).equal(0) + expect(actual.resolver.id).equal(`${owner.address}-${node}`) + expect(actual.resolver.node).equal(node) + expect(actual.resolver.context).equal(owner.address) + expect(actual.resolver.address).equal(dbResolver) + expect(actual.resolver.addr).equal('0x2') + expect(actual.resolver.contentHash).equal( + 'ipfs://QmYwWkU8H6x5xYz1234567890abcdefghijklmnopqrstuvwxyz', + ) + expect(actual.resolver.texts).eql([ + { + key: '1key', + value: '1value', + }, + { + key: '2key', + value: '2value', + }, + ]) + expect(actual.resolver.addresses).eql([ + { + address: '0x1', + coin: '1', + }, + { + address: '0x2', + coin: '60', + }, + ]) + }) + + it('should fetch 2LD properties with subdomains', async () => { + const d = new Domain() + d.name = 'd1.public.eth' + d.node = namehash('d1') + d.ttl = '300' + d.parent = node + d.resolver = '0xresolver' + d.resolverVersion = '1' + d.owner = privateKeyToAddress(generatePrivateKey()) + await datasource.manager.save(d) + + const t = new Text() + t.key = '1key' + t.value = '1value' + t.domain = d.node + t.resolver = '0x1resolver' + t.resolverVersion = '1' + t.createdAt = new Date() + t.updatedAt = new Date() + await datasource.manager.save(t) + + const a = new Address() + a.address = '0x1' + a.coin = '60' + a.domain = d.node + a.resolver = '0x1resolver' + a.resolverVersion = '1' + a.createdAt = new Date() + a.updatedAt = new Date() + await datasource.manager.save(a) + + const ch = new Contenthash() + ch.domain = d.node + ch.contenthash = + 'ipns://k51qzi5uqu5dgccx524mfjv7znyfsa6g013o6v4yvis9dxnrjbwojc62pt0450' + ch.resolver = '0x1resolver' + ch.resolverVersion = '1' + await datasource.manager.save(ch) + + const response = await server.executeOperation({ + query: `query Domain($name: String!) { + domain(name: $name) { + subdomains { + id + context + owner + name + node + label + labelhash + parent + parentNode + resolvedAddress + resolver { + id + node + context + address + addr + contentHash + texts { + key + value + } + addresses { + address + coin + } + } + expiryDate + registerDate + } + subdomainCount + } + }`, + variables: { + name, + }, + }) + assert(response.body.kind === 'single') + const actual = response.body.singleResult.data?.domain as DomainMetadata + + assert(actual !== null) + expect(actual.subdomainCount).equal(1) + assert(actual.subdomains != null) + const subdomain = actual.subdomains[0] + expect(subdomain).to.have.property('id', `${d.owner}-${d.node}`) + expect(subdomain).to.have.property('context', d.owner) + expect(subdomain).to.have.property('owner', d.owner) + expect(subdomain).to.have.property('name', d.name) + expect(subdomain).to.have.property('label', 'd1') + expect(subdomain).to.have.property('labelhash', labelhash('d1')) + expect(subdomain).to.have.property('parent', 'public.eth') + expect(subdomain).to.have.property('parentNode', namehash('public.eth')) + expect(subdomain).to.have.property('node', d.node) + expect(subdomain).to.have.property('resolvedAddress', '0x1') + expect(subdomain.resolver).to.have.property('id', `${d.owner}-${d.node}`) + expect(subdomain.resolver).to.have.property('node', d.node) + expect(subdomain.resolver).to.have.property('context', d.owner) + expect(subdomain.resolver).to.have.property('address', d.resolver) + expect(subdomain.resolver).to.have.property('addr', '0x1') + expect(subdomain.resolver).to.have.property('contentHash', ch.contenthash) + expect(subdomain.resolver.texts).to.eql([{ key: t.key, value: t.value }]) + expect(subdomain.resolver.addresses).to.eql([ + { address: a.address, coin: a.coin }, + ]) + }) + }) +}) From 414862bf5e28ce9e6e89ff8b1c2495e0abe66869 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Wed, 4 Dec 2024 17:30:39 +0700 Subject: [PATCH 13/16] chore: revert reat contenthash --- packages/client/src/read.ts | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/client/src/read.ts b/packages/client/src/read.ts index 29a60653..1e6692d1 100644 --- a/packages/client/src/read.ts +++ b/packages/client/src/read.ts @@ -4,9 +4,17 @@ */ import { config } from 'dotenv' -import { Hex, createPublicClient, http } from 'viem' +import { + Hex, + createPublicClient, + http, + namehash, + decodeFunctionResult, + hexToString, +} from 'viem' import { normalize } from 'viem/ens' import { getChain } from './client' +import { abi as l1Abi } from '@blockful/contracts/out/L1Resolver.sol/L1Resolver.json' config({ path: process.env.ENV_FILE || '../.env', @@ -29,7 +37,7 @@ const client = createPublicClient({ // eslint-disable-next-line const _ = (async () => { - const name = normalize('gibi.arb.eth') + const name = normalize('lucas.arb.eth') const twitter = await client.getEnsText({ name, @@ -60,11 +68,31 @@ const _ = (async () => { gatewayUrls: [gateway], }) + const resolver = await client.getEnsResolver({ + name, + universalResolverAddress: universalResolverAddress as Hex, + }) + const encodedContentHash = (await client.readContract({ + address: resolver, + functionName: 'contenthash', + abi: l1Abi, + args: [namehash(name)], + })) as Hex + + const contentHash = hexToString( + decodeFunctionResult({ + abi: l1Abi, + functionName: 'contenthash', + data: encodedContentHash, + }) as Hex, + ) + console.log({ twitter, avatar, address, addressBtc, name: domainName, + contentHash, }) })() From 4589fb13309d9aa9a86cfb1757a0795f7f43bdca Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Mon, 9 Dec 2024 07:12:49 -0300 Subject: [PATCH 14/16] feat: add getDeferralHandler to IWriteDeferral --- packages/client/src/client.ts | 4 +-- packages/client/src/write.ts | 4 +-- packages/client/test/db.e2e.spec.ts | 10 +++---- packages/contracts/src/DatabaseResolver.sol | 22 ++++----------- packages/contracts/src/L1Resolver.sol | 22 ++++----------- .../src/interfaces/IWriteDeferral.sol | 15 ++++++++-- .../src/interfaces/WildcardWriting.sol | 15 ---------- packages/contracts/test/L1Resolver.t.sol | 28 ++++++++----------- packages/gateway/src/types/entities.ts | 2 +- 9 files changed, 44 insertions(+), 78 deletions(-) diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 7d39f104..edf532cb 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -56,7 +56,7 @@ export async function handleDBStorage({ message, types: { Message: [ - { name: 'callData', type: 'bytes' }, + { name: 'data', type: 'bytes' }, { name: 'sender', type: 'address' }, { name: 'expirationTimestamp', type: 'uint256' }, ], @@ -65,7 +65,7 @@ export async function handleDBStorage({ }) return await ccipRequest({ body: { - data: message.callData, + data: message.data, signature: { message, domain, signature }, sender: message.sender, }, diff --git a/packages/client/src/write.ts b/packages/client/src/write.ts index 40e1ad92..a3d5f5aa 100644 --- a/packages/client/src/write.ts +++ b/packages/client/src/write.ts @@ -114,9 +114,9 @@ const _ = (async () => { args: [ encodedName, encodeFunctionData({ - functionName: 'writeParams', + functionName: 'getDeferralHandler', abi, - args: [encodedName, encodeFunctionData(calldata)], + args: [encodeFunctionData(calldata)], }), ], }) diff --git a/packages/client/test/db.e2e.spec.ts b/packages/client/test/db.e2e.spec.ts index bcec5a58..08c8822a 100644 --- a/packages/client/test/db.e2e.spec.ts +++ b/packages/client/test/db.e2e.spec.ts @@ -11,8 +11,6 @@ import 'reflect-metadata' // Importing abi and bytecode from contracts folder import { abi as abiDBResolver } from '@blockful/contracts/out/DatabaseResolver.sol/DatabaseResolver.json' import { abi as abiOffchainRegister } from '@blockful/contracts/out/WildcardWriting.sol/OffchainRegister.json' -import { abi as abiWriteDeferral } from '@blockful/contracts/out/IWriteDeferral.sol/IWriteDeferral.json' -import { abi as abiWildcardWriting } from '@blockful/contracts/out/WildcardWriting.sol/WildcardWriting.json' import { abi as abiUniversalResolver } from '@blockful/contracts/out/UniversalResolver.sol/UniversalResolver.json' @@ -92,9 +90,9 @@ async function offchainWriting({ args: [ encodedName, encodeFunctionData({ - functionName: 'writeParams', - abi: abiWildcardWriting, - args: [encodedName, encodeFunctionData(calldata)], + functionName: 'getDeferralHandler', + abi: abiDBResolver, + args: [encodeFunctionData(calldata)], }), ], }) @@ -104,7 +102,7 @@ async function offchainWriting({ const [params] = data.args const errorResult = decodeErrorResult({ - abi: abiWriteDeferral, + abi: abiDBResolver, data: params as Hex, }) if (errorResult?.errorName === 'StorageHandledByOffChainDatabase') { diff --git a/packages/contracts/src/DatabaseResolver.sol b/packages/contracts/src/DatabaseResolver.sol index c84b5eb4..8149c91a 100644 --- a/packages/contracts/src/DatabaseResolver.sol +++ b/packages/contracts/src/DatabaseResolver.sol @@ -20,7 +20,6 @@ import {IMulticallable} from "@ens-contracts/resolvers/IMulticallable.sol"; import {ENSIP16} from "./ENSIP16.sol"; import {SignatureVerifier} from "./SignatureVerifier.sol"; import {IWriteDeferral} from "./interfaces/IWriteDeferral.sol"; -import {WildcardWriting} from "./interfaces/WildcardWriting.sol"; import {EnumerableSetUpgradeable} from "./utils/EnumerableSetUpgradeable.sol"; /** @@ -32,7 +31,6 @@ contract DatabaseResolver is ENSIP16, IExtendedResolver, IWriteDeferral, - WildcardWriting, AddrResolver, ABIResolver, PubkeyResolver, @@ -112,18 +110,10 @@ contract DatabaseResolver is /** * @notice Read call for fetching the required parameters for the offchain call * @notice avoiding multiple transactions - * @param -name The encoded name or identifier of the write operation * @param data The encoded data to be written * @dev This function reverts with StorageHandledByL2 error to indicate L2 deferral */ - function writeParams( - bytes calldata, /* name */ - bytes calldata data - ) - public - view - override - { + function getDeferralHandler(bytes calldata data) public view override { _offChainStorage(data); } @@ -152,10 +142,9 @@ contract DatabaseResolver is return bytes(this.name(node)); } - if (bytes4(data[:4]) == this.writeParams.selector) { - (bytes memory name, bytes memory _data) = - abi.decode(data[4:], (bytes, bytes)); - this.writeParams(name, _data); + if (bytes4(data[:4]) == this.getDeferralHandler.selector) { + (bytes memory _data) = abi.decode(data[4:], (bytes)); + this.getDeferralHandler(_data); } _offChainLookup(data); @@ -417,7 +406,7 @@ contract DatabaseResolver is }), gatewayUrl, IWriteDeferral.messageData({ - callData: callData, + data: callData, sender: msg.sender, expirationTimestamp: block.timestamp + gatewayDatabaseTimeoutDuration @@ -557,7 +546,6 @@ contract DatabaseResolver is { return interfaceID == type(IWriteDeferral).interfaceId || interfaceID == type(IExtendedResolver).interfaceId - || interfaceID == type(WildcardWriting).interfaceId || interfaceID == type(IMulticallable).interfaceId || super.supportsInterface(interfaceID); } diff --git a/packages/contracts/src/L1Resolver.sol b/packages/contracts/src/L1Resolver.sol index 0c8dfd44..493df4bc 100644 --- a/packages/contracts/src/L1Resolver.sol +++ b/packages/contracts/src/L1Resolver.sol @@ -20,15 +20,12 @@ import {EVMFetcher} from "./evmgateway/EVMFetcher.sol"; import {IEVMVerifier} from "./evmgateway/IEVMVerifier.sol"; import {EVMFetchTarget} from "./evmgateway/EVMFetchTarget.sol"; import {IWriteDeferral} from "./interfaces/IWriteDeferral.sol"; -import { - WildcardWriting, OffchainRegister -} from "./interfaces/WildcardWriting.sol"; +import {OffchainRegister} from "./interfaces/WildcardWriting.sol"; contract L1Resolver is EVMFetchTarget, IExtendedResolver, IERC165, - WildcardWriting, IWriteDeferral, IMulticallable, Ownable, @@ -107,17 +104,10 @@ contract L1Resolver is /** * @notice Validates and processes write parameters for deferred storage mutations - * @param -name The encoded name or identifier of the write operation * @param data The encoded data to be written * @dev This function reverts with StorageHandledByL2 error to indicate L2 deferral */ - function writeParams( - bytes calldata, /* name */ - bytes calldata data - ) - public - view - { + function getDeferralHandler(bytes calldata data) public view override { bytes4 selector = bytes4(data); if (selector == OffchainRegister.register.selector) { @@ -170,10 +160,9 @@ contract L1Resolver is bytes32 node = abi.decode(data[4:], (bytes32)); return _contenthash(node); } - if (selector == this.writeParams.selector) { - (bytes memory name, bytes memory _data) = - abi.decode(data[4:], (bytes, bytes)); - this.writeParams(name, _data); + if (selector == this.getDeferralHandler.selector) { + (bytes memory _data) = abi.decode(data[4:], (bytes)); + this.getDeferralHandler(_data); } } @@ -384,7 +373,6 @@ contract L1Resolver is { return interfaceID == type(IExtendedResolver).interfaceId || interfaceID == type(IWriteDeferral).interfaceId - || interfaceID == type(WildcardWriting).interfaceId || interfaceID == type(EVMFetchTarget).interfaceId || interfaceID == type(IERC165).interfaceId || interfaceID == type(ENSIP16).interfaceId diff --git a/packages/contracts/src/interfaces/IWriteDeferral.sol b/packages/contracts/src/interfaces/IWriteDeferral.sol index 08f7a5e8..48a3dc11 100644 --- a/packages/contracts/src/interfaces/IWriteDeferral.sol +++ b/packages/contracts/src/interfaces/IWriteDeferral.sol @@ -44,11 +44,12 @@ interface IWriteDeferral { /** * @notice Struct used to define the message context used to construct a typed data signature, defined in EIP-712, * to authorize and define the deferred mutation being performed. - * @param callData The encoded function call + * @param data The original ABI encoded function call * @param sender The address of the user performing the mutation (msg.sender). + * @param expirationTimestamp The timestamp at which the mutation will expire. */ struct messageData { - bytes callData; + bytes data; address sender; uint256 expirationTimestamp; } @@ -87,4 +88,14 @@ interface IWriteDeferral { domainData sender, string url, messageData data ); + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice View function that simulates the execution of an encoded function call + * @param encodedFunction The ABI encoded function call to simulate + */ + function getDeferralHandler(bytes calldata encodedFunction) external view; + } diff --git a/packages/contracts/src/interfaces/WildcardWriting.sol b/packages/contracts/src/interfaces/WildcardWriting.sol index a2f15c79..5dfc2286 100644 --- a/packages/contracts/src/interfaces/WildcardWriting.sol +++ b/packages/contracts/src/interfaces/WildcardWriting.sol @@ -1,21 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.17; -interface WildcardWriting { - - /// @notice Validates and processes write parameters for deferred storage mutations - /// @param name The encoded name or identifier of the write operation - /// @param data The encoded data to be written - /// @dev This function should revert with appropriate errors when write operations need to be deferred - function writeParams( - bytes calldata name, - bytes calldata data - ) - external - view; - -} - interface OffchainRegister { /** diff --git a/packages/contracts/test/L1Resolver.t.sol b/packages/contracts/test/L1Resolver.t.sol index 626fd963..0841f3b0 100644 --- a/packages/contracts/test/L1Resolver.t.sol +++ b/packages/contracts/test/L1Resolver.t.sol @@ -71,7 +71,7 @@ contract L1ResolverTest is Test, ENSHelper { //////// WRITE PARAMS TESTS //////// - function test_WriteParamsRegister() public { + function test_GetDeferralHandlerRegister() public { bytes memory name = "test.eth"; bytes memory data = abi.encodeWithSelector( OffchainRegister.register.selector, @@ -92,11 +92,10 @@ contract L1ResolverTest is Test, ENSHelper { TARGET_REGISTRAR ) ); - l1Resolver.writeParams(name, data); + l1Resolver.getDeferralHandler(data); } - function test_WriteParamsSetAddr() public { - bytes memory name = "test.eth"; + function test_GetDeferralHandlerSetAddr() public { bytes memory data = abi.encodeWithSelector( bytes4(keccak256("setAddr(bytes32,address)")), bytes32(0), @@ -110,11 +109,10 @@ contract L1ResolverTest is Test, ENSHelper { TARGET_RESOLVER ) ); - l1Resolver.writeParams(name, data); + l1Resolver.getDeferralHandler(data); } - function test_WriteParamsSetAddrWithCoinType() public { - bytes memory name = "test.eth"; + function test_GetDeferralHandlerSetAddrWithCoinType() public { bytes memory data = abi.encodeWithSelector( bytes4(keccak256("setAddr(bytes32,uint256,bytes)")), bytes32(0), @@ -129,11 +127,10 @@ contract L1ResolverTest is Test, ENSHelper { TARGET_RESOLVER ) ); - l1Resolver.writeParams(name, data); + l1Resolver.getDeferralHandler(data); } - function test_WriteParamsSetText() public { - bytes memory name = "test.eth"; + function test_GetDeferralHandlerSetText() public { bytes memory data = abi.encodeWithSelector( bytes4(keccak256("setText(bytes32,string,string)")), bytes32(0), @@ -148,11 +145,10 @@ contract L1ResolverTest is Test, ENSHelper { TARGET_RESOLVER ) ); - l1Resolver.writeParams(name, data); + l1Resolver.getDeferralHandler(data); } - function test_WriteParamsSetContenthash() public { - bytes memory name = "test.eth"; + function test_GetDeferralHandlerSetContenthash() public { bytes memory data = abi.encodeWithSelector( bytes4(keccak256("setContenthash(bytes32,bytes)")), bytes32(0), @@ -166,17 +162,17 @@ contract L1ResolverTest is Test, ENSHelper { TARGET_RESOLVER ) ); - l1Resolver.writeParams(name, data); + l1Resolver.getDeferralHandler(data); } - function test_WriteParamsUnsupportedFunction() public { + function test_GetDeferralHandlerUnsupportedFunction() public { bytes memory name = "test.eth"; bytes memory data = abi.encodeWithSelector( bytes4(keccak256("unsupportedFunction()")), bytes32(0) ); vm.expectRevert(L1Resolver.FunctionNotSupported.selector); - l1Resolver.writeParams(name, data); + l1Resolver.getDeferralHandler(data); } function test_RevertWhen_GetAddr() public { diff --git a/packages/gateway/src/types/entities.ts b/packages/gateway/src/types/entities.ts index 271bbd17..3b0cef86 100644 --- a/packages/gateway/src/types/entities.ts +++ b/packages/gateway/src/types/entities.ts @@ -30,7 +30,7 @@ export type DomainData = { * @param sender The address of the user performing the mutation (msg.sender). */ export type MessageData = { - callData: `0x${string}` + data: `0x${string}` sender: `0x${string}` expirationTimestamp: bigint } From 53d8660426bab9ca04ede73006c08315832691f5 Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Mon, 9 Dec 2024 07:52:16 -0300 Subject: [PATCH 15/16] fix: typed signature with data property --- packages/gateway/src/services/signatureRecover.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gateway/src/services/signatureRecover.ts b/packages/gateway/src/services/signatureRecover.ts index ab9de7a5..31f4c35f 100644 --- a/packages/gateway/src/services/signatureRecover.ts +++ b/packages/gateway/src/services/signatureRecover.ts @@ -13,7 +13,7 @@ export class SignatureRecover { message, types: { Message: [ - { name: 'callData', type: 'bytes' }, + { name: 'data', type: 'bytes' }, { name: 'sender', type: 'address' }, { name: 'expirationTimestamp', type: 'uint256' }, ], From 6297f60857aa4fe58aced9148b7c61639fd3b15b Mon Sep 17 00:00:00 2001 From: lucas picollo Date: Mon, 9 Dec 2024 08:47:37 -0300 Subject: [PATCH 16/16] fix: test helper sign with data property --- packages/contracts/test/L1Resolver.t.sol | 1 - packages/gateway/test/helper.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/contracts/test/L1Resolver.t.sol b/packages/contracts/test/L1Resolver.t.sol index 0841f3b0..92ed5164 100644 --- a/packages/contracts/test/L1Resolver.t.sol +++ b/packages/contracts/test/L1Resolver.t.sol @@ -166,7 +166,6 @@ contract L1ResolverTest is Test, ENSHelper { } function test_GetDeferralHandlerUnsupportedFunction() public { - bytes memory name = "test.eth"; bytes memory data = abi.encodeWithSelector( bytes4(keccak256("unsupportedFunction()")), bytes32(0) ); diff --git a/packages/gateway/test/helper.ts b/packages/gateway/test/helper.ts index 14cad22a..675c02c9 100644 --- a/packages/gateway/test/helper.ts +++ b/packages/gateway/test/helper.ts @@ -27,7 +27,7 @@ export async function signData({ verifyingContract: sender, } const message: MessageData = { - callData: toFunctionHash(func), + data: toFunctionHash(func), sender, expirationTimestamp: 9999999n, } @@ -39,7 +39,7 @@ export async function signData({ message, types: { Message: [ - { name: 'callData', type: 'bytes' }, + { name: 'data', type: 'bytes' }, { name: 'sender', type: 'address' }, { name: 'expirationTimestamp', type: 'uint256' }, ],