From 44a9963d4c78111f77caa0e65d677b8b46d6f2e6 Mon Sep 17 00:00:00 2001 From: t11s Date: Sat, 1 Jan 2022 15:33:13 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20V6:=20First=20Production=20Release?= =?UTF-8?q?=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: t11s Co-authored-by: z0r0z <92001561+z0r0z@users.noreply.github.com> Co-authored-by: alephao <7674479+alephao@users.noreply.github.com> Co-authored-by: 0age <37939117+0age@users.noreply.github.com> Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com> Co-authored-by: Matt Co-authored-by: Andreas Bigger Co-authored-by: Ryan Co-authored-by: Zefram Lou Co-authored-by: RagePit <35245632+RagePit@users.noreply.github.com> --- .gas-snapshot | 173 +- .gitignore | 5 +- README.md | 5 +- audits/v6-Fixed-Point-Solutions.pdf | Bin 0 -> 170456 bytes src/auth/Auth.sol | 66 +- src/auth/Trust.sol | 28 - src/auth/authorities/MultiRolesAuthority.sol | 123 ++ src/auth/authorities/RolesAuthority.sol | 66 +- src/auth/authorities/TrustAuthority.sol | 19 - src/test/Auth.t.sol | 167 +- src/test/CREATE3.t.sol | 26 +- src/test/DSTestPlus.t.sol | 48 + src/test/ERC1155.t.sol | 1777 ++++++++++++++++++ src/test/ERC20.t.sol | 351 +++- src/test/ERC721.t.sol | 748 ++++++++ src/test/FixedPointMathLib.t.sol | 46 +- src/test/MultiRolesAuthority.t.sol | 321 ++++ src/test/RolesAuthority.t.sol | 172 +- src/test/SSTORE2.t.sol | 10 +- src/test/SafeCastLib.t.sol | 20 +- src/test/SafeTransferLib.t.sol | 16 +- src/test/Trust.t.sol | 46 - src/test/TrustAuthority.t.sol | 56 - src/test/WETH.t.sol | 8 +- src/test/utils/DSInvariantTest.sol | 2 +- src/test/utils/DSTestPlus.sol | 86 +- src/test/utils/Hevm.sol | 3 +- src/test/utils/mocks/MockAuthChild.sol | 2 +- src/test/utils/mocks/MockAuthority.sol | 20 + src/test/utils/mocks/MockERC1155.sol | 42 + src/test/utils/mocks/MockERC721.sol | 30 + src/test/utils/mocks/MockTrustChild.sol | 12 - src/test/utils/users/ERC1155User.sol | 56 + src/test/utils/users/ERC721User.sol | 54 + src/test/utils/users/GenericUser.sol | 4 +- src/tokens/ERC1155.sol | 253 +++ src/tokens/ERC20.sol | 15 +- src/tokens/ERC721.sol | 220 +++ src/tokens/WETH.sol | 11 +- src/utils/Bytes32AddressLib.sol | 4 +- src/utils/CREATE3.sol | 50 +- src/utils/FixedPointMathLib.sol | 165 +- src/utils/ReentrancyGuard.sol | 3 +- src/utils/SSTORE2.sol | 56 +- src/utils/SafeCastLib.sol | 3 +- src/utils/SafeTransferLib.sol | 1 + 46 files changed, 4836 insertions(+), 553 deletions(-) create mode 100644 audits/v6-Fixed-Point-Solutions.pdf delete mode 100644 src/auth/Trust.sol create mode 100644 src/auth/authorities/MultiRolesAuthority.sol delete mode 100644 src/auth/authorities/TrustAuthority.sol create mode 100644 src/test/DSTestPlus.t.sol create mode 100644 src/test/ERC1155.t.sol create mode 100644 src/test/ERC721.t.sol create mode 100644 src/test/MultiRolesAuthority.t.sol delete mode 100644 src/test/Trust.t.sol delete mode 100644 src/test/TrustAuthority.t.sol create mode 100644 src/test/utils/mocks/MockAuthority.sol create mode 100644 src/test/utils/mocks/MockERC1155.sol create mode 100644 src/test/utils/mocks/MockERC721.sol delete mode 100644 src/test/utils/mocks/MockTrustChild.sol create mode 100644 src/test/utils/users/ERC1155User.sol create mode 100644 src/test/utils/users/ERC721User.sol create mode 100644 src/tokens/ERC1155.sol create mode 100644 src/tokens/ERC721.sol diff --git a/.gas-snapshot b/.gas-snapshot index 25f3157a..8d09dbc9 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,42 +1,151 @@ -testAcceptingOwner() (gas: 139707) -testFailNonOwner2() (gas: 3773) -testFailRejectingAuthority1() (gas: 119902) -testFailNonOwner1() (gas: 3742) -testFailRejectingAuthority2() (gas: 119999) +testFailSetAuthorityWithRestrictiveAuthority() (gas: 126002) +testSetAuthorityWithPermissiveAuthority() (gas: 127687) +testFailSetOwnerWithRestrictiveAuthority() (gas: 126166) +testFailCallFunctionAsNonOwner() (gas: 4191) +testSetAuthorityAsOwner() (gas: 23802) +testFailCallFunctionAsOwnerWithOutOfOrderAuthority() (gas: 135733) +testCallFunctionWithPermissiveAuthority() (gas: 125973) +testFailSetAuthorityAsNonOwner() (gas: 6960) +testFailSetOwnerAsOwnerWithOutOfOrderAuthority() (gas: 135873) +testCallFunctionAsOwner() (gas: 21371) +testFailCallFunctionWithRestrictiveAuthority() (gas: 126125) +testSetOwnerWithPermissiveAuthority() (gas: 147508) +testFailSetOwnerAsNonOwner() (gas: 4309) +testSetAuthorityAsOwnerWithOutOfOrderAuthority() (gas: 234329) +testSetOwnerAsOwner() (gas: 3998) testFromLast20Bytes() (gas: 191) testFillLast12Bytes() (gas: 223) -testFailDoubleDeploySameBytecode() (gas: 277076930206519) -testDeployERC20() (gas: 885671) -testFailDoubleDeployDifferentBytecode() (gas: 277076930206511) -testMin() (gas: 715) -testFPow() (gas: 1738) -testMax() (gas: 757) -testFailFDivZeroXY() (gas: 298) -testSqrt() (gas: 2342) -testFDiv() (gas: 764) -testFDivEdgeCases() (gas: 543) -testFMulEdgeCases() (gas: 823) -testFailFDivXYB() (gas: 319) -testFailFDivZeroY() (gas: 274) +testFailDoubleDeploySameBytecode() (gas: 277076930206699) +testDeployERC20() (gas: 873896) +testFailDoubleDeployDifferentBytecode() (gas: 277076930214885) +testFailBoundMinBiggerThanMax() (gas: 309) +testBound() (gas: 5520) +testFailSafeBatchTransferFromToRevertingERC1155Recipient() (gas: 1041163) +testMintToEOA() (gas: 30265) +testFailMintToNonERC155Recipient() (gas: 71897) +testFailSafeBatchTransferFromToZero() (gas: 805864) +testBatchMintToERC1155Recipient() (gas: 946375) +testApproveAll() (gas: 26509) +testFailSafeBatchTransferFromWithArrayLengthMismatch() (gas: 681042) +testFailBatchMintToZero() (gas: 127242) +testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient() (gas: 993087) +testSafeTransferFromToERC1155Recipient() (gas: 1210543) +testFailBatchMintToWrongReturnDataERC1155Recipient() (gas: 314473) +testFailBatchMintToRevertingERC1155Recipient() (gas: 362536) +testBatchBurn() (gas: 146591) +testFailBurnInsufficientBalance() (gas: 30352) +testFailSafeTransferFromToWrongReturnDataERC1155Recipient() (gas: 243471) +testFailMintToRevertingERC155Recipient() (gas: 263148) +testFailSafeBatchTransferFromToNonERC1155Recipient() (gas: 849621) +testFailSafeTransferFromInsufficientBalance() (gas: 579173) +testFailSafeTransferFromToNonERC155Recipient() (gas: 100376) +testFailBatchMintToNonERC1155Recipient() (gas: 171010) +testSafeBatchTransferFromToEOA() (gas: 817122) +testFailSafeTransferFromToRevertingERC1155Recipient() (gas: 291604) +testBatchMintToEOA() (gas: 132842) +testFailBatchBurnInsufficientBalance() (gas: 131673) +testSafeBatchTransferFromToERC1155Recipient() (gas: 1650504) +testFailBalanceOfBatchWithArrayMismatch() (gas: 4798) +testFailSafeBatchTransferInsufficientBalance() (gas: 682003) +testSafeTransferFromToEOA() (gas: 609087) +testMintToERC1155Recipient() (gas: 612041) +testFailBatchMintWithArrayMismatch() (gas: 5118) +testBatchBalanceOf() (gas: 153798) +testFailSafeTransferFromToZero() (gas: 57667) +testFailSafeTransferFromSelfInsufficientBalance() (gas: 29956) +testBurn() (gas: 34098) +testFailBatchBurnWithArrayLengthMismatch() (gas: 131065) +testFailMintToZero() (gas: 29205) +testSafeTransferFromSelf() (gas: 59828) +testFailMintToWrongReturnDataERC155Recipient() (gas: 263102) +testInfiniteApproveTransferFrom() (gas: 387796) +testApprove() (gas: 26558) +testMetaData() (gas: 6966) +testTransferFrom() (gas: 388134) +testFailTransferFromInsufficientBalance() (gas: 359401) +testFailPermitPastDeadline() (gas: 2197) +testFailPermitReplay() (gas: 59949) +testMint() (gas: 49180) +testFailTransferFromInsufficientAllowance() (gas: 358925) +testTransfer() (gas: 75628) +testBurn() (gas: 52492) +testPermit() (gas: 56782) +testFailTransferInsufficientBalance() (gas: 48240) +testFailPermitBadDeadline() (gas: 30486) +testFailPermitBadNonce() (gas: 30436) +testSafeTransferFromToERC721Recipient() (gas: 908869) +testFailSafeMintToERC721RecipientWithWrongReturnDataWithData() (gas: 185732) +testApprove() (gas: 96031) +testFailBurnUnMinted() (gas: 3379) +testFailSafeTransferFromToERC721RecipientWithWrongReturnDataWithData() (gas: 213867) +testFailDoubleMint() (gas: 70935) +testApproveAll() (gas: 26585) +testFailApproveUnAuthorized() (gas: 73181) +testFailSafeTransferFromToRevertingERC721RecipientWithData() (gas: 259577) +testFailSafeMintToNonERC721RecipientWithData() (gas: 115867) +testMetadata() (gas: 6492) +testFailTransferFromWrongFrom() (gas: 71032) +testFailSafeMintToRevertingERC721Recipient() (gas: 230626) +testTransferFrom() (gas: 551359) +testFailSafeMintToNonERC721Recipient() (gas: 115042) +testFailDoubleBurn() (gas: 74563) +testFailSafeMintToERC721RecipientWithWrongReturnData() (gas: 184893) +testFailSafeTransferFromToNonERC721Recipient() (gas: 143245) +testMint() (gas: 72701) +testFailApproveUnMinted() (gas: 5694) +testFailTransferFromToZero() (gas: 71031) +testSafeMintToERC721Recipient() (gas: 408375) +testSafeTransferFromToEOA() (gas: 556215) +testSafeMintToEOA() (gas: 75400) +testFailSafeTransferFromToERC721RecipientWithWrongReturnData() (gas: 213093) +testTransferFromApproveAll() (gas: 553534) +testFailTransferFromUnOwned() (gas: 3500) +testFailSafeTransferFromToNonERC721RecipientWithData() (gas: 144048) +testBurn() (gas: 76417) +testFailSafeMintToRevertingERC721RecipientWithData() (gas: 231396) +testFailMintToZero() (gas: 1253) +testFailTransferFromNotOwner() (gas: 75544) +testSafeMintToERC721RecipientWithData() (gas: 429537) +testFailSafeTransferFromToRevertingERC721Recipient() (gas: 258848) +testSafeTransferFromToERC721RecipientWithData() (gas: 930031) +testTransferFromSelf() (gas: 103082) +testFPow() (gas: 1651) +testFailFDivZeroXY() (gas: 316) +testSqrt() (gas: 2492) +testFDiv() (gas: 733) +testFDivEdgeCases() (gas: 581) +testFMulEdgeCases() (gas: 801) +testFailFDivXYB() (gas: 294) +testFailFDivZeroY() (gas: 271) testFMul() (gas: 669) +testSetRoles() (gas: 33023) +testCanCallWithCustomAuthorityOverridesPublicCapability() (gas: 295417) +testCanCallPublicCapability() (gas: 39631) +testSetTargetCustomAuthority() (gas: 31736) +testCanCallWithCustomAuthorityOverridesUserWithRole() (gas: 334265) +testCanCallWithAuthorizedRole() (gas: 97461) +testSetRoleCapabilities() (gas: 32997) +testCanCallWithCustomAuthority() (gas: 466959) +testSetPublicCapabilities() (gas: 31468) testNoReentrancy() (gas: 1015) testProtectedCall() (gas: 23649) testFailUnprotectedCall() (gas: 30515) -testBasics() (gas: 76765) -testRoot() (gas: 40181) -testSanityChecks() (gas: 11630) -testPublicCapabilities() (gas: 41708) -testWriteRead() (gas: 53564) -testWriteReadFullStartBound() (gas: 34778) -testFailWriteReadEmptyOutOfBounds() (gas: 34479) -testWriteReadFullBoundedRead() (gas: 53761) +testSetRoles() (gas: 32998) +testCanCallPublicCapability() (gas: 38436) +testCanCallWithAuthorizedRole() (gas: 96267) +testSetRoleCapabilities() (gas: 34588) +testSetPublicCapabilities() (gas: 33244) +testWriteRead() (gas: 53511) +testWriteReadFullStartBound() (gas: 34725) +testFailWriteReadEmptyOutOfBounds() (gas: 34432) +testWriteReadFullBoundedRead() (gas: 53708) testFailReadInvalidPointer() (gas: 2905) -testFailWriteReadOutOfStartBound() (gas: 34393) +testFailWriteReadOutOfStartBound() (gas: 34346) testFailReadInvalidPointerCustomStartBound() (gas: 2982) -testWriteReadEmptyBound() (gas: 34692) -testFailWriteReadOutOfBounds() (gas: 34500) -testWriteReadCustomBounds() (gas: 34906) -testWriteReadCustomStartBound() (gas: 34821) +testWriteReadEmptyBound() (gas: 34639) +testFailWriteReadOutOfBounds() (gas: 34453) +testWriteReadCustomBounds() (gas: 34853) +testWriteReadCustomStartBound() (gas: 34768) testFailReadInvalidPointerCustomBounds() (gas: 3143) testSafeCastTo248() (gas: 433) testSafeCastTo128() (gas: 455) @@ -68,8 +177,6 @@ testTransferWithNonContract() (gas: 3075) testApproveWithTransferFromSelf() (gas: 26416) testTransferWithTransferFromSelf() (gas: 28182) testFailTransferETHToContractWithoutFallback() (gas: 7222) -testUpdateTrust() (gas: 12713) -testSanityChecks() (gas: 4838) testPartialWithdraw() (gas: 68803) testDeposit() (gas: 58804) testFallbackDeposit() (gas: 59068) diff --git a/.gitignore b/.gitignore index 88cb1153..5dfe93fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -/out -/node_modules \ No newline at end of file +/cache +/node_modules +/out \ No newline at end of file diff --git a/README.md b/README.md index cbefd300..1e3cf76c 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ ```ml auth ├─ Auth — "Flexible and updatable auth pattern" -├─ Trust — "Ultra minimal authorization logic" ├─ authorities │ ├─ RolesAuthority — "Role based Authority that supports up to 256 roles" -│ ├─ TrustAuthority — "Simple Authority which only authorizes trusted users" +│ ├─ MultiRolesAuthority — "Flexible and target agnostic role based Authority" tokens ├─ WETH — "Minimalist and modern Wrapped Ether implementation" ├─ ERC20 — "Modern and gas efficient ERC20 + EIP-2612 implementation" +├─ ERC721 — "Modern, minimalist, and gas efficient ERC721 implementation" +├─ ERC1155 — "Minimalist and gas efficient standard ERC1155 implementation" utils ├─ SSTORE2 - "Library for cheaper reads and writes to persistent storage" ├─ CREATE3 — "Deploy to deterministic addresses without an initcode factor" diff --git a/audits/v6-Fixed-Point-Solutions.pdf b/audits/v6-Fixed-Point-Solutions.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5c4243425bf4fdaef1dcd87eceb2365ba97bd6f0 GIT binary patch literal 170456 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 ?uKH_LBG3w}|gz7C!T-OPS2=2wL82qXf;9 z-OHdUsT98T^w0O!Maa4)<%kF3f@jRi<9;^m%d)FsPf}g0U}uK)C@Z?rBgui3^IdJh76YQ@3#G`FDpUYl+;u)Kr?wF{~nb?4r9<&Ngvu8(~?}IUILEe0!H2 z8kz*OIm2hFFbp~w~r&JcPbEv8z01k)7qp*}*d}WOA(F{rT zvooBeNfEhLEiFMco!lSG#zv^{7VUcW?xoCngTM66wDO#xhvG6=`JO7AEBd6K&2RIo)@cV=2(#P;=EhEsU`(gsNBAPpB7LqRZmmc@)vMNj z%x&WkwwaE+E8E!5%4b`U>Z?KKA>X?BY4~DSkgI-s_yU zRqfQGzy_sU+27g~RS^P{p_x8m_;&2$fP?0jXWLRT&+qa@lp@Fbk7uDweL-KbFJW*& z$?C@j&&xi^WQwGxc=w`V{utT@+t{VqPK6Q$%*BJFEGUG`3s{GJY$4k=C5+sMIFPvR z8Gz3@ZhNmR9ueb0TO(KCJ^w=Vby`kv+s~O)7d!YHOi4TXn=%Z0EnP~78%tz#e$a@4 zD#{8@6HbYcX^c&v2mH_ z`rlJ$iDDTW{v?%(mW#r}-IR^RSKgp$16FIARxIZQVRAJ~#jAQGz&#IhAw2h9I}$tP zz3jKVpYaSfQeo1ViK%+Q=%3|SIgLvrPXSrxgQN1g`g&yRY2B?aX&q>$Mo`&(OhF}E zZQnw$oP&TYxcs#mIz?vGuO$n{MKMLk|nNfLBSlj^|p+ghz0KgN2P z5a_|L1kGC67uXA7pJ`<22|Vo0zoz_C`gx|N! z_JJhR0W1ZfvcH;K@i0;vNA_E2KVje>zrEDhg`BeJ1OGqj-a4$Rb#415r5gkU>F%5~ z0@A5;cPibjAl)6(-JQ}6lF}t1-7P8b4c6Z8R@eTX{qDV8@3GeN&p14Qsm^;|;~qEH z?>f)(Wv9g#a*GdgYpP-)Cu?nO0TLy0yUCbNR?q)v8%ly3; z?M{kp)|n7BPpg&%1$Sd-YI>Z-sO;BdpPrRSpvlY0WqrcEa3Mb1(!}Tu_sA`Y$i>Sm z!2Z-3(HU#nFyDJ_ly!g-ZP=kyvv8y136_l&H3r&9g?`Pwfub?Pc?&VrpkorJI@AT4 zM|$+!rVMp2;1Ir2WEYLL5ghG;HJrZj-cGRxbDULUKuDI)ruMOH1qnmWAg(kBTC3Z)H%0#^g@s44W6Q6`eDC zf=v{Elx#GFqQ<-E3VBwYsx@pwwApDx+!OAVKc11fte+WUihPr2nf|4!;9iFs#v?)V znJc_ca}ABKNlMI-r((B{D$TT~=E$l+CN*QDo0@ENwB5}WJGEAir>its?^n6*PU&v$ z=u$ryS82BO$u5S@iP6|E4L5}2ibYGlQ3CW@F zD{nqNtE)B9#hWjgkM#d}(u3^}A)_EbWXA7G;2C83-WJOu&@GA1<`}kJ!im z?cM)64gF4lfR;Z%fY_M-E;Icb7G?zq5C|Ar_*41E-~H|pq10v%G5(MJOge`h6rosNDl4HjVJ z3D6jFJ_H2(A2|4+!eruL`f~>-{zYZ{4weEVTz^1i0LpI$P9Sjw5HtRxGJxHZ0m#T; z`*V5uf3On2PD_8NjEA`}KveldVX!=mg>eA3kAKxY1E8EgKSO>Uo<0l`0Q{5#khucG zMZhc#Co3>m0AwRPOjiN-&HtLE_%~rH@UZ0g+4oW{Y^@&cc0^$fdhkmNn4eea&cRi! z7DqWBB+da!_#sL{zl@@!+=>Eo>~=*13_DUmmoA+l9inJ{*5I4_^po(2vtz4g$oKpK z%d&E1_ai8i6*o8WY2ll1zMb!W$x9nLp1#L#xOjP$6*Wxw=B5`&2}nB>g>ATD3@6T1 z=)Dso*gKGTRv4#j`a0*M0Vlcr*;HQER0q{Q5l)Wuok~D|avM3uV8yptL43YFFIP(W z5D7nC{V|ePCWZtP!CNVy$KI37j;E4=!t!dAnQRhHS!&ewQ550%-quy7bG+oSE3xz^ z{g1i2L_`W?{DWmX#7&Az+SWU$_Z>Lnp7HRCqPSUG5&EaKY#wJFaodE%1AY?jY0VvjWPT4{%};qW;VABG{zcd9!2oX!?@F zF9>B2d3$-i2Bva@_8DmhQ(LCu3~(*LgVvMq@M2keux>(Xbqf&T)WUq1h=x(%=#&b{ ztryWL6fNZ!9vNr4`E7I;`ozK&cbW)efhDTVId*$rb}+^UD@xm$o0`Oh^i3u4v66gd z?eN1?Vk%IPb4gQyqUU)U}(V>n`iuFs+7a6e^N58rj)Y2A?u9 z&RY@=7yR}(m3I;YI6``of>AjBQ#{%NmY(3D1^Hx`5QiWf?x-{@Udo8Z;SK~f zXhB^9hC@?Qs7N~GBk@=2qBV)BC3T)pT2COEK7m<-Lawn7mmyJ7TO;418H0cGgcup+ zMf(i9?7eA?&m_n8hVu^FG8L8~o6;5eF>Eqr2f}D?gl$v=5rkJKAyJ>WZ6uUeq7Wh- z7xD)dqD{2kPh);=SxKx8-sm59aB6GqBd{PW;Ol9hw}#eIm!G$s^!1x+7KBHH^;8LI z3RU?UOYvw?McTYM#So402D~THKq*LP=5Q|aW04IanWDKv5C{~oKH9~w;F?ZFk&IOz zG%$0H<}PuHnlq2_8B(*+Ui#57*fg<-B9p2cN|`6BXek}+%OB+tbkWDWIE(MDKJ~>#H77<2cy*rjf zjrM&ScyV9~ygtKNmTj@xrt|PgM6LbSx~($rqV&Bj{Gr=jdbV5S3W{ zr=O1#4^Bu=Uddg=%>ZKSVp=`?P!ozuH0P7_*_CA@9(IZ)Rl$ySDK@Q3Xzjyl=gzmQ ztPaq%nx=7OOmjqti&Gh$xxA1?AhATr%2v6mELlwW&KyZ}zx3B+86g4b>DcfaNUF>@ zuw8F;6;XN1^j|z37WZm`gnhT4uKPU2#Dw>S2>IB010Gb_>zD@2hZ>0-uVD6xxmu zO=L`iOXp|q@uG*Vx2`~%Vd@_psjlqa?0b0&V;kzG0ILqU(i%(wRFbVQn_aNK66(=QyI>D{5SoDNB zj~h}}Di^H_vE<|b)I=h5(~9+C17rVkl_Ot1y=3};&Km^7l%s3@e#B9=?{(S{hYCKt z(0pM0c3iJ{8u|{m4&HWn?@c3nm^Sn>76lZcQ~pc3H247a&iCPn;G$&}ZKzYlOza4o zx*TAMf>gd(eq7=eXH`fGiZ8~OohUJH%*4x55+_6I3UiY66I%^sxcR3HABB)##0fU~ z^!X`Ijp16msJyvWR?s;T3U+QNd6QF#$@~oYrB;D^&6Cs0=nPd@oxC(1%+bq>Ruu#s2TveUUvn53cQJ^h@ERyEw~%1&RgAMpeY`^XR% zIFoB~W7~WXIGwIRt0Ix?cC;<}7P;P`F|JeDllJ+5V(CpLqhI|9-K5higIEY4y&?jrev=_4ajofQ)h zkzVDqRGMQAM#kYS+GDunS0{Hw_U7{`u178|O-bS~PEQ_*EZ0I$;-dSrCFCh+Gwug1~Did@qj+X@cUf&Fld|`1;e8Fp6AM0#`|<9nPge!8E5% z0v#VSJftAXlR!GRJGfPxZ)jv4Pf}byWy+NiJ9h;RZ=$jvg&JMkBfNYU7C+qU|8{mQ z^hJur6pIMVD?dLo7TPi0R*;?l7NA6kgv5v(wp$7J1BpZTI#H-LLa^d5)TC&k62lUf zAcRwu=QK+7D$}So9cDf%>8m&46%gW$+s#KobojDWS`mPSyZ zm)MW%4H&N_FJsZ|1_lQ3Cl;isJuk8IS~zxxfxZs;V0nD8P6^XC!0r65&^9;ZU0al= zkxiE#;Gy>noC=w$44zQ8Uh-tv)@u62I=Bfuc4?at1FAgcx7$_*P#tE8gP^7Wnn+b# z;e=K{6{A?(mXIMf3f$4>hEk&H*J}FKWfLHSCB8=aBnOGLW)>@z{Xxf5B;Atq6ldB- zO$9-tR2CNlSq3Y7*RscTD#7zb-IG3b7q4vGYB(*wHLHLArd_KNtnq$3SUDI{S-!kO znDn+sWbrZ-mxj4k^K($4wYY*_PMa(|PsEGOc0*1dLY=~}G9xh3h%@sq*}CgogA{X7 zZm+=kFsYR70+T?-@lttl7wYA8kdWhzbyZChPF5xtj*dTG6s&q3wx`-Zlq5OrEe;>- z1DCUG-Qxr|s^&m4h~es)YqL?cK)4sz#olE+vfy)zEF8nT`{mm9*xm-a=^As}K4h+VpQ%i)#Wgf-#HIQYxoeNbP=hI*yErF00 z{=qo;&1PnTT}Q0da#^26yr!U5k@XJxQu9vk&TsqY{g#qF2(Ocmv0~*4gWmET%n{KS zG6g_k(P)ik=pl8g?5}iJMU*9YtrIYL1P86kML3ml+L)AHLrT$K@=lxYo7)B_Oq%ef z9IsCp2fYcF26v89M=jCUUFC(@x69^P{S7#n zJ9KWKlOSCCeM71C1+X=nVtJ7ziH4B5L#?3`qCQAZY2=3FRa(a_A7q5rBg6{mwY>Re zR=N0`Xr(qEwk%`p^+X%aR-5j*EaY8gV@)J1Zdh)^yGQYqkuBAXP##?qp@FZ^7MGj! zoPv0HUH5gbR#E&3Cgsc3@O{df2jWG#C1H$?M>SkzTt;XxP*0G}p6c=*lk3a|fcaRP zRknB49&xh-5EO?M2l9Isdbt*Per3xWxWh5J$($w8UR2eKilf(f+=Sb9DX4KHxWKj; zX))9Ae&pL`tYl*&5)zNn%2|w~gFE3`mx>5`EiAc`OOJzC2F^4!ED#y`VPKj z_-LY&$2riamgLD3W%}`Q4paHwBteB@W2acbo zv+nRq4jHoFgv>++-fT22wLu7?rCy47a46wAgq}oSw!0@EphBF_hQZx>1J{Fgzv;Ek!0XMh_rn6{1VFG4JLl{bwaf)Lk<|l(9S-;&v>m zxBUF1BASSot(DP+;Bn2$i{@>GYdn?n2jM_r8HUfJpH8Z(saslOs78^jroi^p2R(jQ zKa8+)L@9qG%0pkQGR|K!Vz{2;&DwRlcTA}ohDvdelFGCK3)PZLp}I7PI`z>@Dp*^P z9!<00jPeqSx-$LDt0Jb_nt07-E;eGNZVIc(C2+D*m4@a?>J4dWLpjUdtnVnTm5^+cIX&i`K*6s z2K$ite4022GQPh= zJZU9+VN~_cFoONpVFWWX&~XJli1COxfUf*+-e5#*AV9bUe1`}K;QbF;DTY=Cb`Nc0 zGea|b7jr#Jli$qFj4T-)fd&mQ?9OOrX>Vj}sRx9inCrP%IoRu%S?X9ASy;j19RA zxVS(DMn-@Yo1T#Y2yl<%WaTnu2N?jqdPaIb57vYJNVotbc|Nqyfnc5Q9b|T(W5xmk zki&oQk^aAu68{v2!~V0p1oVeM1$F?hFoC!L$OL2_Gcd8TvOmB!AYK~aO@FTD?C-7P zALD?2zXHDw6Ta6z8xz2k0I>}hU<1enD9xBSSOHNnaMyGEu|4$Pt4jbQO^|^htAQ~G zr!gBVVA#q80{qeD_hvaOz?A^uN*z@--@_{V@T1>9L!Upj_Ey>TOzkpZ3A5k8_ zvk*{wJ;*ZuQ63Lm36N#7|GD!Bf7>ko&9(R=)%y>8>7N9e0AKp^1)BeINw}DQVr_tz z$RAN2z;lxe1o#&GSRQP^=E@9Ux4*z?;(sWQ->n75ub(J?*gb#;2^-)=4p?>k1JOU2 z>oNhxKn8dRKwhw;=O8;-J#Sh2Meu~@*07|0+LSlXn$hUr+pg#fIG4cyYAFHxfQAOycf4BbO!G-xcyONq%Bu=BCj zI)azQn^y#N>o2I%y?X6EkdK<}TQ9xi$ECEuO&9?G?T z+d&3otp(d76a*p2$49-bsh)1vyF;)a38ZQIuXV##i|_}(T#G$D&CPk{%tEufx>b8e z-xY({Q87oOl}SzC-yovqBpNE1SX?4iGx>Pk-g=QxT9NC~E^pXDwL*6cNs8|UjmExE z>2sG7^U+dd(A{m2M(VYl z@*+3S*CgiToz&y@_l;CWH3jby@U51wu3H&Ym%im@VrvagjjYCm6~B3O_HBPVzU*t3 zvEc5r?IR$m&n}jtKQ9VM5#7%U1AZ>S+ZAy13Dn|wp!u8o1x7-4+ol6<^=6dUp6 z{yJVFmlVmpG=|h>;I^oMPj!0d=nH|M(-41BT-yDkK;7}6v1_7Azg+rt^82qdFI7kY zy=tRl(+ij(oSMXZlY~GhssPbt6osmBE`_@jxh-;{y+u2a?X)txs=D;{@o;QZK zrmyQRi$^k?D*AFuq9X8O)eurzTtKQfGeBuIXm0h2?bw){*?88ecf^X4e5%%S<(V#7tln0lH2jBP()L})zX;xVyr zQHa#@*Ma6<$-U#KRb(tUi5#?kLQ|w!!Fo{4B5_plDjko_0xBQnwPR94;Un=ds~kgN zZ#Nsgx2>5=GCtQYZFPD(uDsFixPd3@rKvr(DTEPZQO9;Ch%%bmF2F_-65t4%L5UED zie#=$N`l|Ks4rxbwN;XanQ4ka-eOJf--OIdr@y zq{Y=f?FhB#YAbqdFkc_3JLy|c)M8Q?@;w3JC`J}>$$ty3ePKn0o}qs#O<)uSW8XEt zoRHwmJUooGCz==gF+;tvu93NTDfFPaU$iKqS;o`L-+p5?GR)Cwf!h@u9*G;#0X3wk z>3z&|iJ}H=hht;7x5w&hkQ!iS#vLXZR@EKS*j8`%FWrR}E1{C$PAqNob!}_JXLf2Jr#i!I(UpbCLyhXek!fe6+|U+YHZH@ zDSW`6*gd|k?pu#j0T&RF^qMqb$oild7~YddLq z!bzg<`{F5XxQ<#Z?>E z{v$f|$yUH@cWg46v&jDoC8&;N!3}#%U?8>~6#gj{&haf{{!<%=NNWeEn?Vf&z7-cH zZIjBzeoG(_>k!bUU{g}PR5LJm;v)r3nlc$=7}UZikl#qG0%eI5l~_S@p?$061S0i@ z*EW%vDD-}%>w?J7rNL~FhWbJKv31I}PsnE%LJBO0FsXtWqXv=k^9-ueYLv2<~lP88ERr~8{4L@G9)}k<$gXk?yhZkz69!5itkzwSq zjxDWt5*w0|FjWGXmMm_zY`5PWWjCX?_5R>% ze}W9DH>}6lGI2{J#FHFPs{0x1 zsWYz$)U{}#5Lm=sLR7;Q%@v|HOcSt|Qut!^tk&y!VV4%Xxju6WZMI&4m67D+*n?v{ z)QI3u>1@!!XrZa)w*+j(8b34Vsaov6?wySV^>e(}WUP2PEVw|7$=~UGTSWT)b8PS? ztH7%bSjck$hJJGFZ*ir1B=Q3zlcN?IXlQMu*Zw@NYDK16gI7w!TY&Dw=JL6`WZghS zB!yH-J?YoCn$+4zF(mfISGfoS+hSDE3}}G6C~@l=Q48f|8+#fC%J5^OLRB?Xw{HN9 zKs)Kh&bEWwTLmsa>G?DX_}h zwd#}6;!2m{7VZIl9lDzlU2Q{xub|5D(|j!I_IT;W0kvgk+rb=Uf29u@@vrl;lo&wS z=5uJR$%0Cc#>}iy+&&#>FJGil_9oWQ6>ao^wxMTvR}9bUYAE{ZqbX4(IS5zDl-29C z);XZJ7nd3bi*Bt8RXlZ2kVcI*hlV-OyXlW$Rr2we<;*D|l*R3iO9rr1($_adobf4p zB`x&=24;|X!h)}Da4$MTD{84 z@W#?1dz0n|ZOQ$?SlvcXvq=ebcG*4sS=l-GnIDN7S}N)d*$HP`3Qzg2*vZfB%ngk}dmNg$W@ot;$_2YbIj#hZ!(J5Ex*lfwoJQ1Jw+&~vByrj7ZiIDO zcr=M%T-}!$M(j?SP&pMuw*sCuX)dTA+ymX4^2Wfks@IC6^7T5F3%ko9%b3s%>0m@M z-#)gy2Jy|v4o~jm?ba?HwY0L)P`4bmcHE#Q$=a^hWxfiZk#!EQh!^|ZC1$>z`Bkf8 z1G?!dL&)(W1;f4yr8^dBR2)@>26H!rT;3>wAEa1fDp?^>(IulJuP$oN*_6Wwa%uqkld-kXSi`(UEoR_X>e@> z{vw~6`gNyvpE;x9`Fl9*XGX-VP2gTnRqZ4-M-ar`f0$*$hkLE4w@z2RWKG3f``HVt ztsIiDwvEx+dxgWLYnP_9p+`qI)vJjnF^1?Hcq&Kx&@*IkxKM_sYS)*@V?xFsCN?k^ zQhW!m!}C4favR4hgl;#0Qp6UUXbKV%3R*&2F_{}m80_d**|x5P`AkyAUtVNg8fCeM zOwGEGTq%<-#EGCkmP^Vf^fRg}f0b-?x&))t8izT$6DwZT1*O?oqH<-X#>|pT;(3hQ z7vQj}tZQULN0$n6DV_F6vMzHzi&o@4d0A9A@>%=XR4My(Rf+ecAdJv^;bc3#d1cgF z;R|pDzV#B{z0A3qGuEKYnQbHW04qGMbAgs%NazlimVliYeV+TdTQ@1{K$*_*7QGzd zXS3&sDbr$#Sk@Yaa+SyB!sumty@h8!DEiWXMv^Died-G_0lIecEMKw`VmMSueY}s*FPM&m+p6PY;NPg8hH{ksmLv#r`q+2GW<6 zfBzpj;k-E{#m%D^n<-b%IIKLhwP)aMgo9mgZ3fNhOorOAkkshECDzq5loxdnzQjT&e5Tr+uv5C( zAdIl{XrE?7s|Y8l>ZDfxZc%A4t{)vVT5TP!9osJ#j&{JB7zXjBCBJ;PvNOHTy>TV= zwVgpxV$ey3P0~_(!Cq-m#9d^U5n zP&En*7mx4hSjFh~6%&kr_7#>*4}aH-Z*Y{`5i*7U#;9Qks>-r?TsEqmx6-))zu2LeNCvdO zS2hGzsZ#90?we8r0;MON(8?4Yf*`lF1$mf#70e7Hn{%w^rJ2^(_0-{~uY?avWU;ax zC`Ba~jKOA$B#BM3Rl|(*t#G$L13%-$%M-E|YI)hhG4z_%YfqaVLyq+_-vW0pzBW;} zAETyhy!btRE>nW>yfjhcRmO_hbpzoJY~8qd5?n6td}fNV;#}_54rjBOc9I;?J<7_9 zo4J2RiX6Y*fd7sjejtkM00QI!eslg0k>dZ2Km8s$Fadze(1_WX704U{vFic!jai?G zjf=|&#AV3E#=&gB%*AEM{5$f-@#`)2?~NfK`G|pwlL?qh|K1p42LY|-2jj=TK(Fj4 z^2YMBx{Kr2`|AK}czEA{wCy2n>N|O32O7)(`~1tVz^}K{e<%eOR=~uK^TE#dA3ef{ za6uql4)j;6gx?WJz@hRFXbiG5aRIdLw=A{qmGB^a^KmP#Z9=@k1dzXdu}D$G^W^C2;;k5IBFmYYyxQY=E;h3lJ*_@W+2t0^5V# zITt6}Ux7dVv@5dzZUuh5N&fwj$Oe4ufB`iyjPL`E2ehxuK)&K1xjFv+!T4X%cwl(q zSMPz}*z*HD;`}L3^*1yRsDJ+osN|>5p84k|BImD9Vmzz>2XL?drso9EG6N?I;DZeC z_`iyp^T$o__iz8#PlFF{|F<~|CN5wb06n<>KFs7i%wYh>f&cQ}XL7bPvT-o7G%#Yc zv$MCdHDY1>?HOR9X9rA}0Uq#{_O^Nk_I8XveYPw=o1An0`myi(ea-xUNZElSCNNyX z#>xOh6|(~_t`Cg#k3RPN{?0ZwU^3+3&}U|0Ghkt2;^Ja80OooOS&f0IJ{G|69*{$_ zau^#j|2%BQ`NK)?U&1cnyz)EPWeErm{SI#)ayQo>ayNTYkF_a}@yKSOfy^qW=n0e9B>m{2*&!jHSFU&G_p%8mNuCuHO)Q>Cwx zP?%rku9{<{j2~ot_D!G9uzhT6B5kh7He8!Ldrs&y;rlK<1axdFZBTP;1TV}*xvRU+!rv(O zU$^4x+ihvn5Ku(Rp$x23Ay!=t?jZ-Vl+l1I#F8|q!4+r^_MtkNrZyT>SpMK-5@n7`FkH(|YUi}u&W%5O7Hq4Gnu&b%+ z6JKcH0+(-3qpm{JLbeu`5h90~0!56$Q~_mzoL8^4#4fr!^i$iK=T4_bGHCABMlBpc zX_wh{*Q;{UV%;3Fxbe@|Bao0bBBV{uqu^$$grqK46~buhccHAyhhcMFwYgz%Eo`U4 z&Qol}>JCFFH_{GGH8d8wR`iE6OyFSp&lPWH57e@HUPH~`8FJ$p)>j1UJI`|EoOClu znkS4;qPpeG>Q_=U>WYrp%cv0yXS0Exq!cSTR-a3enGS?yX5sGconxFL0@LKo%*u8A ztWek(I!CGmJq=r`JSv%|S11e(t?OBJ2BXEMOqf|R*`jNxo4&%TZCXskiRC?TRc(rg zx=jk?`)VYsr^8R*XLd$8%I;1pr+SAJ8D|&!TC?je_2!Y=eYM$B8;+)Fta(8`RqwJ> zlY__;;!GF)Hk#oAXTC`USpu+Q#CRpfP)yE%I4Wv@Q~F6gfXZo#_@iODu>F*Ztn~id znv#IC3O8FpUz*c`=`Ww~yC{0&$S3=fo%J*j3&jhwwa@v^yD5NSWS51rT2n|>G$&1x z#Fh5UqRg}mg5A*SXI!{Ef||ivEO0DEa*yh`7q%y#W})q``Qkx@*IDxOiq0LHAIYX#_3zKm)_2hU2kni13_M zW-Y9Mm=!Ha%!iC}HNKT`Zn8z+FXNA_=IR$_s={abmTv-_-ID_~wOB@8!l?14A+|)U zMR2B0^U9{#4agKlt#&00+>xkSDZq1N>?uly50?d=prc$M)o)}WPRbqc!-S9%^tD_L zLQ{qun8{{rQHVXYw_e|l@f)tMiFD-TWk}}wl!60K?!6h{9FrH=<7hHV6~&`MRUdGB zE5aL>seA>$r?bo>Zy-Z|^qij8 zGH)PbR5;e01jTcxcMs;lY7ASF;#Jk&KNIX&DGuOwVt^dK)=p z#a?Sq4x43BK4KnK8joO}OY8l7`BG&B&&APzi2K`jJXfT@Y!5%7!mzNfH5=HBrO;0& zV@U0e*QH%l(SLf-xfDh|31#qH(u%RGCQqd!H%#H_3~Q;4`-i$~+{Cv%(a_rl+JU<# zAKPDuYw|_zbSo9$u<7PAqNL7h@k()Rnt>B?q90OAb>tg;U0{?iI??$A^OA-4BF3#l zyeqc(PTt>T`kCr39^rFU>ls1vkz^+f4%()+!KW1hI&{o56Y=lBi}~B%FF!Yr*xNV? zfm=q~SVV3r(}^h~5ONWqtZQ8KuU{0@<0hd0Tx3!?%r;8RN~7fb+_pMhQLiFcfLa`j z=vlRXod{W~Z^IJmoDX!s-Z4bfVP=A!WISZ5`pACo!1EX^aF}`eYt|F?6F#fZyc^|@ zT?JPL4dBPu)n?`9JIG?a!t(IJmQNS0AwCu}*UO2xF6(3aM!sWAXmF;2dPMB?X+#QT zVVwrLACj`>?K`Z(A_)9C7W)`jQTM zlFg2}A{pr}z0ya-`L(lP5PCL(bc1WFIkHe2d%Ue#SbGba^-ykYA+OOb!3$E+G@^Po z!BF$yHitljI8tlkTb{S-K3&z1GR>aj6e~3?4>G`7*AeM5^yh>yy|m-h^lhq}Qc1FQ zGGk|cJiIH5BE*8!>cWdZDG*OSmoPN|)--??E5fPUuQEs)HFgzD4_5Hl%Mhu)e8O%M zud7`DwiK+(R;LTz*rI}Nk2ztB+hp+M@M;wvI{Gp5kGhqx-xvnJVID5wvS?FJR z7oNYzUH){as?HNPH4>Ji zd+d~MLmxk10v8XR+UexMp|j|zWICNWRwKgk$S_7?1<=)~wW?e9M{OJ1M9jOpOgAFs z#T`-HebJh2;Hx~ws!7VpWLUu;vH`ce_0*N=j&BkByWY1pFEPWUp5K*55Lom>4vXVN^ZY!Pg{NA9)~$7Aiz0jVW@*k^H#Db=Wg+6U~LI>fGa zEyu*Nw*;+Uxu7kC%jq4__Is^7U%pDPf7qAz** zC<3le${-t>EMNXp=K`J-;k<>yw%EH*dmM4kc0+UO(=|1rwwc+4BrKFE8~6GxU((%r zF@p%tGBXS;b{lp0N~sbw$mYG|a_nHx=@&o2(+YKCxu1mM<@u1seKwgO1VZ`zO1U3R z<2p=~Z*d!_#%1^>XVFI=qgb*QqCASn9bmj)bHHD$o`D^vYqS2K(Tf&C|Ay5?Dg;{i zVmaP_#okzkkGHhgN*8M$94a6~q)TJj@dadkdlgG9&ler$7|teC%E^;=F))UGNsHw3 zmc=XL?_RpTzix{j^Av-d^e3i91$UJ@@v_FKAD@+eir+fzri;}^*NIWckHfj|#S6>* z_A!}g)R}0oxd5UuSa_)?kzu4>#2&>COfMfT<2;m<5KWku)3m2&p)F200>q+Xd6t5K zZLP)~%^HvRTib6>k3Ht);1SM1`Pn0$ytm48ul0!IA!9!hGVrc>bR=oNp=-cgettwQ z;picIw8-=v2QZ7R6nC)F1=pd43m07@CZM(poNcA{boRov2;3zP^PXW55G14yF9~rV zno+c!QDeOeN!K_em|=m|Ke*eDr0t0riI?J$o<<(8>9n_i9``-$DHnoR8YBt95;FRd zlZUez$e77QKylYqKJodO?jT|%n%NP@G2pn+Mc&q$4GIf`Htvv&`esFqIQhwNVJ34E zTBbhJi~w$1aA76Yqxxhqaj%9-MvbCCE9f0~L5|s;*GF2w@tCqNbP!I9G?nnrzimOAEC`p2>d2@`G8}Qcx_GX4cc#!_N%3Bmy?cO$T4u6xb*?#^ZvN2M$z#3QiXy7zn>dpt zR{TpCmGevuh8trN>U;Jh6^4@0VLXokqa|sw-K7S4!*PyTC=(f+N*4=?_a{qwE=CHQ z+5LWQ1I9~;WH2*_ud#zK+Fu>QZ$CaduT;M_CTfOQEZD52LokrKbfd0g17 zAA}H|*Nr~hl%!Y(#2ZE*(GDee!*wHJa~KYqnS69$jVK<0n=Yt^mEY|!-VN16cIZsp zxCfVy7)ShP`~%1WezoKCz3U13TbkdG1NMIp^JDpQ^%uX{yggj3Y|ZS=jO-ZYt$++X z!M}gOBkTXZCE@z@ z*6M0UWyih-!JTgk%Onl71`~K%l__Bpd#@5|#gI zO@6(b{6o1s_(gKC0`0aR$^~#w^~7& zK!fzFo5%MXh3z4!^`}%qK;;Bzg;}}&H=Iv?20TCp@2@TlaF?(GTL(J_Cm_%NzN@kU zTL%juApI}#mAsy<8U1rTYcqR2b4I{0!9vg8=x+e)KX)sB*gyW|G4^+B^6SUg@5KVl zu>i_?7Jz4de+aVzTM3AhnfZUg4#M{H37?ti*Uz&L^#W*V**MuB>`4D{#%JYVV_;@w z`;%jz|E)*OKP}3ypJZ8pnJ2)smJ4w41%N0!8#6Hc0K}hi0-g+jWh)1e49&#$NA51a z-&OTk4FRR5k+Fe3s~(3T;Ja>M%%sl_*w_ zTyxOI*tT?%BqE;K!gG7<_cxTCZ}e+#-nh@-I^|_Pt4+Nh2sK{1y{Xf=$h*HbOk}Q-Amjg1@x#Y5X~63;X(gQcI6PoCWPxOl`Tk=n6FZu3(x(t#zFc` z?|vvLRyDg{V@)Ax5j~l7cC6g!({1CRYQnpHak(dD6Yo?g%%mc329(Kn10@pE<&WaT zyaTwg70*XCs9B>`Mx7S|)sbn|B8KQ$KRQ3g)czURNW# zsI!>xRw3ht@?h!Bd}2{GEw&v?*pp!|824R|Xisx;>*2c#y2)9*^$InVLq#G83MaTfVBY)8P$^k@`})^-*XV~%p0Nm&i#`7-Y-IdOOl z@?y8A3$~ofsOixoWtRlNGZcx}r*RKS28x~(UAvD)9OBekt3;L}z|e**+v*2;kMN@Nij)B1bofbEBMS3s76$jx#7Yluw@R-HU)^g|SO1 zU%}u(g<%XKl%T+^laUHSLHO$k#6+%SS;7xN-2DY!I#Gq?p#SFmk9Y2Db$%UO&x7B|w=8vk4E zt2H^R@MR zDSM2PR5bnYXZoQojM%&r| zuV3~uv->)AVgK+0z^lpMO=vc=s}y{2rG4A_!S!{_8%Y($D9Jckr?mHZDhOR1a%C>A zY2I>6iFy8t`+5?-IoVW|_V)TXR|i8O1LD4Mm4-{fJ@dl zR&G~ijaV5|O4p&0*Phy^r~ES`c6=638-^|CjqaAHNb8k5jD3!rL^_P8kodM{50xe` z%}?s~bgV}yxT59cTBX&YrHFG@_6D|bu0xdMCB)(~ck|&WS~K}5MuNIS+@bLxuY0ZW zq-xM!^$X8mS$BV{od?r!xYQlyI1T)8K`Xa4GtmOA6{3{P?tkTZyKVVrJU zB!Xg-@rwf-rXCF?jhbeF-n|nl#KyqxB)~b=vaj=VcusRA&&dMa8|F-bqH0kiu z2DKG2)fM(&snljY<#rdXs-3z@f_Dm-)L4qi?QYT;}mk_=9$|SI?D7pb;So^K{vnuY@qjCQje^@(qLO#!={Ky9F8t>WwZ^4U$!Q&fF&~pW_$e>GgvA8SHGI z(){-X`f&)3;86y@fU|Iev=(EEdCp$Kl=pMqj=aycRx3)#7gwND-+Cz{TZ>#=^5_i@ z7RW3)H9dD6wKJ5yPSj|#VgB{)CkQtzGj;ALXJk;LwFs6yI&CUXSyq*qtyKYSzeKiK zhk6fm*L6qNYm?@Mgcg02WkD-%=r9Z;q!|VuF|=9t?df?DM?2{Dby9W`pN7F! z>GQfW*}Y(CRDgkYOzCIF11(@1MF~+_uz_ZHd3RkrOl^m34kUc!*xXOB_(qZWj_d0< zE`30Qn+Wa@y?$cL3*N2-$_%CJ&r)Lh_DjI__yw)2&>P~C?=qijzrp#)RO!!Om>QF= zd$1y}y~|&gmfxvHY;_D_r8i@+qLWF^C&hORk+3C!4p-)w$@hZvBL7ASN+WDnVNkA}7je4dk5FQ2yOY=5WaCWa{3}rK}TPTfrd@e6xTRRt6U0DFhTF-kSitv_(@1zl zQ7SZaKsD_8&!qJ%z?9u}OMb2%2R}Qdx#7!z;*J3e=n3}>N5A-Vco;2&UBdD)c5OeP zij#xtC9GaoA-WTJ=_)aD+45EUExgmEpL;5n2AHl~Iq&C#O!Mw+XB#k#jhw7(399Jw zJe0vqf1)dc;Qc-+{K6VWiEy2leQ#6+mS8K7ZU5^YBcMIw8YY6{eY2@|AQ(GxC# zQDMD!)3@0E11d+?ccz0PzamCclu?j1l+r$|C-76R9cj(K`~^-x!*j z%^s2Gb3bW4xDBl9bMq$|wNqt%+3o9Ks3M5R=XZc?_;8;bzltIC}bN%N}zsW zK8d0xO3oVgd~Nb-k}wS{n>)c{I`g)i3PpE(au^HL4*}rtkJMp~{P34MlzKbf88evp zUq*TH3ZaN-X@?xZT`-G|MtNm)?x2IwtPxoT2;DMi+xWu1L?|l`wnk{Qh!2ju;jiLT z`RoKwWNR~B2tT?JL^K3RUDmjZKRP8WU%yk{WXh?0Yghm|C z^4~)AT{-j4OIlidu9``>?}*ed4K>&3hlZ)vhYActAkM(FWl)|v>r(hMD@TJ@GdCSx zvjmv#H&&`0jBD7dg@ z>9i@3hpki>;!o~u;!g@Y;OA4TZHHs2f{pN(D@hLR5=0Tr!5pfxPGTH5DKTbB-&$XA zjULY()khmCZJ*MU1oksKBf6p|r0{c2^9&a|*EFBoGnn$_jMA=|scgO)XDy>kk+H!> zjOmj_?>Q(yBx$9C*bhI`_xW7^rYEZ)$Y~^Pxp}~m{5fcgGj z$k%jz;*Ng4pjTX_E92nKM$g?v9%(&px5H?>(8tEp!a-k3LX{K3H&hFt3n38_M0gSA?i}gYrSaB8Tn=~HFV&<HIEo2Hr834w;=-13|@Se0dN~ zXRM2sve=*5<4F3u#Z}Wwsr<^`Pm45T3(%m=GJQLbpV3!jGqBAEhrB#tfwrBms<|FQ z!Lw?wFJ7Zr^q3L9D=|O5GU%>ExyYee+?*j%xQ2VjEqA1owLCd1oGzFYS6OmZ$U7Qi z`;W)%3X)5GyN=Vrz7xEKSIN^h-2u59&DJi8Fc7THrvk~(sZ1<}@K&*Qr;sPJhL^eT zQcURE+&|LaugWzOXvc4|%3dFfd8D2B9GLJjsUx=9FPR(3R()h-WFX|$)azJu&W&}k zce%H~F6O%HM@+h}y1Aiyc_)!R0c~t3>@e-3|8+OS;C+)}FX0D0)=UZDFzWH%WL($* zo8KM@v>I<@ChW5W$dgV6`9MO((&2d=?{RqEu`0?D478?hHsXxe z&VRD%qO{@Jig&u;8fLQ+sx?%I+hR(Z`4#gSn1})O0RgM+Kzo`BT7Mi4CZ?^8ewo@B z+pup?VkaH--E}DYNGBgW}x zBfh_=*TsasrxnN{8A+0f8(wu5G{aZwI1+MXOFa?=F#fpt`_H`q=Ku3WcF>XsXm$-+ zEd?d-0!{Ea7`d4_LA*i$P;>_iGY9kk&N`5zi;;tgsktK%L)7+dLv<~&pbMv3kWc8?C*%jlCzT+)=zQi4)`(LsT=wp#X2_S^=iPImU zyRyuqnkX_ckyR|tBm0pug4~f%0~EM8jGoEY5l~Feh(XR8a3{O;IlYQC_v%6`paxU`d7} zNjiK}Nvx3p)mRFTQD^ffn(3oz6&??xgX*F{+QithzWAs{X0t-p0D2kaDrH25NiVE2w9>fT?vTcVt#DtS-?S5T8Z;z|h zvSkg{lFyFbm!CPjXW2fXzXvEbFV-&;1uuuYKU~!7Ir~h^7qlO?-ItBV4^8x}g|Kr7 z0LJ7+e#9!gRz^bRa~J154v%80fZX_cPC=*(i|Iv=ePw)?rV4N;$fmga-6O~^Sb7^Y zywvFF3Kp`%YDLK>_;zSYfS1?pH5@THG1x3%?xY*m4*VS$Vf!Owwm!H}>ME>np5Lnu z{&d(J#cq-3>>?{$pzW?CQgd9V|ZMtscUA8Xw%e|NZ=SWEHfSsCT3!PZtWCny*k z_mg8dQ(~qooi6xXUsN}1AlB7GAe7#k;XlkBISBt?D!S(=)-y@40?wy)+hC5P5lkfX zj~x$^xyQr1udVXxc2iYC6Gop^TF7U4F3Q}#u}DwP_%u4kBHvHhUmQ<&M|(S1B1BKI zwFen9yefV`djs=hH_eVdqFlD5gJfgfAX5O_X_zik_N_s+|5`)!0=zWO`xVxAEmFA` z>|*P~VpaX-l_2h)qB0lsot^qVC#U%`dwjHB0j3^^&96?}*Rgsd^{S1G`H3UiUmaiL zIT^wChkO0LEt#fjkCFDexm_>sH=B-PaH{N`iPp{V9+MksAV6^-yj0Q4{X*E|;x?pT ztbnF)+NX06Nt$brQ&%OOx3PlO>^sim*ImP;M@tK$M#-L?r8@32yO&#L#{Kra>^I@% zH2N9X2;CieR=a;9+)!+$dW)dYV(tvoG_XLW!M@>JRvHI%a!BJCh3*nwS24dT?A(mf zj{=u*jvSkAlc2Uh75`)k?It_Y)qx#~@GW@Ub|xX^+94wx($Z7RMx$iW3;ow~Ho|42 zM!ZS})c}EOYM4ViwUr{t{I+*;RD=y^czT>`=s3`=o3D%U@u#bq?r3KnC0cQp01||_)@XEw7rhe3hj2tbgR-?GiHwXO4!e= zu1%@Yr6XtKx&cUN(qzh4G=w=F3A}{Rd~c71;9GOst6Sd$zN?JtzYjozVF*#h;wxqo zaXW+A{7j;&iD$Py`@NcxhtOhVS`&i4qa%Qw@mPpe1(I{eTv$}dL$H<+>|sI#H&e$& z$g~9(N*~-GX=<`4U>pCTYN!b+BYz_t1-8@(yF};*%A}zx`-j{3nYz`%!d39AUtag` zaYXKc$J5Lq?;S;NT;tkmC!h3QF;x+*gb^$?65GOcC1+Oqa8v#gi z*RS&fI$q9glT_MWadO9t$Y|b0!>$+CZ-g?`C({&q9#?=N3@WANC^Y1q!)X2?*iLU(gy{7V=zpsX0hX@%akIc< z^To;~)iHtN!=-$VZ?~^MvO3pVgLjt*+S#Q>W6<2UPW)452|W;*ASLsyGL%- zeR*pxsyD{d_PecKUCdvQR>=U?NXrKWjN>$X41)ntr&IBE9qA90#mk1wtfm`#eyg(L z-afRmaOu(^*PC$*v^d&FEh!2%u2$K{nhSWb^PL-F39avpPIk%kiEon)pW4 zhp&+uW)WYXhR)4GWoQ13mx`2AIQ>P&Ql-m?bu*p%0OUc&`x3~11E;&9Q=U`i@+~`m z@=Y$OUkB}xGg?H$ck?TF9DS)l6zXO-Z-EM2ox=v2(q#08+ksy^9OE(iD-La30JUQ{YHax5$G*15S6{v zyNZR94J%eGWX&2bT!RV=6|H8Edy?l*0+mH085?Ytt^%F_qR%;FaEoTxq4s=BMjN$P zVrsf~W#-atRR8b%&cTTd|pI6~6lQpP%T=eHOaFq_<@ zyngs*`_pu)4)1QVUPBlj%b2C*s`Ob`V#XqU?C3znoA$edW0Qn@56+fvC=Tfm^ubF1 zH|?ts4Rk5`7+%lAS4v=Py?)tzkZ;EWIjv~a5S$^$cTDc=QwnW@tefqBma0#1<4p$6 zbA!#k68`lt6fQO+E~wz64PlT4&b+J!~6fgA^{-Z!)$k6P{d*ZQgxwCVd|2ABjcN6fhn z)@>h3yb^5gsDTAoo-;=|!Ybxt9Xg*;Zk?(XKN7BE zU=fTOKh#ND$)teXP-MOjo(+?Z-S|pTE9gXgI+x-SeE`w(tK~Pz>$jLNxRakntU8tn ze#R5nnP_xhtlbV@@$g`@P!ImJ#8@a=`AV)~-cPX){7Bt0B^|w*4EoVVOQYTaO!-R_ zPRN++JRKlhfY0W8`Ked|!&OdL`Nu8tS<+P6nB>3@+tF^=hBoe<38Q#Toqq4Ot>-y3 z<-9n<9Y=_-FG^%FZP$S`bUM3Gv;z|-HWt`*t~A}a3m1Hf07C9`y1&N$);;CLi3}cAiYJ!7!*Gn6xUc2_isRIRmA!$Mp1=ze(oqTm zQq)%z>vM0-Ya*E&rZ$lF!2LP>Tz2pzdkzsY;I zQj{4wMpAxYg+vn$hTlg1BY3v~5VvSyBY{raZr)xITZ@crb9%)#DGv}RSxOcuDbC2r4}`W^mw;y*UH1CN$(KXGO5^C5gX*@ zB=s1$qQCyO!*<<(XggQ2_0!qLD$ko*b8+oVpFp*oHG0$EGS4E@pP7eUu{;f?dOlR~ zxFe}zFw`vFr{^1~#GIp!tephrsR=A`V~K9yNoTwda+2p1S6xM|xNdj8z2tkJ>s6f( zYw|^-#*9RKceW8B%R5c4WC(sFxn5}voXXHtZP>_m?!6NHQq?S=wC<%%O>9m~;#ue> zO-vE0{c4n!(5DF)HQMKE`VqcL8VtJ|#gWr3bN(PocbK-}$H@%Fcd@TT?4>TgcLWGM zz)tEhv|cKb?_^$Thp7ylGYfCY*7nKUj+J+YhI!j9xmgGyx@RN^j1;O_=Zbr?R)0wA&V@5f4bJ z;Ix4E(S^bo@}V7MRDG2n z!F{LjO_NDDg;dfyR3r!{I&>qT$~fH<$}M5IIw~t}bF83dVul;?c*l%0y9nM%6fDo= zv*(G#?_C)yfPn=7FlFKgsP^IARqh z{cfkf!X|N6VLDLg+S2ZY7OIM-j?!C0W!6%umbyEN@NvHnj@Fvq{#kpK)>=`U^9v=7 zIT$Lm7_zW+B3|449ea}X1MIv#i4;r=6mD)sS%_3j`-eozlAq+gKYsp}ywC0G)2(md zRbU{{>2qn1XyYvhKbX;{dB3qcUHI^sTv@RHaE$8&s&stc|N15cwjr%jegUg)$2u5zF<7qSkJn! zrohW|PM8nqkEY6AO_1LnY#1GF%{R>#r`4BM<#0!1DtVO^ZqLb0X7}iBZ4{~7uT%k7 zNMYC4FAm%^g5JphB3jff^EM@vDILf~-kXmd<0bPzCE3F`;ovdg-1n#zIQP)IxnStQ z&;Ycu&z?QUL#u+O6t4r zG@>n^(?klPyD#xS&6l>^UiU9fFfIF_IWl+Le$7FK6!2}4hr#oN(!+Jr4TZM;=Cs4c z>~aI;1=njXS$n-|E})whI;8sJ?R6lgN>@H|P_+DS=8@pq=1TK{jPL+Ycn+vs-1UAN zM8BVA(LRD?pVW2{?pvVthP_QsuK>n_Ch5paeOSWO1n^Ia8>o<@Teu={m-!A%T?*A5RX! zuh=d3uz?!q8c^u1xaEYIQ!Y;ZwE}#*oT}YSxkxYQ_uDi`OIUNFxqQ*qwH>LQjQnVM zlB?g_om?DBF5DGAG?VYHcm#zcomhr>#pBnln8tgijZOu)tId!k+P2dq?w~RQmYu62 zEMfDyqZ{40a&-&rmslBO&T;msj!by^zM6Y)_ufg?CU%H+tz zIV+5(Kuv{~!bIa4wBwfsuG^qjhkMGE=jJ0CZV_Pg;qae+yX_uH3?UWrIW&?e z@=~z~5un&`n`L?r+a7O@F9i=C$l?QVpwo4#Ton{e*J&Fb?hOVu+&|!?KHMFJwEu{{ zJ3IE_xpss7A1!_UpKqId+C$3?N+|=1Q+Uc)&(6pV0MR`VbAb3RK%1PH|8MRN{MS8A zTr6yc##~&ipcFTtls=&SP3!=}rxYv3%-pOdoLsC%h8!kr+|2*HQ}ijz(?51Z{-NHlIQw}Er9)EhzH)jbqINow{=Hky zX30MV^(LU^*TUF0ySJY!+6CX=Q(@Tn_}Dy{YsY(^pFcK9Szq1`Z#iGBZ+M)pI-0dV z{sf-M&c|a}>$Pz#r!m>eNi8I;NwM6H6Kqr~@ZKwpdDgHHjOKEYs2mm;QGL_Yl$Lu{ zC1fKuVXsm|PS%XC1BY(pl2#I-TUb5!E7`p!%G8sOlPJq@_?O5f+RxMaU`jk*hSFG7 znT+goo6ijo`=>+)@=HoLjr@;uD2?R>6v0hK1q$-0YU*skEU4(#C*W4Ad{(8ZpAx%v zL||DI3U)`Uwt?7mdE*d?8pIT7K}g|&Nsx771L81>q-PyblTiE#`4(v%9!TA7ZwE$C zrj(jcMgW0)RZQKON6MB1)v{W@8RLx7to@c+?6K>LY(g(wmjWY9bX6WLFB6tFRb(lY zJ|*Fw(kjD9k*M-Uov>naG?SLgvc46FhI~Vs{aXvQ0yh6N?=(z0p5{$|4=p2wBGEN% zMN0xcckf%i=dMB}p}qcE`(VhO2#z7w5d5!FMXy*P`UysZ2&>Y2kVH29 z>9U13Q=)ij+m>SJp~C}k-@r6B7?Qq1!m)fSymtn}XV-GeDKo-WX#FuQl=u<6Jm9?} zV14P$8L(!`>LXJ4+HCS0CrNY?g;+lg{srx(e z6TU}}}XOurl4 z%9-zsX=sPn0dk9N!GGxZk<~FK-JmM}+i^%IR-IQ&-zIc*Yyvw{++$99CFl6D(P7Cd zw$t9Hu!aT&=@UAahRb?qRdoNoi=EC$M+H@neq>>oi{eUX4n5gU`bGi_x?O$^t;JBr zUv?#=?99l#d_tw)mO7LYFt~E`MviUdQa#*NN-i_p&Q{z-ycVE}-6nf-mPTeyZ*E8h zYUPm1zDBr6HIJByOoR9EtnzUn6I)ps*Vim<5( zh6N-HVg*7TIiNQRPx=bi zrIfg9umz%*$8*n*U6S`$6phz$TeyUnJmqr)dx)g44{^5Wey0RmKZaHMW?v>rUl%bi z6zeO;t}fhDC+2~P4c(E;IX!^URlZ2?!L27|zwFnoxxR35t?2``wTIl@;hD{PqWN}Y zKXwAb>iDVZj~08`Kes^RGuIAot%tJfuW-K$uaSG=;cEZh&HQB(=}S<8f`O?C9MgLf zM_VTcBNInrK0YP|2U{a06Ckk`lY*E8F_W^1E0CD!tu;tc(J_W}ZNOzQH67A8i(CjoU%;-`M@`LqOx7U}7n1RL?w z&wijUNq};qy?pxgwRa}Q<_03RuEbhT8=*n;LByQg&-8RQHnu=VVy&mG{(pc4;NN2b3LgDOAO8Q&rT(vC@!}pJLbzv0p4|hqS^nkd#mQJ-oQ(Au zwZFo}`V8Q|x(>^~hl}-Zp>_Y=TnCh^@(CxgKlo)!Kpu&ngw9z0XN!--9MO&yYN4Ed?FDpazOm ze!}TFizBFbL5<_ZW1lnCflj`7>~mtW_n@4!&tG`XL-kKx1ZZmYKLD2f-vbMZ1paRh z>@)Q?2$C1np1GJoM=$mKnU5EA^y0D4oUHGOxn4Z>nWYnS^nyJo?&#?eJd+##6Qa+Q ze}8jTj(?9R*VCn+dSB3x;lF1Ee`d1)Vex|6GhM-Z;^)wOQ1Rli&ym-lqZib;U$6&} zcRykH99Rh|UOW~QzVvkRB}(k6{9E7j5;F8Rh&lf~!~jtC$^U;~{J%MzdD%GjPyPj9 zeg^Mf+d{7Yy8D-r<6lN~Ad2w+rg#5bEC1SkzAUZ$?b&nxr@Mc0sK3=O$ER_!6KJ3g z0#3%<7&PpDan$D_FlYez;;7G~8c-Mi;+oH$3aEAXqXqcWxIahJFGKae;Qss|{{59% z{=D*wdp`O4pB>-7SNT*M{=G^7XmI~>H-C)nLGy>_reEi;W$P7vX_@`P|4ht7ELql5@D?8JFRm=9YF_MGz z=>a_JHD)ulVdrpU<8-wJP4`|N?4LIU0C9w~f|l|A)XQ#V?qh>mS0d2AnROEL;xu91g616J}#( z2Bnv0e;UR7yMU_DAC1JH)`Adt;gR`Y5(a>n zRoFqs{2}bb>BtOF&futo1y|dQbc(9PA+MIM_M=VYI!mt&2S~(3~BF-`|8;*jN}j zK>QSc)>RifM{{czBSUjTOM6hoejZ!=m+Jp=PY~^ZInauo#pnY!2#J4{W8q=}HHEDI zfPxj&))@nAIjoI=Y){fJW1~L}e$o2ZM*OK0`lpUS)Br?tU~Br+6Pkh+yZ|rV5ge14 ziR05+(8~(aUo|{6GJj~0HL)=TngNJGv^Jceq3VD7&4QJgo%5eId+)cxdMb-wFnNDE zZ1u7|s=t(TDZX{)StKzR5qSgSCzPhbY*kGuMLzH;l)m$|HVmzQsDJbt3vEMG4off! zv)BjySbIqVC*XpjeWvh)fgRc6qtcH>lE~4=kt46H?90^4RF6{I11EWvOvhd2#h>6X zU|c9-)ci)X$EYGv3UES9y;F0hkwly)L%o-+2sveawH-KURLFokU0q}$P3CssLQj0P zjBR=~>}d9@c9U~Brb{1LzhW%^o{ijB3NPcgk5Kz1}lNd zy=GMnlYVM1ys2)Jx+!PP?^!}KtWj;Q{>j-}9Wd}6l5R{U3(X|jSNlYbz8g%sJf`+% z=YU%dTWf{q4W zZVC?c2r8=~{od-izY{0F^t*+FyRR~gX!D$EkAGm$a|bs&1uad3&EIivF}^|IVe$}8F!dC zRE6U+kM#iQSgI3OqDb}LIgHCw&4tSTTSL`4F?d=MU27U&MB;FOl%+qS0-Pi%C4Z;H zo5m>q{=`y|&iGuFmHd+KbYIj{Sap)n5@BorkvnNp2Uw3EGYr6w(~raz4I2tEls6aA zo^vU%9>H@eGj+~>Ex}e&FlJH+l}sV@0MTDj0ov3Eo>l*~mES?vN6qM@VY5vW_mu?b zE`4moN!Qd=9g|nHRWaTdSs&A!9EMk$k(VzLH14it`s(7OQLl+cLpEhoR;=_gU~BBt zvSHz8PYF{iXFo?m3ebN<+Pi8%3NExGeD4V!>GyUZ#*uj`kS4;{{w>k3f+dCUYa`bo zQgx_d13=PuJ6TM_8$Nmul02y1ZMm;SosjhG$c$abva^Ums%L|`a(Gc2%m4om{Nm2?A#wYN~$e@ah6KA_Eq^evzxEB zcv76XG5HR1e-6ZS*T(@?$GG4|W=xq+1GjQh=s_h^LvhV?xo|gRRPw>@i4{8~SPK0G z^ZL@e*0WL3{_J1Dt+v(81cFlnO(vc5dz7$yheXiNQDxKseZL(hkn@K7r-O6x!d79o zDf7!jIr+~iQmXox%R4pkg4yRQCGe(GV%Rs0q7a$WX6Ajr&R449OjaEBInKM}U$oUrVNNMI?` z-=FtJRAu`n-)L-k#Ikzp=M^jG3O-C|@z7Uc{yMg(%lN2uPkc8j(v8BC)FvD07}d-- z(p9k9kL6d}-FDoLi$j8uC!O0am7rFErRj1)B|qJNH2E!FfDL6+7=5(KWo6SHcTCrN zU__0zw9@;`@94AS{D_Vdivg>*Ju&)aU*Iqe@4(CJEAXPyfymtlDc)F{V_nskIeH-{ z9soXqy^kNd+^?Yq3PgdZt-0q4H~-t56%QXxoQbWO}ngpbqGr^|ajz|{tM zF&8NK^#yHWrLHL9dHLY(3Kc<38(tJzvsXm z5G?tjJjPYsp!m!h7lIai;LC;GGJ+(D`{N5*`JKsyox}HY3@g z+3q^3!-PH>V9 z%Up!$>PP2SU5+sUZ~NaP7eosq&8y{C3R)kN&M}ems!>?l^^oHiicBr%SL(Hg6t9}R zA@RwHAWu-Hq$t2Bol%&)lo%1fAnZ!Zj;04&f1@}Ho^|-%IHf1<5}AE>klLW>#hDsbj0s!rK)FJ;Q8<5($q*lIo7M`KrJ~FY;*`2Jd8v!jt%>C%Feb z+A+2E?L58pN5VKHseT~kHSOy}EV-p+jj3zhH}RGU>G7WjIi~CxGen7^MPOV6O>^%# z(5z==?6Xgae_xmN1R8CTi5sKfM3J!uWXLDQ%Gi@vkhsJ{b04%*c-SW{5$lnRe862{ zyUg?2M&Us^a@@vm3bZNUoaDISxd$aH%-6@z7KyV99w+{Wnglx>^iCw=*#G#;aju;R zSD1@5TPO)YJ~GVy1i?PoHO@8Ws`RRqO~+mLM%{FrVM?dWa>k3{@+0)%`bNz!D7SzX zE$?^cVuzk@oL6q-;n-3MMzh9H7RrY zA`u&{reJqhd57B+QEh`6Ae2&|t$NP$E~bsrRI7Z_^n4Q??onmSWH_9>EBj0O=Hn>j z=fH~8Uoj%3*jQO1^?{dR^h@xz5m0L+k~^YZuD|@3!vYF;VBlRkZG$L&!OeM1GZ%8YAd? zP!nlp1fR?p+KeK!IRveZktNO`mH)3mOQ`_6;zPO{O3iDgFeq1fSM*Y!um@FaGQtGP z*lTKVwT`cqEWSrLMj0s_k;lz5oOBNZihI*K9=oHIjB{S>r3Y#I$JM;|@id^Fy zCCKz-B@peYmg@7fP~rkcMRBsjHTT8MCg0^9Fw0k?Oz|}231nyZVh-4@L{ITGFgvlL z=L@JOG~sJ4z!7yr%}T3MF^R$xhsK{0;xI(zFuf&v-wP!0N{}l^m&?|}KcGJ8!g77Y zBnz<1Zj$qapcHv$;k7$9u*k2_y#&71m1+m+aZMp$P*6!UlwhVabOMhIXg$uM@08_^c3cqA?R?OMa@-D_Vlj~W&4&+NJFYOP8)4)|pq-4(1ko19{^ZW78!yDbkvB3pH!jx^p_ktvq%%~QELf8-u9r8|6lE2B7iY-_`Vi@x zlyU?Ui?ijJ_ERVKGy_x5_i^gD1YFc3OnXs}N~t+)h&1bq zJih3ub1ryf_2unNf1c2+%TSr&=loXLM)dTi3OOmpa zINUwhJ(VUON78N$>Gf;P2muyy7N5oIC@%IkCkAE%w}u~EGW%}{pqA~d7YmxC8VdV} zO0&7?E4yL$DCw6|6UH6n#%K;3MZXq8JBRe9!KQp$gm7&7?HY<@nmZ4xLnebQ9;Y~f z!Z1&dNK+c2pM3*JnM9r**$wGE{zRo-X2LN#;Kp#18?`s}K%e=;HZIRXOzRR^2q_f* z{^t%2D=SS?$qll;a37UUdGPg|oeba7d*vQ=F~V@ia5kj16M+&X`c7Wsk=UD2nnfh^wq#D8W^|WseX*JwBKk&!~ z2CY5o*DVwcbsoz<{RDV=KVq&uJPu|hj8D>UcKn%zAG_O~V$9GH`D?D%6Z@n*B@qT$Y6Z zXI^QXlgBo=yZ++ZYLrHwxxAD1ku6SD8)3hNz)p;au)#nyjE2W_G^7Tvz2+hzhL9F- z>6edhcxvsX?xQ`wmHo^ICwHYaDW#7SXT$W<^v|>^gkw5dP5?98Y$TOmIng+PbWMwY zN5)*1Su{Cih1<&p$V^%>-#6s>uwBq}xX>+L-G)70kU@Sni*p*RyF`cHWdKyBwu;&c zzJPfxVzcL~kf#&6U)wxX9uV<5DznL93I!|I17T6XFEe)(W%jx}yy1-wYg7ODf#@X* z4*?kI@Q-l@k$Rj(gCw$FaDJ)`1xSo24wW`YygsOW#~4lMJm4as6x)5d>3%j4O(WaD z7JM0vKpToCukGlQjKCH1)<%5u%Ua#B*32zEg}!{&$%!3Az9vTtuX!Rz)p;TgTAgyA zCiSCmphE?Td3%oOe7)pwTBdQ26K|rU%WV(+!XpvbCR`xv%ZF;)#y5<4 z(#r-nVPx<#Ir(P0xu8c+menExuF3gp9v+z`>VtaT~TIj&R9#{{qUTgp(I8mp_*40lv7W5YFfa+5{ccwh$g;Ud;5h zFE|jYdBWERmvfTDJyf9k9&1}^!6)k>?shDXNbHgOM%j`XG9W4iSzS_GPrM!Sl;S2N z^ZG);qB82Ry9%BpDW^=B6KN3vh&R5oEs!?D!)x3xk>$IE-Vy)#nETQP;*utoOE57M zQ55 z0siV-xaQ?CLyB=fd-whGdb9|hO=-|?wNz`x^@kHCJvgWL%bosfNFT`XSq zM)&(2zWI_|TDp{DB%9=0M*;I`DU-a+U@ggyj<#Hv&PX3bLLNGVOT5b2BFS(ivXJxT zW<##tG@-{(RK_MHGiIO}doPXVr2$*i9;?LzB)?NtttD_F%6Hvvi)G|c#YJWHNMspH zW`weDf5`b6b4UlI0rJpih6LKKn2FO1s|DCVHGO!lG5NFSrWpyHWYNRi?mu5Q+m7TAl^1h-pJn5!|H*p_GJ%;cWr zdAzz8`I)^gu4d4e`8k&^wVoF!z7jd^4>p#PFv?X7YXoa)CSs*&MqbBBZ)Zb25ompg z^aFAfR;5_x+`!2>L^>|el}A0W2NUW_luUSO<=2oii{OOP68X&Fx?R04G-?iqg~d4=3R#MWBakp$XqIHM6%^{V)al5*Gb@dpsT)`y`|?wG zAnH72USwvCmcR5%W`X*53&EdQ``{v0Wwytx?ftuWyL+8`Z4G@|MnuMCxPX1r96-Q{ zQu1Z3oy~C3e3%mh^44^HVwDhWeOujzn$>z`h^J^YB2it4=Sa&<6Mq-+?~!9zdL3-G z^0v0ROxe{9Au>H9?q4YtZrrc2KBywGQKT@sm^Nvn?-@0EZW?g666UpND_3l7h; zf7R0;Ay0&=tUj9=NjC!-1`IW%-L(V|MUS!yZLuYhHe z)pSP1J7()kfA|sP)@`4_S8k3YWGyq%GoOT8`@v;^rHG@N@$1=G5l&S1M4dd}s(?k1ij%254R=>Ad!U~6DfDs(Bi}5WA zj5zBg-Tg=dQJd0xUs~pmko?<9nfhQ5f=8UBX&r%PP^@`D`55$}wCbqVp;QwHUrBg6 zqPHQT^WaY;?E=syAQ;7%){tW%2oY|Hp}W10Nuank{fPiDep8obH!M@cSb)gjgUkJ4 z_#T)wmsn(j9_n8teJJfguR@70d_8+aLP;>~BSnRGAfi4PPe~MT4fEUb&ox6p3UP0pU~2x#31W+W}m@M(k(e_%I(s19FHA5tR$u z{24S2%E(Mei18t zJrS#(Ju)bQ!7>KmYYyt64X+r*S|ZmsDq&9G)FWLf=-(3feZ)`Cg|)|^7j1!Kl&*5z zk3g+hZNO!G>jg8@RTyfi&%LIp?&nINopb{>-MJAu(~Xf*O?U#)0RBa+1yz;Q##bd2 z4;8ulogK#7h#mV{#Y)=LtsQ*7mI3=3rFwuXUU~1rPt@fQ-W8r6UIU$7{wsFv7+0)v z$(HnWemkP1J0^%+i?^4KqFj-crsRreBv~leS_B0dBS7|cJj{k%?Sfn=oLr5%Sue>QI}HaQRif7 z@~T&K$m-mb%rQ7$$Q21+%oS=s+3~Bjemj^GG*{FUC0FiqQqR5<$1AiotYhh2lsgPv z@jJZ!{A1j;^$LVSHEmRIm=oX3=F%c~4M>-dl0M+I3SXvd6e^~Z#3E}>(c zm&XEY$adH#l2>3Sa*rEMH#3i`Jogokb#jk)dfR7XK6#UZ zw^%&)liAy6@(+la-UTwnO#JVPea@JGH^ z0rwLn&;j&t{$KXqK#m(Ekq%eO6%U}p(^mV-O%H|(L#5usNIVZ-#T~cZCgV z^SL{RY2or0zjeLov(q)+z{(}qF|I9BMJv`>&UE&g`Ix(C#zxXeNj5#69!1#_<(`k) z{MocNAGK#zYKkr|hxkC9N#>S`^-zlH_HEe6hY!m*#pb4Y6g%WQ=m+XU`ds48K->e4 z0-dg*8#Z|Zb~#@7J@mN2fQMupmx6?eLpyb@gTBLByk^FNMKj~~230BVR9QFiK7VKd zeBj{yrf*n_*Qqa7^c@G0%Oo#cIDeEF=d2v79p->5GtxO(W&`ef;#&v;T2ZB$pjLoE z(ww2Vg9<#ISxpJ%4-0_CH^$nqIeEwlSqX=CJo}%_P*gbRST1_Z*s*;8^Qo^+-S@Qz zQ#5duXjpb_9A|A3)^KRzgZM#B4U<2;N1sk=ziN;L+fyu>KQeyoX9i#~p|z){mK z9zDTIIEy9q0ZaMrNJok)f04+}fx^hd+$tQSHBY^nk-l)PXZAp@cyZIA(w{J7c++7% z?!gz)s>G<0aWwn~t=4CrO!2J`=VzF`7S8%1-l+5E?rlk%P>UJP9}59h#!Oy6zQCK; z&!s3gXd^ZE2cZ?XYZHd_2hl_mA0}cS&|Zv%o2aNHF*qQVnO4$%nhP@g9WfpaWSM6l z8U7fV2~shIckb>n6T}bXN>O&@V>FEp!8r49(B_tE=`U>T2;&L1zP1`rhDz5-Qkk=@ zpRd zVe|DMPA z)0YX~_%&Il9dTrpeh!SwO$kvIqeEi9(S>W}&tOm0v9%rvhs7`1cSuK%13^HRjh|&| zfpk(tGDiDhbTM-?PN^d+mk@y>t9I0}L=y8}6L#LzL`JI$6)2Djf`&TTW9z3Ho&o`# zMxj;isxfn0utrqRC>Uc<(?x*aZUB7?0Ur&kH0b@lzmN4!TJm2`mrtG@WRSNJ?;X*XKZN0p-g$o-@_69liMODH5Ptf(kl&;VmTBOM+NA0Bs==l`G>nq$-7 zl$I1>!aib!l;%<69<8l1%z$tO0jfMwR9a`jNHpEdyLd@`%HiDyc48o>RZ##_(AL(} z)K*sTXWMOV`=uXUBeha*tuhMO=WOp22DJk4i`QMNVeDArA($#wT^R8W3h9g$k> z9kK}X1#qK5TU-Z=Hav?zRr%~dYr4!wPt{I;`N8D=HVNaBq3i&$Ra?Cg=BofK#Se3n>)q3|vC%p- ze2=jqrxxajpjrlV{`(^qG9x47gSeR_&z$_3>DZ?Yxaub_!-0W*b>GCeZ}9j)jUg%- zjn8AZFQsa?Hc%zn4>~aWHLfA5PrNHx{`0`OaHaBx%#G9iBd>&GX}?Vg00IGP)RId1DvGY4uRqZjD3U z6rz>1%8U!t|D=(g`M`w4HMady>B1S@C>1VK9~GdEkMuOCHhT^f#yg!aH3ihlRAAR|OV#e+&R5cObvHF0cXoB8adDGe@#6$G zqV2LP^R@NAEMTB^eW^sT4}2{-i<-sh^mEdbvYnJdiSr!=M9~t5)9wEUR6wi0^R>jC zd|~RQDaog%UPSuWCjXd29=??APGs)Z0%* z>|0rm8%$Z}QVDtg))&W2+q-Ic!N5qI#FJ<3CEpRtmv43c_;UTaO;7wT8A@uWJ%3KZ z)Pwd=sotOxukK3iv&Q&BH&l^~_U_rVZrUq9 zcE0sx@|z_4yBH5A-y>JUAv9aY?lK4L)Uv9+!l6S; zu8)n$nRzxocQ9$E=hKBCK+i=SkkriG+^*Dm(?Z3n^{q+Wn%!b=&k)oBtR191!HZxK0_CmI@XoQ|f6>Ns@h z5dU;H^(n2w`4M2^9uQLRwK}V4d+NP}=B&C+AXL~aJSI@#Vjw7PgtEUZWDq_B`WepK zHhj3aa|O1HvT#&Q&jI862IP1)7xB1@loPUTW3nzs`r8jqrZFa?#;{5IksorYHDR`z zOr{}|ttO*!$Yfo6>a&E6#U(F`%PK=ZOEt1kx)jEbC){T9kjViZL#v6QtTNm|*$;IR z247H=oI$hMqsPjemslW?!UE$LWG zpCDKAz?X>}qfy72R9OnBCKE%J*45;xHrsSsjv7EVZ>5^?_o>6+MrTK7O&v~*6?JRq zTH{*lvldA&8@+V6W3=ncoT2_nj_I!HITQQ~Oq)&t+6{UItdR0$($5;tIL;+Sp zzcFOv^J25G44c9dZB0xyCN!+70sEyCF98ZG0lWlk3_g85<1_3uDoR8aY*@mBnL!{8 zOP~Sx+D6cI`=A)&affQ}TVCdN+fi(Fk-UPKLv@#z^|h(7NS>^lyx`R>7q>1Oa{jCL zzP9|PJ=-q7eA~7wFF&(^zDh(g@X;ANlc~3o$>fvUZ-1QJoxJU<)l7}2irA{yctQheUw(xnHl0rHgQ}*S{@6D7)k(K~Sh7sK zY|!PQ-=6W@45YFeHIRv;K;xxs)1bAl$;+h99=B4MHik4FosHcGV+>c^7T z4lz6gC5%PlH4G1yG4Z2a4>Z~Bls_<`x7uj-IfoNQyMs;+I$0WPyA_*GcYrcp|678O z{Lej&NhEt7dm4XOqVE7{fbI!tpY93yY312~JjzsWnq*mEnr&HOU*WjUzSsV-?_>Wl zpJ|`*aR>FQ0X0_*s=D8&j^QXC1_gaR&af|NP!*l-xq#0Z2>6tM5A~U6!x5XE@e0BkORu;ERdae}f~*Yog9g&D zG$V;oS2$LQ9MrdOp9+*ZzH*^R;u^*dmSs^sSxG(7`|tc}>m8R}br;#=`0h`yo*4Pi zf%~Qhw{IU@J!}7!2S1*>;8%C8bG-EC7u)L|-uuAXnZ20=PDy$dq;0q0sjF7^J0apua&|oNZ1p{KRFd!A0BW9B~2dxQBWzwd} zG2SK?kCiaxd#HrnV6UpF!8Q3PPX5uS)TiuK>cMy!b8PZ^NM^TrxcOSMINUbXcClZW z;J!dT-#OcTk$JiETJt*Rb^Zs<21ye*(-}=>izpL(E@5-Zent8OVSio4O!{DFT%xyw zJ^(p%enO8_N=T;Jo}FTQc8cv?y4c<{LtCU#&C5hYYnHP`bR#=n1CrS-?X(-4 z+OIwTy!Fnu@E5hgP?_s~nPn03`P*J!(*p4U2i z#$COFzq@gN@*kftYXLWFqAGQu-ZqoR_kbz&L*h|3K4u+f9InE39QA2+&Qap(T*pzL zR_z>Rg)2OcGGDk9C7sR&Sm7vf)N#~vOfL)eI@5g9ou+N3XHC**VYK-cQLv+UgGndI zlEEm*xSp8J&j}*#LPWs~)MOTA;R*T#`^%POOTxh3P?}8eoIz}-b03!^Ln1d+VaP5V zhO|||17C9U!q84C6J|M)7pah&!+qq9R?39IXm(ZrRjEb=$||!u7&x+<)uFpA?PNWl z@4vA%n5`SfnOIk=pQwD_cWeDC|CY=7Y(5QI*}=xq#^jW*>&7-$0O@OgJ2TI_(^dTxoD4 z#mGXZlt{wELJn>n+Bpi|tU<$tC|!t!9Nbv6b2NJSe6vD)vQO7Sdw}3*o7wP^fW;7W zxdL{!gc+@(7z~Y|ER_T0GS0j{yw;=(izlPVOc&V#6Bn35^9^F`3gvDUXdcU|r# z$3u=MO|P5Y_A7da*HYvY^rcd%u>%(r0X2>4Fu3dv$8#2|)8cSitY%a<2?rx2VcCLv zAd5BOA{n9{w~FLdriyTRN!S>9wi)Uob)~vN71jTRQt@)7;w9i!z0{jkD&CEneJ|+) zR&py!UcXk$uAfsbq0^PiY3ikc?S)XG@NsCsJ#hnS;3I34UU3Oo4)_wpS0Azj_X$7w z0a15-gG$E1w#BGuz~z+DGK@|Bo$HPZu4>!9e(L%j+is+9c0NAtnw$0$W!VkKpXnsc z>bmO>-gjr~xEeS8`qAXY(~~Fu^z==whnYqhgZ$;fbto5#sIUjk?_+#yZ0(tVmK`l6 z8z{)KQKXBF!s-19M+;{rUYctTW~ms`TN2Z$H7#T%Au@vq#NQ*BFcY&Gtr)+Q7j&8p zK>|^g)#BR|HRw?pPkH!?=Hc5DPi8N1=(R)YAG5sGa8y0mz<8^8K@J%vCtSmFhH2C6 zle7iGYBCBJI&u2@7igk*Rk%b zcQn1I@x=^>DZ^*1C|l6FYbo4w5II)Bw--8@tz>p)=|u9trdG0bjX1lpq448QZ_iN+ zrVO5ZE*-pgUR&oSFJJRP^2puSeYXAG&dPB&j$Qh|eV46xSe$4%zjRFLpfBH<)tLPD zm32q2B(>ynvh5FB51f3r;oCR>g9Mjl{WlP;Mn1(%F&J=d7Padu9YuzJU_4n(_o&z>rq4?yeJQyDt{|6-%akR? zrKXF_D?Hc3I*9qmn#@=Q;R@^b~-QN%IBH+JTsqX<~O;r8*$B*a8cLB-cM)OHolsy!gY_DqIgUBqZjIQD9TEo&Mu%!NHjiU5LVzE9IncY>^cm_;yIJ@bb?h1&UzxaYr z_U->-!NN5+Bu~8gX7a>M=U%&D{_5-I&Rg4m)W(Ucwr;;_e!y!HN;xwjSe zJh*mm3JBT1;SXfe{A;eBF>B2=CsSiKj{8mXRS$2?p2soC4&r*aWe-?UR3dCRT;4SD z6_8^jE$MvV!d;^5R=Ew%;<+s3;<8!5F_K;;xk(o85^lGeJ?v{cmBAk7Rx=CeXWb4R z9yf;2IEmWOG&#Zf+nZi#dEi*02V-!rjdQq-AFkNEww`fgk03iL9%m6O7AK4+gl{Cx zs!cbUF#9Nv?ZMgP9gH`W@l~RX&mS^BlSY-TwD110>mZ^V5vMG^L=iXnY+R`S#Zymz z);~R}r2B&s75#mq-HFKA?x~Tv!UgVyzIl-qzAJ+3eb)!?bZ_(R^?l*~L_4852Dt+IG5`xUna*01_TF0XREExg156^ullJk|SHfMr|A8r^ykn)EODd zNKluNm|dx%VF_vY**`Uqwh9N)J{1LQl|~;>eC#%UTo@xwnfxgE zP4d&^&fo46Dt^8D;9cu+sYWT;gIkkp#Tfr6w_idNCD6%ws;d@-YN8Vjv>Hu*pzI{@ zUM2HJU5=+Gf-o4l@Xz z!bbI3=_%cQ^_Xf@q?RH7 z$ijs2;!EQDqOcL!xt)-NalC22{Ju%pXd)&SSFQ3(GF>S*%T)fA_4PN>?4E-T|9Nq0 zeL3pUqu{Nn_8qM`QqA7g9c6E-vae|4nPXALe{0l(2Q3E=u94C{vSV$Fabj&t@T_Td zZK73BuJCw8NI8iJ?0v2fE6~XTE&mg(d-svfU*G#C`TC9# zd4Y1NL#&ti|1R0>Y0?{QQ_h@304piV6!MkQ>C24h4fY zP;fm<@*3Xk^1#;HgqiKVyfK}L8Me4Ef0Br@)lX+CUePk|R!;E6mj^QMReosW z43-Y)(r;P}{Ow9Ze86;X*PD}u>Q2P!u{=H|eE^!iP92t`!)1uQ#AkLu+*ZX7L(^xW z>@7>VE!^#((?TO8T$Vm3k3~>jl~uBJiG?@O*w*Kp=FVHaVQTXq)+c{O2CnLNX6=Zp z?n%B)7M>FuI<5btTh}MIOC9xl=A84J@`An1^L8}$7ADx-bH|KYRQTf-+0<{rhzZO2 zx%u4Gr_#mJtH=jm?V3f;&!r?ir@XJ9C1$XY24&`1s2P{#Hp4Z!8{tmr5#d4e9-+~O@ys8n)-v!?U?DNYBU0-OyDWcKSeKG2xO-Vma3yp(u!Vk9pg2i2ZmUM70v z7+c<{%V}o|yx4YNVDib2k0#&v^VR@~fMkNPfO}w1VQva&Y(e zrzaA5nU3j6h`*uGbH>yg_#0*~PvH#BRjMzPtMc(s`A$aVmXi8!GxWDvhW;+g(7~T! z=uEWXG%oI4I&^uWui$6DhGM_|5Oq0TpO@;`PhHFgLFaNi-43^)^9!CZv0D(lN+3+! z23we&h{WUUv$IvCfpNUY?Qz>(PKx{<4VPtJ3>6@k-$TBCWZISW%bLcnxarX9Huwg8&k;$nn#o7?MM+CD&lJF zp&sr_4CFoBAd{|6Vh8WisAiUD_&-&7Td*@uHe-@(t*@OBgO2#I9ey86>8w5Y_^Dn#T4p#B1&#b&#u46AZlz>iYP`kgk6S6_ ztZllqxquYp@*&IRLzc^jEH|bZh=Hpb1K$H0a$>X2K26n(Q5#NV#zN(T!OjM|&Rf`5 zTlT8-yRRyCMuze;OpN#gzCcbu(3xUt)D;WGl&BbsM7`$RFu1LbFcx+?G#O)gQZ!5g zMieBc4I!uxhaq2JpY!p3ew=+FQ{80}FwsIAvrp7^nu2qCFRfs}RZ`fjZ*+2~v zrAGE!x9pTD*(p=lls#Q=+sFaoxh>eei%V_c8)NJ-=i??N1|yzH<SY;bwx#4R$`<_ld99cUy6WPOociMytG`e#z46&MvE$y;kHa zclKiAx$gioYjNb5C0m3eC&5(-6QRg$5-4m}+D;!zgk_o@Mu9Vjzz0S6x7GTIpDdBO zpIah7X-I2vrt6rt+jM}wg4F$eJCljq0d!WRPZc}~lTu_v0w~AqYd{4Dh4Y|dG26H{ z!qLKj#6ErL!9pfG%a*JrK@Wr~dZPstJvA70tOrJwQEf!3ce7GP71*2JOy9oC_U&Zb zX`i97qu`qT`_-3U-p{tA@pyWCA^TZcNahoy<0x3B< zsT0l?FE(FlSz}g=R8p$UeJ$f?tuRbZC}Ye+EQZ_Z9l|Dgld@HKNY>e@)nX}?s8f=t zVltacB?W_uX@YeEN#NE}QP|Ia+0QvuHcyTAW;?Za(5+x5y;~(sX(zoCZj;`?x7z8A zwLwdmRvL-113PRXMl40!(X$fB9+z?1;Rm9iSr@BBZKqQn*Q7?NSwhL9TX))+YRX|> ziZ)bxI~iN@Jt@X~-J(YtaF1G3o&K0ObNg`T%J!;juHai$^m>=VsV(X6z?AwCx%73Q zsn_|Aw6?{Bc|9yNWS2^BzB5k9%5J z(!dVkfQd7jhMvBaB#au8M2PKnl26Vqb?5XUGl=v=a>`@LI;rEwZ*Cen{@23EA4Z7J z|JX-7{G-OC>n>bzLd+5(doy4CAgg`0i@()3X4SvqHM#lg5_W`)A%&tYlNF~dD^wN) zMK4mSmlaXq3Y2M!?7GBKa9v_anocKWHJa45E=g(8#C0PPWnwUtNA21EIIzq>yVwjI;g5zBk~ze3po1{ z_weH;TZN(_;2hNVE@gHE&S9I97*U1&-oJZ9m69k+2g|DDyd3`B?(Q56mZgI%HIn|C z*BGgiElzX}7C*k*fx+B#Fc*U^7W{69D>MJu4+^<(rdgnzumzIX?s{6F9Z#Q3;)Jgf zSK?$h|Jcm_YCc+t?@F(M1^mRFsP$QiQ*}E19=~4{Rnckm82#cl&u+_87Qy53`l*(i zu#Iz!^CWzAQk{OPI@vbEG0ii>JH2j6)=xnBOjpcM-zfT?7EJ+V- zW>~Y^F>bw~CC#^~L5d?A4%;x?wL_(aH;=jC=K3#_&nDNB%l6*WFuM0O$?K#Ji+#@S zg-;|qJ0BIu`jykKcA43kKwaua=}Y8c7xW}g-bxoW2E-8C3^+s1h^|N~ z_QYcYq-sz9*l20AXH={~njEQ%Es`!1R!HlG_0p|yr| zNdqLYLAu$yDfZ`>7`-3uxQh$}`b$W}W{*p)qcMn1C5%anw(1ituPogXW zQhqRK6{wOQl=Z&swEMEt?&~_@^TixqHd&6W=s40Rh&*tNi;g2JIxLVF$R^HlCiQ7W z%|)~*%y}Wqc_GYsAsjaz`*;~`kemZl|3qqt#;73jL{pqOxBYTt}h*_en`#tOuLtXW!h&)xU`@#*B=$6Cnn zXPKm3*!juUg^!?sy_x)w_}`g7{p>k+H^kReU3T_pKip~eRiJ-tXI?>tjm2{dD%6Z`&G?xAmz}w>bUv0>Gd{~t zjKVuSSV>uu(3)^_V>mQSrJ+Jc(R>a^LcHmgZ*_tUV? zZP3}-_tuhEuXlTVIYAGjICJ)eyL&c^9uI``xEk>y!&sDHmZ`eGy3cGCgE7lpy6o>S zbXhgP_lOmIx2WT%#SL2i;{4`(AwSPcyS=dIT;%B%`czj(9UC zXXzhtam5mkvv*`wCH%YT^k>sj*DqSSU69!nB@9Z!T4h!H+wA?>f=QNeTg!s$t}my` zhAXojoh1>dQuCbXLg>1d+WKzC${x4J5fOS(R9+FT$oPBz@V#{1!53CM_v)A)lSikH zADDdM)Z%dM2jt$>o5tRDf3j5S828NbyI#+Y=8wH7xrFq-W_>@S+{f0bT^Qu@a|P>^}A8_T}n<=A5kz-sF$bbPVI4>!xCZ4!{#_Y8;E8D zv236q8;J1fPZv%HqS-(!8z^8*ID&zw0VZd_qrVZB1VD8DIst+38`oq3)026c7*)zO>Ho2;8$!OWJcI2^P4V?KSXki-BA zeRi>|cMRsB9xxX#_h09y{;1nr94v^EsN|NIj!eH&3l{5xLASt-fjH{3hV(%b^BVa3 z#*(Ay+n*?o=3C50DICZR`W0Cx3RFj;`FWV8lY;(YUxKmv23&iOx}lh#xN+-NB^nt| z8p&d^k?2S}X-Qa$*(2Gbk>WG;*;oJitc>crKCjWoppX+q&25^=IpIu%n_#3)yz?UR^eyP`_;~d_z?!j z*(ji#vDa_xbxS>a1H0sun33)gE_9B{AT2M;oJ8jr@Xu)YSLm5{1TLq?Eqb_6vE?B) z{c-b*XRcWE@Wk=c2P7{z>%4haesjzH-(M?rShsI$xwonxd9$v0#kD`){dDs0caS&K z3vZY@q-ogjc@fXdc;)?b7X4xNc`vN8Tz})Lv&WT}FX%B~*TomT)U@n#CRwFucXjah z!0#n;bX&90kd1{T8pPQL{vQ(R+iBBIE&bl}ah*nJ3HyeL zkX90 ztn1?M<6U7JbtH4ebxFTu-oE{Zzq9Wh(DG+zicVrm7-H5su}*nb5#8;X)5QvLfHFcn zQ@Pms8|gEvYyxUyKi1Ldo!OPcndM3hoY>8J?~KvxPL6i%^9S&Wj?e|FC&I5{~EfM5rddH-$*YbhOTAA(BNX*xl_wX z`i09F747=&mg&*-Z&2gVK+0_yL%$DhoYEh0*~G>Jvy(r*_G0pf#RtyVe#Ps%rH+$3 z-c6po|3+f|To`wFUY4-hAgsvd2N5$tylYn_2ewW6{_gXoX2*Z(Eh*!&}|#k zb!Pibn%UBaYt)hv{V&=wA1r^7{_M)YzoBEtQt4HIuWGgR6Mylc8oca^_B@!uu9zxVmVk$K?nyyo>({D8G zHytw>HPd(#6{%67nNPm-#AE~oo2;qfFXgeeL9f>o$*Cw3Am3BTNvWjAM}4jtK+(@p z$Q-Kh1I`{*;}z1ZY*a8#NWx4LJ*s9fZmJb9*n-%kIpU*)}T+>@3UR>Rt;? zqzR~$ji{hfR2C6YQL$o+67!52lYk}mgtipXJU5Z}p6!9LB$BA)i3SUizacyYcJ`g~ zyZ0`$n)mtq7v|hEcgnqU&hPyC_jlfL!ExHi)Y*+rUA)MQu#3)!_;_ZlJ>D4`KbJYj zKG!)XZapYHX!=v_wpu8e$VUo%K|7s4U7HY@&QI57N9OVKv_+9+{4(v@h{;E&8<#A@o;%ro&UJ!+0kkM699 zBxe~O<18Y)i|+a%NtKa!I1-6Fnk-jF9MW}c%`~%?9kwjnku@nE;Z55jOpM$T&&Dm& zlx0c6u9!=evrE&5^VmrEPqowD7iRFI4!3RP973(A2GKkhgnm)F z_CG$VO^#Gh?DIcNu1TNp>C2_dUMRg@FUG>9R|)6qeBk~sYS|C_lBF;IaaRxf>my^h zuDi4I#~yu*@j2jlLf;`a`#fX$URD#ltbj>|S<^I z7v#1cS6GQ!lR!}wH#&`>k*J*=DUFmz>Z6P{r#+-Np{!F?aA-rWws$i*>i$voj}jG1 zi%Ey0?_z0l6}n2rbzGy^pf(wG&IoRlG)kr4I8K_*bxCv7S;lndVl6NqXOS&-oj-U)pbc>NE?0GS+OO`$cK1 zO0*^X=bO#6<1Z~$VyaB`{bgtQuo*K2owGv+Av)E`in~MRXHnmZ%gV z)6jKfS-S0nLMoXNtgEaRQdA^ZI3z2fP}VEL!D3y{YHV23Si+`R7UPhv)7o1t5fmW{ zBCEL?*0!Vbwq=zyO0U8y+tKzd=lL9YyQ>I2t~JHlWw91{ldEKzL^$%hD$0@blMc~k z(iQ(CaiHr!7wN!C=^C_DR`Y{a1;rMTKhvZOp+nkPe()B;&Y8e%J@3%~NrTa*QfFzR zh^WC9PZb@S|B@<(yaPwm{ZmD$I#ncd-@7SIUFW@SCS42>qN-I?orIW*st#45JxqIAC`QhOV&oj{VbUAC$jVn8u`a)>5VxY^ zQX^P?GHx$ve?!W&nqjA*`nUV~aQ;8mgm_udiL_yBJExpg-Bv?ZP-TRvIU>iloW`H#aoK(2-iH5k_1o z5EAR_=iKLL45aTsjkQGL$A<8CO>oQM`)I`G;E)MNDV| zW&*~(4UK50sxcC)B8~W~DdUd)7neNt>P+^j9`@OJXZ-3{$Ha?iPRL!Q1vn0t!WcKt zv3Z0oUdS_|Lzasu{F>u)h!MO{H_cU{s{M1)&;8;vG(jP#HWQ@ygzTzFjoli#3;po@ z_e%@JsrP^T{{575)|VEb9=KAc8{_!AC|E4c69FplL^=C4$L2&D`*jt#+H>;5G$HoT zzq$mfYHPJ?+Nw}b>5BK?M?WkrxL>HJ*XaLiX#ueUzF=5}Sf!|wRx4o=+-awjCH4** z%93Hpa8H*O+;kK5mz~kSpG$KmGYw2Te#&i@b-5{_Cz~4erlz7kBGR53)il1TOYdr0 zs4r-mJM>Qdw#J`EA5K24M;d~Zw|c1JL3>Xmo@v;cc%fluVpqc-A|EtJr$-URkx=-N zFvD?eHH+_>hmrJ4DQqy_oq+G_1p?OXlZyxm?f5NEa2 z7GlGz!twJOFK@(+=@z5YxYt-~^c(zIW4-Z}!5RYzyN2dhs`)&qU318-qV*6AfFu|K zg)A6#RA(|E{$&_xHntr<*M!_(`h;({;e~k>y_bYR&6}rI=?)+_)VbZAET8G zXgC6lyIfT*wUvD5$(Nq3RLjw+t)(e76n0eW*#_W({0lV}Ne}L;HZToqx8vEaQSVY@ zdA4q7-TFGdNS%Mw&ad0v|6b5J1W|&zVMU0NRn`=S7JG|$T@e*&!4Y>6{puK`G{K5=Cw15iP0?ebO_9bDLfHOj;fQ2wvHDM2K4K}j@o=>#gj#3&@|_3z^836h5Cfm>_FOW>f%I?S zxiqbp>CRCP0{9o&;a^*!-b19GjKSd$jYPuHSWO)(hz9nc2jop`$Gqnku77dt7310# z?YS7Wj=uedt1C9dFWY^`?axfHDUE3FP}5~(gl}1T32!Nj4{vNI_btqVM9;W z=9J4$IB8~gy!);R?)hg7S#j{_ttX8{A2g(`hKViX=FXmc(p7Xd-9}aujjUQs1$x!J z7V(-{%eV2PdA>8VA%ioS>U3-R)bx_fnv5_i)Di7So*6we*(G)9=bBy7A0!t_i}g#) z%c7Shdo%BAdt!SM{~h`=_GRMZioKcsOd`v-m@VOi-O?pBedy6RkYmCAWJlPatjHSVWH91c9-UYJ1* z%Iv5Vj|w3JN?FtB+^frwjiD3`kc-{QfOKLwSPz^RkOc=vt7pBiUMZIx#=QuwLF>>4 zbP#bF)QKh|7Ev3Tt{X(xR0XAQ1f+++(|{aGdI(65YF0gzw&AT&dV44i$!?JFj1rY& z+Xs0EC|ND_GR@%JK0=v&gQ?XEb96$IF+dK;Cz$Rk!qM#!t-~vE#A0fy>)CLu4E}Y( zlRZl}Ua-E~Eq(o)7Z>5ej2|t3?w40Ce~#bL_s@GL-}~AXrLRiw{T%(|r5Sg<`TFix zpuW?T{{8F$!gZ7AAI~$f{)28cBs9PX%Fvq7THcsSWtM%xxWPoGifAg)62j^^C#{O{ zG^ZjXB1&|?L>Mp;5Ky#Q6KP>#z4^{7o{w*5*Kqov4uG+;hBE2XLZ`*1g{H;khUUf| z#t*YU)gQMWPim5$P!{3^>_YxZZHc~0e?r?LZ&kKvT2#AD`xvuE^?Byy<_#umBFa1K zhC+VRTyk4$n03rv<{+^bywdN%J*LU62Lx$u4>TFG){uZNSEs;tb|~Pge|K*LTB=p^ z8Db3*Tg^4{r_Mkwctl+AeT)ESo(mM=0y&HWB1i%ej88{^+#*1F5#V=`+Fc^bh@B!9 z4TwK0^kX7OC()~01@9Jzr3yO-+;JX(lqIn(^|zjfngkU{25J1^slLZwt4j&%x7` zv%=HT7vp*SeEGug+;nf|9sb?W2Z>KYpM<}PeUbPCcuO>s$tNj8nvkU2NgRS}^&!zw zxJ{pcN9$w4PF68lAt(#K?x6)R!=fZP)+ zy>$gN1B!&01e!@y7N(SO8fplD>v4d&z{w79xf71lvCsEP(#LUDL$iCT+NfY7lFg$G z!gf$O>~!S(oj1ID<-&Jvp8J!Q&3)PDu3Y}hr>ckr2$*;a!|rz75Jx76_i2{*dOd@{`>QvmA*pZzr2eKbZoz}>DCMH?%RW> zY9r6SW98H6?AT*HC_`kZh8jvAmcF&J>vvp&?!WD{OP-)ICq&3$75_F9Lyc}_SVm@| zB{4MNCYB@~)_$fxtxHM0LEn(*O>hZ1tZp(>sE~A4Gt&x+;Cwj5u>zy44Wn>>2*xnv zaz3;K1~aC4DkjIU_&x+VTbqZCEI{s7KAkD7VNk-Qt2g25Wc4y(h>tWte59JLXQtVY zjC}3KkHdca_;V2RkfI6Y+K8SHRV$ds;)xg04yKAZj1(rG&mS&da5SDw%}iKz2l5BH zXaJ=H>TeZoFPl7U*@7$z6484t*-0_BV5Sf;?3-@95#`BxTuKeC*0w@B4P+5MP1$sW zmdx3-c5Ntm^YSz2q(%-u>-1f_*q`3ry{Ite9OviCn7J3+ee5E#@=h&HWj`k?uaap( zzjx=VDj#lEbKx`9(P2TZNK`bdb>ZfkqBzN)ulQ6aJ&Z|rGE zheQy?R+bqGf_N3v%j_mb=XQLZYw_u{sf?~pYf3cInrkgDZuWJ6l>El6hY&+99^V~9 zR?Lmfjjf7t&16dOjAr1*G2q6r0d5=vZcNL7z}e3|ZcN`r%YyjaIOe6(Lgz@Z){nTC z0Oc$*QI4t305N5Nm@+_2ncA1kUFJt-zsY6HPIEHRG`z}`z^hGIqh^xQidhW>AJX*7 zUOgWZc(a+vH!rKA>^VQ__%iMGL!MSShXxi6%l9y?lC#gpK=1RSN8Ln($0+OuY>9dg z=S;bDj9B9k(X1=45OC3Yb@*w^uDd;MpyeC>eDJcje)!@wPt5=Gx_|%tp(n0e`P8qj zS@G1l$*H;F^JcYgxC?cB@E}5WKe+1H!f$r1c$RH?tM{chUVi0ex@uQ547(p9dG|lh zM9Gqh#0pUK3=FRv*T#-!cjz3Pio_Cym}F~qnB@^;rg<@}Dq0|OwP3md-q!?if}HHO zjwtlYs8>c&5V%nnz*7y-hUvu0)Pl7EMg`iuoTTrQeW$NL-bE)GqSsKlh61nP`QfdQ z+B^xuj*W#8g$>bzQM@F&F1jJwALXJr3{xG3DGkHqhjU)8r$z4SAgzO%W$4CP9HgcB z`6fr)7;p--a00sklz+iSvij18SFIU{U^8x;#JAY^}0R*^1&jz4=e&7>^ zV2C+G&{9a32!~*(@9#z?Pis83dZs}%8nJ6^n0g^%ukN|Ncloa;^jx`U$`3n;9q{#i zU622)?>zj->T9Rnb6wvHgge|$CbxrP7Z?#e=_aspJmBNQi4{LeeJpS`91F^3c^wWc zB#01srMI9JpcSDdAMOiO>AnE?+!p|!`@GT+7z{NQS^-)STI4O*Q-ef!|H`xjR~`kK z3M1(7PnOrn>*Nh`ul$jGP!<_EBQKFx$!q;nd*yyv$;d>17dgzz0!@x}Pk^glj~Ia$ zI7JY1Jj1Q!)^QuSUT!ZZ^l}F|%y3z5H+jZ!o~eiFmHp}GVEQ?QUY`R&!3BbX3qtB} zp9&=fZj$s}B3Rl1rPs(*d!BZ`P)OI(ayRbaKS1D<`M$lUr-%Du*RG=xuI}g_iadX` zG!>14*>uo}?ouyg=S6rmFZ6AAeyQW(JI@%_pw+$eo?#5F2CWV)$LIJHxmNx*o{vd9 zFLE5__z;716|-TDvw2m7A*~CBR29UuZLT4bIu;{bP|qpK8Wm;KPIa=%QqY6jPKVG3 zL4aYcf=@@S1Z!WTqo_(?=L0WQ6QS_0s>Xhoi&9&nRV6=RSMu zt#7Y=$QXYgH|OZOofGF#9!b^``wc}<;a9e@K|uX@yggVgU%SI#VU^V<3~CjEIwzbh z%#vAC{|A3qVC4W^?d1R}K}QZctY5!Km@$LBO2Lkh4MEW9;AW?u2B8l2kfX!H35ZG^ zbZ;SN1dii5pq*rIaj%gy;9l3ek_Pj2&hJ=6LV5g7%6w^ll7V0OyOK{rhFZD zHUE(Oitq>SJz<~lnfO1#w^GDW6rN={EC?dq@J-=mNy>>rSQG`8<8r(b=6OXSi;bfp zdyc1JSXE^dZaXqvndiV2TP@LNqqATOTVCSz8lp=3s4$>D)vNQDV?3hvh}0utoI{9c zb)C#)!qCW~bB6(A2C`y2$P%ni>JcK07z}I%EPW=S>3^*ndr>(-0X_N38cj{=K1@@S zh)F#VRwC9?tVrVmv_2v^#-VVkNC-^oV4?LR9{L0sW#n5}EXQ@4sZUIAua=^#$juc+ zS*ob$5NPzMqA1Yh-7Fk7R{0eZ0cO$7_`xTkzjsp=Wb|)}(!+ zxvMSoptU=`H{KuTvQpRxM;&6%AR($NbZ^0cXpLaS8Ge>5>D9);hKYkcqk<))f+eF4 zSTZWG5w#lL;isz*H3uvimF9y2aj1$P^Em8+>{7v4QW5#DPKr~u)l99L_`x_{5?>eJ z5buq1aTd2mqChuMprI&GPc-MD5^dWD%l=DaM){60W0x5-oX_2QUFW;T(WDqu?;7~? z%$q}C;0$^p?}0hIcrwLv3|Kf(!Il+C5fxUj>TJPCAyaXDf}zOxZYo59F#G^p8C9%) z^vVzBJ~G8pdYTrEyW&Z%?t%5AmrNXfUEdY>w#zO(e7yx&9!t}pjk~+MySqCCcXxM( z;O_43?gV$Y5FCOA*Wm83Pu_3ele5?U`*T6{bWK-Fbx*-F!}NX7%lT5!Id<~?+Tq1= z%abg`Xcs%|yYk&nbFJCDm^c3cgt-YyMUm9Rta== zD+a%}?>nb38$sY@d`KWASdYMcQI;(20~g~a*ZlR&P-N}~UltEUD#<^eV8a}nPhrq+ zK;)vTS9l2{Osqc2IH`T{#Pxp7>-TAa*7FeEB|wAk>9_EEox`rz?VbWpV0%nQt285uH1r9C6XL2jR_e@kQ5kL;#J^siAI->aM~`7 zOR5@7I{3HoPCvh!@;Vsm$!j75?glgnq*eV{)xzdq5FL|~iWTT>FTzlaZ`+|H&TzN& z#f8F1i!`u7+QC5u;eEJZp$U27($6hrdq1zUD+oitef#mXZr$@y5@D2KmK*w2BY$^Dg!$l6y{ly0)AuWE- z=?^7e$0Uy`@o(*w)3!QwxbcEhi0-G}Kw6EFA^q!0MB z4C+IU`S+@&{#{|wO;?`WR-@M85}Z#~I8U!&i!A52ccFc#twe6BwZ)CQ{+O+l-p;r4 zQ~#}ZOM@jB+B#3;_ZwP5TKfL^$mCfhvO3ydZ^QhO@+SWGhuMsuW4xE|kV`2J<7&K}ZsV+XTFBf~HpmxtkX?fh1{ z!M-_ZvCx>Q*Qeeh-BNxPJ6=89YV5@OGHN19$<~Ltr{W1Q`qQyw_(;lSpZhzWTO5a^ z0Tq1;CJpRT=$c4Rd0qKtNkZP*C~`|vO6KqG&ad#3)@e(1Rqu^AtmnO9kXe_6FYPzY z51Q|L@Y$z+2AFnGbP>M`R8U>5gX2EEL$@iR2I5hI@~IV3Pehk1>86u@yJQWLH@P9p z<0~hY5Xb!%V+D&q&5fLhyhS-h?LrC494BK?@JZQbiS$P0y2cd&Miz-j@^&4%JIA2s zFe>~7#X2scNNnZ8r5%t_VzvSv6857~Q#X)y9y#%rrMds~$!c5cZ4q9HWp`e5Q5J>V z2GyQQA4^_v6QZ%UeR+NuG5`M zd%dPbloK#BU5GE!d(75)fH;o=r^S>grmxh|_u4)Kuh(rm^>tPDnwx(v&sRmAfhY9e zD{0i=G`-oY>*4bInbWjpGmlPO+IpXo>SA8Yf5iP=$L6(fRPIew7S8Orj<&Rb;|l0^ z1=<q3e z%hmPl-lGBSpp2%wmcygby?ZTz?{D2JQvx(|2(E+EaR~RUqPZJFiBlupd@wk@E$c?< zs6yUGv|r7~z@xJrI?Qg<=nH*B4@hlllaBE5u-w$BM@}+nk$G!FO4l&?X<@Fkw7e_@63z1qK(qhJdO8@SaJli zyW|sY6YvusWa)Q0 z8aA87N6kFmcsMrG>)`Gsf}V8B6ZH`WY06B=Jvl3vD-vQUEoSWHtQ|E+z8;Oh#{TOV z82GIwmAltT*zsh%YvGOrz<+dr$DP7*a3uJ4#?KymCR6f`J z^1R*C6)|&;EVeiaTkQ&pFzTXZd|Vha4W#wE^R;pJFjZ1O2ylWP zy=te`M7qg2kab&atfXvun!g<^fSu9li1juhx6b zlhS;Gh4;oOZWvkyot12DqE*6a3durQjL9es;KSUt?1}C(i7T=`5q*LNlSX0n)zUo7 zGmRO$cC3TRC6Yt>MT-A4AJwPS*SN2$&+tuh8x`H(-}G09x~qLAd}{E_5U8j2kEom| zs@RyC=kHBN)l4_tioo;W-$?l#czBGvUhIxZsdEA4{p!*y^zMx?oN*P6j2Tjx6$#R zsiYvFvT7CRg$C&upRc{T6nURF&+}%wdRz3E1_Hj^y}6?x$fQhgeX)vJL-~@8Is&C% z-T6Z{Y1tTqM1D1H_wDy=)0)nGGmCqi>B(ZmkkS^un%k8G5xhc16g!4Crt}oX@f1e$ z!Ms`C{NN&J)Cm|s;S!W}KWl%ivE@~u1get(W%R^_paMRgtqz07@oM=T{2-b=bbvLs z=t=j5zB5Ad{BXRzCKr2r;og7V7i09$zp6d)FdXBWUY`#*d^Tgh4BaFciUxb^q;Iz9 zy3%db8nzTemJE`Xl2`_6D_n(gfw7?NZCgzi=lCVRCwJPzS?f_yk&UPeh5{w#7j@Oe zWW9@n?i0qp-aBS~UegAe(Wilk>3Yl{UYAozqj>_ucz0w!xro6foGdCLBhSS*<`_g% z9k#D28v!3}MO~_n)gmizn{*=)XoV^%O^H+9WCE^{LnWuEz&f^O!PPX?lq0^&cVc{G zy@TVFdDq=AwDJ2*g^Px>!X0#>U>O(Qyk`N0V4|5IX^$Y`zt~fA%Lug)IpaR{I(*hQMc99X%Xt(MKDc zZGExL?TMM|>k7xVHlN_x;Ie^vxh`H_#DB<%C8^bZfyxp|$KF|F}##h{3{+H(DlV+KT0*XtFy*Jb9 z<>PF;igZ>cfS79mAz#@I*%}7p5!`b5c^d!CbbGuI%TRaK3qy{or(y3zzV80yXt_1F z+0D1|D6|~+Q%du0==E&*%FGwB_k*JM_?V#Ed9qP2L?c^__Q^j)LkDY)#YdpN6~Y-x z_rQ}Robi^FLKz%k5(L9lWQ79D$NUR{CGK~#^43fQmTFWaa`$qwsHPwJj)I8_quz)cLg^Eo$wCJA1c06o^%d6Ik{8^sCi>+=4%QX%H)WX4A?ds}EcN zmv=~{^$=(%Pz3XD>US(`A)|w_CQkmO{8)=vG(y15-cJ`qSj z^H6y$0ja(cm~6SW(qff~cpBgFt(=-TthJn2R7GbMRo6+usDdAb7&{S!LcVgpd5p}H zJI+^&)5J)7YW}ciyw4uN|gp` zS>Hc$6_rsKNgiA`zS!O-KpG+n**NIBoIr4dD=RU?upr1W``R#=x_XiJgV5wM_>_AE8T6 zw_M*^4{d*eTtb$-)8WY> zJ%tSgg#FfS8#m}Z=$f2L6O;qxZ$lD1m;*d=*}M!KqfKr#=;|tzq687O0)zgNS~h#> zzLe3E^LCeL>Cr)Whp`2{mIWt#pS;_o;N0`$#afCgbBS`a7nGQ^?4>a4WOnzcpNO8v zDx7VK2{0FD(q z9YN#54dU;G@b(JXKEB7S^%17tjyisZ>#Rr1xOvYNjBt^PuQN)rCub?cDarcs(5ooi z7J4xfw%|8>^m@rWg4I>_68jjtjJ0Rzja8R^@qZNVQISu3QSVMt-{TeGf^ zqi3OQ59vlSD?mj;DuM>1TWRsbM525W9wOkeI`xOQ8JJ?H_WZM0$Dg>WxBXZ8R z{L|SCa8FP}o%=K5Q=$`GBY~s9FoCeQ}8cQNun75 z?8NZ}#gk7|8G6dC+vs`%PD*lj;r99X8S%GVO#S{Q>@eg?L@W}Ir57Gh<}jNd?CRK% z@C1un+Am3sF-mokmi)mKtm&-paFas0d=y67lT}o&Mbqw#DignpbA7)`v9!HZAXX2( zlr$AzqV~H!9fq2n-JOEH3Xrp`>fk8liuuNG9!tB@O*C!U6&Fi|&(kVy@}s%|TSl35H3!9j%KFKVHg4uvBY9b!wA2ovr@;hNx~T2Ngx|#Q!&%L z)d_ybz~zz94xe_NouVmkr!Slw!5#J%R~IWx+(8gs=81qg98Ch>lOc;s;bAi7(R|R4 zaU|J%FFDc+sS&MqAUvyr=PzrjE{?xYQ|)z1YV$z3hhhHG)zzic_8H!(SQ1ut*ER+w zTtjnOeC5z2*Oa{uOZU=NFm~>9n%?FnYlVlJ?K^!?B@Z@BFdRlw9{NdSVFVnJIS&%c4}{nljUEEtOeuIC-Ir9XCVV*TZ(d<( zY+6vo;@9_^o64>g4_dR7t}_s=LfS zFf}54-C@k^OBIWo8DDzW>YdRsHH{M#L!LVDAc`{+c*jIl4MH18}~8 zyu6Ve;EeEpzG#J=t&D8tQ~_sfjVxS<*kBliT};gEU5U5>d}bC-RyKgxf5C~D*=WoBhz=}N@;ha}Fz#L5iAC=K8cTbT&k zTiBWr0eJL(S{D&<@Fdcq=ip!i5X6}{xrpdlS=bmjnK%LPWELhSCL(4gK+9b0+^k#x z)bk$;0D{)Z#x}?k@A3>{%ZzMs<=5i+M3z@@gx&q7>pW3I!pj~H9Hfa zdrU+C+%f|fE5L34$pL8P49x5-tZaIK{u2R!&8)1f3;-%R7a#*bdjBo_SN~V{&k?|w zn}~&r=%3n%Sh$H;|J@erpPGr-{~+l9Od}C12N5eL5eEkc12;Dd!1#}fn~0Nzg@Kua zlbe%>4bbDiXE~Ue0DT5n5^?_1`@d!X>Bj##A|kGThVr-UU;Dqc|JD61_xJ3-+Wx0^ zf6sEWu`+P6adL3{Yb;Dee{KHVw||cH0ELM-xc}uYPQY*hfbf55{^iPlhWuCe_lV`6 zPW?UlQ`6tvKW@+?0?aDFhkuQ^08{*TV*g|I|J4|vx_>;QN5suc#0{9rKUWYZHxV-z z)4$gY7Yks~{I^SbfD21Z<3Ee=pUA-pn56%GE&cx@zgd_C1c?53*zwHKQ(m_ZW`grQ zqPLR}EYA%-~t_3s&|N8o& zm8&!CYTbKx&U;B#_9RcyPTneKv6P;KCgJh|V-nG16*;91d*Mi9G)VEPAQ`MsQOjKrjV_Hx~d@p7}pe zdB8$Ybx^an0@#=V0?nUAp#1L$%k_6v{Fe*Q!p+Y0CzM;+x|%rye!?HH>Syl{tts*Bvf0Qecc!nTw?E? z5nA=n3+Krppd#P}b%SQpi?fmBBmJOv?zkC$kL$A~yUs?w(FrXqM7lg*r)mqART|S# zd)e%{R@3JwG2l1oAL;&rIH!dF2Pc%$IB}_Mz-H${iSrgiX zL^+^3d@}n(W_z>nH364rhoBHHTK3D!MmAIank&fC7d|u0dZ))2o86KoN~3y^S%z-> zeA(|Mi6x?DF$&wBp`kstApy(U*(I(e62pO&)pL(Ko{r;PK&=i~t+sr!ArQ8!B$mOG z1-YrgGf~&8R9KMrPqr{W!lDhaz0;oVFfTg1#Nav*a!WP+25OJd_iZD|-R%ND@`E~D*j~`~lDqZMWL$lOW2O9V=~DT<*z&#Y^Hrxq<8r|R zTPEgUl)o%|YV|O6?LFfFXEOe$*-w+7Cav5E15RBk?(Us=Zh{vbvpQ8RazUB4u3s0? z9f3EJU{LoTMpZMe&Gl%0>}32b9prN^V?)PC(8yEO1S`9>c5KG9D%OwKYXy7%N=3e4 z2+_QoeDGuo-O#}pFMX6Q$dcw0E>z1xRCmI=4(`y@(`t^Nb z#xGdmOJm)DCu9RTY|D+G{*Yx1{Sed8YDS)|RigTgxHi5DlO6DUeZOCWChbd&Mmo{- zP|kSJ!k{eeC!x#)qi!?H+!$jx_A!B&MwtC>-&7Z<{~i^_ z$8G+g?=C|7^WrVG<16(GHq(y(JfiKRC!GiA%feiKm>+!Wa&f3&s7}L&j>03heJYC~ zeA!lvJxu2mUGC)U@t1_@cq>jrO-A*gm|oyq=yJO>ZC1Hj0`4N~r-tu^=%PhtrNLjK zqG2G#VQy}nhqzY@YYhAIGZXU;Z@o4c%7^Tmw0}?(3)heHNTYeip``Hd=@d_<9nd)jw;t-U9uXV$EYf0Pys1cUBOEh{&FZaR zW8}P-vDR$ZQ0b@`6O)IVg&I6tk-=SL7C5KP&g|qWJ6{D&XWSDw6{r{N#i6%l7RXJH zJk&kgy?Vqx+P*4RgJ1)8S{vKJ$h*&tKQd@njLVtx%oe-OHrhnhUzE*Z2Q}F zlExqq3X*wCJQdqLDSCkt$60c{X1+yp=z{RGV2J(mvofQR@ejiU_^U$(^g~0kR(od? z7J>k7EBpcs$t>gMQ2vt>|J|JFyqZ&g=csP-kfd&%h0N&;2A|(Ewoa>Kba)t~b)Q%5 z?9bhP))js?KSujA?;ANbkoy3qz!w4AYZ?RVUdmY%#q^lNgy>`Q%b;_cW2_`gkm}2+P@zIGc)o)I}e~j8U7l7iHv%4#bNVdgTucgM$;ByR-%0b)Yd~ID2r3b->bg$;l76XA`c0X2gEv*PX%*6Z*<6wC zhxcz7I|XlKb?pkMfWL+sp;(AvIp(Bxdxj~I!YDkU2*xG^`$VgVRr63t{AvKULD;4uI0U@$XAq^K-4@Em{rLu{et@uH_w-K*+x`n~$ z`a;+eV8>CMDLdlt=CR(h{Kih7s-N?p1c>o2V?R^4c&`y@KnGE$u@j95Tp!GkhMJkaZUL`7Otm ztSxaX33DQQ|H~P|M}e3`*#Nl$Nn^B5+8icd+GItmr@%Rjdu-qRIInT+eMiI}MDO83%rFRl zIO@XM;nFd(5{|xsW6$sWU~Uij2=U0_k@1?t8&rIF$oic6X7={$&E1dlv&=Egk$Dpl zW(JCXFndsXr$kCTKZ5}y8WX_9@J|)1)U{5iOWsWUD6}CxDFME-?_!3nSha*Q8aB!#}=lz8sO8wb)mBf;;eK-;iJ+ zo7`b`4uK=pNPzid(}EkmX31fld8)Ko4_tPi<&;-D@_Z}~@0|DqaYjL6|3WjI z_-5t^=NG^j(nFhBfL?KLFF%a(NKq%Ro23a-PkbJ4iu9b=yccsU5=HaGzsWi9YRWo2)cBdq z4<_V8aMzzcEb1&pJsnOuNIr;R6yBiolzY|5h1JnR;1`SXu90F6?#+a`8N#^3TQt;o zJxCN`>>X~rj=Mv$HhdmFHyqy8-PTBn-mWA2V9K55jP87?pEL97WREAx??mv1fPf;i zu~OWj*@rM$DJd{BxUudCR!@9&XHK?3jVX)SlXlD=kEayUod}Bd&*{a zc#$5(FXo0?TLauoh&EK=5Z_=W4G~p~%xe!m0^X)6FtEhGjSopJ2eB@KVJlMm$WXyW z<>z9_U^=}z#Y!7`@z&{EF$K|$sAn`1`figY8%p`WwC>mqUNhgM{fz3evM~m2qwz!T z4)fl*6*^!{v}Pau_U;v}U;mo!LJ||@SsGMHCQZq_FO-Dl?nNXGN+p()hr+nyvVDgl zfW|~YqZuR2_}T3v{k!}U0B&qFVaJE$QSU`??F2@P*~a{yE;eVWNc45rD+1966 z+NyN;kKmexVnpq`yV3<|9{hNJDI-HWWHEip6SpK&j^F{nM^Cz_F@1KgJ~=u#O+Evnpq`jC9kew>>o;vELX_A8 zf&oAm=M4LUz-Vov5lpY=PyO z$+qo##sl6)O~>E;R<|H)A@Z+4FwyW1BjyX=0f@hXv5Y0V#thIC0uBQR-EXmG*owWA z=jIilZKSceSY0`wRJQKj_wKyr8uRDwyjaZU_iHt2hGCC_sauR;_K37w^uc3+61}K= zb%6)GS3i02G!t?LJR=Wy&-Al@WueLn{`$UH%9T5<`uZ}$L{A@E2xnz4xZRm}h%dC` zx@YIurS%$Ye^w1`G7VQgq@IAv?5>e+ZHX~#zz~VqFu>3YA%HOG0~oE>wR`I1q~@*r z?#{L0>(t3+!ptcHKAONE3mFTo=E*FX@6*%fmLazO0zV;E2Y}H|{JLvDWa-A)s1`0E zy*t@i={fnQbo67Mr$8q;EhtuJJ!Qto?k#J(}g@iAFqJ*Fvys1LKwTcQAp*FB?F(Fu3 zB{WjMJ+o|oI|3M#g<;Fc$Y8A%lJ%h!Aj!Z$D(>&^oqwMh3p!JKetzB_(wt7nsi}FH z6V2p9Y|jCoI23V?HP`t1m0OfD1yc=l&G4WUlnN&5>H!1YNO*cqTwFXMu<@-b$kFq6 zNMgB8X6J@DU9|gsw2>ORw~6LKSQQJ~0!=}cF_5?t_U4J1nP!5wh?W*M7?V0*RYk*g zH?_%1b|G+jeT>;$ogXtEyEU39VGIrrvLhw8^)OJ>w?fa?aDzcBHVAzSGFcj zK(57E>yNU8#4Q3ni_CXS!S3vjJcht5g4~ECR9g2$^2I3&m7Pd3e5l|9%eEnJ+#h%14zBDhF^w92hOHbqc3t5ugg|$> znXns?%M7O}GNQOthFpwrcE2)_UZYAK@Pu+W>LRqZ!=}{gc=}j25|m z`3eHCN1TLa{_zO^CVPa=w)Su~dp9@-nLwb95$Ud71N61~u;AXNCF^dkBY)WjI4992 zJSWL!@lL%F>~2qc{TtXtunr30&^wNOs1EY_4#zO$Ho`FEjy&BFF+5>d7t;Z9JFXM% zMsPd+HR(pjX7&@32mhmyC*(%_6PB&G58k!t6MZwO05BraMMyZ32>`gv0MEOfI1Jft z^DC?!x0P};oRWP5v`^Rv#+}F=00{kc#kH$H9DK$0hT%lW7uZf{94WZNkhH_v0NV)3 zgOndCKYV1!xqAyhC&L2J%i^9e*W||{K3YRtq8__?p5PB8y)X)-y%4Xcm)p}xJEvE? z!-9jX5e`C#yTG0p00c9`HAgr23~4VsBiUw<$F}&e2m3G%(kB=rDM5$^(pI!d|(cweh`;LAMl%^d^I~q zSLMTmJFo!0GkJHuC%(sbLLxtSLK1?&c%)ARLQ;ZIK~jQX1*AUVH&7qg9>7xXBhwGw zyQ1C|Wak48N9qfnLFxzvA9ixB}mmenM$P>=lV2?G55RtVjN=UhtPL zCtP-Yagr-1{LGa93@=;wyk^b)s4ZLQTXKDA%aZ?;SUB0THB%&Kj->lz?QpNeR8KSSN;evK?PKFW03nHID7K?Ay~6u z@Q2#$uU4Kv>-qvM_wOX~^Y`87*2$d$pr8Nq?IEY5VX{vUo_kECi3EDQHs@5}9Pc4B zs@wwWn=N|FtD87~A%+MMmTGkp+Ll_kKE@yRy#Owm@HAsOiSSg_<_mkDt>-)=LCrIM z=f25=((n3fVl8GKm_t=dl+Nr~JMl+29%S4*ET15Tz_>kv1A?=PTNCJbL*YXl$VVt7 z+XK=oh)ysb1f5T)hXZ85L;jscHAJ`X5bE!uIBGbLNXnVW^v6F7*CQ|BT|wxCH=+R5 z49x~Voe39g1aE-iWAv3u>|BJm98-B9W!mxp&%FBDxLUdT7~sMa<*wuyrBz%bjB z+!YjSo=r))vnu}3-%{ezG%@lmH>PPZ{Fqufo?1zm(lp~GncJn|N%MCp|1}cT*EwXn zbj)x`6-fb_i=kiz>TJZ0RF|p~1Dl_(6YNLv;xacQfzj{Zvz3RC`dpi7-ZnE{;P)d!3nWVlj^@Atd(cHHTZc+v>Klw3f{L+v9g`6s8 zf0ObWi}UprBd4^Bqq-e3-L>}(o*xGf*^e$TaWzHvrrPDUbib5Mw#$q-%ZYW~RwROS z^=r_Z+d*Ng8l6qrUD=@CHbQkFvLOBH;wBg0&No2YkzP7pd4^6t^2$4O*sF9j3pL-z zODtp0*s^52txfI81Y(^ycY!u4)Ot%<`I^zJ2XzTj3W~?~TClXMq2DRs>;{$ALr7bo zC9P38tWmvDNAW276Hq2&6YlG;ITa7a!Q=34s*J{DP-O6-vGad<#YUW9(qsZR7 zeW&=zq7Xh+XLjp~BU`%k+HpX2G}$Pdsxx=;kizgai*~k?rD7p@6l#q}QTNg1VtYR^tSClBDJG(e-T04cMB^-ZAZTs zxEF#u?_-$72fr6G{2-2${0sSK##vw4;S&{CL~R|vwN)cyxd=C@AM2~cESHf8QlmIg z@2|(Gcg$=Q?{{tZMucSoYuM8d+4-Y4P`)8`$KzM))@Uz4R%Bz$D!h260xQSC_~+u8$z0JUHG}QxJKc`!zzD8c zT+@qR(K~6IQ_zpqi*!QQO5my25{dwVEsCSWQuL2lpjvBQ(znu)|= zl>s7JVqk3@pu-UdRc0glBP`M2rRDJl^6Ptzr7%Crqo=$AM$&p#cPXRv*k@R{Vk02h7!}RCwQ@?w$xb6Z9`S z*DrwJ*@pUy^_YzQ4x&?Gmi!orJ(}}Na!KvH{0X-73iZ9rBAq-+TNLkzg_u$K3hlg* z91gp+7n&OEM#+x$aPhGtvT)5$ktMdMdkLi(gaKZe;;MV8ZA;*<#Q}UOLhcv|*)USE zvSTRv3$DNw1uKs86*z;W?~rq9Auf_d=ABM6%l7_~?HgO-59B>Q>_H#P$L@veVHY&6 z=*Kd1=tYylN6YgtO#a1LsWi@now}~VuE6Tx?JLp#*=ptPDx|AFpNo)9)fMas=J|gm z>ICU)NM4ZL<^3Md-K$TWAQ(4ONB)W;dBJ?=;r@iUv$sXjGsgR-Q2b~aE*PcnB59B8 zb>zO^dr{Avbh3SfebSsFi2geDMvec0onhx3E=brTC25~=Fq+FdF;M3-{)6;G=iPwxSz}xiE&dTy zkI&0t-!&1tYIjS+Q_#Hb+GCN5POuKKOW7~xS>dJaK=9bVdkZfKS9_dh~kxC2)rD+?KlWb?35MT(_ekW+L{A-))5~W8!D{TWs9+a!K5iM6`b~(WxKhBK{FVF*C%X#ZDU7|f zvA&84=a$5jNm+wyZ*41d6aM_SLbhxs+1+Qtm@o7Q|a> z2`HQA!6>E4G1N4N<$?Z;HNC)qG)li9CZ@MMSXtOwJ z_2~PLCTSvlX`uyr<>>pA+GdTr3tG;~T46cgS;VTNNy~4alE318eFywxP~P-)YfFkt z0X~u11;%1)eWzrqF@bxqRh z>dr$rJ6b%bq)CHYVF#ezj2x(CVWNr81Q4PaeVdXFH5|@&DzQ7SJ=LrS>aBA zb$WZwHc{9yBJL~A0-mmf0Ulu?<9n-Ws%Q=o?C_by%bsBxR!Ziv4+UF!PZU>5V#v0J zNeBzt1e#UF!)NLe!pi5aj*F3(O@ACP>r6A8H$slmRgDP7)X(B=S{#NP^m*1kmD(J9 zPo2cBiJ^n6h)0*LjZak=O_M@VUs7HVu@Zl`!YeO5PdZ@MizM*D8jwgpui?RnN`P`! zRC8P=pL1{2E3&O_C|XEY>NFLs+qRk85s0h%P4%;N##PNi9bvt%=9rEk>CUO^`x}Bm z1Defojga!;5t;9jc0?dOw+>93YV%PuW~)X-MyCZX#Y+kBE+j3<*LMrFbgU|uMUbm!p9ara1Q~Eong!pG|zgbbjzb4Ou zQb6_BTEAPH6@SY;OK=}wms6PNOLt?fk*lNFg8KwH683&9~#L z6&LK~aCLqp-{wJWbCo!*d8?XRC4~pqq<+$TGwg(9tqc`xE=m8_y&uzU0iX?P1aM* z(NN@ft2nw(1Yp~P~eE=$9xYQAbXd-0rvm3X=!Qv(HRDPbuV8YxaYPdnb5y0VrN z!~)G`vLvsmHo~v@;QhD_;MgVFx%VX&@%6mtY9D4Zz%#&?!1?yBYsYtU*sR@shYpWt z+ij(%Vg^7r!!TQ1e$5um9?c5r=MxX+uUr}#`2!Aj8`S|yos zJmXS#u9~!3xI90y%CqQFyQh_@k86%~Xp>pCn!l73m$O%SaZhDSES!bB&ja40?mMKQ zz`jq$eH7I$!knKoBl8MJz+V$f<0$Ud3_fUuNr@$%bA$>dFX!v3Sv+tpSh*@1#+5(R zUhDhNGu5k^F2&8M=Q=q!$zC|&xZoUgA0x1@Zy37{JTj@DJ|N<3w|871 z!hz)jP@IJO=8P3PrCO~sOv$RQDM;CIBCDenm(JQS*DG@ymwV~uh{kUy>8UiR{Ks>Y zWe?j9a7@Ao@IZqnogHgGDL&Od57+;ycVqH6;IK}dMk&XqTx!;e^Y)qb(jbRDy%t+95&YF zc+OFqEK6B_O|P}b6SVIY*i3i7zD@6S8VPD!-oXA`*I^vbz4YMgNt=n4AqPD>~OoKheqw-<(@g!i5HMm|oNSr$Yj zQYbXHbD8i~jE*wP$yuvPz96qcfTexd9$wpNytW?6;f?9qS>iDN4N?@avm!pwAf6H9 ziqdz;0pS)eHDE1EZM`eb53rd@;c~h|c$mX>TfZYRN_wXYSLN~rQnixEy!njW$onK>@vOqJ#N+vftx;XHg*$gc<8N@-OYga@73k+%iMJ?cA;Iv z=?5O|9(SMn`*_?l2^K#*_Pf+!Xb)__b4}AX?7P-;6=2)(gukO+U|gga?C3ATE}5dx z98zOED7eKHIx?l{@GDJe#2hDQR;{aypUPHEV6ZREup~>H8>{E$QT?vwW1m#7a6Bw7 z!*)b#F7hEiA^VlN&VbyspDsftKR9H9E&SvUFkfL`u8(>_pdo4dQzZIG7n*wH;*8^Eh0{M@ms=n)WE4p9Ya3(Lv7_ zEQKjsQa38XCJ**A%Ir`hoA&6+WMFmZG2qHOKLnF8r=S%jrcA)g%-ON+Ri@E)X~WPS zDs+2I5BWMIcgOpyG25%FEs3j03Q(WPAqrv&zKv1V+vn>TY#-(Gau1tkU+>1@djoj8 zvJxx0bp3h1cZk^~55Yr=zaaE%RvfpxY zQb{VOlB$zRet&%{76}imh2gJ_X_H0G`HmB*%&Lb0UaNt%!Ic9X z+o7=%c`CFtSw*NQ2Yy;ejacU};uB->1@TNJvbe(-^4Ogxzie(OVMXI8bWFK@Cy!Bv zOMXs-+tUG6FYJs3l8Bj{!ew%z9YKM=?y1`^t215?HKDswa{0- zaM2A#Sd-O$<4PdU-$p+Zv~RP3kRC=D1AB8wIyM+%=GvDAPI5Zj$w&6L-PQEfBNNY$=NRM`3%R?TzoYb2q)PUraRgrThV$%6ti>>>Pio>d@T^ z&Fc3ZFx?yj%f$xueHSRvod|R`5E$4w?M;wKm{*{|?4?k5GaY7-A2~U7_BH5tT%Pz|;eG1+q7>>Wz-TU)7NtcIhV;gCihAOFj zb77wzb{`UYZyn#+o@D+uN$Zwt%T^v#CRVC9C=@GhNHc9>55gzN9D>?ZWKNzmn{|04 zU87cueh`8_BlPq>7V&Erb4uZJT;|f#$sO;k)c|WS-v+70u@K@^Xj2hDO|M$qCBG(d z@;qQPnPjOoc;|q4b8KDto~rQhhtBxI%AeN3Ve+Y(I!mZ5k}(^NprQK#oXn3CX#{on z>O`>0_&ud4S37+9yD+6;MFb`KAht^&uAS0Q+_)`i0i)rm@dt$&6F~`0h+|rOShs+) zrV4d}ibZ+U;eF*_#l=eGL&UmpBvtDoRFzRUT2p@|AvXT1Z)7v}i90gUVx);BomwSc zx-uKn9UxV~Xb=-xH`GWDXwfa4^Cm8yp=K)#n{yK&8ZAp!M{ugFxa}OeuJa7WmXcO4 zGeg~Eq%+(HL%~5)J)br-BSwO@ge`>j!ToOWI?=NpPk4Sq^m#DGN3$3I>-HISs2)gFGn|wCsuc#?we|8z9;c6VGimdXVjJ^_55;786{VfP2y@rCy z9W$_unuYW94Td>NJnc3HdG?BODS|mM*s^mzBL^i(^~Irq{XDBc3Wnif zMDbWtbj#0eMU5o$vrSGg9L|=57D+$F~cH zG?(|+66B5cY|H7U)3xBh_ITa)<5(dOa{GPtqn^~Ak!*K|XLD^Wo}?^y(wj&^*Pp(P zN82e)d@-a?qpVy9$Maejw=YlXP%M7(D75ZtxjS9oTtBA!1>QP5PAc%I;7CI@Cot%# z0lHh<9Ui89xQTNoD!UuYu!VMp$fRTW_)eV4OcqHX?anGhFSVZC$z(nur_=b2f?NU! zVdSFbQZ)SyagOe;f4OWb2R@=aMEy*+l3 zXj)ynzYEZjza|2wNQP`6jVw6sy=IUk{b1;+l)*$}FuE=xmZX84;!U3KG(lsK@Rs>r zIymw#Qjw_^h(*6th26~5BCC`VvH*t;6WQ^882mu*4bS0#*(BNi$2`jJj@!;*S<58n z9+$?~Dp~jGKm7IHqjTpZQ`gruU|H2L*irm2=Qvp&vB z`$3Mu#;OvIVF!9`zBj4SjO3>bFcI=z-gf;1iExS63H zwmCRUpn&p#h$YK>>eO=6JrXyzT$vhq)0tD=GxoebO(dd;9^1IS zxS%=#+Ux6_)grf9?HxnD$htbw(&aBTka}Zx@OesF!i=mpi=?SNJkkq4Z!SM-Dcsgs z&~sGt2vk>BU#PaMFBzFJVOaAc>)wTQM+ z8gQ3@X<;-|WF5pUnzEI;Wg-0`hZcO<8`*m%I^BPH*YEL?T(^?nYVucK1RcTa6h$)z z--`a!qa6psRpH>*2zg4P*Kpw2DMk73x^7<1V78L@S6xkxs~>uE8{_>6bj>yd-D%|- zGWIf|Fy%fS+$n5?#0P%85WT~WD&MVF-8$rYWM{c5S-Uy+RD^JZ zX=bg!(w^ROG_@2ixP{cp8$rvJ^W%my`yonMk(u04-|793)ZqyFziBvgUbP&St1@m1 zF=m{lges$7Y4-za7^n+9?JKSysW%hhWGIBGIcxk_xWAlH>ZM{hxp_x&6v_wNhZFMn zkoRyNjRlhq3GhBkjRo+_+6;3P(^jJN?Z&cXQ(p({=%wt-X*&H~hR56I3ng=Sy<^(e zLtThH5X{p{`lMuTqTU`dTk{`XCt{hmrq+j+#_I;zch^pVt5%Be*0P52)5bE=BRbT$ z8zT-{`z-+8h;ZB>p1k!h+T{lRwey~h^nC30Q*6b;@ye_zOPw&z2Wb%oJ>-9_U^Zj{ zF8;i>5=L{ygp2~Qz;9#yQkk|ECoIEZ_OVSrG(PJMUtGmNK$3EvR8u>a$U6flZkY}E zmMn4H6hF#e9q?9=t_y0}pPAHhRp>F>_O>5z#%gOP0s1`g>eH=O?B};734M-#moIMM zp4u`Fhe1vciLU8)yDJq|_7MQDkVf))T=p!UER2%m3ms$ZoyvX|0*1Y2naGw>*#d@>tf=&s7vfxmDviEL{Y5yn2UtI4lOs=Dk+G^Ryl!(&CCXz$k}@rt)(( z_?%xAh)bo&^6d}7kw<&hT*v$5Gaq?QbLes8@miZPl`$-yYH;FtD)*gO6ZW55wL=Lj zodUip+V7W*Qlx0HJpmjQ3US92joqa2Bw*5C@C|HB@Ci946wegd#^8$l1pw;5O7)zx z8rqhtP?LN6aRXAUXjI2uqu$GkOzSa~$-unyON}GX?bw*+Y3Ru6&1I`JJ;i68LM$dt z%RS=5v{|dAWy?Mjqth<&vgW4}q)mf)b7}ol?--=e9>o8kw%+b=cYS#nOBU32r}lc;?$xxle5YKiSEV9X@$tB; zyy|9sK0tNDkP58`7*{(e8(xeNwuUiU+58oG0&@fIug0Y=GHi}h7&qm_?po12GzDZM z^M(18KKC!mH&(2BBTXu{+)V-|SDoe#=8$+z4;|^awo}WwzTH8?7*!1IxnSWUnxnYa zEFrilrGckrr zgulKs$O@(*SUS0TvcUzw!lWKST5h8#tedz3zE|T{-x3__l|4r>$)x(eod}sn z&d^?Tbx-wixi|H?ppS0Z6#0f~l^ikLkPhUgLqFY+g@=hZ3^zk3wfe3R`P0%&O^<@R$yuuS4NFgxD7E$ zXOK|_QT+A@Ka!x*w-go4zPK6Vl z=y`n`v~)?O#3K7qd$x?aTdc)a>7+XJkn;xzE@d_}7k@61FBW>p2=8H{!&OP*)lUC9 zq~))P{c6CScZ0L`N`Kn3O+i581iq5zTD`Ykt*?^Y(gy-%^e?kSw6bHElnf=!1WQ$$ zm?K^u%7;PN-MpI3B(2?g2R=%;bn4$-j!j|3%S+_2W9I0g{?u1r6YR!jjaiZj_S#!i zI^`bnG}AH*k=5)S7{hNn8=5(umV-7#A0LOZC@;6N1iMgq*LsSVi%U``mZ{j`oH9_* zJ^n!UOzkQjYvvH3|JkLptLufkZG>ujozRRcX{Fb&J>GRf^mD@=ztv`K!@G`c!8s8b zv$P9i6a|JK=VqE0B}Pp}Ud;v}fLCz)maN8gGWz~{@7;E>NZXRSk`kr=`0iud@#Q-F z2Jf<)?I~+F{HcUVKr`~nd0ABW>vbGcKyeR}_*YBZ*d%(jUpAT^9^C4}E~yL#X{s4K z`G2Joi5*+aGc4~xfo#(()>6C{QdCFD4SZDi<0fiiu-_{hs)pxA)kmt=nv*%y%Z1eg z8Tq(UOOfzU8OFWI9^$pf(@!RJl17AWh#SedVn+@31K#UbZD5 z@8`6c?#dB%R8jkI*S55o8W_AX6Lmas%@oLIah|j-w?3yHmc4|R^jmk3$yuybnjw{F z`MPcl?eb7={4v`1FE5d-X+2|e{-o)#svH$u(5<^3rba4al`|3qxZ2X46w~- zFyUSf?mui=XrHUP`yPL>H>O>r>=vd(HzHa8+(3P}+46La)@^!s;8ZWnmUThCpAEr!%{XqF7&2IM zy}w9*9l3SY1>Rh zZM93;vuB0+v$>FWh|f;?2@VAc0i1kw~tn#)X2n z7|L32txg6Xk!KRw;G2U{fmE5w#$?$yPxfCOL&iv2^r=_n9}7CmePpWKF^Qr=D}@%% ziRA`XSuR0mW9yB)Dfyz7dHtgoGwthuz8WgVLrQj^n%1iVDek@syn~76}&h1Ig z(eMwRb!;J2-%cBk4Qy@K1EU9L8(%xRue&f{6(T1ud6w%L5V5kxCCqw1Y3%+sNX&>R zr$wq8*LK@xbK#)DXj&&5X9qF1NRK6v18t+iqzMPHn#FA5-Ip5Y8|Ea;mgbjTmXQ|S z(x7|(%sk5<@lHpjX;#*^q~(i#sNDg%v&`@10I6z&Ux4Ss)8urCBHp6{3Et`b>-C?$H_fz1M3-N4lR=gu(MoNTDuo^`K?+`sGEC88yi1?!o=hthz>#2{}gEXqk){(UlqwC zqB*FntSknY0YcKW`FYkf>n`;^>>oLP2mQcp%e0tL&u6SHY2%=G(cHSp1zjzi%Xg?0 zkW(Zh`ylzoc>N`{+N4EabX=B)UGmOY%+V{L&d6)F1u@{q5U~<}2*NNz;?EEeLSqfh zSh|;IxeC<+#Zt(LlCd+k(Ua6{!?FcHI>1~HnpwaLN=edMR7^x{n{=z}T3^Qw*TP|f zEPRzJL}V5C09|XIxn`d2-l4(2b#i@+D7al(D`O73L&P@ptaCHX?!x?tTT2?CBPArm z$313xwJzuD!g_vZVb6gGiH$4euBJ1u)4x)qJLo|rgJ6mi!G93|%=l_DYIx>^trkC9 zAQ5i1CstoOiJ<-BM=me=gjExTPlk^Y?!r0zi|34SR4RlQ)r~b=s9$*=Iw?%Nb$FFF z@kFg!p^-PA^w_k}+J=LQ2z-R2^=-Ku>mu`9SWa!Tc(en;`l(Do`VspjEn zByNhFP3a(INT)}sEj8XYuok_tsAgTwdR+9opjZ3APYb(mv>H{tU2;DC zXM=}oHr!lVd?&`G8!3_*r{Op*kE$YG4SyY(DhBPj2L_0QT*qLuyf5W+U-?eT z`Y9-W-!i*56kk0~J-qy|Hy*xiyWRGiF(@bE**TYev03GJ+okTL10DgggmT`OwbsMH zUqZJJZcVoYwb6CeZy^0vP$@R}N7=h)jT77CF6|hpnyg)5?N^Ed+_*KA$OD3_R}ju! zTt~<1vln*uSDVg`?a&HX`MPPOm)!$}nXu{p`tfCH6j3bp(@*pUt~DHuTMnoV$iXgb z|2$aS=HBGqN9i)Khr*{oTHm@Wx4Gb=JT}5Cq6*5(JcNQHuDL+EYeaSk<+u9xAg0n+Wbmfqg! zZNIWm7oVOHq^3^eH@!UDja+XeiqZ>yZ3eF&RS}H1?`|)7!9RgGA%LI?*R2jy(>Sqc zzhh2d|Fu4RqLa-{&_wcA$71mas(@IQaZLJ$jm}3v0`ZAl3M{V!K+@@By-1#s<-?GG zohsU{XK_fsW@T@WelSD8>LVB4HC?>zon~$)5fkNN{>X8B!yeW;+S5ZTSYFdT=hzI2OnNbNH8H(!nNxrVqiQf?37(qu|e(5Swt(b=F<|aobu0SecbX1HYQ(GB2 zvSoM>`&5Jcis+E=5UZVS-0T zYF3})=TEpl%rfts)i(%oKiWTJy1nq!TDp|ls1to;V@*mNo)d&KnlD@o6-$L!XYk1W zvXcca`6(GO=E?#;{9U;CJQ`rXxYG(o?!NmZYq4wSQgm}WR9;ldN7-XyO^=exo2()u zh-cys4jk+6mO+rJk_*d0Vvp^92HAMILo|Nj|dE znK26be@IU5&aGF$EImFH$sru7V$FmX1yW#ur$pki9V^>5$BQ<;@CYzLwVAr*S?xP_-=YUF06;<`G}yTRwzodT4sN;nnKqe?0yC{L9QV|5WcoM`R( z>iE95^<%e$GFezXwxPdxWp)T=HBQ9i|5dgv|Kx#xq3|hAqLgXU)gucfN%$R|ta;4+ z3|)Ke=p7W?dLm4y*X5`J8Woi_=Tp@@=z3tCoT;xB+pHGSv1Wz(`uyRmZ>Pvixid>MCv$4NZ(!1xCr_}gXuhMU{yM39e7k!1%D(9SSKmZ7)A2h2cj_P*hSzn>tQGdbxIy)j`(h z;WW9+nm_Glb?%GwLW@UEY@O=PxMhkxbZ8S2NCE_dyixXs@;(OaIo`KZrKvhgii`jg zSWpe|@m=x2!0jJsRW8B#U|~BjxLo4wz~Xol0XUu8ZKP^|g#6rzCKXO2u4iO=tkF;% zU?R?XAa0=cw44&Tyzt;c>FkF1qEDIVD`wWSLWp+-ipKKCUlG<>--g`^fHz(~ltPRK zEZ?NOH(p=I)Xx?t90LW3I(rH$6+~m}S5t@aj-r0o<;{ z*R{HjhtQu$_V39?yl6C?xFa{)lAyb;=x%+{4F2L=^%qwa&R@*w>sypp<>c45pC)BF zvFy|0#;7{KX1sfdnDO^K5!;kTg@^@Y)%}n#SP{#uW)<=mFZ~+&(ipH5Y_=P7f8)Sm z=j_G|+GxSd5M2)CCsV+gls}N+USG9Yo4ixRfyN@>n`G&uF zzsMpt-OdL*l2M!iHL0AC)SKMGB)6Z#QV@m zj+1retER;OhJVYv~fru|{L(h=TL&q3fHyeT*DCU;50%r+fkZZ+RO9gCyAD_`E) zrA{_5Ff&o3zyIADV)^vF{((OIQycmxo%SD;+JA6yBLBk0F^Je&8UKGa_otZlpLpHBRP%q%`zLnx zPonLgseb@%|K5dvK5=vNe6n=RES&!#sImP^bN?=={pag{8Ib>}p?`MmpW@s<^Zq5p z%=|ydX{;=tz53tCX)ON_Ic?-qPD5#Xp}S8eB#rnYg#<@K64{r7oUjksrK=?K_h&!P z=R8%DCm4_eG z7x7@XX)AbyQS*rb73mLHX4Yyy#xALo!>DB%fk*i5q*Qg`S&_uoKa9n9GY8E|hC=z3 zENaYvY*a6%qG&a?%A^zIIl7IXvBzr;oH8<+^!zk+3u@Zdjdrj1wUb@Ewz>CBx|TP0 zn5y!0v`5~-?L;y^{`(sCZ?xn8FrEJ~FaMjD_CHSJ|1FyK@2iK>e?`;&{rmryC5H8% zefkHQ#>~m`iDv#E(KHrr7OsCq)5bi%XancgKX@kCULe?*ndwQ{5W)vpyQ;XqvXX{tzA>5XmD|yC8i8teqbQsYPloP|;fFI3v;`7Mi{MW1bmB9NwD3`eAm10Vwxhzq8p)pDx?$ zMR144G$WB1U9_Zz9gkcbb^LxD-g5UMaHCeg@$SliJMU^4nlW9Ix^MkrFBFWT$6ee} zvb}J(ur2HhZ{Q+dkvx~3=@xG>0?BSdG&4{MJc%#v{+3u>Z1|n0`rd03t1Ch~Nt*#~c9*%fdOa6RPsB9` zWqAO0;cFIdlNiRg%UsR2uMQOGmDpOuG`_>HG%6z`De216yZBVwJg8q@^y7NJBET4n z!qlbg%WC%9(?7goRx=gjY#}OrZZ3$B)?spky^6M8+ZMNLR(d2 zILI^Q{4ThKB1_hsZ5hVd4L1Wi31d3|KYTa4bc(;QfJ2T0Hj)2tu{#DNPo5?e1K#

*`1Nbn;7m8?zgs_5u3-TXhez{v=!47fEk#<>76xM%Oqa{@@ zUD`L*!wYiWQSPS{`j}u0$?Fr4iz5TvhT@A4BR%Dxe?J>pu~!rq2{)Ggp>A`&WwZ|+?J=Y-;aL{?IWV*Av+c{Qd4A@%Uq;*7e703yFPg2h;5UxcdQDmSs zK!i#CzBOy&H{^#5Mnwcyd(tfdex1T{7D-{dPCw;tdiMTQJDK?G^o4nbBr+0G(J!Z$(l zju!K(kQ~sxzKQ0}4&tRj!1K!JD9 zd;0CFmd7zkdZJ&T8LjJIzhHSw|HvPl3A=%P{$%@|37&-Le&dp<*A`dCOK;EC!vpB^>sUzZtU(68c(T_KM5c%nLeF=8`cYh%Q zfA41Pzo~XdCtgH@$mAQ>Jm1asHXN8n@f0-qEXA$OnUj!BW*hCJ~Bo{Npx`bZd$RyH1jx1x&#pJ`GJ%oBZY zlS3Ry9=B3*MKsW3!y{Eb#sEp+V$w_g0i__r6V?y~Z>;2mEIWiMsWG04fXnD%jUt(h zOxRcWPyfy_2pKO`z}Rj+do<#AZF^@l7In+pKLA1&{9Tu~ykE6k zWza+^^qULuw?MOG))B<}O@~(MMPLG^@M^)=%Oba~yj2^p%F*E7MZ`T3=H6ZsYm}P% z*ceXSX%{B)`EO{7hGI=5y~b5p04|*6tHCz3NxErS5X_6JFfw`_^-sH^ZUgonydZ?i z7N-D$Bs|vB6t?43w&59)4lEGq7*H)Q1WuCSE5#2AYEL3%QTTH<6nOuW_^D3ldp*mz zvJB+mqN0X_{RHFu7u-mYKan*0c8$#RsS*ieuUnj1-RdNoPuYNzrwijIZx_PDO@`3 z*H!X?Gm>cePnf7uVQ!VFn>;gc*DySPu5lvCnMPKui_q}z3);|*IkTlV)Qs7J6PBAD zu6Lr8wkPGhB9QI|)ZA=km4A z)kfUY+IfZTUv(?eInUCv6da)Z1OTq*A@q-N>U`=pXcJFzc6fj^BVrtmG`C&{unx=Z z8|#*-wCo>PTyw^5DNS~cDFuITRCt(TZc&sw1^&&=fa#h+vdp&B#Jyki9Mcn>gK?E? z^4r9%r@x!nH|?3^*)u*^f3dR9cZ>2tkKG5o$P%kKpvYuy|{)!UZBhuJ1( z*$WV*{PS>DMKVjykNJhrzqN#btXsbSmAS<7peQTo$$pb6*N3w^+ABM-?Ks=Vplpm; zCB7j=U2{QOSK0&X`j5*WrUs*#@5ToJNG)b6#U;#wRF&3gp!92es4weX5{fU)Pw4wT z1s>z?mW*88#L)99{$4EgqM2eA<`+ja^emaT?V6TQd~}cz^wlH1`kUCWh`;Ru;D);L{C6QS6JlIJ!U3hy-KH9UP>ic|kXQyYSiXkAn}TmSMj}EAPz)60!EHJIXvaJ7azDy|YH=#i zJe927c_j{zvkh>nn0^^Ezg4zmye;+lF`yUV6drkrNR{XXe-x`N?!}^zc!jD-#Jy7^ z#U&$u|>%p(@{{U3%B102c8L3rkLUpJ&OHF8yx+Pfd1s- zXOK6&FDY=E*F)N468&5PeX(zv@;srJeMh0ZVYF>A=tl#I1%ip%pnK>MML5GBfHH;P zfj&3r=@#QccTH{@>$?D~8g6q@+OS^Hp?5N0$y556Zo{B2q&$X({sL&5Y=1=;V!cA9 z^wHn;q-12fB0x_{eoPDv2YB_0VWVRwKcbRwh5jg?6%BL8JScY~!P6vDqhv zvH{Xfx0wLxhTCkEf+pMOlya&^!RX z!L~Ie5}SQUXaE3TKQ|!Mg>v1{Pze1cd2vJx54{{v$82C`h>UK_S{)vm0XWs)Mx@j= z+vcEbV5^P{EeBW`Y-3V#vQ>wMCZ#NP^~nIr%(lfT=UJ{G(0`@4WriLk|LpCn1(cZ@ zVxrTfxCMn`0@&HEFwjSme-8J>1LzI5zffYaRfmKI0_gP(1<`MklSlfjC{0;d;zMx& z0HfU4&|<(W8%tp55CC9kNQ#c0{B1&v0DUL9$Rsx?)RM9!xyU%zKh%M;pOqyjbP51u zVF?RWqWrw}(JjV;E)Kw8jhzr9L~jP{GP9(I8UPRs44KhW0Qs!3J%1uYjVa@jzjggd z2=xJwGowz3;iFdrekO+)kfw%GQ;q_}8c<{7x&%T?nkkClNScgnSei1y7R>Ah1`!@S zx(LCLNeE#(PkPo69t6OU&cnObzMpHOx3#0Jj|Y09`-$Q=H;!L$u^9%4*WMG)}Oq z#6HJ$I57SG%tqpkxQe(6SaGtUmAk4uP<}16qj}#UI}Ym?pT(LJQ^8ghSHV*417fF) z#E&#rvlxZFN)qA>aYVE+$I(Sm(@Hb;1foKV4?#bggRhf&-H=^}IgmMo;W||Dz}gP? zAZk3CV^vJ_G|;Ex#3}*_W7UATu}a3j4hrgqb=dftq*nXpzxmq9W2_2DHZOBF)85nu zXp1!Ym|CsJ-QnSV0%mI=QzD zpm1P^#oZC?toRWGsFLO_W7=I>Pw_c!dV|_q_^1*%uK1ijts!kjondoF2zHAtT}bUM zailDrrNAq7WF$?b_#8703tTUB@}IVszEC{QzhwluQWNAk0jE)=ZIyWeM<}2GAdmvA=nZ(UxGYl~sYD0Dl6H~yq&_RSr3C2!U6huEj}WCRl|3S-U8Gm2twFc6 zAO@grPF>10r8FK*JHP@Gltf)#cuNX$2I^+lB~IS}GxO?#rfY#}%9`R96d*I|^6Xo7 z&>k>V>n^}AGq)~cT1i@7NmJzLi*z`RfSd&?h*BD!h9R3hYnor$k!I}kR`@i%G#kxW zHhac2yL2S5K*=LQ2@>QXtxPRjz#cj+CXG%VS*Rjufe+G@<^Yz^s4AA^s|XjTPyduQ zqK+(3k+8r3#R8>i66!p#z$3F&0L39nBp?XsC*U>pp3-m*d-^o9v?z^&Uq$hUMJgHcjaQixNI zQ;buJQ$SNjQ{1Mp2QpI_pMX=^fwh3Ug1dq>f_FmNfcrqMLcM-{guayTn()K(JMqf` zTL4oBR|gYGFXLGIgP_Y6Oc%Bd zW)W!-X7Q^VW)<}1SA8~pbbYc-lP;Jp89x`BlyXJuTfNGX2Vc}Gq)P~WhE4Y_M?Vff zGw@RIQZPF33F@>rb+Akrb1%C9`FP}o&9&XJl=irGhc-9gc8#pE1FoZoL!Pwt1)*Bu zh5Q$Qt1y+N!$2BGQ~QB=8YMq1`l9LdBdjrJ(>7U!8)$?wNr3T{Fo}$D6*Wm-r^98- znh{c&?Wy3aI8zwEnh)vS{uGxx7bKa?2ia?Rysl0jTrd$@zMQpgz$Rdm3@qtfre|0TL%+jF;fsM3)%7jCzf}^7THwrMGmcrr3^au z<{3yB6+8wF+XeQg#2&c|{19dpd==&usR{ZK(GLBRZWFsp2wVu#2CM?o2SOWC8(JIu z748vs6SnKYZ^Z8cED3A_f)Szv+zFf!tOMqiz>e?|b5o{k156L>{VO-@BfcG$9gH3B zC3Y80mzkeDcovu(gdmI;)Fa9z_@;N)moED*Pd~Y?RzFv;0We8$NibVTBuFH1BnTuh zUa&L2Mz9y~7cft7Yd^K`@LfoYkZ$Nz@Kxwlke4u<&RxI!&it(X%KaGp)WOog)4-S@ zIl*)x%3#~ze!{weS7BZ9Z}N2EcGddL`KkIP_?dyxfsOeUf{lR5fLDF#mjROp(}$11 z2FHTH0^5h&2gST-cUg7OcLBhxxUSg0lj+lD6EZ_H<1l|=Mr8(PCRBj^23rI@3swTI z>L>jjIvbxEiy4L)R{`c5G&M93Oad$nT-;COyC5<0C-4i70E+;L@QGv^LnwjMfZ-A2 z#6W!mtAcn2M+3X^YsmxkgvrosFw`q zzit?Zr_J#%k1bCa&TB_@ubYPd9XK%_eFbG?EDlX6N}${B$}Vn?-{jV~N;vCLpLCUX z0yBi7-|(^jcDU+oA-R~vZ2V5T(>`Dd>s4T4&mIG*+sfC7W4#m`m^5<@K)0F?Z7alv zAsM>DZAa>8HaP)T-=0*v4oiRDxF_v9{$TuJ$VzN|`h6U2r}zO5i4(fXj*yp22H*wZ zd$p$%MMQ9Lz3_H7q=;7oQ;w%`@1@fzFNaYeT!#26!IxHrKC&po{m zFWGqo?Cbz6+?C4@_0U``@~?5JNF&)_s-zth83(q*wZpd23XVkhV(KPbTQoSh)&r!2 z^99m3&(_vg_s*laKh7B$Ne{V|r|U2uD4i@9incL8Q5+X_jnupoW7U z#asx!RB6-1 z3D>PyOM}GRr7|fcVyp)p5WK3N`j_O!p5)NX(%D+=C0+w}*e|xZBgxL+Bgn*L<}2Dy zI&28~@DR^Xs9rx$J?AEXP~ zk9T=@bt20=s{v&Sgnhe`1%~D->Y==W>X%q==d6o?8$GIi~#OXO*Q(6t{ z)aZtw_Ujccuud!HG#^hk_^{-ce7?wQw-HPyILW(2{9bojuutTJnt@v^uRll~q>uW3 zP7MW3D8ftfcfHRX)4R>rPL{))Nv_kZ&a~Ts>LZMCjo%>aID*4ziRlE)6Nw(io@ zNS{eJs`gFxv`N154~(3I`Cl&H3U_92<6xkBzrgY$y{Z)7Z#YBDK$m+T(H4 zq1^>ZsvcY>S+DMaYob@Mf{GU2Z`&g6FbCg5IUimjIGUiWXus{_gFb|R>WpO5u2|t_ z(2G&rpSz4DU_+%RhUS1JmWr79m1P}GuX2GlQHtb`dV7OaWFo^mGpg-AW~U+ZTt;eK z)v$zg(bh|%aR_YzMCsTTDJTit&BUqkW4;%~-QdF8BvH+*$cXAs}7SNjJAw zs}Pw{2a=(^`oPQw+ZXRC$EUpFhx~X0MZ3;&^(v*_VVKUEkNSSD3%z$~H}cEV>i$h^ zl~_}w95)#~dUllJYTgNye2%6os1n~}Y>I7n2okY32@#-?n!xF3Y&9|xkcfbAebv8O zo^ru%=ILRJeAx)(ieBuT&?hd1GDK8x@z`l_&ADoBof>0lS%nt4 zDC^DooiK;Ah(9e@Qtu<4PW&hNF`nBnmAbbYMSJm3AsGygcc5h}xUzGSe)dnQSX^h3 za7s63Es$ss`})%Mt8>(e+6ftP zs_btQcPsg{>QB-cuKRIDj+FSi8p*t$c3R!@YRKhk5WCn1HmbzwoPH87aFsNO!m@O1 zLA+Q+Dz;B|d#;QDs;I2I)bKvkXm+QchDB1>VlC5D(qQXKwXLd9kmZ>)#|( ze!$*wgCUCVng1o(g~01lU`J?d!;A!OdEJ6_5Hv2%l0oE2{*j3OhDtDucL!@=lpck4 zCE1{F6xhEOo05?75lhd^mY(!C50FkX0#xlo=AIEMJk5Jt-BoT63)J9A!2l~ro%&O! zHl|u0NM92;1wT)3TB{+0Rd)xH@M2G6DFR}cs%A#x@ z`WcR+$Vgutw$?J@z0zwJt|QxD%biOl$>mv^UJ&H^YEA?5=- zN_wtB$ROv{H_Z_I_$(rJ%G9uVluX|SB;C6fnN8ts`$BAt{8UVw%dY1 zeWO5_)^`!DB@xfbnT(`N$?6iDGd~j`RP8C2Nkik@Wb$AmWiBV@^SE5TnrjhxzQ5Xc zu6czv&=fgk;k~wQ3Hw4$x2~+8Mu|!g14t5)BqFIn0VOL*k_{rDWCj~=G55Whp=#at-mh0bcb{{1`p(|F z*Q#%ou})!_`ehy2nn|nTxIT9Jq3vJQt{cilvsyaFy^R^Y^waCEv(KZ$ecdB`z-}(_ z2`E*mC8C~5*k1R#tEk+?uhlH?Ln}hgKQhqo3sMzQfZlk`HO^h7?VRwhsM#l5Ub!uB z{H9vl+1KJ4`cyEWqKDz%) z^?n|gg6w>g<^i%8=icoHX9ZI%@}o%F&daTyH9XPN`^U#p#^Q4A^4aJ&uShzPhYOYN zwMD!3KB@+_?+M0p9V2gd>%Fk1X=q%1djB39z$v!E*Kv?l=tN_ywype!B1VPYx!zGU(j{~=_Wn~&Aul(PTC%<8qE-SOQY*O3Iw$naQeXbEtY zrqmRR0$Byi`OC9SFI99I49b5~qmDyP9i7<7v@Xts+Gua)_o?SZee>wx8$x4 zvu2-2O!v!_rR%CY9bYS)39Vl!E6M1SlfU4!IGYsz5f>x>921^wWmq^ zES;a@(_aFtseX<}29@x{}{cjW$dD?iNU>EDH+0r0tTfz)isgdgPQ}vA^0p zJGxdfaIW!=mWxy*hNr}*=bBO7LT`O;yqLF-$;Ab28Y|hR?B$zy>X3|cMeC6KRP!rq zp@0C-#CP}?ueuvfmwpjVt8BAfUfRvRduhJNE-^Y*tPZot*cUc+!aPDvS4~hq^pr$r zWlL3d^6)8)CtF2U@8D=!#NoG^)~RyJQT8E5Hw%BVMje#gb1CR@`lz|@Rh*S#%LrrC z+djS1x`*_9VHG7QfNNAV!*g>gLt6_JWU=d+iPdT7fe?XXa=$)wXmg*X^6Y_jW~ySr z6JJYmjn{dSGR&*9-b;V`xCT$_Y_x)b?veb`IQ|5K!ckA{`=XtRd7qc}yloEY2vwSj zpY({(>=|09&M#}O=h5MQ>=UE)3~OHq89o*XqpA9E$1NXR(x~Zk!k-~Eo%@`UW?7t6 z$H6rB_2S80O540n%J+mwdroz|vy_;YQl)Dg#wSu$MdGpJqw2~rID$xy^>(4KO7R19 zAu-X|j>YW{4CHyWO!%!<&8Wtlz8_uderWHv7&ZApPPE=gJtuB4X%gZxV#&|)eT=QR zRE3d$B1qNefq9Aig_Qiq*>WfAGt4=o?$%Mqd2TJiz@YK6VGL} zi@QIy`#k&5{xQlTjqAV#2X2-tZBYOR?h;O+De7fY`hfXFXKz2TNA`)8Mn&z3zya&2 zUk9J}2jxb|>Wsgt~Z3%0W#0R*yE6SI>@ekmb7gx7SxuI^zbEpyXSh3}oSlbBp zsUP%Fy2Hfy+#h?R_a(O7T}T@@r*a=kb+I`ZI6n)?JXq)Vs$3_5{_uOnv*bB&y8NGiQqaq1D&K6 z(Hze)lSJ|x&WlU6zbpOR#mQU8@($HXwx&KtlS#Jix6V3m&33ZJ#oyjBr-wz~9gC4% zy-{|U>VGTLdO1e*(0;CH&XKWwF`QOfBB?i-S5?IBF+4i>I-YY%d33i|{EvjulNz@d z^DpqIUtP?P)>A7?60*NLZY#*rG{4I?ySsdyj(R541(^KeOL6w2TBdP zn$~o5en^Ovj*8>y>%Eu*F1LF4zq4;W`Wc>6*mVM}>5FwQ`-;Y>=58-;^Ot6tHR<+Q zSImw<8^-1$q$bS@5w7kbQ}#O-jcXS;OsdOw=2ocLswtaQF;MUHgcx<)0@8#rS5jUb z=4KkP0P>BmwMlZzxb~5IC2B5?^3sMe4n8DfvPV#n0$!rxV-T(WX9&zdg?`6Ek$!*`uA6Za=7-;3bjx3()G>xChKRkRohNC z&6bv3D}$DIWb^Qoi$t3rb~W9RUO6xsX8D{`j44}2en)8`=O2BdMSJsxKZ(_1CRbeg z<8d{IjbfkL;JHf2EG`vmK~6;hW|zryNd}1jp>9XF&&JF5{gGx4tPwp_%Zl~AZJw!x zBdyb}pSd<(N6}tYDPOgzQls3yb9{%b==uZY^{Ub#v*19vR}Lqwv()JNF|1QhD9Uon zmlR0?#aKx1;rqC@?wwQJj6M~{iPyF@mavH@@UBgJSvIUOdD1=Qe>8+Mz0I3Yjw0VS zF}0tG1IxZkfL@GktrT|raXl&@AREekY4hJ1<5YSrrXR?~#H+JC6r zzMHo6lZ8=&5S08SM?Q@Y=hSC#afkM;{zTh1{iabSF5o|f?r zZ;R<}$=544j79T)uU?TjJDU(qxBl#2q0;3+|L6)p0f(j~4Y@Zu-;n3@7qqo?m!mA@ z50^RGcBSW+z04XM))VNgsM_iBxNpE=tbyZ$TB(G$f$n^&uXEjzrmxpB6ADK&Sm znt{1H=S-7b$miP!K|TGqCTnH1r|g$po@tr%7F;oMbTCl)<@!{DL$V{tylS^|#84g! zdke36W8KvI{a3HKX-5B$P7h`Z+ZnS_YHZ#dBV)I>ZAi{qQAFpF7=izXf=8fdP{|ec z@!W8`VN;3ZYNp1Vd*dN{^AaPq934IhbukyH@N!56lCx0jSY)O_pg+~_ zRQCGggI6OX7WSww{)mV$R9~9g_xXcmJ{L26@!~#ajiTBHjo?zU3A17FF-LrfuE8jwB>Kw;tqwQj?DN}@&aMmq* z*5pz@gO-P`&BPXtH*UFhdV{HxNv21yKa~(TVa^}>sV2kH{M?GR;=QS<>y7cx^W^vP zoUlmW%QJ3UV)lr>bNr6f;UZn(A1dQs%`#u^UzuE)O)vT)DHCw#>2tI2kr0=J#T(hl z^wM4}-6zvJta5P^BEt6UdK%uV23!h-#`@PX7Zl=WJO>7j`7CEDFPyB6eUWIKpMOg; zt-QLzra#X^-M^-YC*Wsn%gJE3;?98Jl15ir6y@0q&Sw$%rpJ=G4F_A^oZ!pN?LU1r zf-!hUcaCU6E>nf~-lx0=^fqLI*5}VwfqfyIJ|8&^Eg~DrvVTfPzB5(`)69>fHza+# zy85j0%o>4VwwcT(4Gji9p&h9?9e<~uTJn2%{mg1E5Y<#DKNX^xQN<_HU~*@zG_LY( zd23u{jUX{J;!EbV0ASOe`*~(zafVH#v3FUK{ms#U%7vpQ;eHH_LaSXO&hB1oB5hR5 zyt$3%>z(4O+)9(besi*Bvp~NSk0%iSegpk~WqwZvF?~aAZ zIg0M*&O3WxK@vd*0AX&;e}6^X11n7+gEB+{i9mqAOzv_1(rK9WAwwa702zMAUD^7w z#(7732M;Vjq{zUZ0w+TlSxpaXC&zP&&h}2{v6~+chZ#CbN-o~mGtvZ{3jAQ|{qB4sPD2PU~7cs>8&!|9uv{w6eG{~ekwydG2l{^Ab& z|8sf>o4&&yQ*a~zf<0C-#=_rG-^}nP#{p0H2ixI)q7o2E!OJ~(&b$J#P2B%2FCVzP8dU6DnC|3L6b?X?9_@QZRZENOn3B6O2gQqlwCVKR$$j3zs_SeWqH=UXo zGy@I%RVmG$H>z(mDHF3yjiy&vI_rv?&=>DNk<0!<6pD?40^k3ciL!}zBok#59{*sX zc)Ojq!{A`d0~p+24;CPkiDaxD_AeP8-nRK*o&QS)fe;zy&25q4aCjU-h9{D7c$ob3 z&vke*2?P-`_;)-BsRM^25RvPM0Fel6Jr75u5D91*8G!HqKlQ@vKnP(UjtJq1sC5v5 zf;tZ(lVEP!*7G2kjf0SpaGPan>pBvSNJ8o*;Q$<5fw!CoQ3x~xRBwnApM0aB>@?GxN{Iu>&Qe@tYB6jOn%(zFPRL%w*Vo7 zd0Qy^0CWt<06;<60zfF-lR*L=r2{0vtVx7k7#qkOK@iM!MXUpeXiUP_h=kk=5Yd=~ z`!gy>00~b*VhD~D9vLeDHW_6L2_hqL0FVirRdMtAyS_kVR15%!2%>C;$PijaLETjl zu1&}sfjGFDAoPMT2@fr!P>`4e@pu#_K|BeCD-aK$Y7U$=5T%1aAR%iVNFd{p_X8w= z$QprzHZL{O4*2j0J}6`Ys=jQNXY@J{rn4h#25@B5Isn}VDF8efqxMoz{fz?S0O1=PKQgLs zL2xNS)*%RwNA_uWF9nB;9|RClc!A*m1Be{qH?JxJ_pl5PBJ{#C0uqz3j0p4Dx5NsM zBT#Ut^9U3WVIQ7IBqL`FSOy~R3?6R3Frj{{4k8(k?5lWqh9V=d0n0$-d;rfwL}Xup zWe~Ct;^Fpz?5ps6fCG@e0U!bTM=m|wtR0=syJ3zVl|AX`d;ZT^Pu9@I#RCiHZF3rg i|9UywxnN;a|C|inJ*?e4{+tWp(_uVfL`7Bg)&2$4o#Cti literal 0 HcmV?d00001 diff --git a/src/auth/Auth.sol b/src/auth/Auth.sol index fe5c7078..2cf75592 100644 --- a/src/auth/Auth.sol +++ b/src/auth/Auth.sol @@ -1,22 +1,13 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; - -/// @notice A generic interface for a contract which provides authorization data to an Auth instance. -/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) -interface Authority { - function canCall( - address user, - address target, - bytes4 functionSig - ) external view returns (bool); -} +pragma solidity >=0.8.0; /// @notice Provides a flexible and updatable auth pattern which is completely separate from application logic. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol) /// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) abstract contract Auth { - event OwnerUpdated(address indexed owner); + event OwnerUpdated(address indexed user, address indexed newOwner); - event AuthorityUpdated(Authority indexed authority); + event AuthorityUpdated(address indexed user, Authority indexed newAuthority); address public owner; @@ -26,37 +17,48 @@ abstract contract Auth { owner = _owner; authority = _authority; - emit OwnerUpdated(_owner); - emit AuthorityUpdated(_authority); + emit OwnerUpdated(msg.sender, _owner); + emit AuthorityUpdated(msg.sender, _authority); } - function setOwner(address newOwner) public virtual requiresAuth { - owner = newOwner; + modifier requiresAuth() { + require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED"); - emit OwnerUpdated(owner); + _; } - function setAuthority(Authority newAuthority) public virtual requiresAuth { - authority = newAuthority; + function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) { + Authority auth = authority; // Memoizing authority saves us a warm SLOAD, around 100 gas. - emit AuthorityUpdated(authority); + // Checking if the caller is the owner only after calling the authority saves gas in most cases, but be + // aware that this makes protected functions uncallable even to the owner if the authority is out of order. + return (address(auth) != address(0) && auth.canCall(user, address(this), functionSig)) || user == owner; } - function isAuthorized(address user, bytes4 functionSig) internal view virtual returns (bool) { - Authority cachedAuthority = authority; + function setAuthority(Authority newAuthority) public virtual { + // We check if the caller is the owner first because we want to ensure they can + // always swap out the authority even if it's reverting or using up a lot of gas. + require(msg.sender == owner || authority.canCall(msg.sender, address(this), msg.sig)); - if (address(cachedAuthority) != address(0)) { - try cachedAuthority.canCall(user, address(this), functionSig) returns (bool canCall) { - if (canCall) return true; - } catch {} - } + authority = newAuthority; - return user == owner; + emit AuthorityUpdated(msg.sender, newAuthority); } - modifier requiresAuth() { - require(isAuthorized(msg.sender, msg.sig), "UNAUTHORIZED"); + function setOwner(address newOwner) public virtual requiresAuth { + owner = newOwner; - _; + emit OwnerUpdated(msg.sender, newOwner); } } + +/// @notice A generic interface for a contract which provides authorization data to an Auth instance. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/Auth.sol) +/// @author Modified from Dappsys (https://github.com/dapphub/ds-auth/blob/master/src/auth.sol) +interface Authority { + function canCall( + address user, + address target, + bytes4 functionSig + ) external view returns (bool); +} diff --git a/src/auth/Trust.sol b/src/auth/Trust.sol deleted file mode 100644 index 76b072cd..00000000 --- a/src/auth/Trust.sol +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; - -/// @notice Ultra minimal authorization logic for smart contracts. -/// @author Inspired by Dappsys V2 (https://github.com/dapp-org/dappsys-v2/blob/main/src/auth.sol) -abstract contract Trust { - event UserTrustUpdated(address indexed user, bool trusted); - - mapping(address => bool) public isTrusted; - - constructor(address initialUser) { - isTrusted[initialUser] = true; - - emit UserTrustUpdated(initialUser, true); - } - - function setIsTrusted(address user, bool trusted) public virtual requiresTrust { - isTrusted[user] = trusted; - - emit UserTrustUpdated(user, trusted); - } - - modifier requiresTrust() { - require(isTrusted[msg.sender], "UNTRUSTED"); - - _; - } -} diff --git a/src/auth/authorities/MultiRolesAuthority.sol b/src/auth/authorities/MultiRolesAuthority.sol new file mode 100644 index 00000000..3329714c --- /dev/null +++ b/src/auth/authorities/MultiRolesAuthority.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {Auth, Authority} from "../Auth.sol"; + +/// @notice Flexible and target agnostic role based Authority that supports up to 256 roles. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/authorities/MultiRolesAuthority.sol) +contract MultiRolesAuthority is Auth, Authority { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled); + + event PublicCapabilityUpdated(bytes4 indexed functionSig, bool enabled); + + event RoleCapabilityUpdated(uint8 indexed role, bytes4 indexed functionSig, bool enabled); + + event TargetCustomAuthorityUpdated(address indexed target, Authority indexed authority); + + /*/////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} + + /*/////////////////////////////////////////////////////////////// + CUSTOM TARGET AUTHORITY STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(address => Authority) public getTargetCustomAuthority; + + /*/////////////////////////////////////////////////////////////// + ROLE/USER STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(address => bytes32) public getUserRoles; + + mapping(bytes4 => bool) public isCapabilityPublic; + + mapping(bytes4 => bytes32) public getRolesWithCapability; + + function doesUserHaveRole(address user, uint8 role) public view virtual returns (bool) { + return (uint256(getUserRoles[user]) >> role) & 1 != 0; + } + + function doesRoleHaveCapability(uint8 role, bytes4 functionSig) public view virtual returns (bool) { + return (uint256(getRolesWithCapability[functionSig]) >> role) & 1 != 0; + } + + /*/////////////////////////////////////////////////////////////// + AUTHORIZATION LOGIC + //////////////////////////////////////////////////////////////*/ + + function canCall( + address user, + address target, + bytes4 functionSig + ) public view virtual override returns (bool) { + Authority customAuthority = getTargetCustomAuthority[target]; + + if (address(customAuthority) != address(0)) return customAuthority.canCall(user, target, functionSig); + + return + isCapabilityPublic[functionSig] || bytes32(0) != getUserRoles[user] & getRolesWithCapability[functionSig]; + } + + /*/////////////////////////////////////////////////////////////// + CUSTOM TARGET AUTHORITY CONFIGURATION LOGIC + //////////////////////////////////////////////////////////////*/ + + function setTargetCustomAuthority(address target, Authority customAuthority) public virtual requiresAuth { + getTargetCustomAuthority[target] = customAuthority; + + emit TargetCustomAuthorityUpdated(target, customAuthority); + } + + /*/////////////////////////////////////////////////////////////// + PUBLIC CAPABILITY CONFIGURATION LOGIC + //////////////////////////////////////////////////////////////*/ + + function setPublicCapability(bytes4 functionSig, bool enabled) public virtual requiresAuth { + isCapabilityPublic[functionSig] = enabled; + + emit PublicCapabilityUpdated(functionSig, enabled); + } + + /*/////////////////////////////////////////////////////////////// + USER ROLE ASSIGNMENT LOGIC + //////////////////////////////////////////////////////////////*/ + + function setUserRole( + address user, + uint8 role, + bool enabled + ) public virtual requiresAuth { + if (enabled) { + getUserRoles[user] |= bytes32(1 << role); + } else { + getUserRoles[user] &= ~bytes32(1 << role); + } + + emit UserRoleUpdated(user, role, enabled); + } + + /*/////////////////////////////////////////////////////////////// + ROLE CAPABILITY CONFIGURATION LOGIC + //////////////////////////////////////////////////////////////*/ + + function setRoleCapability( + uint8 role, + bytes4 functionSig, + bool enabled + ) public virtual requiresAuth { + if (enabled) { + getRolesWithCapability[functionSig] |= bytes32(1 << role); + } else { + getRolesWithCapability[functionSig] &= ~bytes32(1 << role); + } + + emit RoleCapabilityUpdated(role, functionSig, enabled); + } +} diff --git a/src/auth/authorities/RolesAuthority.sol b/src/auth/authorities/RolesAuthority.sol index ef5c89a2..94e394f6 100644 --- a/src/auth/authorities/RolesAuthority.sol +++ b/src/auth/authorities/RolesAuthority.sol @@ -4,14 +4,13 @@ pragma solidity >=0.8.0; import {Auth, Authority} from "../Auth.sol"; /// @notice Role based Authority that supports up to 256 roles. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/auth/authorities/RolesAuthority.sol) /// @author Modified from Dappsys (https://github.com/dapphub/ds-roles/blob/master/src/roles.sol) contract RolesAuthority is Auth, Authority { /*/////////////////////////////////////////////////////////////// EVENTS //////////////////////////////////////////////////////////////*/ - event UserRootUpdated(address indexed user, bool enabled); - event UserRoleUpdated(address indexed user, uint8 indexed role, bool enabled); event PublicCapabilityUpdated(address indexed target, bytes4 indexed functionSig, bool enabled); @@ -21,42 +20,29 @@ contract RolesAuthority is Auth, Authority { /*/////////////////////////////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////////////*/ + constructor(address _owner, Authority _authority) Auth(_owner, _authority) {} /*/////////////////////////////////////////////////////////////// - USER ROLE STORAGE + ROLE/USER STORAGE //////////////////////////////////////////////////////////////*/ - mapping(address => bool) public isUserRoot; - mapping(address => bytes32) public getUserRoles; - function doesUserHaveRole(address user, uint8 role) public view virtual returns (bool) { - unchecked { - bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); - - return bytes32(0) != getUserRoles[user] & shifted; - } - } - - /*/////////////////////////////////////////////////////////////// - ROLE CAPABILITY STORAGE - //////////////////////////////////////////////////////////////*/ + mapping(address => mapping(bytes4 => bool)) public isCapabilityPublic; - mapping(address => mapping(bytes4 => bytes32)) public getRoleCapabilities; + mapping(address => mapping(bytes4 => bytes32)) public getRolesWithCapability; - mapping(address => mapping(bytes4 => bool)) public isCapabilityPublic; + function doesUserHaveRole(address user, uint8 role) public view virtual returns (bool) { + return (uint256(getUserRoles[user]) >> role) & 1 != 0; + } function doesRoleHaveCapability( uint8 role, address target, bytes4 functionSig ) public view virtual returns (bool) { - unchecked { - bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); - - return bytes32(0) != getRoleCapabilities[target][functionSig] & shifted; - } + return (uint256(getRolesWithCapability[target][functionSig]) >> role) & 1 != 0; } /*/////////////////////////////////////////////////////////////// @@ -68,9 +54,9 @@ contract RolesAuthority is Auth, Authority { address target, bytes4 functionSig ) public view virtual override returns (bool) { - if (isCapabilityPublic[target][functionSig]) return true; - - return bytes32(0) != getUserRoles[user] & getRoleCapabilities[target][functionSig] || isUserRoot[user]; + return + isCapabilityPublic[target][functionSig] || + bytes32(0) != getUserRoles[user] & getRolesWithCapability[target][functionSig]; } /*/////////////////////////////////////////////////////////////// @@ -93,14 +79,10 @@ contract RolesAuthority is Auth, Authority { bytes4 functionSig, bool enabled ) public virtual requiresAuth { - bytes32 lastCapabilities = getRoleCapabilities[target][functionSig]; - - unchecked { - bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); - - getRoleCapabilities[target][functionSig] = enabled - ? lastCapabilities | shifted - : lastCapabilities & ~shifted; + if (enabled) { + getRolesWithCapability[target][functionSig] |= bytes32(1 << role); + } else { + getRolesWithCapability[target][functionSig] &= ~bytes32(1 << role); } emit RoleCapabilityUpdated(role, target, functionSig, enabled); @@ -115,20 +97,12 @@ contract RolesAuthority is Auth, Authority { uint8 role, bool enabled ) public virtual requiresAuth { - bytes32 lastRoles = getUserRoles[user]; - - unchecked { - bytes32 shifted = bytes32(uint256(uint256(2)**uint256(role))); - - getUserRoles[user] = enabled ? lastRoles | shifted : lastRoles & ~shifted; + if (enabled) { + getUserRoles[user] |= bytes32(1 << role); + } else { + getUserRoles[user] &= ~bytes32(1 << role); } emit UserRoleUpdated(user, role, enabled); } - - function setRootUser(address user, bool enabled) public virtual requiresAuth { - isUserRoot[user] = enabled; - - emit UserRootUpdated(user, enabled); - } } diff --git a/src/auth/authorities/TrustAuthority.sol b/src/auth/authorities/TrustAuthority.sol deleted file mode 100644 index ad203b20..00000000 --- a/src/auth/authorities/TrustAuthority.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; - -import {Trust} from "../Trust.sol"; -import {Authority} from "../Auth.sol"; - -/// @notice Simple Authority that allows a Trust to be used as an Authority. -/// @author Original work by Transmissions11 (https://github.com/transmissions11) -contract TrustAuthority is Trust, Authority { - constructor(address initialUser) Trust(initialUser) {} - - function canCall( - address user, - address, - bytes4 - ) public view virtual override returns (bool) { - return isTrusted[user]; - } -} diff --git a/src/test/Auth.t.sol b/src/test/Auth.t.sol index 91709c41..5ac743bc 100644 --- a/src/test/Auth.t.sol +++ b/src/test/Auth.t.sol @@ -3,22 +3,17 @@ pragma solidity 0.8.10; import {DSTestPlus} from "./utils/DSTestPlus.sol"; import {MockAuthChild} from "./utils/mocks/MockAuthChild.sol"; +import {MockAuthority} from "./utils/mocks/MockAuthority.sol"; -import {Auth, Authority} from "../auth/Auth.sol"; - -contract BooleanAuthority is Authority { - bool yes; - - constructor(bool _yes) { - yes = _yes; - } +import {Authority} from "../auth/Auth.sol"; +contract OutOfOrderAuthority is Authority { function canCall( address, address, bytes4 - ) public view override returns (bool) { - return yes; + ) public pure override returns (bool) { + revert("OUT_OF_ORDER"); } } @@ -29,39 +24,167 @@ contract AuthTest is DSTestPlus { mockAuthChild = new MockAuthChild(); } - function invariantOwner() public { - assertEq(mockAuthChild.owner(), address(this)); + function testSetOwnerAsOwner() public { + mockAuthChild.setOwner(address(0xBEEF)); + assertEq(mockAuthChild.owner(), address(0xBEEF)); + } + + function testSetAuthorityAsOwner() public { + mockAuthChild.setAuthority(Authority(address(0xBEEF))); + assertEq(address(mockAuthChild.authority()), address(0xBEEF)); } - function invariantAuthority() public { - assertEq(address(mockAuthChild.authority()), address(0)); + function testCallFunctionAsOwner() public { + mockAuthChild.updateFlag(); } - function testFailNonOwner1() public { + function testSetOwnerWithPermissiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(address(0)); + mockAuthChild.setOwner(address(this)); + } + + function testSetAuthorityWithPermissiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(address(0)); + mockAuthChild.setAuthority(Authority(address(0xBEEF))); + } + + function testCallFunctionWithPermissiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(true)); mockAuthChild.setOwner(address(0)); mockAuthChild.updateFlag(); } - function testFailNonOwner2() public { + function testSetAuthorityAsOwnerWithOutOfOrderAuthority() public { + mockAuthChild.setAuthority(new OutOfOrderAuthority()); + mockAuthChild.setAuthority(new MockAuthority(true)); + } + + function testFailSetOwnerAsNonOwner() public { mockAuthChild.setOwner(address(0)); + mockAuthChild.setOwner(address(0xBEEF)); + } + + function testFailSetAuthorityAsNonOwner() public { mockAuthChild.setOwner(address(0)); + mockAuthChild.setAuthority(Authority(address(0xBEEF))); } - function testFailRejectingAuthority1() public { - mockAuthChild.setAuthority(Authority(address(new BooleanAuthority(false)))); + function testFailCallFunctionAsNonOwner() public { mockAuthChild.setOwner(address(0)); mockAuthChild.updateFlag(); } - function testFailRejectingAuthority2() public { - mockAuthChild.setAuthority(Authority(address(new BooleanAuthority(false)))); + function testFailSetOwnerWithRestrictiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(false)); + mockAuthChild.setOwner(address(0)); + mockAuthChild.setOwner(address(this)); + } + + function testFailSetAuthorityWithRestrictiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(false)); mockAuthChild.setOwner(address(0)); + mockAuthChild.setAuthority(Authority(address(0xBEEF))); + } + + function testFailCallFunctionWithRestrictiveAuthority() public { + mockAuthChild.setAuthority(new MockAuthority(false)); mockAuthChild.setOwner(address(0)); + mockAuthChild.updateFlag(); } - function testAcceptingOwner() public { - mockAuthChild.setAuthority(Authority(address(new BooleanAuthority(true)))); + function testFailSetOwnerAsOwnerWithOutOfOrderAuthority() public { + mockAuthChild.setAuthority(new OutOfOrderAuthority()); mockAuthChild.setOwner(address(0)); + } + + function testFailCallFunctionAsOwnerWithOutOfOrderAuthority() public { + mockAuthChild.setAuthority(new OutOfOrderAuthority()); + mockAuthChild.updateFlag(); + } + + function testSetOwnerAsOwner(address newOwner) public { + mockAuthChild.setOwner(newOwner); + assertEq(mockAuthChild.owner(), newOwner); + } + + function testSetAuthorityAsOwner(Authority newAuthority) public { + mockAuthChild.setAuthority(newAuthority); + assertEq(address(mockAuthChild.authority()), address(newAuthority)); + } + + function testSetOwnerWithPermissiveAuthority(address deadOwner, address newOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setOwner(newOwner); + } + + function testSetAuthorityWithPermissiveAuthority(address deadOwner, Authority newAuthority) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setAuthority(newAuthority); + } + + function testCallFunctionWithPermissiveAuthority(address deadOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(true)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.updateFlag(); + } + + function testFailSetOwnerAsNonOwner(address deadOwner, address newOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setOwner(newOwner); + } + + function testFailSetAuthorityAsNonOwner(address deadOwner, Authority newAuthority) public { + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setAuthority(newAuthority); + } + + function testFailCallFunctionAsNonOwner(address deadOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setOwner(deadOwner); mockAuthChild.updateFlag(); } + + function testFailSetOwnerWithRestrictiveAuthority(address deadOwner, address newOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(false)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setOwner(newOwner); + } + + function testFailSetAuthorityWithRestrictiveAuthority(address deadOwner, Authority newAuthority) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(false)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.setAuthority(newAuthority); + } + + function testFailCallFunctionWithRestrictiveAuthority(address deadOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new MockAuthority(false)); + mockAuthChild.setOwner(deadOwner); + mockAuthChild.updateFlag(); + } + + function testFailSetOwnerAsOwnerWithOutOfOrderAuthority(address deadOwner) public { + if (deadOwner == address(this)) deadOwner = address(0); + + mockAuthChild.setAuthority(new OutOfOrderAuthority()); + mockAuthChild.setOwner(deadOwner); + } } diff --git a/src/test/CREATE3.t.sol b/src/test/CREATE3.t.sol index a6f671bd..8120632d 100644 --- a/src/test/CREATE3.t.sol +++ b/src/test/CREATE3.t.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: AGPL-3.0-only pragma solidity 0.8.10; +import {WETH} from "../tokens/WETH.sol"; import {DSTestPlus} from "./utils/DSTestPlus.sol"; import {MockERC20} from "./utils/mocks/MockERC20.sol"; import {MockAuthChild} from "./utils/mocks/MockAuthChild.sol"; -import {MockTrustChild} from "./utils/mocks/MockTrustChild.sol"; import {CREATE3} from "../utils/CREATE3.sol"; @@ -13,7 +13,11 @@ contract CREATE3Test is DSTestPlus { bytes32 salt = keccak256(bytes("A salt!")); MockERC20 deployed = MockERC20( - CREATE3.deploy(salt, abi.encodePacked(type(MockERC20).creationCode, abi.encode("Mock Token", "MOCK", 18))) + CREATE3.deploy( + salt, + abi.encodePacked(type(MockERC20).creationCode, abi.encode("Mock Token", "MOCK", 18)), + 0 + ) ); assertEq(address(deployed), CREATE3.getDeployed(salt)); @@ -26,15 +30,15 @@ contract CREATE3Test is DSTestPlus { function testFailDoubleDeploySameBytecode() public { bytes32 salt = keccak256(bytes("Salty...")); - CREATE3.deploy(salt, type(MockAuthChild).creationCode); - CREATE3.deploy(salt, type(MockAuthChild).creationCode); + CREATE3.deploy(salt, type(MockAuthChild).creationCode, 0); + CREATE3.deploy(salt, type(MockAuthChild).creationCode, 0); } function testFailDoubleDeployDifferentBytecode() public { bytes32 salt = keccak256(bytes("and sweet!")); - CREATE3.deploy(salt, type(MockAuthChild).creationCode); - CREATE3.deploy(salt, type(MockTrustChild).creationCode); + CREATE3.deploy(salt, type(WETH).creationCode, 0); + CREATE3.deploy(salt, type(MockAuthChild).creationCode, 0); } function testDeployERC20( @@ -44,7 +48,7 @@ contract CREATE3Test is DSTestPlus { uint8 decimals ) public { MockERC20 deployed = MockERC20( - CREATE3.deploy(salt, abi.encodePacked(type(MockERC20).creationCode, abi.encode(name, symbol, decimals))) + CREATE3.deploy(salt, abi.encodePacked(type(MockERC20).creationCode, abi.encode(name, symbol, decimals)), 0) ); assertEq(address(deployed), CREATE3.getDeployed(salt)); @@ -55,8 +59,8 @@ contract CREATE3Test is DSTestPlus { } function testFailDoubleDeploySameBytecode(bytes32 salt, bytes calldata bytecode) public { - CREATE3.deploy(salt, bytecode); - CREATE3.deploy(salt, bytecode); + CREATE3.deploy(salt, bytecode, 0); + CREATE3.deploy(salt, bytecode, 0); } function testFailDoubleDeployDifferentBytecode( @@ -64,7 +68,7 @@ contract CREATE3Test is DSTestPlus { bytes calldata bytecode1, bytes calldata bytecode2 ) public { - CREATE3.deploy(salt, bytecode1); - CREATE3.deploy(salt, bytecode2); + CREATE3.deploy(salt, bytecode1, 0); + CREATE3.deploy(salt, bytecode2, 0); } } diff --git a/src/test/DSTestPlus.t.sol b/src/test/DSTestPlus.t.sol new file mode 100644 index 00000000..432cd327 --- /dev/null +++ b/src/test/DSTestPlus.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; + +contract DSTestPlusTest is DSTestPlus { + function testBound() public { + assertEq(bound(5, 0, 4), 0); + assertEq(bound(0, 69, 69), 69); + assertEq(bound(0, 68, 69), 68); + assertEq(bound(10, 150, 190), 174); + assertEq(bound(300, 2800, 3200), 3107); + assertEq(bound(9999, 1337, 6666), 4669); + } + + function testFailBoundMinBiggerThanMax() public pure { + bound(5, 100, 10); + } + + function testBound( + uint256 num, + uint256 min, + uint256 max + ) public { + if (min > max) (min, max) = (max, min); + + uint256 bounded = bound(num, min, max); + + assertGe(bounded, min); + assertLe(bounded, max); + } + + function testFailBoundMinBiggerThanMax( + uint256 num, + uint256 min, + uint256 max + ) public pure { + if (max == min) { + unchecked { + min++; // Overflow is handled below. + } + } + + if (max > min) (min, max) = (max, min); + + bound(num, min, max); + } +} diff --git a/src/test/ERC1155.t.sol b/src/test/ERC1155.t.sol new file mode 100644 index 00000000..ce5ef058 --- /dev/null +++ b/src/test/ERC1155.t.sol @@ -0,0 +1,1777 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {DSInvariantTest} from "./utils/DSInvariantTest.sol"; + +import {MockERC1155} from "./utils/mocks/MockERC1155.sol"; +import {ERC1155User} from "./utils/users/ERC1155User.sol"; + +import {ERC1155TokenReceiver} from "../tokens/ERC1155.sol"; + +contract ERC1155Recipient is ERC1155TokenReceiver { + address public operator; + address public from; + uint256 public id; + uint256 public amount; + bytes public mintData; + + function onERC1155Received( + address _operator, + address _from, + uint256 _id, + uint256 _amount, + bytes calldata _data + ) public override returns (bytes4) { + operator = _operator; + from = _from; + id = _id; + amount = _amount; + mintData = _data; + + return ERC1155TokenReceiver.onERC1155Received.selector; + } + + address public batchOperator; + address public batchFrom; + uint256[] internal _batchIds; + uint256[] internal _batchAmounts; + bytes public batchData; + + function batchIds() external view returns (uint256[] memory) { + return _batchIds; + } + + function batchAmounts() external view returns (uint256[] memory) { + return _batchAmounts; + } + + function onERC1155BatchReceived( + address _operator, + address _from, + uint256[] calldata _ids, + uint256[] calldata _amounts, + bytes calldata _data + ) external override returns (bytes4) { + batchOperator = _operator; + batchFrom = _from; + _batchIds = _ids; + _batchAmounts = _amounts; + batchData = _data; + + return ERC1155TokenReceiver.onERC1155BatchReceived.selector; + } +} + +contract RevertingERC1155Recipient is ERC1155TokenReceiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) public pure override returns (bytes4) { + revert(string(abi.encodePacked(ERC1155TokenReceiver.onERC1155Received.selector))); + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4) { + revert(string(abi.encodePacked(ERC1155TokenReceiver.onERC1155BatchReceived.selector))); + } +} + +contract WrongReturnDataERC1155Recipient is ERC1155TokenReceiver { + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) public pure override returns (bytes4) { + return 0xCAFEBEEF; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4) { + return 0xCAFEBEEF; + } +} + +contract NonERC1155Recipient {} + +contract ERC1155Test is DSTestPlus, ERC1155TokenReceiver { + MockERC1155 token; + + mapping(address => mapping(uint256 => uint256)) public userMintAmounts; + mapping(address => mapping(uint256 => uint256)) public userTransferOrBurnAmounts; + + function setUp() public { + token = new MockERC1155(); + } + + function testMintToEOA() public { + token.mint(address(0xBEEF), 1337, 1, ""); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 1); + } + + function testMintToERC1155Recipient() public { + ERC1155Recipient to = new ERC1155Recipient(); + + token.mint(address(to), 1337, 1, "testing 123"); + + assertEq(token.balanceOf(address(to), 1337), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), 1337); + assertBytesEq(to.mintData(), "testing 123"); + } + + function testBatchMintToEOA() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory amounts = new uint256[](5); + amounts[0] = 100; + amounts[1] = 200; + amounts[2] = 300; + amounts[3] = 400; + amounts[4] = 500; + + token.batchMint(address(0xBEEF), ids, amounts, ""); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 100); + assertEq(token.balanceOf(address(0xBEEF), 1338), 200); + assertEq(token.balanceOf(address(0xBEEF), 1339), 300); + assertEq(token.balanceOf(address(0xBEEF), 1340), 400); + assertEq(token.balanceOf(address(0xBEEF), 1341), 500); + } + + function testBatchMintToERC1155Recipient() public { + ERC1155Recipient to = new ERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory amounts = new uint256[](5); + amounts[0] = 100; + amounts[1] = 200; + amounts[2] = 300; + amounts[3] = 400; + amounts[4] = 500; + + token.batchMint(address(to), ids, amounts, "testing 123"); + + assertEq(to.batchOperator(), address(this)); + assertEq(to.batchFrom(), address(0)); + assertUintArrayEq(to.batchIds(), ids); + assertUintArrayEq(to.batchAmounts(), amounts); + assertBytesEq(to.batchData(), "testing 123"); + + assertEq(token.balanceOf(address(to), 1337), 100); + assertEq(token.balanceOf(address(to), 1338), 200); + assertEq(token.balanceOf(address(to), 1339), 300); + assertEq(token.balanceOf(address(to), 1340), 400); + assertEq(token.balanceOf(address(to), 1341), 500); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1337, 100, ""); + + token.burn(address(0xBEEF), 1337, 70); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 30); + } + + function testBatchBurn() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory burnAmounts = new uint256[](5); + burnAmounts[0] = 50; + burnAmounts[1] = 100; + burnAmounts[2] = 150; + burnAmounts[3] = 200; + burnAmounts[4] = 250; + + token.batchMint(address(0xBEEF), ids, mintAmounts, ""); + + token.batchBurn(address(0xBEEF), ids, burnAmounts); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 50); + assertEq(token.balanceOf(address(0xBEEF), 1338), 100); + assertEq(token.balanceOf(address(0xBEEF), 1339), 150); + assertEq(token.balanceOf(address(0xBEEF), 1340), 200); + assertEq(token.balanceOf(address(0xBEEF), 1341), 250); + } + + function testApproveAll() public { + token.setApprovalForAll(address(0xBEEF), true); + + assertTrue(token.isApprovedForAll(address(this), address(0xBEEF))); + } + + function testSafeTransferFromToEOA() public { + ERC1155User from = new ERC1155User(token); + + token.mint(address(from), 1337, 100, ""); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(0xBEEF), 1337, 70, ""); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 70); + assertEq(token.balanceOf(address(from), 1337), 30); + } + + function testSafeTransferFromToERC1155Recipient() public { + ERC1155Recipient to = new ERC1155Recipient(); + + ERC1155User from = new ERC1155User(token); + + token.mint(address(from), 1337, 100, ""); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(to), 1337, 70, "testing 123"); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(from)); + assertEq(to.id(), 1337); + assertBytesEq(to.mintData(), "testing 123"); + + assertEq(token.balanceOf(address(to), 1337), 70); + assertEq(token.balanceOf(address(from), 1337), 30); + } + + function testSafeTransferFromSelf() public { + token.mint(address(this), 1337, 100, ""); + + token.safeTransferFrom(address(this), address(0xBEEF), 1337, 70, ""); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 70); + assertEq(token.balanceOf(address(this), 1337), 30); + } + + function testSafeBatchTransferFromToEOA() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0xBEEF), ids, transferAmounts, ""); + + assertEq(token.balanceOf(address(from), 1337), 50); + assertEq(token.balanceOf(address(0xBEEF), 1337), 50); + + assertEq(token.balanceOf(address(from), 1338), 100); + assertEq(token.balanceOf(address(0xBEEF), 1338), 100); + + assertEq(token.balanceOf(address(from), 1339), 150); + assertEq(token.balanceOf(address(0xBEEF), 1339), 150); + + assertEq(token.balanceOf(address(from), 1340), 200); + assertEq(token.balanceOf(address(0xBEEF), 1340), 200); + + assertEq(token.balanceOf(address(from), 1341), 250); + assertEq(token.balanceOf(address(0xBEEF), 1341), 250); + } + + function testSafeBatchTransferFromToERC1155Recipient() public { + ERC1155User from = new ERC1155User(token); + + ERC1155Recipient to = new ERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(to), ids, transferAmounts, "testing 123"); + + assertEq(to.batchOperator(), address(this)); + assertEq(to.batchFrom(), address(from)); + assertUintArrayEq(to.batchIds(), ids); + assertUintArrayEq(to.batchAmounts(), transferAmounts); + assertBytesEq(to.batchData(), "testing 123"); + + assertEq(token.balanceOf(address(from), 1337), 50); + assertEq(token.balanceOf(address(to), 1337), 50); + + assertEq(token.balanceOf(address(from), 1338), 100); + assertEq(token.balanceOf(address(to), 1338), 100); + + assertEq(token.balanceOf(address(from), 1339), 150); + assertEq(token.balanceOf(address(to), 1339), 150); + + assertEq(token.balanceOf(address(from), 1340), 200); + assertEq(token.balanceOf(address(to), 1340), 200); + + assertEq(token.balanceOf(address(from), 1341), 250); + assertEq(token.balanceOf(address(to), 1341), 250); + } + + function testBatchBalanceOf() public { + address[] memory tos = new address[](5); + tos[0] = address(0xBEEF); + tos[1] = address(0xCAFE); + tos[2] = address(0xFACE); + tos[3] = address(0xDEAD); + tos[4] = address(0xFEED); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + token.mint(address(0xBEEF), 1337, 100, ""); + token.mint(address(0xCAFE), 1338, 200, ""); + token.mint(address(0xFACE), 1339, 300, ""); + token.mint(address(0xDEAD), 1340, 400, ""); + token.mint(address(0xFEED), 1341, 500, ""); + + uint256[] memory balances = token.balanceOfBatch(tos, ids); + + assertEq(balances[0], 100); + assertEq(balances[1], 200); + assertEq(balances[2], 300); + assertEq(balances[3], 400); + assertEq(balances[4], 500); + } + + function testFailMintToZero() public { + token.mint(address(0), 1337, 1, ""); + } + + function testFailMintToNonERC155Recipient() public { + token.mint(address(new NonERC1155Recipient()), 1337, 1, ""); + } + + function testFailMintToRevertingERC155Recipient() public { + token.mint(address(new RevertingERC1155Recipient()), 1337, 1, ""); + } + + function testFailMintToWrongReturnDataERC155Recipient() public { + token.mint(address(new RevertingERC1155Recipient()), 1337, 1, ""); + } + + function testFailBurnInsufficientBalance() public { + token.mint(address(0xBEEF), 1337, 70, ""); + token.burn(address(0xBEEF), 1337, 100); + } + + function testFailSafeTransferFromInsufficientBalance() public { + ERC1155User from = new ERC1155User(token); + + token.mint(address(from), 1337, 70, ""); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(0xBEEF), 1337, 100, ""); + } + + function testFailSafeTransferFromSelfInsufficientBalance() public { + token.mint(address(this), 1337, 70, ""); + token.safeTransferFrom(address(this), address(0xBEEF), 1337, 100, ""); + } + + function testFailSafeTransferFromToZero() public { + token.mint(address(this), 1337, 100, ""); + token.safeTransferFrom(address(this), address(0), 1337, 70, ""); + } + + function testFailSafeTransferFromToNonERC155Recipient() public { + token.mint(address(this), 1337, 100, ""); + token.safeTransferFrom(address(this), address(new NonERC1155Recipient()), 1337, 70, ""); + } + + function testFailSafeTransferFromToRevertingERC1155Recipient() public { + token.mint(address(this), 1337, 100, ""); + token.safeTransferFrom(address(this), address(new RevertingERC1155Recipient()), 1337, 70, ""); + } + + function testFailSafeTransferFromToWrongReturnDataERC1155Recipient() public { + token.mint(address(this), 1337, 100, ""); + token.safeTransferFrom(address(this), address(new WrongReturnDataERC1155Recipient()), 1337, 70, ""); + } + + function testFailSafeBatchTransferInsufficientBalance() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + + mintAmounts[0] = 50; + mintAmounts[1] = 100; + mintAmounts[2] = 150; + mintAmounts[3] = 200; + mintAmounts[4] = 250; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 100; + transferAmounts[1] = 200; + transferAmounts[2] = 300; + transferAmounts[3] = 400; + transferAmounts[4] = 500; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0xBEEF), ids, transferAmounts, ""); + } + + function testFailSafeBatchTransferFromToZero() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0), ids, transferAmounts, ""); + } + + function testFailSafeBatchTransferFromToNonERC1155Recipient() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(new NonERC1155Recipient()), ids, transferAmounts, ""); + } + + function testFailSafeBatchTransferFromToRevertingERC1155Recipient() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(new RevertingERC1155Recipient()), ids, transferAmounts, ""); + } + + function testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](5); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + transferAmounts[4] = 250; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom( + address(from), + address(new WrongReturnDataERC1155Recipient()), + ids, + transferAmounts, + "" + ); + } + + function testFailSafeBatchTransferFromWithArrayLengthMismatch() public { + ERC1155User from = new ERC1155User(token); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory transferAmounts = new uint256[](4); + transferAmounts[0] = 50; + transferAmounts[1] = 100; + transferAmounts[2] = 150; + transferAmounts[3] = 200; + + token.batchMint(address(from), ids, mintAmounts, ""); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0xBEEF), ids, transferAmounts, ""); + } + + function testFailBatchMintToZero() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + token.batchMint(address(0), ids, mintAmounts, ""); + } + + function testFailBatchMintToNonERC1155Recipient() public { + NonERC1155Recipient to = new NonERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + token.batchMint(address(to), ids, mintAmounts, ""); + } + + function testFailBatchMintToRevertingERC1155Recipient() public { + RevertingERC1155Recipient to = new RevertingERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + token.batchMint(address(to), ids, mintAmounts, ""); + } + + function testFailBatchMintToWrongReturnDataERC1155Recipient() public { + WrongReturnDataERC1155Recipient to = new WrongReturnDataERC1155Recipient(); + + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + token.batchMint(address(to), ids, mintAmounts, ""); + } + + function testFailBatchMintWithArrayMismatch() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory amounts = new uint256[](4); + amounts[0] = 100; + amounts[1] = 200; + amounts[2] = 300; + amounts[3] = 400; + + token.batchMint(address(0xBEEF), ids, amounts, ""); + } + + function testFailBatchBurnInsufficientBalance() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 50; + mintAmounts[1] = 100; + mintAmounts[2] = 150; + mintAmounts[3] = 200; + mintAmounts[4] = 250; + + uint256[] memory burnAmounts = new uint256[](5); + burnAmounts[0] = 100; + burnAmounts[1] = 200; + burnAmounts[2] = 300; + burnAmounts[3] = 400; + burnAmounts[4] = 500; + + token.batchMint(address(0xBEEF), ids, mintAmounts, ""); + + token.batchBurn(address(0xBEEF), ids, burnAmounts); + } + + function testFailBatchBurnWithArrayLengthMismatch() public { + uint256[] memory ids = new uint256[](5); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + ids[4] = 1341; + + uint256[] memory mintAmounts = new uint256[](5); + mintAmounts[0] = 100; + mintAmounts[1] = 200; + mintAmounts[2] = 300; + mintAmounts[3] = 400; + mintAmounts[4] = 500; + + uint256[] memory burnAmounts = new uint256[](4); + burnAmounts[0] = 50; + burnAmounts[1] = 100; + burnAmounts[2] = 150; + burnAmounts[3] = 200; + + token.batchMint(address(0xBEEF), ids, mintAmounts, ""); + + token.batchBurn(address(0xBEEF), ids, burnAmounts); + } + + function testFailBalanceOfBatchWithArrayMismatch() public view { + address[] memory tos = new address[](5); + tos[0] = address(0xBEEF); + tos[1] = address(0xCAFE); + tos[2] = address(0xFACE); + tos[3] = address(0xDEAD); + tos[4] = address(0xFEED); + + uint256[] memory ids = new uint256[](4); + ids[0] = 1337; + ids[1] = 1338; + ids[2] = 1339; + ids[3] = 1340; + + token.balanceOfBatch(tos, ids); + } + + function testMintToEOA( + address to, + uint256 id, + uint256 amount, + bytes memory mintData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + token.mint(to, id, amount, mintData); + + assertEq(token.balanceOf(to, id), amount); + } + + function testMintToERC1155Recipient( + uint256 id, + uint256 amount, + bytes memory mintData + ) public { + ERC1155Recipient to = new ERC1155Recipient(); + + token.mint(address(to), id, amount, mintData); + + assertEq(token.balanceOf(address(to), id), amount); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), id); + assertBytesEq(to.mintData(), mintData); + } + + function testBatchMintToEOA( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[to][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[to][id] += mintAmount; + } + + token.batchMint(to, normalizedIds, normalizedAmounts, mintData); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + + assertEq(token.balanceOf(to, id), userMintAmounts[to][id]); + } + } + + function testBatchMintToERC1155Recipient( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + ERC1155Recipient to = new ERC1155Recipient(); + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(to)][id] += mintAmount; + } + + token.batchMint(address(to), normalizedIds, normalizedAmounts, mintData); + + assertEq(to.batchOperator(), address(this)); + assertEq(to.batchFrom(), address(0)); + assertUintArrayEq(to.batchIds(), normalizedIds); + assertUintArrayEq(to.batchAmounts(), normalizedAmounts); + assertBytesEq(to.batchData(), mintData); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + + assertEq(token.balanceOf(address(to), id), userMintAmounts[address(to)][id]); + } + } + + function testBurn( + address to, + uint256 id, + uint256 mintAmount, + bytes memory mintData, + uint256 burnAmount + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + burnAmount = bound(burnAmount, 0, mintAmount); + + token.mint(to, id, mintAmount, mintData); + + token.burn(to, id, burnAmount); + + assertEq(token.balanceOf(address(to), id), mintAmount - burnAmount); + } + + function testBatchBurn( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory burnAmounts, + bytes memory mintData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + uint256 minLength = min3(ids.length, mintAmounts.length, burnAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedBurnAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + normalizedIds[i] = id; + normalizedMintAmounts[i] = bound(mintAmounts[i], 0, remainingMintAmountForId); + normalizedBurnAmounts[i] = bound(burnAmounts[i], 0, normalizedMintAmounts[i]); + + userMintAmounts[address(to)][id] += normalizedMintAmounts[i]; + userTransferOrBurnAmounts[address(to)][id] += normalizedBurnAmounts[i]; + } + + token.batchMint(to, normalizedIds, normalizedMintAmounts, mintData); + + token.batchBurn(to, normalizedIds, normalizedBurnAmounts); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + + assertEq(token.balanceOf(to, id), userMintAmounts[to][id] - userTransferOrBurnAmounts[to][id]); + } + } + + function testApproveAll(address to, bool approved) public { + token.setApprovalForAll(to, approved); + + assertBoolEq(token.isApprovedForAll(address(this), to), approved); + } + + function testSafeTransferFromToEOA( + uint256 id, + uint256 mintAmount, + bytes memory mintData, + uint256 transferAmount, + address to, + bytes memory transferData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + transferAmount = bound(transferAmount, 0, mintAmount); + + ERC1155User from = new ERC1155User(token); + + token.mint(address(from), id, mintAmount, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), to, id, transferAmount, transferData); + + assertEq(token.balanceOf(to, id), transferAmount); + assertEq(token.balanceOf(address(from), id), mintAmount - transferAmount); + } + + function testSafeTransferFromToERC1155Recipient( + uint256 id, + uint256 mintAmount, + bytes memory mintData, + uint256 transferAmount, + bytes memory transferData + ) public { + ERC1155Recipient to = new ERC1155Recipient(); + + ERC1155User from = new ERC1155User(token); + + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(from), id, mintAmount, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(to), id, transferAmount, transferData); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(from)); + assertEq(to.id(), id); + assertBytesEq(to.mintData(), transferData); + + assertEq(token.balanceOf(address(to), id), transferAmount); + assertEq(token.balanceOf(address(from), id), mintAmount - transferAmount); + } + + function testSafeTransferFromSelf( + uint256 id, + uint256 mintAmount, + bytes memory mintData, + uint256 transferAmount, + address to, + bytes memory transferData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + + token.safeTransferFrom(address(this), to, id, transferAmount, transferData); + + assertEq(token.balanceOf(to, id), transferAmount); + assertEq(token.balanceOf(address(this), id), mintAmount - transferAmount); + } + + function testSafeBatchTransferFromToEOA( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + userTransferOrBurnAmounts[address(from)][id] += transferAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), to, normalizedIds, normalizedTransferAmounts, transferData); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + + assertEq(token.balanceOf(address(to), id), userTransferOrBurnAmounts[address(from)][id]); + assertEq( + token.balanceOf(address(from), id), + userMintAmounts[address(from)][id] - userTransferOrBurnAmounts[address(from)][id] + ); + } + } + + function testSafeBatchTransferFromToERC1155Recipient( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + ERC1155Recipient to = new ERC1155Recipient(); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + userTransferOrBurnAmounts[address(from)][id] += transferAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(to), normalizedIds, normalizedTransferAmounts, transferData); + + assertEq(to.batchOperator(), address(this)); + assertEq(to.batchFrom(), address(from)); + assertUintArrayEq(to.batchIds(), normalizedIds); + assertUintArrayEq(to.batchAmounts(), normalizedTransferAmounts); + assertBytesEq(to.batchData(), transferData); + + for (uint256 i = 0; i < normalizedIds.length; i++) { + uint256 id = normalizedIds[i]; + uint256 transferAmount = userTransferOrBurnAmounts[address(from)][id]; + + assertEq(token.balanceOf(address(to), id), transferAmount); + assertEq(token.balanceOf(address(from), id), userMintAmounts[address(from)][id] - transferAmount); + } + } + + function testBatchBalanceOf( + address[] memory tos, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + uint256 minLength = min3(tos.length, ids.length, amounts.length); + + address[] memory normalizedTos = new address[](minLength); + uint256[] memory normalizedIds = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + address to = tos[i] == address(0) ? address(0xBEEF) : tos[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[to][id]; + + normalizedTos[i] = to; + normalizedIds[i] = id; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + token.mint(to, id, mintAmount, mintData); + + userMintAmounts[to][id] += mintAmount; + } + + uint256[] memory balances = token.balanceOfBatch(normalizedTos, normalizedIds); + + for (uint256 i = 0; i < normalizedTos.length; i++) { + assertEq(balances[i], token.balanceOf(normalizedTos[i], normalizedIds[i])); + } + } + + function testFailMintToZero( + uint256 id, + uint256 amount, + bytes memory data + ) public { + token.mint(address(0), id, amount, data); + } + + function testFailMintToNonERC155Recipient( + uint256 id, + uint256 mintAmount, + bytes memory mintData + ) public { + token.mint(address(new NonERC1155Recipient()), id, mintAmount, mintData); + } + + function testFailMintToRevertingERC155Recipient( + uint256 id, + uint256 mintAmount, + bytes memory mintData + ) public { + token.mint(address(new RevertingERC1155Recipient()), id, mintAmount, mintData); + } + + function testFailMintToWrongReturnDataERC155Recipient( + uint256 id, + uint256 mintAmount, + bytes memory mintData + ) public { + token.mint(address(new RevertingERC1155Recipient()), id, mintAmount, mintData); + } + + function testFailBurnInsufficientBalance( + address to, + uint256 id, + uint256 mintAmount, + uint256 burnAmount, + bytes memory mintData + ) public { + burnAmount = bound(burnAmount, mintAmount + 1, type(uint256).max); + + token.mint(to, id, mintAmount, mintData); + token.burn(to, id, burnAmount); + } + + function testFailSafeTransferFromInsufficientBalance( + address to, + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + transferAmount = bound(transferAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(from), id, mintAmount, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), to, id, transferAmount, transferData); + } + + function testFailSafeTransferFromSelfInsufficientBalance( + address to, + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom(address(this), to, id, transferAmount, transferData); + } + + function testFailSafeTransferFromToZero( + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom(address(this), address(0), id, transferAmount, transferData); + } + + function testFailSafeTransferFromToNonERC155Recipient( + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom(address(this), address(new NonERC1155Recipient()), id, transferAmount, transferData); + } + + function testFailSafeTransferFromToRevertingERC1155Recipient( + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom( + address(this), + address(new RevertingERC1155Recipient()), + id, + transferAmount, + transferData + ); + } + + function testFailSafeTransferFromToWrongReturnDataERC1155Recipient( + uint256 id, + uint256 mintAmount, + uint256 transferAmount, + bytes memory mintData, + bytes memory transferData + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(address(this), id, mintAmount, mintData); + token.safeTransferFrom( + address(this), + address(new WrongReturnDataERC1155Recipient()), + id, + transferAmount, + transferData + ); + } + + function testFailSafeBatchTransferInsufficientBalance( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], mintAmount + 1, type(uint256).max); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), to, normalizedIds, normalizedTransferAmounts, transferData); + } + + function testFailSafeBatchTransferFromToZero( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), address(0), normalizedIds, normalizedTransferAmounts, transferData); + } + + function testFailSafeBatchTransferFromToNonERC1155Recipient( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom( + address(from), + address(new NonERC1155Recipient()), + normalizedIds, + normalizedTransferAmounts, + transferData + ); + } + + function testFailSafeBatchTransferFromToRevertingERC1155Recipient( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom( + address(from), + address(new RevertingERC1155Recipient()), + normalizedIds, + normalizedTransferAmounts, + transferData + ); + } + + function testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient( + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + uint256 minLength = min3(ids.length, mintAmounts.length, transferAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedTransferAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(from)][id]; + + uint256 mintAmount = bound(mintAmounts[i], 0, remainingMintAmountForId); + uint256 transferAmount = bound(transferAmounts[i], 0, mintAmount); + + normalizedIds[i] = id; + normalizedMintAmounts[i] = mintAmount; + normalizedTransferAmounts[i] = transferAmount; + + userMintAmounts[address(from)][id] += mintAmount; + } + + token.batchMint(address(from), normalizedIds, normalizedMintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom( + address(from), + address(new WrongReturnDataERC1155Recipient()), + normalizedIds, + normalizedTransferAmounts, + transferData + ); + } + + function testFailSafeBatchTransferFromWithArrayLengthMismatch( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory transferAmounts, + bytes memory mintData, + bytes memory transferData + ) public { + ERC1155User from = new ERC1155User(token); + + if (ids.length == transferAmounts.length) revert(); + + token.batchMint(address(from), ids, mintAmounts, mintData); + + from.setApprovalForAll(address(this), true); + + token.safeBatchTransferFrom(address(from), to, ids, transferAmounts, transferData); + } + + function testFailBatchMintToZero( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(0)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(0)][id] += mintAmount; + } + + token.batchMint(address(0), normalizedIds, normalizedAmounts, mintData); + } + + function testFailBatchMintToNonERC1155Recipient( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + NonERC1155Recipient to = new NonERC1155Recipient(); + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(to)][id] += mintAmount; + } + + token.batchMint(address(to), normalizedIds, normalizedAmounts, mintData); + } + + function testFailBatchMintToRevertingERC1155Recipient( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + RevertingERC1155Recipient to = new RevertingERC1155Recipient(); + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(to)][id] += mintAmount; + } + + token.batchMint(address(to), normalizedIds, normalizedAmounts, mintData); + } + + function testFailBatchMintToWrongReturnDataERC1155Recipient( + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + WrongReturnDataERC1155Recipient to = new WrongReturnDataERC1155Recipient(); + + uint256 minLength = min2(ids.length, amounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[address(to)][id]; + + uint256 mintAmount = bound(amounts[i], 0, remainingMintAmountForId); + + normalizedIds[i] = id; + normalizedAmounts[i] = mintAmount; + + userMintAmounts[address(to)][id] += mintAmount; + } + + token.batchMint(address(to), normalizedIds, normalizedAmounts, mintData); + } + + function testFailBatchMintWithArrayMismatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory mintData + ) public { + if (ids.length == amounts.length) revert(); + + token.batchMint(address(to), ids, amounts, mintData); + } + + function testFailBatchBurnInsufficientBalance( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory burnAmounts, + bytes memory mintData + ) public { + uint256 minLength = min3(ids.length, mintAmounts.length, burnAmounts.length); + + uint256[] memory normalizedIds = new uint256[](minLength); + uint256[] memory normalizedMintAmounts = new uint256[](minLength); + uint256[] memory normalizedBurnAmounts = new uint256[](minLength); + + for (uint256 i = 0; i < minLength; i++) { + uint256 id = ids[i]; + + uint256 remainingMintAmountForId = type(uint256).max - userMintAmounts[to][id]; + + normalizedIds[i] = id; + normalizedMintAmounts[i] = bound(mintAmounts[i], 0, remainingMintAmountForId); + normalizedBurnAmounts[i] = bound(burnAmounts[i], normalizedMintAmounts[i] + 1, type(uint256).max); + + userMintAmounts[to][id] += normalizedMintAmounts[i]; + } + + token.batchMint(to, normalizedIds, normalizedMintAmounts, mintData); + + token.batchBurn(to, normalizedIds, normalizedBurnAmounts); + } + + function testFailBatchBurnWithArrayLengthMismatch( + address to, + uint256[] memory ids, + uint256[] memory mintAmounts, + uint256[] memory burnAmounts, + bytes memory mintData + ) public { + if (ids.length == burnAmounts.length) revert(); + + token.batchMint(to, ids, mintAmounts, mintData); + + token.batchBurn(to, ids, burnAmounts); + } + + function testFailBalanceOfBatchWithArrayMismatch(address[] memory tos, uint256[] memory ids) public view { + if (tos.length == ids.length) revert(); + + token.balanceOfBatch(tos, ids); + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) public pure override returns (bytes4) { + return ERC1155TokenReceiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external pure override returns (bytes4) { + return ERC1155TokenReceiver.onERC1155BatchReceived.selector; + } +} diff --git a/src/test/ERC20.t.sol b/src/test/ERC20.t.sol index 86d292d3..16b48b56 100644 --- a/src/test/ERC20.t.sol +++ b/src/test/ERC20.t.sol @@ -20,6 +20,190 @@ contract ERC20Test is DSTestPlus { assertEq(token.decimals(), 18); } + function testMetaData() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + assertEq(token.decimals(), 18); + } + + function testMint() public { + token.mint(address(0xBEEF), 1e18); + + assertEq(token.totalSupply(), 1e18); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1e18); + token.burn(address(0xBEEF), 0.9e18); + + assertEq(token.totalSupply(), 1e18 - 0.9e18); + assertEq(token.balanceOf(address(0xBEEF)), 0.1e18); + } + + function testApprove() public { + assertTrue(token.approve(address(0xBEEF), 1e18)); + + assertEq(token.allowance(address(this), address(0xBEEF)), 1e18); + } + + function testTransfer() public { + token.mint(address(this), 1e18); + + assertTrue(token.transfer(address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.balanceOf(address(this)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testTransferFrom() public { + ERC20User from = new ERC20User(token); + + token.mint(address(from), 1e18); + + from.approve(address(this), 1e18); + + assertTrue(token.transferFrom(address(from), address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(address(from), address(this)), 0); + + assertEq(token.balanceOf(address(from)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testInfiniteApproveTransferFrom() public { + ERC20User from = new ERC20User(token); + + token.mint(address(from), 1e18); + + from.approve(address(this), type(uint256).max); + + assertTrue(token.transferFrom(address(from), address(0xBEEF), 1e18)); + assertEq(token.totalSupply(), 1e18); + + assertEq(token.allowance(address(from), address(this)), type(uint256).max); + + assertEq(token.balanceOf(address(from)), 0); + assertEq(token.balanceOf(address(0xBEEF)), 1e18); + } + + function testPermit() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 0, block.timestamp)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + + assertEq(token.allowance(owner, address(0xCAFE)), 1e18); + assertEq(token.nonces(owner), 1); + } + + function testFailTransferInsufficientBalance() public { + token.mint(address(this), 0.9e18); + token.transfer(address(0xBEEF), 1e18); + } + + function testFailTransferFromInsufficientAllowance() public { + ERC20User from = new ERC20User(token); + + token.mint(address(from), 1e18); + from.approve(address(this), 0.9e18); + token.transferFrom(address(from), address(0xBEEF), 1e18); + } + + function testFailTransferFromInsufficientBalance() public { + ERC20User from = new ERC20User(token); + + token.mint(address(from), 0.9e18); + from.approve(address(this), 1e18); + token.transferFrom(address(from), address(0xBEEF), 1e18); + } + + function testFailPermitBadNonce() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 1, block.timestamp)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + } + + function testFailPermitBadDeadline() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 0, block.timestamp)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp + 1, v, r, s); + } + + function testFailPermitPastDeadline() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 0, block.timestamp - 1)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp - 1, v, r, s); + } + + function testFailPermitReplay() public { + uint256 privateKey = 0xBEEF; + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, address(0xCAFE), 1e18, 0, block.timestamp)) + ) + ) + ); + + token.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + token.permit(owner, address(0xCAFE), 1e18, block.timestamp, v, r, s); + } + function testMetaData( string calldata name, string calldata symbol, @@ -43,7 +227,7 @@ contract ERC20Test is DSTestPlus { uint256 mintAmount, uint256 burnAmount ) public { - if (burnAmount > mintAmount) return; + burnAmount = bound(burnAmount, 0, mintAmount); token.mint(from, mintAmount); token.burn(from, burnAmount); @@ -52,10 +236,10 @@ contract ERC20Test is DSTestPlus { assertEq(token.balanceOf(from), mintAmount - burnAmount); } - function testApprove(address from, uint256 amount) public { - assertTrue(token.approve(from, amount)); + function testApprove(address to, uint256 amount) public { + assertTrue(token.approve(to, amount)); - assertEq(token.allowance(address(this), from), amount); + assertEq(token.allowance(address(this), to), amount); } function testTransfer(address from, uint256 amount) public { @@ -77,7 +261,7 @@ contract ERC20Test is DSTestPlus { uint256 approval, uint256 amount ) public { - if (amount > approval) return; + amount = bound(amount, 0, approval); ERC20User from = new ERC20User(token); @@ -99,12 +283,62 @@ contract ERC20Test is DSTestPlus { } } + function testPermit( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline + ) public { + if (deadline < block.timestamp) deadline = block.timestamp; + if (privateKey == 0) privateKey = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, 0, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline, v, r, s); + + assertEq(token.allowance(owner, to), amount); + assertEq(token.nonces(owner), 1); + } + + function testFailBurnInsufficientBalance( + address to, + uint256 mintAmount, + uint256 burnAmount + ) public { + burnAmount = bound(burnAmount, mintAmount + 1, type(uint256).max); + + token.mint(to, mintAmount); + token.burn(to, burnAmount); + } + + function testFailTransferInsufficientBalance( + address to, + uint256 mintAmount, + uint256 sendAmount + ) public { + sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); + + token.mint(address(this), mintAmount); + token.transfer(to, sendAmount); + } + function testFailTransferFromInsufficientAllowance( address to, uint256 approval, uint256 amount ) public { - require(approval < amount); + amount = bound(amount, approval + 1, type(uint256).max); ERC20User from = new ERC20User(token); @@ -118,7 +352,7 @@ contract ERC20Test is DSTestPlus { uint256 mintAmount, uint256 sendAmount ) public { - require(mintAmount < sendAmount); + sendAmount = bound(sendAmount, mintAmount + 1, type(uint256).max); ERC20User from = new ERC20User(token); @@ -126,6 +360,109 @@ contract ERC20Test is DSTestPlus { from.approve(address(this), sendAmount); token.transferFrom(address(from), to, sendAmount); } + + function testFailPermitBadNonce( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline, + uint256 nonce + ) public { + if (deadline < block.timestamp) deadline = block.timestamp; + if (privateKey == 0) privateKey = 1; + if (nonce == 0) nonce = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, nonce, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline, v, r, s); + } + + function testFailPermitBadDeadline( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline + ) public { + if (deadline < block.timestamp) deadline = block.timestamp; + if (privateKey == 0) privateKey = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, 0, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline + 1, v, r, s); + } + + function testFailPermitPastDeadline( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline + ) public { + deadline = bound(deadline, 0, block.timestamp - 1); + if (privateKey == 0) privateKey = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, 0, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline, v, r, s); + } + + function testFailPermitReplay( + uint256 privateKey, + address to, + uint256 amount, + uint256 deadline + ) public { + if (deadline < block.timestamp) deadline = block.timestamp; + if (privateKey == 0) privateKey = 1; + + address owner = hevm.addr(privateKey); + + (uint8 v, bytes32 r, bytes32 s) = hevm.sign( + privateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + token.DOMAIN_SEPARATOR(), + keccak256(abi.encode(token.PERMIT_TYPEHASH(), owner, to, amount, 0, deadline)) + ) + ) + ); + + token.permit(owner, to, amount, deadline, v, r, s); + token.permit(owner, to, amount, deadline, v, r, s); + } } contract ERC20Invariants is DSTestPlus, DSInvariantTest { diff --git a/src/test/ERC721.t.sol b/src/test/ERC721.t.sol new file mode 100644 index 00000000..c8a71685 --- /dev/null +++ b/src/test/ERC721.t.sol @@ -0,0 +1,748 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {DSInvariantTest} from "./utils/DSInvariantTest.sol"; + +import {MockERC721} from "./utils/mocks/MockERC721.sol"; +import {ERC721User} from "./utils/users/ERC721User.sol"; + +import {ERC721TokenReceiver} from "../tokens/ERC721.sol"; + +contract ERC721Recipient is ERC721TokenReceiver { + address public operator; + address public from; + uint256 public id; + bytes public data; + + function onERC721Received( + address _operator, + address _from, + uint256 _id, + bytes calldata _data + ) public virtual override returns (bytes4) { + operator = _operator; + from = _from; + id = _id; + data = _data; + + return ERC721TokenReceiver.onERC721Received.selector; + } +} + +contract RevertingERC721Recipient is ERC721TokenReceiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) public virtual override returns (bytes4) { + revert(string(abi.encodePacked(ERC721TokenReceiver.onERC721Received.selector))); + } +} + +contract WrongReturnDataERC721Recipient is ERC721TokenReceiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) public virtual override returns (bytes4) { + return 0xCAFEBEEF; + } +} + +contract NonERC721Recipient {} + +contract ERC721Test is DSTestPlus { + MockERC721 token; + + function setUp() public { + token = new MockERC721("Token", "TKN"); + } + + function invariantMetadata() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + } + + function testMetadata() public { + assertEq(token.name(), "Token"); + assertEq(token.symbol(), "TKN"); + } + + function testMint() public { + token.mint(address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.ownerOf(1337), address(0xBEEF)); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1337); + token.burn(1337); + + assertEq(token.totalSupply(), 0); + assertEq(token.balanceOf(address(0xBEEF)), 0); + assertEq(token.ownerOf(1337), address(0)); + } + + function testApprove() public { + token.mint(address(this), 1337); + + token.approve(address(0xBEEF), 1337); + + assertEq(token.getApproved(1337), address(0xBEEF)); + } + + function testApproveAll() public { + token.setApprovalForAll(address(0xBEEF), true); + + assertTrue(token.isApprovedForAll(address(this), address(0xBEEF))); + } + + function testTransferFrom() public { + ERC721User from = new ERC721User(token); + + token.mint(address(from), 1337); + + from.approve(address(this), 1337); + + token.transferFrom(address(from), address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testTransferFromSelf() public { + token.mint(address(this), 1337); + + token.transferFrom(address(this), address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(this)), 0); + } + + function testTransferFromApproveAll() public { + ERC721User from = new ERC721User(token); + + token.mint(address(from), 1337); + + from.setApprovalForAll(address(this), true); + + token.transferFrom(address(from), address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testSafeTransferFromToEOA() public { + ERC721User from = new ERC721User(token); + + token.mint(address(from), 1337); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(0xBEEF)); + assertEq(token.balanceOf(address(0xBEEF)), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testSafeTransferFromToERC721Recipient() public { + ERC721User from = new ERC721User(token); + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(address(from), 1337); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(recipient), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(address(from)), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), address(from)); + assertEq(recipient.id(), 1337); + assertBytesEq(recipient.data(), ""); + } + + function testSafeTransferFromToERC721RecipientWithData() public { + ERC721User from = new ERC721User(token); + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(address(from), 1337); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(recipient), 1337, "testing 123"); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(1337), address(0)); + assertEq(token.ownerOf(1337), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(address(from)), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), address(from)); + assertEq(recipient.id(), 1337); + assertBytesEq(recipient.data(), "testing 123"); + } + + function testSafeMintToEOA() public { + token.safeMint(address(0xBEEF), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(1337), address(address(0xBEEF))); + assertEq(token.balanceOf(address(address(0xBEEF))), 1); + } + + function testSafeMintToERC721Recipient() public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), 1337); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(1337), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), 1337); + assertBytesEq(to.data(), ""); + } + + function testSafeMintToERC721RecipientWithData() public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), 1337, "testing 123"); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(1337), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), 1337); + assertBytesEq(to.data(), "testing 123"); + } + + function testFailMintToZero() public { + token.mint(address(0), 1337); + } + + function testFailDoubleMint() public { + token.mint(address(0xBEEF), 1337); + token.mint(address(0xBEEF), 1337); + } + + function testFailBurnUnMinted() public { + token.burn(1337); + } + + function testFailDoubleBurn() public { + token.mint(address(0xBEEF), 1337); + + token.burn(1337); + token.burn(1337); + } + + function testFailApproveUnMinted() public { + token.approve(address(0xBEEF), 1337); + } + + function testFailApproveUnAuthorized() public { + token.mint(address(0xCAFE), 1337); + + token.approve(address(0xBEEF), 1337); + } + + function testFailTransferFromUnOwned() public { + token.transferFrom(address(0xFEED), address(0xBEEF), 1337); + } + + function testFailTransferFromWrongFrom() public { + token.mint(address(0xCAFE), 1337); + + token.transferFrom(address(0xFEED), address(0xBEEF), 1337); + } + + function testFailTransferFromToZero() public { + token.mint(address(this), 1337); + + token.transferFrom(address(this), address(0), 1337); + } + + function testFailTransferFromNotOwner() public { + token.mint(address(0xFEED), 1337); + + token.transferFrom(address(0xFEED), address(0xBEEF), 1337); + } + + function testFailSafeTransferFromToNonERC721Recipient() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new NonERC721Recipient()), 1337); + } + + function testFailSafeTransferFromToNonERC721RecipientWithData() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new NonERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeTransferFromToRevertingERC721Recipient() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), 1337); + } + + function testFailSafeTransferFromToRevertingERC721RecipientWithData() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeTransferFromToERC721RecipientWithWrongReturnData() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), 1337); + } + + function testFailSafeTransferFromToERC721RecipientWithWrongReturnDataWithData() public { + token.mint(address(this), 1337); + + token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeMintToNonERC721Recipient() public { + token.safeMint(address(new NonERC721Recipient()), 1337); + } + + function testFailSafeMintToNonERC721RecipientWithData() public { + token.safeMint(address(new NonERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeMintToRevertingERC721Recipient() public { + token.safeMint(address(new RevertingERC721Recipient()), 1337); + } + + function testFailSafeMintToRevertingERC721RecipientWithData() public { + token.safeMint(address(new RevertingERC721Recipient()), 1337, "testing 123"); + } + + function testFailSafeMintToERC721RecipientWithWrongReturnData() public { + token.safeMint(address(new WrongReturnDataERC721Recipient()), 1337); + } + + function testFailSafeMintToERC721RecipientWithWrongReturnDataWithData() public { + token.safeMint(address(new WrongReturnDataERC721Recipient()), 1337, "testing 123"); + } + + function testMetadata(string memory name, string memory symbol) public { + MockERC721 tkn = new MockERC721(name, symbol); + + assertEq(tkn.name(), name); + assertEq(tkn.symbol(), symbol); + } + + function testMint(address to, uint256 id) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.balanceOf(to), 1); + assertEq(token.ownerOf(id), to); + } + + function testBurn(address to, uint256 id) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(to, id); + token.burn(id); + + assertEq(token.totalSupply(), 0); + assertEq(token.balanceOf(to), 0); + assertEq(token.ownerOf(id), address(0)); + } + + function testApprove(address to, uint256 id) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(address(this), id); + + token.approve(to, id); + + assertEq(token.getApproved(id), to); + } + + function testApproveAll(address to, bool approved) public { + token.setApprovalForAll(to, approved); + + assertBoolEq(token.isApprovedForAll(address(this), to), approved); + } + + function testTransferFrom(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + ERC721User from = new ERC721User(token); + + token.mint(address(from), id); + + from.approve(address(this), id); + + token.transferFrom(address(from), to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testTransferFromSelf(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(address(this), id); + + token.transferFrom(address(this), to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(address(this)), 0); + } + + function testTransferFromApproveAll(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + ERC721User from = new ERC721User(token); + + token.mint(address(from), id); + + from.setApprovalForAll(address(this), true); + + token.transferFrom(address(from), to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testSafeTransferFromToEOA(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + ERC721User from = new ERC721User(token); + + token.mint(address(from), id); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), to); + assertEq(token.balanceOf(to), 1); + assertEq(token.balanceOf(address(from)), 0); + } + + function testSafeTransferFromToERC721Recipient(uint256 id) public { + ERC721User from = new ERC721User(token); + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(address(from), id); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(recipient), id); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(address(from)), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), address(from)); + assertEq(recipient.id(), id); + assertBytesEq(recipient.data(), ""); + } + + function testSafeTransferFromToERC721RecipientWithData(uint256 id, bytes calldata data) public { + ERC721User from = new ERC721User(token); + ERC721Recipient recipient = new ERC721Recipient(); + + token.mint(address(from), id); + + from.setApprovalForAll(address(this), true); + + token.safeTransferFrom(address(from), address(recipient), id, data); + + assertEq(token.totalSupply(), 1); + assertEq(token.getApproved(id), address(0)); + assertEq(token.ownerOf(id), address(recipient)); + assertEq(token.balanceOf(address(recipient)), 1); + assertEq(token.balanceOf(address(from)), 0); + + assertEq(recipient.operator(), address(this)); + assertEq(recipient.from(), address(from)); + assertEq(recipient.id(), id); + assertBytesEq(recipient.data(), data); + } + + function testSafeMintToEOA(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + if (uint256(uint160(to)) <= 18 || to.code.length > 0) return; + + token.safeMint(to, id); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(id), address(to)); + assertEq(token.balanceOf(address(to)), 1); + } + + function testSafeMintToERC721Recipient(uint256 id) public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), id); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(id), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), id); + assertBytesEq(to.data(), ""); + } + + function testSafeMintToERC721RecipientWithData(uint256 id, bytes calldata data) public { + ERC721Recipient to = new ERC721Recipient(); + + token.safeMint(address(to), id, data); + + assertEq(token.totalSupply(), 1); + assertEq(token.ownerOf(id), address(to)); + assertEq(token.balanceOf(address(to)), 1); + + assertEq(to.operator(), address(this)); + assertEq(to.from(), address(0)); + assertEq(to.id(), id); + assertBytesEq(to.data(), data); + } + + function testFailMintToZero(uint256 id) public { + token.mint(address(0), id); + } + + function testFailDoubleMint(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(to, id); + token.mint(to, id); + } + + function testFailBurnUnMinted(uint256 id) public { + token.burn(id); + } + + function testFailDoubleBurn(uint256 id, address to) public { + if (to == address(0)) to = address(0xBEEF); + + token.mint(to, id); + + token.burn(id); + token.burn(id); + } + + function testFailApproveUnMinted(uint256 id, address to) public { + token.approve(to, id); + } + + function testFailApproveUnAuthorized( + address owner, + uint256 id, + address to + ) public { + if (owner == address(0)) to = address(0xBEEF); + if (owner == address(this)) return; + + token.mint(owner, id); + + token.approve(to, id); + } + + function testFailTransferFromUnOwned( + address from, + address to, + uint256 id + ) public { + token.transferFrom(from, to, id); + } + + function testFailTransferFromWrongFrom( + address owner, + address from, + address to, + uint256 id + ) public { + if (owner == address(0)) to = address(0xBEEF); + if (from == owner) revert(); + + token.mint(owner, id); + + token.transferFrom(from, to, id); + } + + function testFailTransferFromToZero(uint256 id) public { + token.mint(address(this), id); + + token.transferFrom(address(this), address(0), id); + } + + function testFailTransferFromNotOwner( + address from, + address to, + uint256 id + ) public { + if (from == address(0)) to = address(0xBEEF); + + token.mint(from, id); + + token.transferFrom(from, to, id); + } + + function testFailSafeTransferFromToNonERC721Recipient(uint256 id) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new NonERC721Recipient()), id); + } + + function testFailSafeTransferFromToNonERC721RecipientWithData(uint256 id, bytes calldata data) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new NonERC721Recipient()), id, data); + } + + function testFailSafeTransferFromToRevertingERC721Recipient(uint256 id) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), id); + } + + function testFailSafeTransferFromToRevertingERC721RecipientWithData(uint256 id, bytes calldata data) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new RevertingERC721Recipient()), id, data); + } + + function testFailSafeTransferFromToERC721RecipientWithWrongReturnData(uint256 id) public { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), id); + } + + function testFailSafeTransferFromToERC721RecipientWithWrongReturnDataWithData(uint256 id, bytes calldata data) + public + { + token.mint(address(this), id); + + token.safeTransferFrom(address(this), address(new WrongReturnDataERC721Recipient()), id, data); + } + + function testFailSafeMintToNonERC721Recipient(uint256 id) public { + token.safeMint(address(new NonERC721Recipient()), id); + } + + function testFailSafeMintToNonERC721RecipientWithData(uint256 id, bytes calldata data) public { + token.safeMint(address(new NonERC721Recipient()), id, data); + } + + function testFailSafeMintToRevertingERC721Recipient(uint256 id) public { + token.safeMint(address(new RevertingERC721Recipient()), id); + } + + function testFailSafeMintToRevertingERC721RecipientWithData(uint256 id, bytes calldata data) public { + token.safeMint(address(new RevertingERC721Recipient()), id, data); + } + + function testFailSafeMintToERC721RecipientWithWrongReturnData(uint256 id) public { + token.safeMint(address(new WrongReturnDataERC721Recipient()), id); + } + + function testFailSafeMintToERC721RecipientWithWrongReturnDataWithData(uint256 id, bytes calldata data) public { + token.safeMint(address(new WrongReturnDataERC721Recipient()), id, data); + } +} + +contract ERC721Invariants is DSTestPlus, DSInvariantTest { + BalanceSum balanceSum; + MockERC721 token; + + function setUp() public { + token = new MockERC721("Token", "TKN"); + balanceSum = new BalanceSum(token); + + addTargetContract(address(balanceSum)); + } + + function invariantBalanceSum() public { + assertEq(token.totalSupply(), balanceSum.sum()); + } +} + +contract BalanceSum { + MockERC721 token; + uint256 public sum; + + constructor(MockERC721 _token) { + token = _token; + } + + function mint(address from, uint256 id) public { + token.mint(from, id); + sum++; + } + + function burn(uint256 id) public { + token.burn(id); + sum--; + } + + function approve(address to, uint256 amount) public { + token.approve(to, amount); + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public { + token.transferFrom(from, to, amount); + } +} diff --git a/src/test/FixedPointMathLib.t.sol b/src/test/FixedPointMathLib.t.sol index 16485649..da53e67a 100644 --- a/src/test/FixedPointMathLib.t.sol +++ b/src/test/FixedPointMathLib.t.sol @@ -49,33 +49,24 @@ contract FixedPointMathLibTest is DSTestPlus { } function testSqrt() public { + assertEq(FixedPointMathLib.sqrt(0), 0); + assertEq(FixedPointMathLib.sqrt(1), 1); assertEq(FixedPointMathLib.sqrt(2704), 52); assertEq(FixedPointMathLib.sqrt(110889), 333); assertEq(FixedPointMathLib.sqrt(32239684), 5678); } - function testMin() public { - assertEq(FixedPointMathLib.min(4, 100), 4); - assertEq(FixedPointMathLib.min(500, 400), 400); - assertEq(FixedPointMathLib.min(10000, 10001), 10000); - assertEq(FixedPointMathLib.min(1e18, 0.1e18), 0.1e18); - } - - function testMax() public { - assertEq(FixedPointMathLib.max(4, 100), 100); - assertEq(FixedPointMathLib.max(500, 400), 500); - assertEq(FixedPointMathLib.max(10000, 10001), 10001); - assertEq(FixedPointMathLib.max(1e18, 0.1e18), 1e18); - } - function testFMul( uint256 x, uint256 y, uint256 baseUnit ) public { - // Ignore cases where x * y overflows. + // Convert cases where x * y overflows into useful test cases. unchecked { - if (x != 0 && (x * y) / x != y) return; + while (x != 0 && (x * y) / x != y) { + x /= 2; + y /= 2; + } } assertEq(FixedPointMathLib.fmul(x, y, baseUnit), baseUnit == 0 ? 0 : (x * y) / baseUnit); @@ -99,16 +90,13 @@ contract FixedPointMathLibTest is DSTestPlus { uint256 y, uint256 baseUnit ) public { + if (y == 0) y = 1; + // Ignore cases where x * baseUnit overflows. unchecked { if (x != 0 && (x * baseUnit) / x != baseUnit) return; } - // Ignore cases where y is zero because it will cause a revert. - if (y == 0) { - return; - } - assertEq(FixedPointMathLib.fdiv(x, y, baseUnit), (x * baseUnit) / y); } @@ -140,20 +128,4 @@ contract FixedPointMathLibTest is DSTestPlus { assertTrue(root * root <= x && next * next > x); } - - function testMin(uint256 x, uint256 y) public { - if (x < y) { - assertEq(FixedPointMathLib.min(x, y), x); - } else { - assertEq(FixedPointMathLib.min(x, y), y); - } - } - - function testMax(uint256 x, uint256 y) public { - if (x > y) { - assertEq(FixedPointMathLib.max(x, y), x); - } else { - assertEq(FixedPointMathLib.max(x, y), y); - } - } } diff --git a/src/test/MultiRolesAuthority.t.sol b/src/test/MultiRolesAuthority.t.sol new file mode 100644 index 00000000..85308875 --- /dev/null +++ b/src/test/MultiRolesAuthority.t.sol @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.10; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {MockAuthority} from "./utils/mocks/MockAuthority.sol"; + +import {Authority} from "../auth/Auth.sol"; + +import {MultiRolesAuthority} from "../auth/authorities/MultiRolesAuthority.sol"; + +contract MultiRolesAuthorityTest is DSTestPlus { + MultiRolesAuthority multiRolesAuthority; + + function setUp() public { + multiRolesAuthority = new MultiRolesAuthority(address(this), Authority(address(0))); + } + + function testSetRoles() public { + assertFalse(multiRolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertTrue(multiRolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(multiRolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); + } + + function testSetRoleCapabilities() public { + assertFalse(multiRolesAuthority.doesRoleHaveCapability(0, 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.doesRoleHaveCapability(0, 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.doesRoleHaveCapability(0, 0xBEEFCAFE)); + } + + function testSetPublicCapabilities() public { + assertFalse(multiRolesAuthority.isCapabilityPublic(0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.isCapabilityPublic(0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.isCapabilityPublic(0xBEEFCAFE)); + } + + function testSetTargetCustomAuthority() public { + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(address(0xBEEF))), address(0)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xBEEF), Authority(address(0xCAFE))); + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(address(0xBEEF))), address(0xCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xBEEF), Authority(address(0))); + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(address(0xBEEF))), address(0)); + } + + function testCanCallWithAuthorizedRole() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallPublicCapability() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallWithCustomAuthority() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallWithCustomAuthorityOverridesPublicCapability() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, false); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setPublicCapability(0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallWithCustomAuthorityOverridesUserWithRole() public { + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setTargetCustomAuthority(address(0xCAFE), Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertTrue(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setRoleCapability(0, 0xBEEFCAFE, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + multiRolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(multiRolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + } + + function testSetRoles(address user, uint8 role) public { + assertFalse(multiRolesAuthority.doesUserHaveRole(user, role)); + + multiRolesAuthority.setUserRole(user, role, true); + assertTrue(multiRolesAuthority.doesUserHaveRole(user, role)); + + multiRolesAuthority.setUserRole(user, role, false); + assertFalse(multiRolesAuthority.doesUserHaveRole(user, role)); + } + + function testSetRoleCapabilities(uint8 role, bytes4 functionSig) public { + assertFalse(multiRolesAuthority.doesRoleHaveCapability(role, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, true); + assertTrue(multiRolesAuthority.doesRoleHaveCapability(role, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, false); + assertFalse(multiRolesAuthority.doesRoleHaveCapability(role, functionSig)); + } + + function testSetPublicCapabilities(bytes4 functionSig) public { + assertFalse(multiRolesAuthority.isCapabilityPublic(functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, true); + assertTrue(multiRolesAuthority.isCapabilityPublic(functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, false); + assertFalse(multiRolesAuthority.isCapabilityPublic(functionSig)); + } + + function testSetTargetCustomAuthority(address user, Authority customAuthority) public { + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(user)), address(0)); + + multiRolesAuthority.setTargetCustomAuthority(user, customAuthority); + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(user)), address(customAuthority)); + + multiRolesAuthority.setTargetCustomAuthority(user, Authority(address(0))); + assertEq(address(multiRolesAuthority.getTargetCustomAuthority(user)), address(0)); + } + + function testCanCallWithAuthorizedRole( + address user, + uint8 role, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, true); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + } + + function testCanCallPublicCapability( + address user, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + } + + function testCanCallWithCustomAuthority( + address user, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + } + + function testCanCallWithCustomAuthorityOverridesPublicCapability( + address user, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, false); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setPublicCapability(functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + } + + function testCanCallWithCustomAuthorityOverridesUserWithRole( + address user, + uint8 role, + address target, + bytes4 functionSig + ) public { + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, true); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(false)); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, new MockAuthority(true)); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, false); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setTargetCustomAuthority(target, Authority(address(0))); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, true); + assertTrue(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setRoleCapability(role, functionSig, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + + multiRolesAuthority.setUserRole(user, role, false); + assertFalse(multiRolesAuthority.canCall(user, target, functionSig)); + } +} diff --git a/src/test/RolesAuthority.t.sol b/src/test/RolesAuthority.t.sol index 2ff335dc..88c43fcc 100644 --- a/src/test/RolesAuthority.t.sol +++ b/src/test/RolesAuthority.t.sol @@ -2,97 +2,147 @@ pragma solidity 0.8.10; import {DSTestPlus} from "./utils/DSTestPlus.sol"; -import {MockAuthChild} from "./utils/mocks/MockAuthChild.sol"; +import {MockAuthority} from "./utils/mocks/MockAuthority.sol"; + +import {Authority} from "../auth/Auth.sol"; -import {Auth, Authority} from "../auth/Auth.sol"; import {RolesAuthority} from "../auth/authorities/RolesAuthority.sol"; contract RolesAuthorityTest is DSTestPlus { - RolesAuthority roles; - MockAuthChild mockAuthChild; + RolesAuthority rolesAuthority; function setUp() public { - roles = new RolesAuthority(address(this), Authority(address(0))); - mockAuthChild = new MockAuthChild(); + rolesAuthority = new RolesAuthority(address(this), Authority(address(0))); + } + + function testSetRoles() public { + assertFalse(rolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); - mockAuthChild.setAuthority(roles); - mockAuthChild.setOwner(DEAD_ADDRESS); + rolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertTrue(rolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); + + rolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(rolesAuthority.doesUserHaveRole(address(0xBEEF), 0)); } - function invariantOwner() public { - assertEq(roles.owner(), address(this)); - assertEq(mockAuthChild.owner(), DEAD_ADDRESS); + function testSetRoleCapabilities() public { + assertFalse(rolesAuthority.doesRoleHaveCapability(0, address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.doesRoleHaveCapability(0, address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, false); + assertFalse(rolesAuthority.doesRoleHaveCapability(0, address(0xCAFE), 0xBEEFCAFE)); } - function invariantAuthority() public { - assertEq(address(roles.authority()), address(0)); - assertEq(address(mockAuthChild.authority()), address(roles)); + function testSetPublicCapabilities() public { + assertFalse(rolesAuthority.isCapabilityPublic(address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setPublicCapability(address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.isCapabilityPublic(address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setPublicCapability(address(0xCAFE), 0xBEEFCAFE, false); + assertFalse(rolesAuthority.isCapabilityPublic(address(0xCAFE), 0xBEEFCAFE)); + } + + function testCanCallWithAuthorizedRole() public { + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setUserRole(address(0xBEEF), 0, true); + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, false); + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setRoleCapability(0, address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setUserRole(address(0xBEEF), 0, false); + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); } - function testSanityChecks() public { - assertEq(roles.getUserRoles(address(this)), bytes32(0)); - assertFalse(roles.isUserRoot(address(this))); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + function testCanCallPublicCapability() public { + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); + + rolesAuthority.setPublicCapability(address(0xCAFE), 0xBEEFCAFE, true); + assertTrue(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); - try mockAuthChild.updateFlag() { - fail("Trust Authority Allowed Attacker To Update Flag"); - } catch {} + rolesAuthority.setPublicCapability(address(0xCAFE), 0xBEEFCAFE, false); + assertFalse(rolesAuthority.canCall(address(0xBEEF), address(0xCAFE), 0xBEEFCAFE)); } - function testBasics() public { - uint8 rootRole = 0; - uint8 adminRole = 1; - uint8 modRole = 2; - uint8 userRole = 3; + function testSetRoles(address user, uint8 role) public { + assertFalse(rolesAuthority.doesUserHaveRole(user, role)); - roles.setUserRole(address(this), rootRole, true); - roles.setUserRole(address(this), adminRole, true); + rolesAuthority.setUserRole(user, role, true); + assertTrue(rolesAuthority.doesUserHaveRole(user, role)); - assertEq32( - 0x0000000000000000000000000000000000000000000000000000000000000003, - roles.getUserRoles(address(this)) - ); + rolesAuthority.setUserRole(user, role, false); + assertFalse(rolesAuthority.doesUserHaveRole(user, role)); + } - roles.setRoleCapability(adminRole, address(mockAuthChild), MockAuthChild.updateFlag.selector, true); - assertTrue(roles.doesRoleHaveCapability(adminRole, address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertTrue(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + function testSetRoleCapabilities( + uint8 role, + address target, + bytes4 functionSig + ) public { + assertFalse(rolesAuthority.doesRoleHaveCapability(role, target, functionSig)); - mockAuthChild.updateFlag(); + rolesAuthority.setRoleCapability(role, target, functionSig, true); + assertTrue(rolesAuthority.doesRoleHaveCapability(role, target, functionSig)); - roles.setRoleCapability(adminRole, address(mockAuthChild), MockAuthChild.updateFlag.selector, false); - assertFalse(roles.doesRoleHaveCapability(adminRole, address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setRoleCapability(role, target, functionSig, false); + assertFalse(rolesAuthority.doesRoleHaveCapability(role, target, functionSig)); + } + + function testSetPublicCapabilities(address target, bytes4 functionSig) public { + assertFalse(rolesAuthority.isCapabilityPublic(target, functionSig)); - assertTrue(roles.doesUserHaveRole(address(this), rootRole)); - assertTrue(roles.doesUserHaveRole(address(this), adminRole)); + rolesAuthority.setPublicCapability(target, functionSig, true); + assertTrue(rolesAuthority.isCapabilityPublic(target, functionSig)); - assertFalse(roles.doesUserHaveRole(address(this), modRole)); - assertFalse(roles.doesUserHaveRole(address(this), userRole)); + rolesAuthority.setPublicCapability(target, functionSig, false); + assertFalse(rolesAuthority.isCapabilityPublic(target, functionSig)); } - function testRoot() public { - assertFalse(roles.isUserRoot(address(this))); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + function testCanCallWithAuthorizedRole( + address user, + uint8 role, + address target, + bytes4 functionSig + ) public { + assertFalse(rolesAuthority.canCall(user, target, functionSig)); + + rolesAuthority.setUserRole(user, role, true); + assertFalse(rolesAuthority.canCall(user, target, functionSig)); + + rolesAuthority.setRoleCapability(role, target, functionSig, true); + assertTrue(rolesAuthority.canCall(user, target, functionSig)); + + rolesAuthority.setRoleCapability(role, target, functionSig, false); + assertFalse(rolesAuthority.canCall(user, target, functionSig)); - roles.setRootUser(address(this), true); - assertTrue(roles.isUserRoot(address(this))); - assertTrue(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setRoleCapability(role, target, functionSig, true); + assertTrue(rolesAuthority.canCall(user, target, functionSig)); - roles.setRootUser(address(this), false); - assertFalse(roles.isUserRoot(address(this))); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setUserRole(user, role, false); + assertFalse(rolesAuthority.canCall(user, target, functionSig)); } - function testPublicCapabilities() public { - assertFalse(roles.isCapabilityPublic(address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + function testCanCallPublicCapability( + address user, + address target, + bytes4 functionSig + ) public { + assertFalse(rolesAuthority.canCall(user, target, functionSig)); - roles.setPublicCapability(address(mockAuthChild), MockAuthChild.updateFlag.selector, true); - assertTrue(roles.isCapabilityPublic(address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertTrue(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setPublicCapability(target, functionSig, true); + assertTrue(rolesAuthority.canCall(user, target, functionSig)); - roles.setPublicCapability(address(mockAuthChild), MockAuthChild.updateFlag.selector, false); - assertFalse(roles.isCapabilityPublic(address(mockAuthChild), MockAuthChild.updateFlag.selector)); - assertFalse(roles.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); + rolesAuthority.setPublicCapability(target, functionSig, false); + assertFalse(rolesAuthority.canCall(user, target, functionSig)); } } diff --git a/src/test/SSTORE2.t.sol b/src/test/SSTORE2.t.sol index ce454d07..8b0cb496 100644 --- a/src/test/SSTORE2.t.sol +++ b/src/test/SSTORE2.t.sol @@ -67,7 +67,7 @@ contract SSTORE2Test is DSTestPlus { function testWriteReadCustomStartBound(bytes calldata testBytes, uint256 startIndex) public { if (testBytes.length == 0) return; - startIndex %= testBytes.length; + startIndex = bound(startIndex, 0, testBytes.length); assertBytesEq(SSTORE2.read(SSTORE2.write(testBytes), startIndex), bytes(testBytes[startIndex:])); } @@ -79,8 +79,8 @@ contract SSTORE2Test is DSTestPlus { ) public { if (testBytes.length == 0) return; - startIndex %= testBytes.length; - endIndex %= testBytes.length; + endIndex = bound(endIndex, 0, testBytes.length); + startIndex = bound(startIndex, 0, testBytes.length); if (startIndex > endIndex) return; @@ -107,7 +107,7 @@ contract SSTORE2Test is DSTestPlus { } function testFailWriteReadCustomStartBoundOutOfRange(bytes calldata testBytes, uint256 startIndex) public { - if (testBytes.length >= startIndex) revert(); + startIndex = bound(startIndex, testBytes.length + 1, type(uint256).max); SSTORE2.read(SSTORE2.write(testBytes), startIndex); } @@ -117,7 +117,7 @@ contract SSTORE2Test is DSTestPlus { uint256 startIndex, uint256 endIndex ) public { - if (endIndex >= startIndex) revert(); + endIndex = bound(endIndex, startIndex + 1, type(uint256).max); SSTORE2.read(SSTORE2.write(testBytes), startIndex, endIndex); } diff --git a/src/test/SafeCastLib.t.sol b/src/test/SafeCastLib.t.sol index b6f78591..ccce1d32 100644 --- a/src/test/SafeCastLib.t.sol +++ b/src/test/SafeCastLib.t.sol @@ -52,61 +52,61 @@ contract SafeCastLibTest is DSTestPlus { } function testSafeCastTo248(uint256 x) public { - x %= type(uint248).max; + x = bound(x, 0, type(uint248).max); assertEq(SafeCastLib.safeCastTo248(x), x); } function testSafeCastTo128(uint256 x) public { - x %= type(uint128).max; + x = bound(x, 0, type(uint128).max); assertEq(SafeCastLib.safeCastTo128(x), x); } function testSafeCastTo96(uint256 x) public { - x %= type(uint96).max; + x = bound(x, 0, type(uint96).max); assertEq(SafeCastLib.safeCastTo96(x), x); } function testSafeCastTo64(uint256 x) public { - x %= type(uint64).max; + x = bound(x, 0, type(uint64).max); assertEq(SafeCastLib.safeCastTo64(x), x); } function testSafeCastTo32(uint256 x) public { - x %= type(uint32).max; + x = bound(x, 0, type(uint32).max); assertEq(SafeCastLib.safeCastTo32(x), x); } function testFailSafeCastTo248(uint256 x) public pure { - if (type(uint248).max > x) revert(); + x = bound(x, type(uint248).max + 1, type(uint256).max); SafeCastLib.safeCastTo248(x); } function testFailSafeCastTo128(uint256 x) public pure { - if (type(uint128).max > x) revert(); + x = bound(x, type(uint128).max + 1, type(uint256).max); SafeCastLib.safeCastTo128(x); } function testFailSafeCastTo96(uint256 x) public pure { - if (type(uint96).max > x) revert(); + x = bound(x, type(uint96).max + 1, type(uint256).max); SafeCastLib.safeCastTo96(x); } function testFailSafeCastTo64(uint256 x) public pure { - if (type(uint64).max > x) revert(); + x = bound(x, type(uint64).max + 1, type(uint256).max); SafeCastLib.safeCastTo64(x); } function testFailSafeCastTo32(uint256 x) public pure { - if (type(uint32).max > x) revert(); + x = bound(x, type(uint32).max + 1, type(uint256).max); SafeCastLib.safeCastTo32(x); } diff --git a/src/test/SafeTransferLib.t.sol b/src/test/SafeTransferLib.t.sol index d691044c..1e570b2f 100644 --- a/src/test/SafeTransferLib.t.sol +++ b/src/test/SafeTransferLib.t.sol @@ -127,9 +127,7 @@ contract SafeTransferLibTest is DSTestPlus { address to, uint256 amount ) public { - if (nonContract.code.length > 0) return; - - if (uint256(uint160(nonContract)) <= 18) return; // Some precompiles cause reverts. + if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeTransfer(SolmateERC20(nonContract), to, amount); } @@ -164,9 +162,7 @@ contract SafeTransferLibTest is DSTestPlus { address to, uint256 amount ) public { - if (nonContract.code.length > 0) return; - - if (uint256(uint160(nonContract)) <= 18) return; // Some precompiles cause reverts. + if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeTransferFrom(SolmateERC20(nonContract), from, to, amount); } @@ -188,17 +184,15 @@ contract SafeTransferLibTest is DSTestPlus { address to, uint256 amount ) public { - if (nonContract.code.length > 0) return; - - if (uint256(uint160(nonContract)) <= 18) return; // Some precompiles cause reverts. + if (uint256(uint160(nonContract)) <= 18 || nonContract.code.length > 0) return; SafeTransferLib.safeApprove(SolmateERC20(nonContract), to, amount); } function testTransferETH(address recipient, uint256 amount) public { - if (uint256(uint160(recipient)) <= 18) return; // Some precompiles cause reverts. + if (uint256(uint160(recipient)) <= 18) return; - amount %= address(this).balance; + amount = bound(amount, 0, address(this).balance); SafeTransferLib.safeTransferETH(recipient, amount); } diff --git a/src/test/Trust.t.sol b/src/test/Trust.t.sol deleted file mode 100644 index 76048864..00000000 --- a/src/test/Trust.t.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.10; - -import {DSTestPlus} from "./utils/DSTestPlus.sol"; -import {MockTrustChild} from "./utils/mocks/MockTrustChild.sol"; - -contract TrustTest is DSTestPlus { - MockTrustChild mockTrustChild; - - function setUp() public { - mockTrustChild = new MockTrustChild(); - - mockTrustChild.setIsTrusted(address(this), false); - } - - function testFailTrustNotTrusted(address usr) public { - mockTrustChild.setIsTrusted(usr, true); - } - - function testFailDistrustNotTrusted(address usr) public { - mockTrustChild.setIsTrusted(usr, false); - } - - function testTrust(address usr) public { - if (usr == address(this)) return; - forceTrust(address(this)); - - assertFalse(mockTrustChild.isTrusted(usr)); - mockTrustChild.setIsTrusted(usr, true); - assertTrue(mockTrustChild.isTrusted(usr)); - } - - function testDistrust(address usr) public { - if (usr == address(this)) return; - forceTrust(address(this)); - forceTrust(usr); - - assertTrue(mockTrustChild.isTrusted(usr)); - mockTrustChild.setIsTrusted(usr, false); - assertFalse(mockTrustChild.isTrusted(usr)); - } - - function forceTrust(address usr) internal { - hevm.store(address(mockTrustChild), keccak256(abi.encode(usr, uint256(0))), bytes32(uint256(1))); - } -} diff --git a/src/test/TrustAuthority.t.sol b/src/test/TrustAuthority.t.sol deleted file mode 100644 index 1bf54c21..00000000 --- a/src/test/TrustAuthority.t.sol +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity 0.8.10; - -import {DSTestPlus} from "./utils/DSTestPlus.sol"; -import {MockAuthChild} from "./utils/mocks/MockAuthChild.sol"; - -import {TrustAuthority} from "../auth/authorities/TrustAuthority.sol"; - -contract TrustAuthorityTest is DSTestPlus { - TrustAuthority trust; - MockAuthChild mockAuthChild; - - function setUp() public { - trust = new TrustAuthority(address(this)); - mockAuthChild = new MockAuthChild(); - - mockAuthChild.setAuthority(trust); - mockAuthChild.setOwner(DEAD_ADDRESS); - - trust.setIsTrusted(address(this), false); - } - - function invariantOwner() public { - assertEq(mockAuthChild.owner(), DEAD_ADDRESS); - } - - function invariantAuthority() public { - assertEq(address(mockAuthChild.authority()), address(trust)); - } - - function testSanityChecks() public { - assertFalse(trust.isTrusted(address(this))); - assertFalse(trust.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); - try mockAuthChild.updateFlag() { - fail("Trust Authority Let Attacker Update Flag"); - } catch {} - } - - function testUpdateTrust() public { - forceTrust(address(this)); - assertTrue(trust.isTrusted(address(this))); - assertTrue(trust.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); - mockAuthChild.updateFlag(); - - trust.setIsTrusted(address(this), false); - assertFalse(trust.isTrusted(address(this))); - assertFalse(trust.canCall(address(this), address(mockAuthChild), MockAuthChild.updateFlag.selector)); - try mockAuthChild.updateFlag() { - fail("Trust Authority Allowed Attacker To Update Flag"); - } catch {} - } - - function forceTrust(address usr) internal { - hevm.store(address(trust), keccak256(abi.encode(usr, uint256(0))), bytes32(uint256(1))); - } -} diff --git a/src/test/WETH.t.sol b/src/test/WETH.t.sol index 36bfa92b..f2d8b9e1 100644 --- a/src/test/WETH.t.sol +++ b/src/test/WETH.t.sol @@ -64,7 +64,7 @@ contract WETHTest is DSTestPlus { } function testDeposit(uint256 amount) public { - if (amount > address(this).balance) return; + amount = bound(amount, 0, address(this).balance); assertEq(weth.balanceOf(address(this)), 0); assertEq(weth.totalSupply(), 0); @@ -76,7 +76,7 @@ contract WETHTest is DSTestPlus { } function testFallbackDeposit(uint256 amount) public { - if (amount > address(this).balance) return; + amount = bound(amount, 0, address(this).balance); assertEq(weth.balanceOf(address(this)), 0); assertEq(weth.totalSupply(), 0); @@ -88,8 +88,8 @@ contract WETHTest is DSTestPlus { } function testWithdraw(uint256 depositAmount, uint256 withdrawAmount) public { - if (depositAmount > address(this).balance) return; - if (withdrawAmount > depositAmount) return; + depositAmount = bound(depositAmount, 0, address(this).balance); + withdrawAmount = bound(withdrawAmount, 0, depositAmount); weth.deposit{value: depositAmount}(); diff --git a/src/test/utils/DSInvariantTest.sol b/src/test/utils/DSInvariantTest.sol index e42aa5b5..820775c5 100644 --- a/src/test/utils/DSInvariantTest.sol +++ b/src/test/utils/DSInvariantTest.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; contract DSInvariantTest { address[] private targets; diff --git a/src/test/utils/DSTestPlus.sol b/src/test/utils/DSTestPlus.sol index 82dd53ee..b332b7ab 100644 --- a/src/test/utils/DSTestPlus.sol +++ b/src/test/utils/DSTestPlus.sol @@ -1,10 +1,12 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; import {DSTest} from "ds-test/test.sol"; import {Hevm} from "./Hevm.sol"; +/// @notice Extended testing framework for DappTools projects. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/test/utils/DSTestPlus.sol) contract DSTestPlus is DSTest { Hevm internal constant hevm = Hevm(HEVM_ADDRESS); @@ -51,6 +53,47 @@ contract DSTestPlus is DSTest { assertEq(uint256(a), uint256(b)); } + function assertBoolEq(bool a, bool b) internal virtual { + b ? assertTrue(a) : assertFalse(a); + } + + function assertApproxEq( + uint256 a, + uint256 b, + uint256 maxDelta + ) internal virtual { + uint256 delta = a > b ? a - b : b - a; + + if (delta > maxDelta) { + emit log("Error: a ~= b not satisfied [uint]"); + emit log_named_uint(" Expected", a); + emit log_named_uint(" Actual", b); + emit log_named_uint(" Max Delta", maxDelta); + emit log_named_uint(" Delta", delta); + fail(); + } + } + + function assertRelApproxEq( + uint256 a, + uint256 b, + uint256 maxPercentDelta + ) internal virtual { + uint256 delta = a > b ? a - b : b - a; + uint256 abs = a > b ? a : b; + + uint256 percentDelta = (delta * 1e18) / abs; + + if (percentDelta > maxPercentDelta) { + emit log("Error: a ~= b not satisfied [uint]"); + emit log_named_uint(" Expected", a); + emit log_named_uint(" Actual", b); + emit log_named_uint(" Max % Delta", maxPercentDelta); + emit log_named_uint(" % Delta", percentDelta); + fail(); + } + } + function assertBytesEq(bytes memory a, bytes memory b) internal virtual { if (keccak256(a) != keccak256(b)) { emit log("Error: a == b not satisfied [bytes]"); @@ -59,4 +102,45 @@ contract DSTestPlus is DSTest { fail(); } } + + function assertUintArrayEq(uint256[] memory a, uint256[] memory b) internal virtual { + require(a.length == b.length, "LENGTH_MISMATCH"); + + for (uint256 i = 0; i < a.length; i++) { + assertEq(a[i], b[i]); + } + } + + function bound( + uint256 x, + uint256 min, + uint256 max + ) internal pure returns (uint256 result) { + require(max >= min, "MAX_LESS_THAN_MIN"); + + uint256 size = max - min; + + if (max != type(uint256).max) size++; // Make the max inclusive. + if (size == 0) return min; // Using max would be equivalent as well. + // Ensure max is inclusive in cases where x != 0 and max is at uint max. + if (max == type(uint256).max && x != 0) x--; // Accounted for later. + + if (x < min) x += size * (((min - x) / size) + 1); + result = min + ((x - min) % size); + + // Account for decrementing x to make max inclusive. + if (max == type(uint256).max && x != 0) result++; + } + + function min3( + uint256 a, + uint256 b, + uint256 c + ) internal pure returns (uint256) { + return a > b ? (b > c ? c : b) : (a > c ? c : a); + } + + function min2(uint256 a, uint256 b) internal pure returns (uint256) { + return a > b ? b : a; + } } diff --git a/src/test/utils/Hevm.sol b/src/test/utils/Hevm.sol index 7e243947..ba18c2c0 100644 --- a/src/test/utils/Hevm.sol +++ b/src/test/utils/Hevm.sol @@ -1,6 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; -pragma abicoder v2; +pragma solidity >=0.8.0; interface Hevm { function warp(uint256) external; diff --git a/src/test/utils/mocks/MockAuthChild.sol b/src/test/utils/mocks/MockAuthChild.sol index 8332fe78..d2c32760 100644 --- a/src/test/utils/mocks/MockAuthChild.sol +++ b/src/test/utils/mocks/MockAuthChild.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; import {Auth, Authority} from "../../../auth/Auth.sol"; diff --git a/src/test/utils/mocks/MockAuthority.sol b/src/test/utils/mocks/MockAuthority.sol new file mode 100644 index 00000000..acb36892 --- /dev/null +++ b/src/test/utils/mocks/MockAuthority.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {Authority} from "../../../auth/Auth.sol"; + +contract MockAuthority is Authority { + bool immutable allowCalls; + + constructor(bool _allowCalls) { + allowCalls = _allowCalls; + } + + function canCall( + address, + address, + bytes4 + ) public view override returns (bool) { + return allowCalls; + } +} diff --git a/src/test/utils/mocks/MockERC1155.sol b/src/test/utils/mocks/MockERC1155.sol new file mode 100644 index 00000000..ede086db --- /dev/null +++ b/src/test/utils/mocks/MockERC1155.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC1155} from "../../../tokens/ERC1155.sol"; + +contract MockERC1155 is ERC1155 { + function uri(uint256) public pure virtual override returns (string memory) {} + + function mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual { + _mint(to, id, amount, data); + } + + function batchMint( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual { + _batchMint(to, ids, amounts, data); + } + + function burn( + address from, + uint256 id, + uint256 amount + ) public virtual { + _burn(from, id, amount); + } + + function batchBurn( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) public virtual { + _batchBurn(from, ids, amounts); + } +} diff --git a/src/test/utils/mocks/MockERC721.sol b/src/test/utils/mocks/MockERC721.sol new file mode 100644 index 00000000..51227c0e --- /dev/null +++ b/src/test/utils/mocks/MockERC721.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC721} from "../../../tokens/ERC721.sol"; + +contract MockERC721 is ERC721 { + constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} + + function tokenURI(uint256) public pure virtual override returns (string memory) {} + + function mint(address to, uint256 tokenId) public virtual { + _mint(to, tokenId); + } + + function burn(uint256 tokenId) public virtual { + _burn(tokenId); + } + + function safeMint(address to, uint256 tokenId) public virtual { + _safeMint(to, tokenId); + } + + function safeMint( + address to, + uint256 tokenId, + bytes memory data + ) public virtual { + _safeMint(to, tokenId, data); + } +} diff --git a/src/test/utils/mocks/MockTrustChild.sol b/src/test/utils/mocks/MockTrustChild.sol deleted file mode 100644 index ad2c2922..00000000 --- a/src/test/utils/mocks/MockTrustChild.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; - -import {Trust} from "../../../auth/Trust.sol"; - -contract MockTrustChild is Trust(msg.sender) { - bool public flag; - - function updateFlag() public virtual requiresTrust { - flag = true; - } -} diff --git a/src/test/utils/users/ERC1155User.sol b/src/test/utils/users/ERC1155User.sol new file mode 100644 index 00000000..f59cf107 --- /dev/null +++ b/src/test/utils/users/ERC1155User.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC1155, ERC1155TokenReceiver} from "../../../tokens/ERC1155.sol"; + +contract ERC1155User is ERC1155TokenReceiver { + ERC1155 token; + + constructor(ERC1155 _token) { + token = _token; + } + + function onERC1155Received( + address, + address, + uint256, + uint256, + bytes calldata + ) external virtual override returns (bytes4) { + return ERC1155TokenReceiver.onERC1155Received.selector; + } + + function onERC1155BatchReceived( + address, + address, + uint256[] calldata, + uint256[] calldata, + bytes calldata + ) external virtual override returns (bytes4) { + return ERC1155TokenReceiver.onERC1155BatchReceived.selector; + } + + function setApprovalForAll(address operator, bool approved) public virtual { + token.setApprovalForAll(operator, approved); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual { + token.safeTransferFrom(from, to, id, amount, data); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual { + token.safeBatchTransferFrom(from, to, ids, amounts, data); + } +} diff --git a/src/test/utils/users/ERC721User.sol b/src/test/utils/users/ERC721User.sol new file mode 100644 index 00000000..dea9c938 --- /dev/null +++ b/src/test/utils/users/ERC721User.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +import {ERC721, ERC721TokenReceiver} from "../../../tokens/ERC721.sol"; + +contract ERC721User is ERC721TokenReceiver { + ERC721 token; + + constructor(ERC721 _token) { + token = _token; + } + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) public virtual override returns (bytes4) { + return ERC721TokenReceiver.onERC721Received.selector; + } + + function approve(address spender, uint256 tokenId) public virtual { + token.approve(spender, tokenId); + } + + function setApprovalForAll(address operator, bool approved) public virtual { + token.setApprovalForAll(operator, approved); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual { + token.transferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual { + token.safeTransferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public { + token.safeTransferFrom(from, to, tokenId, data); + } +} diff --git a/src/test/utils/users/GenericUser.sol b/src/test/utils/users/GenericUser.sol index d593f315..6680bf21 100644 --- a/src/test/utils/users/GenericUser.sol +++ b/src/test/utils/users/GenericUser.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; contract GenericUser { function tryCall(address target, bytes memory data) public virtual returns (bool success, bytes memory returnData) { @@ -17,7 +17,7 @@ contract GenericUser { revert(add(32, returnData), returnDataSize) } } else { - revert("REVERTED_WITHOUT_MESSAGE"); + revert("REVERTED_WITHOUT_A_MESSAGE"); } } } diff --git a/src/tokens/ERC1155.sol b/src/tokens/ERC1155.sol new file mode 100644 index 00000000..0b018b51 --- /dev/null +++ b/src/tokens/ERC1155.sol @@ -0,0 +1,253 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Minimalist and gas efficient standard ERC1155 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC1155.sol) +abstract contract ERC1155 { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event TransferSingle( + address indexed operator, + address indexed from, + address indexed to, + uint256 id, + uint256 amount + ); + + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] amounts + ); + + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + event URI(string value, uint256 indexed id); + + /*/////////////////////////////////////////////////////////////// + ERC1155 STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(address => mapping(uint256 => uint256)) public balanceOf; + + mapping(address => mapping(address => bool)) public isApprovedForAll; + + /*/////////////////////////////////////////////////////////////// + METADATA LOGIC + //////////////////////////////////////////////////////////////*/ + + function uri(uint256 id) public view virtual returns (string memory); + + /*/////////////////////////////////////////////////////////////// + ERC1155 ACTIONS + //////////////////////////////////////////////////////////////*/ + + function setApprovalForAll(address operator, bool approved) public virtual { + isApprovedForAll[msg.sender][operator] = approved; + + emit ApprovalForAll(msg.sender, operator, approved); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual { + require(msg.sender == from || isApprovedForAll[from][msg.sender], "NOT_AUTHORIZED"); + + balanceOf[from][id] -= amount; + balanceOf[to][id] += amount; + + emit TransferSingle(msg.sender, from, to, id, amount); + + require( + to.code.length == 0 + ? to != address(0) + : ERC1155TokenReceiver(to).onERC1155Received(msg.sender, from, id, amount, data) == + ERC1155TokenReceiver.onERC1155Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual { + uint256 idsLength = ids.length; // Saves MLOADs. + + require(idsLength == amounts.length, "LENGTH_MISMATCH"); + + require(msg.sender == from || isApprovedForAll[from][msg.sender], "NOT_AUTHORIZED"); + + for (uint256 i = 0; i < idsLength; ) { + uint256 id = ids[i]; + uint256 amount = amounts[i]; + + balanceOf[from][id] -= amount; + balanceOf[to][id] += amount; + + // An array can't have a total length + // larger than the max uint256 value. + unchecked { + i++; + } + } + + emit TransferBatch(msg.sender, from, to, ids, amounts); + + require( + to.code.length == 0 + ? to != address(0) + : ERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, from, ids, amounts, data) == + ERC1155TokenReceiver.onERC1155BatchReceived.selector, + "UNSAFE_RECIPIENT" + ); + } + + function balanceOfBatch(address[] memory owners, uint256[] memory ids) + public + view + virtual + returns (uint256[] memory balances) + { + uint256 ownersLength = owners.length; // Saves MLOADs. + + require(ownersLength == ids.length, "LENGTH_MISMATCH"); + + balances = new uint256[](owners.length); + + // Unchecked because the only math done is incrementing + // the array index counter which cannot possibly overflow. + unchecked { + for (uint256 i = 0; i < ownersLength; i++) { + balances[i] = balanceOf[owners[i]][ids[i]]; + } + } + } + + /*/////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155 + interfaceId == 0x0e89341c; // ERC165 Interface ID for ERC1155MetadataURI + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal { + balanceOf[to][id] += amount; + + emit TransferSingle(msg.sender, address(0), to, id, amount); + + require( + to.code.length == 0 + ? to != address(0) + : ERC1155TokenReceiver(to).onERC1155Received(msg.sender, address(0), id, amount, data) == + ERC1155TokenReceiver.onERC1155Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function _batchMint( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal { + uint256 idsLength = ids.length; // Saves MLOADs. + + require(idsLength == amounts.length, "LENGTH_MISMATCH"); + + for (uint256 i = 0; i < idsLength; ) { + balanceOf[to][ids[i]] += amounts[i]; + + // An array can't have a total length + // larger than the max uint256 value. + unchecked { + i++; + } + } + + emit TransferBatch(msg.sender, address(0), to, ids, amounts); + + require( + to.code.length == 0 + ? to != address(0) + : ERC1155TokenReceiver(to).onERC1155BatchReceived(msg.sender, address(0), ids, amounts, data) == + ERC1155TokenReceiver.onERC1155BatchReceived.selector, + "UNSAFE_RECIPIENT" + ); + } + + function _batchBurn( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) internal { + uint256 idsLength = ids.length; // Saves MLOADs. + + require(idsLength == amounts.length, "LENGTH_MISMATCH"); + + for (uint256 i = 0; i < idsLength; ) { + balanceOf[from][ids[i]] -= amounts[i]; + + // An array can't have a total length + // larger than the max uint256 value. + unchecked { + i++; + } + } + + emit TransferBatch(msg.sender, from, address(0), ids, amounts); + } + + function _burn( + address from, + uint256 id, + uint256 amount + ) internal { + balanceOf[from][id] -= amount; + + emit TransferSingle(msg.sender, from, address(0), id, amount); + } +} + +/// @notice A generic interface for a contract which properly accepts ERC1155 tokens. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC1155.sol) +interface ERC1155TokenReceiver { + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 amount, + bytes calldata data + ) external returns (bytes4); + + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external returns (bytes4); +} diff --git a/src/tokens/ERC20.sol b/src/tokens/ERC20.sol index abe088e5..9d6fe64f 100644 --- a/src/tokens/ERC20.sol +++ b/src/tokens/ERC20.sol @@ -2,7 +2,9 @@ pragma solidity >=0.8.0; /// @notice Modern and gas efficient ERC20 + EIP-2612 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC20.sol) /// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) +/// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. abstract contract ERC20 { /*/////////////////////////////////////////////////////////////// EVENTS @@ -33,7 +35,7 @@ abstract contract ERC20 { mapping(address => mapping(address => uint256)) public allowance; /*/////////////////////////////////////////////////////////////// - EIP-2612 STORAGE + EIP-2612 STORAGE //////////////////////////////////////////////////////////////*/ bytes32 public constant PERMIT_TYPEHASH = @@ -93,9 +95,9 @@ abstract contract ERC20 { address to, uint256 amount ) public virtual returns (bool) { - if (allowance[from][msg.sender] != type(uint256).max) { - allowance[from][msg.sender] -= amount; - } + uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; balanceOf[from] -= amount; @@ -137,7 +139,8 @@ abstract contract ERC20 { ); address recoveredAddress = ecrecover(digest, v, r, s); - require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_PERMIT_SIGNATURE"); + + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); allowance[recoveredAddress][spender] = value; } @@ -155,7 +158,7 @@ abstract contract ERC20 { abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes(name)), - keccak256(bytes("1")), + keccak256("1"), block.chainid, address(this) ) diff --git a/src/tokens/ERC721.sol b/src/tokens/ERC721.sol new file mode 100644 index 00000000..33946f91 --- /dev/null +++ b/src/tokens/ERC721.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity >=0.8.0; + +/// @notice Modern, minimalist, and gas efficient ERC-721 implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) +/// @dev Note that balanceOf does not revert if passed the zero address, in defiance of the ERC. +abstract contract ERC721 { + /*/////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event Transfer(address indexed from, address indexed to, uint256 indexed id); + + event Approval(address indexed owner, address indexed spender, uint256 indexed id); + + event ApprovalForAll(address indexed owner, address indexed operator, bool approved); + + /*/////////////////////////////////////////////////////////////// + METADATA STORAGE/LOGIC + //////////////////////////////////////////////////////////////*/ + + string public name; + + string public symbol; + + function tokenURI(uint256 id) public view virtual returns (string memory); + + /*/////////////////////////////////////////////////////////////// + ERC721 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 public totalSupply; + + mapping(address => uint256) public balanceOf; + + mapping(uint256 => address) public ownerOf; + + mapping(uint256 => address) public getApproved; + + mapping(address => mapping(address => bool)) public isApprovedForAll; + + /*/////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + constructor(string memory _name, string memory _symbol) { + name = _name; + symbol = _symbol; + } + + /*/////////////////////////////////////////////////////////////// + ERC721 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 id) public virtual { + address owner = ownerOf[id]; + + require(msg.sender == owner || isApprovedForAll[owner][msg.sender], "NOT_AUTHORIZED"); + + getApproved[id] = spender; + + emit Approval(owner, spender, id); + } + + function setApprovalForAll(address operator, bool approved) public virtual { + isApprovedForAll[msg.sender][operator] = approved; + + emit ApprovalForAll(msg.sender, operator, approved); + } + + function transferFrom( + address from, + address to, + uint256 id + ) public virtual { + require(from == ownerOf[id], "WRONG_FROM"); + + require(to != address(0), "INVALID_RECIPIENT"); + + require( + msg.sender == from || msg.sender == getApproved[id] || isApprovedForAll[from][msg.sender], + "NOT_AUTHORIZED" + ); + + // Underflow of the sender's balance is impossible because we check for + // ownership above and the recipient's balance can't realistically overflow. + unchecked { + balanceOf[from]--; + + balanceOf[to]++; + } + + delete getApproved[id]; + + ownerOf[id] = to; + + emit Transfer(from, to, id); + } + + function safeTransferFrom( + address from, + address to, + uint256 id + ) public virtual { + transferFrom(from, to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, "") == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes memory data + ) public virtual { + transferFrom(from, to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, from, id, data) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + /*/////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) public pure virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 id) internal virtual { + require(to != address(0), "INVALID_RECIPIENT"); + + require(ownerOf[id] == address(0), "ALREADY_MINTED"); + + // Counter overflow is incredibly unrealistic. + unchecked { + totalSupply++; + + balanceOf[to]++; + } + + ownerOf[id] = to; + + emit Transfer(address(0), to, id); + } + + function _burn(uint256 id) internal virtual { + address owner = ownerOf[id]; + + require(ownerOf[id] != address(0), "NOT_MINTED"); + + // Ownership check above ensures no underflow. + unchecked { + totalSupply--; + + balanceOf[owner]--; + } + + delete ownerOf[id]; + + emit Transfer(owner, address(0), id); + } + + /*/////////////////////////////////////////////////////////////// + INTERNAL SAFE MINT LOGIC + //////////////////////////////////////////////////////////////*/ + + function _safeMint(address to, uint256 id) internal virtual { + _mint(to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, "") == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } + + function _safeMint( + address to, + uint256 id, + bytes memory data + ) internal virtual { + _mint(to, id); + + require( + to.code.length == 0 || + ERC721TokenReceiver(to).onERC721Received(msg.sender, address(0), id, data) == + ERC721TokenReceiver.onERC721Received.selector, + "UNSAFE_RECIPIENT" + ); + } +} + +/// @notice A generic interface for a contract which properly accepts ERC721 tokens. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/ERC721.sol) +interface ERC721TokenReceiver { + function onERC721Received( + address operator, + address from, + uint256 id, + bytes calldata data + ) external returns (bytes4); +} diff --git a/src/tokens/WETH.sol b/src/tokens/WETH.sol index 39513c62..5c470e37 100644 --- a/src/tokens/WETH.sol +++ b/src/tokens/WETH.sol @@ -6,6 +6,7 @@ import {ERC20} from "./ERC20.sol"; import {SafeTransferLib} from "../utils/SafeTransferLib.sol"; /// @notice Minimalist and modern Wrapped Ether implementation. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/tokens/WETH.sol) /// @author Inspired by WETH9 (https://github.com/dapphub/ds-weth/blob/master/src/weth9.sol) contract WETH is ERC20("Wrapped Ether", "WETH", 18) { using SafeTransferLib for address; @@ -14,21 +15,21 @@ contract WETH is ERC20("Wrapped Ether", "WETH", 18) { event Withdrawal(address indexed to, uint256 amount); - function deposit() public payable { + function deposit() public payable virtual { _mint(msg.sender, msg.value); emit Deposit(msg.sender, msg.value); } - function withdraw(uint256 amount) external { + function withdraw(uint256 amount) public virtual { _burn(msg.sender, amount); - msg.sender.safeTransferETH(amount); - emit Withdrawal(msg.sender, amount); + + msg.sender.safeTransferETH(amount); } - receive() external payable { + receive() external payable virtual { deposit(); } } diff --git a/src/utils/Bytes32AddressLib.sol b/src/utils/Bytes32AddressLib.sol index 13d9857a..bc857be1 100644 --- a/src/utils/Bytes32AddressLib.sol +++ b/src/utils/Bytes32AddressLib.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; /// @notice Library for converting between addresses and bytes32 values. -/// @author Original work by Transmissions11 (https://github.com/transmissions11) +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/Bytes32AddressLib.sol) library Bytes32AddressLib { function fromLast20Bytes(bytes32 bytesValue) internal pure returns (address) { return address(uint160(uint256(bytesValue))); diff --git a/src/utils/CREATE3.sol b/src/utils/CREATE3.sol index 0c43d594..04e09155 100644 --- a/src/utils/CREATE3.sol +++ b/src/utils/CREATE3.sol @@ -1,29 +1,56 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; import {Bytes32AddressLib} from "./Bytes32AddressLib.sol"; /// @notice Deploy to deterministic addresses without an initcode factor. -/// @author Modified from 0xSequence (https://github.com/0xsequence/create3/blob/master/contracts/Create3.sol) +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/CREATE3.sol) +/// @author Modified from 0xSequence (https://github.com/0xSequence/create3/blob/master/contracts/Create3.sol) library CREATE3 { using Bytes32AddressLib for bytes32; + //--------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //--------------------------------------------------------------------------------// + // 0x36 | 0x36 | CALLDATASIZE | size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 0 size // + // 0x37 | 0x37 | CALLDATACOPY | // + // 0x36 | 0x36 | CALLDATASIZE | size // + // 0x3d | 0x3d | RETURNDATASIZE | 0 size // + // 0x34 | 0x34 | CALLVALUE | value 0 size // + // 0xf0 | 0xf0 | CREATE | newContract // + //--------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //--------------------------------------------------------------------------------// + // 0x67 | 0x67XXXXXXXXXXXXXXXX | PUSH8 bytecode | bytecode // + // 0x3d | 0x3d | RETURNDATASIZE | 0 bytecode // + // 0x52 | 0x52 | MSTORE | // + // 0x60 | 0x6008 | PUSH1 08 | 8 // + // 0x60 | 0x6018 | PUSH1 18 | 24 8 // + // 0xf3 | 0xf3 | RETURN | // + //--------------------------------------------------------------------------------// bytes internal constant PROXY_BYTECODE = hex"67_36_3d_3d_37_36_3d_34_f0_3d_52_60_08_60_18_f3"; bytes32 internal constant PROXY_BYTECODE_HASH = keccak256(PROXY_BYTECODE); - function deploy(bytes32 salt, bytes memory creationCode) internal returns (address deployed) { + function deploy( + bytes32 salt, + bytes memory creationCode, + uint256 value + ) internal returns (address deployed) { bytes memory proxyChildBytecode = PROXY_BYTECODE; - deployed = getDeployed(salt); - address proxy; assembly { + // Deploy a new contract with our pre-made bytecode via CREATE2. + // We start 32 bytes into the code to avoid copying the byte length. proxy := create2(0, add(proxyChildBytecode, 32), mload(proxyChildBytecode), salt) } require(proxy != address(0), "DEPLOYMENT_FAILED"); - (bool success, ) = proxy.call(creationCode); + deployed = getDeployed(salt); + (bool success, ) = proxy.call{value: value}(creationCode); require(success && deployed.code.length != 0, "INITIALIZATION_FAILED"); } @@ -41,6 +68,15 @@ library CREATE3 { ) ).fromLast20Bytes(); - return keccak256(abi.encodePacked(hex"d6_94", proxy, hex"01")).fromLast20Bytes(); + return + keccak256( + abi.encodePacked( + // 0xd6 = 0xc0 (short RLP prefix) + 0x16 (length of: 0x94 ++ proxy ++ 0x01) + // 0x94 = 0x80 + 0x14 (0x14 = the length of an address, 20 bytes, in hex) + hex"d6_94", + proxy, + hex"01" // Nonce of the proxy contract (1) + ) + ).fromLast20Bytes(); } } diff --git a/src/utils/FixedPointMathLib.sol b/src/utils/FixedPointMathLib.sol index 50536895..d01a2be1 100644 --- a/src/utils/FixedPointMathLib.sol +++ b/src/utils/FixedPointMathLib.sol @@ -2,8 +2,7 @@ pragma solidity >=0.8.0; /// @notice Arithmetic library with operations for fixed-point numbers. -/// @author Modified from Dappsys V2 (https://github.com/dapp-org/dappsys-v2/blob/main/src/math.sol) -/// and ABDK (https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol) +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/FixedPointMathLib.sol) library FixedPointMathLib { /*/////////////////////////////////////////////////////////////// COMMON BASE UNITS @@ -46,12 +45,8 @@ library FixedPointMathLib { // Store x * baseUnit in z for now. z := mul(x, baseUnit) - if or( - // Revert if y is zero to ensure we don't divide by zero below. - iszero(y), - // Equivalent to require(x == 0 || (x * baseUnit) / x == baseUnit) - iszero(or(iszero(x), eq(div(z, x), baseUnit))) - ) { + // Equivalent to require(y != 0 && (x == 0 || (x * baseUnit) / x == baseUnit)) + if iszero(and(iszero(iszero(y)), or(iszero(x), eq(div(z, x), baseUnit)))) { revert(0, 0) } @@ -70,44 +65,77 @@ library FixedPointMathLib { case 0 { switch n case 0 { + // 0 ** 0 = 1 z := baseUnit } default { + // 0 ** n = 0 z := 0 } } default { switch mod(n, 2) case 0 { + // If n is even, store baseUnit in z for now. z := baseUnit } default { + // If n is odd, store x in z for now. z := x } - let half := div(baseUnit, 2) + + // Shifting right by 1 is like dividing by 2. + let half := shr(1, baseUnit) + for { - n := div(n, 2) + // Shift n right by 1 before looping to halve it. + n := shr(1, n) } n { - n := div(n, 2) + // Shift n right by 1 each iteration to halve it. + n := shr(1, n) } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { + // Revert immediately if x ** 2 would overflow. + // Equivalent to iszero(eq(div(xx, x), x)) here. + if shr(128, x) { revert(0, 0) } + + // Store x squared. + let xx := mul(x, x) + + // Round to the nearest number. let xxRound := add(xx, half) + + // Revert if xx + half overflowed. if lt(xxRound, xx) { revert(0, 0) } + + // Set x to scaled xxRound. x := div(xxRound, baseUnit) + + // If n is even: if mod(n, 2) { + // Compute z * x. let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) + + // If z * x overflowed: + if iszero(eq(div(zx, x), z)) { + // Revert if x is non-zero. + if iszero(iszero(x)) { + revert(0, 0) + } } + + // Round to the nearest number. let zxRound := add(zx, half) + + // Revert if zx + half overflowed. if lt(zxRound, zx) { revert(0, 0) } + + // Return properly scaled zxRound. z := div(zxRound, baseUnit) } } @@ -119,65 +147,60 @@ library FixedPointMathLib { GENERAL NUMBER UTILITIES //////////////////////////////////////////////////////////////*/ - function sqrt(uint256 x) internal pure returns (uint256 result) { - if (x == 0) return 0; - - result = 1; - - uint256 xAux = x; - - if (xAux >= 0x100000000000000000000000000000000) { - xAux >>= 128; - result <<= 64; - } - - if (xAux >= 0x10000000000000000) { - xAux >>= 64; - result <<= 32; - } - - if (xAux >= 0x100000000) { - xAux >>= 32; - result <<= 16; - } - - if (xAux >= 0x10000) { - xAux >>= 16; - result <<= 8; - } - - if (xAux >= 0x100) { - xAux >>= 8; - result <<= 4; - } - - if (xAux >= 0x10) { - xAux >>= 4; - result <<= 2; - } - - if (xAux >= 0x8) result <<= 1; + function sqrt(uint256 x) internal pure returns (uint256 z) { + assembly { + // Start off with z at 1. + z := 1 - unchecked { - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; - result = (result + x / result) >> 1; + // Used below to help find a nearby power of 2. + let y := x - uint256 roundedDownResult = x / result; + // Find the lowest power of 2 that is at least sqrt(x). + if iszero(lt(y, 0x100000000000000000000000000000000)) { + y := shr(128, y) // Like dividing by 2 ** 128. + z := shl(64, z) + } + if iszero(lt(y, 0x10000000000000000)) { + y := shr(64, y) // Like dividing by 2 ** 64. + z := shl(32, z) + } + if iszero(lt(y, 0x100000000)) { + y := shr(32, y) // Like dividing by 2 ** 32. + z := shl(16, z) + } + if iszero(lt(y, 0x10000)) { + y := shr(16, y) // Like dividing by 2 ** 16. + z := shl(8, z) + } + if iszero(lt(y, 0x100)) { + y := shr(8, y) // Like dividing by 2 ** 8. + z := shl(4, z) + } + if iszero(lt(y, 0x10)) { + y := shr(4, y) // Like dividing by 2 ** 4. + z := shl(2, z) + } + if iszero(lt(y, 0x8)) { + // Equivalent to 2 ** z. + z := shl(1, z) + } - if (result > roundedDownResult) result = roundedDownResult; + // Shifting right by 1 is like dividing by 2. + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + z := shr(1, add(z, div(x, z))) + + // Compute a rounded down version of z. + let zRoundDown := div(x, z) + + // If zRoundDown is smaller, use it. + if lt(zRoundDown, z) { + z := zRoundDown + } } } - - function min(uint256 x, uint256 y) internal pure returns (uint256 z) { - return x < y ? x : y; - } - - function max(uint256 x, uint256 y) internal pure returns (uint256 z) { - return x > y ? x : y; - } } diff --git a/src/utils/ReentrancyGuard.sol b/src/utils/ReentrancyGuard.sol index a462fe83..9caae5b8 100644 --- a/src/utils/ReentrancyGuard.sol +++ b/src/utils/ReentrancyGuard.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; /// @notice Gas optimized reentrancy protection for smart contracts. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/ReentrancyGuard.sol) /// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) abstract contract ReentrancyGuard { uint256 private reentrancyStatus = 1; diff --git a/src/utils/SSTORE2.sol b/src/utils/SSTORE2.sol index 6e388b60..265f4a56 100644 --- a/src/utils/SSTORE2.sol +++ b/src/utils/SSTORE2.sol @@ -1,28 +1,52 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; /// @notice Read and write to persistent storage at a fraction of the cost. -/// @author Modified from 0xSequence (https://github.com/0xsequence/sstore2/blob/master/contracts/SSTORE2.sol) +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SSTORE2.sol) +/// @author Modified from 0xSequence (https://github.com/0xSequence/sstore2/blob/master/contracts/SSTORE2.sol) library SSTORE2 { - uint256 internal constant DATA_OFFSET = 1; + uint256 internal constant DATA_OFFSET = 1; // We skip the first byte as it's a STOP opcode to ensure the contract can't be called. + + /*/////////////////////////////////////////////////////////////// + WRITE LOGIC + //////////////////////////////////////////////////////////////*/ function write(bytes memory data) internal returns (address pointer) { + // Prefix the bytecode with a STOP opcode to ensure it cannot be called. bytes memory runtimeCode = abi.encodePacked(hex"00", data); bytes memory creationCode = abi.encodePacked( - hex"63", - uint32(runtimeCode.length), - hex"80_60_0E_60_00_39_60_00_F3", - runtimeCode + //---------------------------------------------------------------------------------------------------------------// + // Opcode | Opcode + Arguments | Description | Stack View // + //---------------------------------------------------------------------------------------------------------------// + // 0x60 | 0x600B | PUSH1 11 | codeOffset // + // 0x59 | 0x59 | MSIZE | 0 codeOffset // + // 0x81 | 0x81 | DUP2 | codeOffset 0 codeOffset // + // 0x38 | 0x38 | CODESIZE | codeSize codeOffset 0 codeOffset // + // 0x03 | 0x03 | SUB | (codeSize - codeOffset) 0 codeOffset // + // 0x80 | 0x80 | DUP | (codeSize - codeOffset) (codeSize - codeOffset) 0 codeOffset // + // 0x92 | 0x92 | SWAP3 | codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // + // 0x59 | 0x59 | MSIZE | 0 codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) // + // 0x39 | 0x39 | CODECOPY | 0 (codeSize - codeOffset) // + // 0xf3 | 0xf3 | RETURN | // + //---------------------------------------------------------------------------------------------------------------// + hex"60_0B_59_81_38_03_80_92_59_39_F3", // Returns all code in the contract except for the first 11 (0B in hex) bytes. + runtimeCode // The bytecode we want the contract to have after deployment. Capped at 1 byte less than the code size limit. ); assembly { + // Deploy a new contract with the generated creation code. + // We start 32 bytes into the code to avoid copying the byte length. pointer := create(0, add(creationCode, 32), mload(creationCode)) } require(pointer != address(0), "DEPLOYMENT_FAILED"); } + /*/////////////////////////////////////////////////////////////// + READ LOGIC + //////////////////////////////////////////////////////////////*/ + function read(address pointer) internal view returns (bytes memory) { return readBytecode(pointer, DATA_OFFSET, pointer.code.length - DATA_OFFSET); } @@ -46,16 +70,30 @@ library SSTORE2 { return readBytecode(pointer, start, end - start); } + /*/////////////////////////////////////////////////////////////// + INTERNAL HELPER LOGIC + //////////////////////////////////////////////////////////////*/ + function readBytecode( address pointer, uint256 start, uint256 size ) private view returns (bytes memory data) { assembly { + // Get a pointer to some free memory. data := mload(0x40) - mstore(0x40, add(data, and(add(add(size, add(start, 0x20)), 0x1f), not(0x1f)))) + + // Update the free memory pointer to prevent overriding our data. + // We use and(x, not(31)) as a cheaper equivalent to sub(x, mod(x, 32)). + // Adding 31 to size and running the result through the logic above ensures + // the memory pointer remains word-aligned, following the Solidity convention. + mstore(0x40, add(data, and(add(add(size, 32), 31), not(31)))) + + // Store the size of the data in the first 32 byte chunk of free memory. mstore(data, size) - extcodecopy(pointer, add(data, 0x20), start, size) + + // Copy the code into memory right after the 32 bytes we used to store the size. + extcodecopy(pointer, add(data, 32), start, size) } } } diff --git a/src/utils/SafeCastLib.sol b/src/utils/SafeCastLib.sol index cb5adf4e..42011497 100644 --- a/src/utils/SafeCastLib.sol +++ b/src/utils/SafeCastLib.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: AGPL-3.0-only -pragma solidity >=0.7.0; +pragma solidity >=0.8.0; /// @notice Safe unsigned integer casting library that reverts on overflow. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeCastLib.sol) /// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol) library SafeCastLib { function safeCastTo248(uint256 x) internal pure returns (uint248 y) { diff --git a/src/utils/SafeTransferLib.sol b/src/utils/SafeTransferLib.sol index e03230f0..2c4526e4 100644 --- a/src/utils/SafeTransferLib.sol +++ b/src/utils/SafeTransferLib.sol @@ -4,6 +4,7 @@ pragma solidity >=0.8.0; import {ERC20} from "../tokens/ERC20.sol"; /// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. +/// @author Solmate (https://github.com/Rari-Capital/solmate/blob/main/src/utils/SafeTransferLib.sol) /// @author Modified from Gnosis (https://github.com/gnosis/gp-v2-contracts/blob/main/src/contracts/libraries/GPv2SafeERC20.sol) /// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. library SafeTransferLib {