diff --git a/common/env/src/mock/validators.json b/common/env/src/mock/validators.json index f189d0f43..a638f7645 100644 --- a/common/env/src/mock/validators.json +++ b/common/env/src/mock/validators.json @@ -1 +1 @@ -{"0x728474D29c2F81eb17a669a7582A2C17f1042b57":[{"depositDataRoot":"0x07a6bb28725fb29c7f2e8320c22eaec28626baf09d9fec0009b4abb0a5844508","publicKey":"0x91c45c1a57fe36bd9306babc6902dadf099a3e25a8ddc113a6fba460cbdaa2e457bab9a906c2969ce6eeafbafcd87fd5","operatorIds":[208,209,210,211],"shares":"0xabc8077540e55cfcc753bf7edb6bc58af2ed983c1b7c36b8c589866aad6ec7c38b1fbefd25fad3c7d69a7a037175196f08a9fe484b40ccd83dd3a5c1bb58024da881972d87b651bf3243fda5809be278041ed7fef7fed9d325556a2fe148ca1a8c40770337dfbf524be6ed38e75d70f1b5245516a9324d54e991231b32b13c70cf6b65a815283592cb1c624a62eccb1382fb34e571561004ab535e2c925ae06357977cfc4a831591b5f2678b328b6bfe75abec0a85a3d5698bc30086e293aea3a14df1b96edf1edb9fb649ec06c6b96afedf8db57447f9b0a7fcd283728a979f8b1167669e5f62e2a7533ae01ea81faea9ff415c8bf52450ba21c8ecd567fb2c72506e5d37232465f958492df3f9c171f0482213403fdbad2a0afe1ea91bed9f377a6e600f7619c1c77fb9592d30db0b19db95c80ff7d3c7b0b9b538b5844b918cc087cbfef9ce3590a07f53b4473171487a1f162215b528e906a52a0f29734e07e55377f9cd2d6bd405f78dab89e31f2fb0aa1d36635adda73635b7f3a0a737332113eed22860480fd3f731157c7c5720cf76d13db1b9f2ad039b677f3a964e60fe5c0962743a23c7884612a7d68057c22ca3608021f62d603149090cb9d2da7e831460e6a134b0037ebb42986063e3e2de9d97d7b8279f2612b96f841d5d823a16583cfcd184a6feaa80dae3211cc85c2ae48ab7fdf47e0192d63d195a28d2809d829ebe0f03f07437342623ec8b7a27c78ef423776b4c27f26119a490c5650e27271c5410b12e62b461179c6758a7181b529e73aff7ed773cab7c2bcace1eb0b72de1a69c35294edd0a4536b0dd8f711bb3887b947b755e3227a841a3de567e6b04f0d18c2790ca046da3aeef87e0d9779a2b3f6a170ccf1b1294beea6fbb53a1ac53e53ddbd609b058afa1c937f75fd3ca4c6cc9b3b0ea6348ba30de15994e78fbb11e5a79c84a85b10aa6f7ece93cc9567fed7faa47bef334a48c4c75f657b4df7a7b6bb3d07664b87cb6fe820c0a88a4c71796c94030bb5e7bc1bf0786b854cd7a57d8c7c9b78477c5cb29b0040ed97946377b1ea2a1b8ef83046aa0a9670b71c36e3c8ecb3cd11c328fd41ea15172ef3b2dd67b79a83dd4176a510695cf12a2a58a8b89baab9f0eb855ff8a470fe6c645199de88464d18953d0cc2c82c30f602986126d807825f29d42efe005544c9efa399b5fe5203ff761534dbfcf11b710863df4e8007d2ae2088da65b4be436e707b6f31a71806f463e6c37f1ba4a176de857225b559ef0a9075c7f8a5880f35c1e6dc84921529b586c708f0fc3612aa5b33f3e65065ff91a73558f1f5eccee887353d3045021c5ce44a4554fd42e94104595f30e7a2e1f1f7ae7b10709e6498f8c50df384482a808aaea5fec7bd7e91830ed3c5d0238f7d9903998d0aac42533b5fa9eae92d8142170d80e2c464a764df1ceb639730860db9be46af30bf164790e39cc66ab74f418dd9eb33fe185b841384a7f71853236b426551c8cb22281c7b7fe8d2c4db0e4f03a5bce3ba387ec5e68e11648e1815793c9ee1bd36b6e3b345b16f8be9cc50821b1c4a96f3c90b7f35f3a732b85d9da328dc119ff4f43e44b51e7d327c2a3c082eac1f85a213129af85c865b5301d57b591478e874a64cf495f8c3b564b7f2cf3bade3bc79b3716c835907bc732aead77226a7a1a98bafbf4806814323d79d91316d336e992a2d72ef2feeca5772ccaa7e9228ba7ddfd82e37da6d6f6fd5962d23ff11be5c3551ad13a0fe16d64238184648ea06631d34e052aca6eff2925304c465bd00ffb2ce69127f97bf888f016602a7285885177c22bb4511ecd496c4d5c572349662c","signature":"0x8c1cc2b16e697a1ade06c8f8d05cf7f1c278b59c9abb68347d09088586378545dee83085c43f3356d11b32267badcbf21502b01e434cdbbe732c10c165b9e3083e379e36877549ffd865f0d0a4fe6d6d059eb96c44acb949d362743141c96762","withdrawalCredentials":"0x010000000000000000000000a6d2f276d5313ca57561ca37985ac935fa72a427"},{"depositDataRoot":"0x5379816265993a81bd149e3c2bfd724dc4d92c8ae74fe9ea7069566be038232c","publicKey":"0x93d6aef35ade62dd6a8b3b58fcdd41b49cad1ace01bccc97ca48b3ae6bb6dcb46c184cf1299d482a7febf0f67a043131","operatorIds":[208,209,210,211],"shares":"0x97321f99fcba38d540be2aca42c49d8b6d19ad45ec4962b3ef2201adaa5525ae25ad245ca91cf7fb44875d4848db0aaf13842c21f83ab98e21d9a11414254f620c9fcaf1d49483592f773a690c86882dd9ce74601b5bb621b2b6ee4e7472ed9280237d6c81f19fb291226a4808ddb58d465bdbfa4f2ef76faeb74aaca724eb0c806b972cd9d34df2465e5a981d190233877a10200c5b7c72ae4401e4446e7bd9402d12b0dcf208595ba53c74c07776438586a328843a97ff7102225d76371353a59657a0a19203a5e30cb543a490dc22efa5f5d4267ca02c706ae0b8af5fefea2079305dc882edb6b028b970c99fe958984fd6a49133e371fc1b158de387ca95d6bdea5cd492c5214beda6b8ed24f63855dede0751753b7fd4bf689346032547b4a5b15ec62fdce14fd1dac94deed11a84860f4cd541d696d284eb644cf9d19510d4f18f1bfb2652c59c7c64cb69b7bb858bd2c1919bc64e3b23d47a9887274ce7df01bb935d1fe8e272b87e160ee20d0f0e10f845894aa6a16914a30d746bed564050eb31fb47d4fb290b6bcd5f96ce4d4e3aa8c4206f7de8a257def6a042ca3cc50d74e6e60e58e5dc6951da7b461a44803686cc42a90ddf8117e962e87c438da6b3e0eadebce1e60fe26c2c4ef4fea63cf838e29fdecdd6de50f1e882ed70178a6f19f626f676d3c790fa1dd2e24209d7397b412c3f520b148bbba0c592ed7a91ea886aca0184eaa233873cc6f25af8c303955a2dfc2fcae9c5d21674faf2404e7c4d30c855773072677c9a4f19601a9b1d636ba891292ea9edbd4ce5fed77748b66db6e9eabe5d544a401b7504e98ae85c2819ac474730ee73a8db20511607bda9c77c1384adf9723d20b72b59cb5014b0876294e3234bf811797e7a87419c804adf373ed654ac53b158fc6b1f1c4aa405ecdb379cfe69d0f300d0d90f0142fb3e5e4a3892416d71503c0cd564de90aeeab128d4cd06c69198d40441e05ecbdc37eafcce610af015c87f257dba11acb739c02e8d25fa6d4e93938b5edf1dcfb8bec4889d16962fa521a30a57ad76ebb6571b6b14a807e4ae8e899bcf5fc4f94ae0c3cd3f363f8bfe113bca46dac7777dae10355fb06dd1ea545bd37cd39809773f07eb7fbc25795d68df9fdd14c79d0b5e151a7ead668e7df146393cdc93d5d3db85bf04c1f61a7666b0c207806e7fc4fffcdfdc88889db19e68294637630b2e569c31c7ea3fb40b70f0d9ad5c547b34e67a4d715371c625fa6ebc4bfd72a0b7bf2dd8280e531f751b721ad906af8d6467e507f0ccc2cf15ecda1a28c58851fae8aad087eb6092d91c9113ab97a2feb97fde494c5fee749ee77a04bcc759469950b8890f04b9fdb11d9ec102155d36bd49f47bbab370c3b3ab1208938c9c9732a3a24ba550603a1053c3c14140b4273f3537d82259d0ed0dc00b8572b4ea41f431f08da1912b38d6a21e3a1f70b7dfbf33e4b6b5e0624fc8df47c1d411be49c3dde508bc62bff599bfd1b6f0a5416d3f75a76032ed07337217806824168556701cf44384e422e41c8aa511ef13dfd37369d47696e4fd29d4c05d5acb5f7c1b32af60d5f1de2a1bd7e95fbdc448cba581ef8d930db6e0ac67f5d54763738b4ca7818553b6a2ef68fde511cacb9d2a8682e0b732bf6fc1dd2d6d5993c3e533325137c385a5613ffec45043719db640733a267c599ae747fafdb91c412aeee52d461788326aab15af82de063a70be70492f4fcfffcca538e77829fd818e5fc86a2950a26b9d2f20fe951bd075abc0e331c119463188183215d2bd8d17b2ddb5fc6553a23e6f2e527516dce82f9e46ed126b3325231c83cfce921d275dde60db","signature":"0xa4ed14697dc7a472aba3368cbe9d6ed003855ea8844c174c00343bce7517b61b64c12a54d44970c7bd937d0fd708527f04f6dd7f7a99e993fe56c307e34eaacff1467be3700e50a3355adf2d0bb87220d17e977766f25c7d6e1c34f2cd7ebf4b","withdrawalCredentials":"0x010000000000000000000000c6a9457a2e1411b01c0fccc4b863460743057fdc"},{"depositDataRoot":"0x5aef30952a662b022b8aca585e383d4e3b225e90533a42a4ada102c5bfb7ed80","publicKey":"0x93700ce22bba6d27778d83611b2fdaa0394c2875ecba55e15f89b72c0658935de0aaf98782fd95c9fce56f64410d7623","operatorIds":[208,209,210,211],"shares":"0x80d169c603f164ceeafccd4ed9f91b0b5ae0016e4a17668b16404366c7245c1e92b8686fc805b2be952f3d148db79fb10e9c8e8a64e448d8896a0389a027f2ada8f77dc4702077c36b94ce67f365135cad02681e379581b086b47734fe1d835e8f5c458407b9128eaed6356273d4faa7978878ddbfc4635abecb9674e4499e29891a58c5991676e9941944ba6f7d86508a41ad27f311760816699a7cacc39fc002522a6aba760ea2c36501ba80aca371d2a619cc9e37fa4383581c194cfcc3439520ae8e61be9b5873b9e8025d1e350a97fc08083591e984c33efbaeae02dac0be149f72227d534520972dd2508962b7b98cc7d6ed176ca4ccd71d7fc244ccad90a2bb172881f6aa94b262fa811bff66aff2a2b2f445f5fbcb0652323a9cf70f9dde883c16799b964246a569b8ba779e42835a6ee2be5b128d8977f79d219b5c3f1bef842502e3f5d43e566adc9ebf0bd27eabd7899a9e8137ea6221ec2745f34d9804f26530424b860c2774b20678612d21727770deda19ee2a574cff68d033115d82597cc96cef3c7a98d07cb68c6d2690b1f70ab9e70c36db2db93bd3f1dbf62ff3735bf5b38330286ec0dc358e8e0b6ec0c6fa4e12b92a8b2a02d265b99bb2a8c1d517579801f9775bd4f41bbfbf712b37b1e74bc6c80b55b33a5e15f58bf7f66fffdefe61d64d1fa133b7cdab9cfa55796076e09552bd58a0138018aa64dff9f1487fc06cbde99288150550711b0002092745dd0c37f24c1997eae63b377797f905e42616b97cb093562a357f34b27facf98f37bf354cd0f96d2ce9ab2e52c9a8bf4a50d8f7911432340c489f215b0a42e30b636ebe734d2aaa5783f8eed1b4ce74690d8540b942d53dc18e319f5e6dbf33fe820732e096ccf241e9e11610da9e797a3453994070bed4a12598de5ab5ce303b6d729aa9799be68a4dbee4d64d09407f886fc8262953bdd90b6ca15c055619da9272d0f1b1353be79d0c29ac7003d9da69d49ca40dfd2424a7f36b6a659f23274e8f2b3f3af43a57af69469abf84dd9ab1e8da1df752eebfdb8da205aea72cb6910812eb6ca2c685b631873df18924e486060ea559abd6b2ea4c2500d7ac976efa80dd81fe6123f3cbe3ee20666e419ffbbd60702f1a25e7a456331c8d2f635f3a9303a12607c9040bd7a15a944aee98e57bdf591f5d1e8a39bf98bf50f2deafe536eaa94ca08a267e80c50650ff4e9442f7d8bffaa889570418c550cf47ed834e16a49af5da289a43dac3c2552fb9c4315629c0d9abe5442451cd6d9452cb00885bc9ee5ca6db871e7227bc3708443220c813b991336dea7608ee9e9dfb3022c617684998945ae488462b5c0602dd0f6dff85c6beccd687817f52273ff20bdd4a3dd0a83488a06024a0da7d4a929febf931a5db67648c805e335462fa5fcc4e887f6aecfc800cb3a7676a9b222a91e2800c4b642fb5bb877370f0213306a74f53946b23af7ba15cd8994e8269903de39d08dd0b23e99ea97e032293b29527682da40fecc31063bb3e8ba65c2289d69ff8a3e84983a39a2d6e28faf55c9bb647c225d93f8212acbdfb805f439911c7a62e45635bd24e9ac330eeae31ac6f3d7e8df4dba49714e24301fc2e1bcdec4193a8515f6ab950f06131b7dd455aed3c0c977ca6a0b20055987c0b18e790809d0d3c9d1bb51a3247f4b6fab3767fb53c9c47b634a4cbf3e9a117fe45d7926b45c0919ba6d48493fe54231c58a25ddbb4bce8ea7792f32155ebf1d4a70f5014f655bf90dd8e17ef1e9ae78d74fcbc55843e07d5e4a5e60ed5656325c0603c395c98dc515f6cd7d8188d0aab44066d6f9ef6657cca0d9d24d69659fa80","signature":"0x9954a404e0a5f3d3b47a2e8144a60bfeff564f34dbbcb77d0eb6641acf115f41ca6aebf5f01aa3a43c83a8c5071eb8b003a54d10cc3a3d749322a373b033c6ee2ba338507f0a16c43291c7afb4eba14b4f5dfc65f343980c03eb7fd7901cfb4c","withdrawalCredentials":"0x0100000000000000000000007d0dc29346b68eaa4438ed2f8a70f95010b00d90"},{"depositDataRoot":"0x9c5b54780098bf35212c1335b020c07f54a0bb7b59c9d106b2731ddec4de860d","publicKey":"0x938484e7ecb09145d77b63e7bf8e6deba9ca5aa897a32870beed810083a9a22f407be08e775a07c6c701c4a8252da1b1","operatorIds":[208,209,210,211],"shares":"0x9886e04ac65be36e2515994dd7010b788c9f0f03b3bb8a1bbbdf0834754305bf0c7ce89b0270242fbb922300104e2c141627f8690d5d0186bdb23e5fea0649beab70bd7fc19097300b60cd1c60a855248a5aa66f5587de3f5aca5ce2e7315e7aa2a6e37a5289fc559ea2c3e3ca0a740e0e92531e576a5228c59de7627cae7ff78da4741939a0e3a7b81eef1fc84adeb68472fab3b73060d3c549868bb20f94786b042ad6cbc854c512a1330605e993921069822cd596ee181e072ce0a2e892ee81d0b1ca79c840ac23635abf8b77fc3af85631c6633a2570453a933f5724866e12be9bdb76a039f95d12fc5d9cea2220a484c3174f33956cf1db5a2a0c937d110a293b6cad449e5e14bad029bcc476f612577ac8f04731c75f5b2c9d6a0dc350312339a0295dc66dbb61a0015175f803494c7ff88c8e6c27a68e1e5d916e1a682475a82987a5d178e94f48859590db5fcaadabd0b436af8d588528c98e4047c5e55fbe6a0e0e7aad5a15ef927c1b74d235ffe888786f874bc2c54ecc1b16f3f90b95018730ffb004613ddeb8da83c8e375cc40e453c0b2540c2ee96579f5c94858b74cf25beaf0ff67295c41ac4bdd8df0284991f33da95b40d34aebf39efd904a6b71a13bdc43ebb82691d2c59f8c9c23a521bed5aeca5e55bc2621aa72cb6d85cc217c49755560e34a033d81f4dddaf8f20d31298d9ca114e1d2defc3d5fc2569028d277119305413b46fd3b3c33a448df7ce7204f6227928b901f796aeb2d465c0671b25db630ee6b9620ebc8f1058028329e9ba4cf59443388221013c7d68176dbba1e0eedbf11f7592e19b44a92414791ee58e4f27377e3b61a5fd964ce321b2669d59ccae888acd2ec1182cf8dafc1bf7b7ecff3b607b257a0872d7b6715deeeca9c0ca591400b402b1923ac51a830230b1d418acc355b998ea23537114fc4a18a7b0ac41e56a44fba33f011ee46df9c1b3be449ef0b4195a58859c495bbb9d38fc905591a1374e41e2c0a890734d510315e6b2cc10d8981aa6a0d7b9e7233b79dc5b92836b1701f545c581057df845bda795b5bcfce167a4f36afae8c7e8015261d2d2763243868f9d53076dcd1a3a48767841af5f3c045fcd77f70ac4604048bee2457e15bc3027e9c8be54640ca3e81693eea2ea162254ba470602c7a45018358b02e0532ce6d29ca5a181f77444560e8cbbb701ee799e5e9b00da97c61a77fb35eed137c7d94e22b66a8594ed46f3bc7636ed7071f324c5b7de134cf3428b11581564e1229517a076650eb5f85be78000360f949a800317f8e2d364b28be866b64d8c72462c1a39b44d1701124ee3b7fe6543e9e8c19a7d4c6774a8080b4a608cc7118382961bee59e44a6d87baa57b397b0ffa39ead6c7fbe13295dd4ad5295ab70f790d0e9c9d963f80fb291dc62f66a98f8a7632057c92ee22b826c2d5ff8be507d06ba1cf4ad30777ac46aa9a922ea457eb02d7100bd8f207dbc0954d16eaecf6c7666327a12dd53741a476ccb3b4d6facd5a06de58c5f51b4f1e7604eeaf030b1bc1aae074852ae2ccde7753333ecf744a748053eac1d267d0ee228e7cacbb136bfb7c9623ef3fa5f419e211e8179bc323dc147619ad67cac7660966dd773bf52f1eb7867b84a307b34bd1b75adeff379ae15ea990fe680b5c7a7e765034df29f3bf691d7f022d89636ec5a41af6e3e9c7ed77bd14c6c44562f9da6da94cf749b5234af3c1d122bb9665a041e2cb1c9e068ecc4cfab586c32e4ec9f43691a70b00608e6fd45f075d38fc68872b4cec4193092eeb5b8d4f5b08636eff8346325bb1b82d6783bb68bfc6d2db30c2aeee7dd760945bf405c2f5f","signature":"0xb6a835027b3effbc47203432d0568b3375fae7d79eadd5d24f4e6e19475e05b41255b81704009928f038f29f0b1e992e0791a7f4e0554c51a54ed6846feae99fba38ae3eabb5359d6b9253befef59c14c1a288c5d4c4c86646e59b5dcc2144a4","withdrawalCredentials":"0x010000000000000000000000eed7c51817bb85062499d91bfd2488a97ee6e218"}],"0x0086C88CA523F27e4bD7f233bF7C722154757044":[{"depositDataRoot":"0x07b19248ed3f893008cf611a87436eb773d753e20e11f2d8dee142a25383825b","publicKey":"0x94f4e8e9c40677af4a7a92c9529feeb104a647fd1baf64ab27afea94d93177d08b7ab980ecc02384b11b1f00dc98cd9d","operatorIds":[208,209,210,211],"shares":"0x976af84838a74cc4ef6f02fcd5527b885f919fb65269945733f53998c9c65e0220db874a84f2774e34bea983f626da5502fc58faabcf9b135f2aa1b0eb9b00ec0c9f1520ecb9c5575d9f001931e1fa1f691a5c78ea575d0a039e92aec75235dc81fda99fa0992de3fc1b71127490e08d0271999c0d27c4934effd9443fe257e119313732a69f615515c1cbb01142d88f95d7aca4fd14570aa3bc2672fd4d05871384c5f75f3c21f856fc189bc1850810cb2980f5d5b4ae16f2d6bdaa579c81e9ada26b44c8bee239d8e47680c7958afdaa74835bea3f2c2f8ff14f9774c8d7d48b7dacfcfef9d83c647cab45b0380877b7006172ea36326c2a794a12ff0aba99634eddce79a0592cf4396f7a8b36ab09db44cb9461d71b63ac173c48bd8537c0b4e17f84b7d13cc0c8e2e4a1d6d63c26d87eb125b2e67b4056cde26d6470e3980574a9b6a60b9b4ce21c3779d636638b3633fec96dbbc03c1832a752bcdc19146a3d5006fa77db246285f451484838f9605c4ba34cd1c2f63375f282c3be148bc57f75b02ebe60626c49643cb3d09489057432f7795555bf70257e42c109a3b7494bcfb01aae5e975d405fc724697082df3808d8809e4623ec82e607663cebfb0712297e5e5a59e649b7ff28e3d8689acb08401e999ebb79d9bd44dd214b6639b82980c6a7133d5b08282682c7d55b45dadbdc26d0a220598c75bf3b9916cfa956a90daab8114a480d2a1364b43aad89ec5460414da52dd026d08194acef8bda50c2f576eea947dfdbb7f9364c035cf29b8fc8c1f8a5b3f5b54d331f25f018ccaa75a482b6f2fcc2d58a766d1dc3f1b473a9e023356fa354e7989a19b11113ca673d1d37c46e1d9f821e6706d951377a07eaca5f45bc43556a84a3b63b2f4926bf6c3cc402b11a9e3a5704e35f3ed77f3c2988c8b56d07cabf124151b50bc9b5cefebfab099193c94b3c8ea7ae99960850c73027094de1bceeec9fbc9a66e7e5234182450a28343336a00c29cdc91bfca8efd71f5694aca9854b16ffb4ae3f712ba0d35a7c8bfbe4c65031214d42b9476b251b053abfd49c4bfe38bd1b1c3d544acd1ec9f178f83a9acc2e35e1745d9e9ecc8ae2440e9909300d1b467acc2fcb67023bdb26f1963294f49c758c464f12942f720a57ccda87ead784ca38f175d813e568fef0399e445037451f2f5b821d53fbb43dffcfc2c91e3d0ae2b452f09a0c854079ae2ef938539f4bf291881d6be3d7b957c6226ea601936d5a7f5ea84c7555907f2c615fae04919b090a0246c762290a77d63d0851962fd2161bc38dfa4a6061fbdd30726568382db9b1211423be446f2b68ac11ff066e14bfd82ff74b71af3fa98532017a8d16b94debfd8dcd2c41fb01745cc062694a2b643c4d85e37823d24b05803c3700c16c3a859615ad866ff9e4048b32c33b8a3d77c7c4d9a6e01b92c586e5da2d802bd76a8807ca8d011c608bf902d139aef2a3f74ed494aa70514cd5d047e3d64bb6b0676bf36eda34c98f61e8af69dde7a4853e97e03ff689f3b1d3c52b27a98415c2ff1b27225a725a492da7d3c2f04f3ad193ba72a2b8f42ac864e5a0b59d1dcadc294e3bd49bd9c935859834b12ba719de45bf55469d14713b7c79825c6679244d24a66b6ca2235282fe3da56618a2303b040a9d873b515dc3ce9dcb5b5c587fc53332db6fd0dc709fbf9c4170dbdf0b75bde43fe265ddc5ef944f224de0c0310ec3b12834c206d6fcbac01d958b90e332df36734782b955c836d0849c96bbbbbe634d68aacb048e7d0cc32c43f5c0ececd19cc42af14810e41220bbc641806b0fd3049153820790a0ff9902dc963c7000ab025ee076","signature":"0x99f103665d90e34c06e68814113f135dca1197c9715bb9fa88f2ba4a8fbe4fddda97a7b4cf525a5e464c6c5b7892df9a06c1dafdc1d416993cc393ce1d7d7d7a5c3ba11c7b1c779985befea81b7b773e92209f53a01ec03f8ea1638c5b79ae83","withdrawalCredentials":"0x01000000000000000000000046e2225d8a4ff3c5cee4400a2f68bed19ef8c747"},{"depositDataRoot":"0x96172758c962ba0f3c54026dd41239d68d42aeca09c4c7721e1cebe85e20d410","publicKey":"0xa83f7b63f40ed0839f8a1e7132dedfdd253cf8bb5f2456e90a2fefc823985253a6c39fcb5d1ff29a7e639e8924db7eec","operatorIds":[208,209,210,211],"shares":"0xae10356ffd1693424698645c2a24c041eee47e932c1766075afae3204e8d9495aea5ba657c2b55f43f4345ec447560ba026cb1d2d38475d6fbe277899074d1a118830dd08f7359ee31227517f5752f577ec981f0c445e870e7fe4f357ce89c4ba42a84cf857702022997e727ccba46bceb630d46d9eb7b3fb249abf25fc30e5cff599e0d8be9afddf5a35f6bdf205aada46dd8fb6351d2cb45d763f439ed5086449717ad5801915773bd4db9636c8cc80e980d3fd151b2b470fb8d25e4082c78a7a27ef6259f8dab06d8fe69e2fcf8b4b89e5ac93dfb46a997a1ad95fafd10cc165f27e3e38cfd8f5d33c6bdf2351207a19a2d2e280cbb4930f4f1df2afee5b8f644abf2522625c98541a5a2b28d6ee99eada702da4ea35330a30fc0c96ba46f87528eade3e8342c0bbdf17716c302946b708d9a62198720a7d62535e3f096fbe5538c956d4a001bbbcff142220d4d3aa0681b929277cd2682b732b5d380d9f051d2c3c2100db9a68456020d77490e273202d65c4468bcb1654e7895185407c735e871a3856be94154811139a28ee35b422d50702d7cce4251008a76decb669e6d1583f21f90bba125ccdf1795984921d35890a329d9c1c759865a6c4d33be43f9333bc3606089ab3cfa987b3a3e1ea0fe8358b2e75bc47614ee914e177fe45471e41faa64ee4523c062c1866bcfe11c644c831f5fab09d9d7aaa1851655c796a5db7450f498bd170d66586c22eb75843433aa16963c4649c93da7c985be0a5792deb70a7db33d27d2d55907eb2ea0868c1124060a1baa6f1a222c8c547c3b10f41a9f2e8169749b8132b71c384011f37750056cc35dc60206144c6c9fae89a36f9e49bd6467b47cabaaf4d20f24aad6c1577acc6b5c095ef70f23c2e74e2251082d5f77e5aea6e6d843d2b7d3f63886a2b2e2aaa635aad9a3da5f60ae925d740a18a7e0b0ae6bcf6c195ae8b6e132a5f794f54548d8a94c2d8b5cc5109b509ebbfa127fda102dbf69ff691a31eb020183f0f712fad7b25451eb6743d53bd37b974fab2cfefa170cee8e4399c0adff82ca9e3906172144f4512888213bde0ac122c376aebd74f8af7cb29659432331694a27cdf5f6517dcf4355c6bcfa8f818a1a2f2a5ace4d5f39cfa079ed17743015eb4d2878236c2a222c573752ddfe21eadbe9f7fc0678e616e625a0541c5b671813cbbba01d28a81b9af69463af05be38d9ff16d8d36586d3c585aabbc0725d63848a452cf0fd3fd30c482d3eeaa3e7768267c257632183bc23baf84222e858283504f3b1b27a29d3f1275038217359a9c29e3f7f2b49d81340c72ecbb6251c23ce00805a74bdad9edf7399f97a7e2d90729d52aab643fe07f34f18cbbbf704b95005b08b5d153f5a58d66acbd57a8180079c7c241adda0794d2ef40324681b7c08cfa7be957cc79450dabdc9c18568b77637c7b9d396319e798b76fc853a3d8a44dfd0a2f279b3e47c581d952aadbd86343a9984910207ab867066921a1a4d1345620882b1aad35473dbeb753aeb3289195bcea81f4aa1ff8a7ef6945e7579cf31c7c12b4b7679385eec0b64205defd419d84f428ac3b126e0016795093028a93860897581b549efddbcd4f55bc18cf12407412081129ea05a54c54b549aeb26fc0ee43b0636ce67dbcc6a7f661ed37952f0e18a03f91b67dfd154bfd9ac24e89cde2f8df3fa85d18192f55946fda1dafa32c9460a80c8c1581719025fde2ca2c60918afc4c133e5c9510e73f229669b2789d85ea6bf4dd9528e31e554573e4c5412d873dbe89f7d865711872aa229a1de952de13b7a27237e6ff145ffa0efc08ad08c0395bac0386ab2e9c592917e12","signature":"0x80ed7bd6bb2cc591c92974902c3fc99cc65af74a5405fb2e2b695bf2dad94fd0ebe4949dc5eee47d62c5fb6a9d99746400a89f67c9882eb4368cb9da40f4ec4372882effc4d99132032ef2dac40578110d490e2a9f7bde8abc42f6e8b754b255","withdrawalCredentials":"0x0100000000000000000000007172607a82934639cb025f4546ce9f5816aced9d"},{"depositDataRoot":"0x637ac573dc74503227b81f82e3d82d8ba97c46314fd1e4be294c8c15359afca8","publicKey":"0x94e0cf08a1db5cbdb2301324a19e364e65f279b74de4b0a741e8834bbd56c769678799a745bcef49323233a2cd26f2bf","operatorIds":[208,209,210,211],"shares":"0xa942ac5210936faa60ff04333424d05171c964d56b6aaeb6974998e8d361934f85b17b903a6b472340115bcb7f5ab09f17d28d577c8a4a2deccb5bbd50dcc9b813ea0b683221ee76205aa0f500eb7ba9393ce5c32c3e028014c48d0ef5ee4f7bb41e9f58bf77fc1569d05bb5cfff83099ee921262e8f872a66b2fd1c6ce93bafc87df75a2a66bd4c57543957286cff8992d867f474bd01aa3a3a661f9cd51be56d153feaa6cb14a703731976198183909c40f290cd827760a17c9db929d1d195873efee581ac10ba354eeb6f93794eacce05844fb4e19c548c8c39a9d641742afebc6fff08550d65a8b38cc51a6d1e07801e694c1e33f3eaa02d20da2a421f2b3db5b5bad185c926c6a17c633d2a78c508158439586bafa2c8a44ab3fde390b42a91749ec927dfa761ab705d85ee47da7546ccb45e60b49fcbb6391e0efad879be4dfeef5d3281f3df6bd1970072f3e949250e35cc61f64286ba67e2aeb9bbeaecd292eff5772e8d217219cf3c0b0d0f05d080f16e80ce30fc764a3f1817bbb818502acf83258571ff155dfcbb514e78e965f577abf361f040a9d5faeb7f81cb56d0be7eccd7333fc132f7e369aa4d79c77949a8fd4eebb0a46c2a722e6f7e394a4898dc7e688dfc682bd1a878ed8c320f672bd715d1b8873541f5975a5290c3714da56573e6447d2270def1f25d71fa7e420732871b4ebcf16dd668fe52512e7d1d151eb7514386d3e9dcb2159d9c6df5f2daec76093f6c8be17b262d444d811acd2e8c5978e59959065f425ade6fa43895e0f34782551c38bd9c3ca6594a0c2babbadec4bf188a22799349cf3986e9aceef189de80d9ff61f3d3619b482ab32e7019ddd998b3b16cd17dfeafbca0b7296eb9971ec3415a466950b9975350ace6ee6dd5cab5556b158cd813d56ed654eda4a4ac2674ed39e0f9ac696c55a0a83475e2eb92102df5d9689fccd97b3919493165ca668d02e925e80e27fb756e83d5374d2d4791990a64dbaec876f5225192389d53dd0cfe34c9df39fc2fdb70a6b71c64bfe84c1fbeea46c47442c571f5b36f42b4225cd778bc98a417e969f79937bc2d7f867e695b92f73d3b55478bf50f30f1f6b9f4d1bba634727884f350ba64ee67e9b00c7115c86b9e59d342a3806b37d7df489f7512cade8f2de061ce7b6e436a9b95eb476c6de540c62bd993b8fb4ca11ad9214614f555aa1d25e35acdac590fd5cd0d4d327c09605f014848d98c60758e3c24af0d9c3030256af7ffb2a9962452d4c4a579f9ac81b8dee6857ff5e7a5eeaa52518b2f1df95fc331ae0f5ab84983c5369e2c21eb6030e4574629824d03e3302c0450c86b055b459e3e6f2c447ca84a39ac1e5245b32e5fd87ef3fe20e04f7c565831c3770506de5b6183b03dacdf4cdfa662b20ab849eaebe68dad01fb16216e45ff6c1144832a3be32a377bdebae56a0a05d5c8b957b57fea24888a22b1b033a1a3d82de03176a928828be3138bdb433ac8c293430cbd434c0c47d4356786ce8e4ec93dec405cf95a66545ab439589432563103eb0952022530b6c686d7160d5e77ca764c946f7b6972be5662501cc4f46c8804535f65dd733d461e6ae40043aa2a9fac382acf8e7d62243fdc10e90ca94608e6bf69aa3e00b544b4714357cb1a0004db295f9fa288479a8d4ee05fcd7a33a19adf28e92e4f7b135f428858914e100b4de4c857bd960f85d9e61a39d6f65cfe360ae4f120ecabd1a9ad7ee4d7096d6a43b9da9ea97db4bbeab137080d080a805d83be8785f31326f775c9b12ddf6f8b3d06a11d0437e2e2b8d676c707e9417cee0e2aed33ef31ecb3025219e66df7d8b80f2f7326662d","signature":"0x851cf1e43c8d42d5da49e47c40513e030c50b1dc7fd378111c705118c11d58b3ee45b0fd1ea88dc77a0045d364cc91c614c2095625cab2ca623140e77facf827e0d8cc639e524dd113bb6c9874f46f06714ed60ae527ec70bc1ae6a9b0039350","withdrawalCredentials":"0x010000000000000000000000755e81deb274d40bf365f03bee8c4a6066180303"},{"depositDataRoot":"0xc7ff6d10f26a5bfce9f03ad873e091cb387ce6360052fd76be4448c301aaf663","publicKey":"0xad4796b9f5661c6be33bb9c42fbaa7712a97b9fbc78fefa6a1752b5e1e78e390328196386421546de0a7a4c3dd17f02d","operatorIds":[208,209,210,211],"shares":"0xb254da886784ad218a23f0685d73d6ee440ef173c86f49e335b3eca3e9498f402e8858bff37523fd3765b5e399cd69221438fd1489a8808fb1b65d65556e8a838d9275e2efd88a049b059457c475e6abc30cb1c96e1f178245dd565a5123f7d998a25e381f7cec6a753d61d210493b65b266095b8fa00cd3811e1082e358239206c0f5f73570ae6925221f5763bb48d999b81dbd5d68697ea641bfb41cb00cfbf0739ec2799d270c827419298f127ad5c413602a29675fe24c80bf27b7027267a8fa0559ca416fb1a68a8e167fa20e4b8360d14e9018bd36593e1bda63c5b05ce74b363e034ebe5116229867e4fef5b78f39c706ed87a1ece9cc235ec2c4272dccb94a28a6fc67cc7a4a9644f44585b250a0a8b071bb2f1c9aee506e9fa9bc5221a7c16a6e1fd67f9eb5a8c9c56e10d5ecdef8f05756c1a32c0df6bd9d9d885ea209725b30d4a3bd2da80bf9a6b96956e941332e8ddb1db6380ad252bdb5ef61ca7010bf4231a8019babec6b85d65e735a07aa99b6007a1378ff852623e95ba15b4c3ca2eb42377f6904ec9b643a7768d3ca90343b6f1d0c3cb458c6773ebb6fd3a3d0881188beea0a8854738d86b6899e42979af0c30048a5861be9ddbb1645b8d8954935a1651940f0b88327178481678d6619b6b7c046a5ad0fb927b2979ce78bd2962cde1b01758bc920a0e586c6e08f68bcb6db0c8622b5304ae87f2748fa40042ce0b8fa4c1ab5d34ba724b7bcd222d61f7203300a0cd2c80cf6471814566b8efba63358948eca952d1794fea4591f68305c941b00d7183cc88d111efc4e14df7517c96cdd5185d4fee00f6cc95cb3b556275b14a79edd7a664b375e637411612a4bfd63c569634389fcff6bc1f9bc7fb8d4f19215f866b56411bbf256a304cbb3a21e0cd6317330925d70d4880fd223c4ac48ae29a2764078232495b7b6fb856e8bdd910869912fe670ecb959c13e9aa4e1ce0ab58eed3a51aa412fd8a6fa3c14b1f99a831371e3429c9adc52a2c17ef9cedb7e78b83dd230016176c2583a8a3e66f42ab088dcc17819012944141192d75f0e0be29d440f31be7b01e9a69eba47dba5baceddd508fdd17a0fa09ddccd2188737fb0fc25c5d61075df4db49ff99ef7f49bc7b9354ad50cc0c3920b7c949265c7f495ee1bfdfe2d9f0f1ec96dc45a556207f775952d6e1d70f1a97759c45254d2653e33f18fdb30b29c59f71725bb175f02fcd323a0b774977d7ccde13e65cafc0eefcf360ed36b8a760f18c1f75d251f2e96ddeb1a86d21fbb1b0cf4daab0087edea1ee625def046970538aa926e326638227c5abf0de1acc0788d88b9a6d6df8c74599c9383c7d6a927aeace58624a83c66a905295fa27560b8689a561ad130e5f5622837fcd66dcdcea8e39ff18550c340141029d9bc67d5b52fc7e594dd794a609e06d4381289922783eb7a07acd8883d22b204d5729aa198442947d737bced6e604d0441ebca2e71177e9363b7b66a717993585ae25e2345a531a08fdf5384aa387788233b84f82bb498867108764416e8d0948562bc221cbd97b2ebd88faf3ca269488d85ed453cd3e07c55e7118e114d43c46e46d2289cf022743da7aaebec0a3bb0321dbae2c61e1105e073ed165edbe361d28d50f4c262b7a6c1cc554368612d06cc467ed20abe257aaefe47dd053f4b4881946af68a62f9c96dd3d60da603db3291b7d7a9b98471c5001ab7846c141ccebd3029d491523a5431dc6e5fbf4bc50a478ec52ce2fe204fb137559caa2759cda03f985fb6621bd594d27fce18326d85f81058a3ec24b27b160b18bed4e033bcec17edd17f09edfc0b8fb1bcd83894f2d8cab7823e","signature":"0x837109b2254137804acd977fbb2fab5183f64e0ed2797a925188a1f4c8d0eb562372291cedade3e65da69bc4e341f55b06bf902bd8bbae89c53801aa4b13d8e6505364e306f50a691208067f8bee1924916277916ed9822e7214f90614345ee8","withdrawalCredentials":"0x010000000000000000000000340c9f1044d1bbc10285f8bb1637d1df08a56010"}]} \ No newline at end of file +{"0x728474D29c2F81eb17a669a7582A2C17f1042b57":[{"depositDataRoot":"0x3a597b0294069aaf015890535027ea6f038d2c7564fe273d3b77c96ab1b26102","publicKey":"0xaf79de0307a5d004304e0d84261fd2cd2ff79fba9ed642d3217702fb544ad6404aa34589d1c8528cb47844f06980ba5d","operatorIds":[208,209,210,211],"shares":"0x85b35248d6513faa0bf962b9099fb705807289e233ddb0f7493afbcbf0328cfa7bbbff35a33dc1dcae89bdf5284cd65a0525e47edf49cd44bdd47cfbea76229f9ee5b96234772a911115e1a4d402812dee5a857e8794844e6e023c85b6b9ecdaa47311410ac0240a9f5ba1bd98bdbe447f8a0824b30134fe7135eb7a59735af5d0da6caa6031f5466ff933b125513cf0952036b2091c372f6ebde9b7e5da2391a91f9d0c5596a80b1934b1c292f16e8585296f324fd48860a3b4ad1a5747a94184cf02339eb566bba7c6ad4f48c5d5d734eca6b7ea4c438983f534c82801bc68113e1b3c24f9342c27164dd317da79748256c1835d4779b76926a0c543fe5ea1278e47dc506dc8809f0479d8cf7512d1db83722f70ce37b4382e56cc11d6864402c5f0848a4c8ca0dc086520dfa96fb02108868cdd46117351a66dc7ff9eecde33ae8a24e34473e350dfb9d811c7bfc29df8572bfd7531c0a53a7a4f11fe9c3cbcccc09c49064f1a1c1ecef2185832ae75a1431df1854c506b8617e1d9f2b1d21ac95fff92270128dc91c01363bc4d7bf20b07f362009b9e8e22aee336535af926e15c81147523a9b07ede7dda839d5f47c655e8e1f27fa145a1ec2940a9932804044e6d9ce3c3690bd0e0fd1445958b7a0d41cfa9d30d934b575cd92da451f22ea97991819441d044268ac48041a039e8a3ab979a39862e241ba8acabc832213d73b3ccfece4096bf07640fd67dd63dc5b9b5fb142dfe59e1dbbd2c212251541dad0f2bdb328cd9c301ab197ef1b431dce36bae4162cae1669e71a63fe0c75ade8845b310d98d843567f16b740ecf0fa5581aba25d885898a62dccd5e82f9fef27d1d67ad0aa55ce0885f6b962d2bbadde2beaa0b984f8f0493ba03abd2820cbbb69ea99a2e73a5ac9db7a548683361d6a9a12fdc716146fe4e27e7abb14f0465a1ff1cca143a53f3387d5721d60a43bcab7683b487ff7b520eaf5020aa4e5f4185c0f85ca6aeb26c0c2c0c03a79f76b0f22d379d7a98bbe9f5a0c237eaa99637dd9095d69271345dd1e11fa2e748ae69376d8516f0827209b0f0f191ac2f25769119bb78fb0ea1943000c48472cceb5322f62f96a136756fe0612c8953d31489793cbc5dd9e1de68392dda6bfb0ca2939c4891fff14a20e472b32b0399c0ba8e93d208331bd3591aa70e14a1f8e9824a5a9cd965bc70ff644f45f5da51f875a0bb90dee49ecefea0111deb8859b24f206ed747d9b52d5a8a3cd56dc6684125ed06c9b082e503313b75c93b9f79062ad1f5a6a48fe5ea7c666142bd1f094dee137396a92af1a9ba69469e57196b35e87bf9390ca72796dc6724488c09609375a5cc468bfe9f4c339569b12873ba024aad79e24d10e6248e2a82f5937f7460d7275f3321ca51782a0c162a296b2c7fcc4500091eddff52d3ac15254f54f07999fdd040ccaec4119c08c9de9156f05d9aea6f0744e18143b4a261d06018db3706064524f59b540bf48da6b640f4367572265073bf1ef089afbfb8a5e18cf08836a17fc89adc496c0506db8892aa0b4cbfe4f1ebd6a7db56e3df3c12e11318caace963ffb6a9f6f22a616393733441478e03efb6f9c81bdf54bec54cdba01ebba982625bd299681e1bf5b5b2af1d788a487c3d4036db260e4ff025e354bca5ae093fccae561e9f1db6515abf5ba81b8d90a4f8348e43fdc07734e6d84cda9c3ec2785218da316c6c584e59a0697901281f306dc0474487b03d172675962ecc3ea53f86b1e850e628489df9d45176f26f1ddee2897fed4317245502db0913c3473ee36d72e4a191e9145e18f815b9c7c33d588dfc8bfb00af1caf44191c090bf88f","signature":"0xb2d0ebc125aabc27f49f318ec8f932b76e0bac20527a1780848f893950ae0150dc0f8e0641b3fe6e9916658eff76c6250598d29e8b084cc5a40e2ed5c6f164eba66bea35294c67f9535e863f29b2079253accffefbd05ed052662edbe8dab04e","withdrawalCredentials":"0x010000000000000000000000121a5b4c7975e8794b8f2f43d69def78facb56dd"},{"depositDataRoot":"0xb4eb86cc066b73ec41d3ca9226e348c0811bfe741276e33f15c310018d082f76","publicKey":"0x959ae6e5086ab047f5a269fdda36313d3e163b7f14bc2508c8e508e2a69f42e4b86159543e2abf36b0ae9a94e553513d","operatorIds":[208,209,210,211],"shares":"0xaf52de9d2265f7a5eae61d2ec5390d6d2cce48769cca3a4d42537f568ff7bcc1516b7f70e911699059efdb3a53897514104e9dbbaaca185d0b55895e2c25c4a6f04bab187ec6b146e83d59341695155ef49e93a44d18d254115517721f2fe0b38139c75e4b812bd8a02fc99d57f6e3f51733f2478dc4885aa3f7be39ccc8f432e1f16ad6d0459f0f3c771721f80a5fc695f2a82e4d022a958a90ebd43c897075bd5b1129ef14cf06cfe4f3fe5c3390e557f9f56ce2a9e95a2433d6947ac1f87a8fb994175030296cd5b6272f8e87d99846f9b9ebd3ab28a9947697d0651a51db9b1e17fdf3f9ebabb480ecfd97555525b1334e1d698a11354b300e4276b637a5e9fcc5ecc4de48df9b6bd215095cf754b234c47c9916c9f24e1980dedbe9d51a0129fd07416d41a531fdc67faf01fe878f7d3af92500b10a21a6d4c6f135993e0726449e45d72cab56167e3672cb12e0d5ff12bf89b9f53cec7109804c77f82ca0e168dc4c8e75804467219ca202b0292d6e3db07515fcbce66f122af2b01189643569f6ea1235612589341ea10ab350824bbe35b16bcdb8cc6c2dc97e9c38de87ba7198b94eb97142e449abcbda786b2b9ee4b7af72a91cdc5209ca16419de06b4d8181ccf25446c525f129773da94c3048943d3623c101e914ad6d86fe60f4d561be3bd62b62dbb949e1c1c7f544ba4ec24f33a58a6f44d3ad91c454750b60a75a793b8f58e082de1e675bc0b417dc76c93105a94d0c3418738ae8bf8fcf990532a008c7c3971d2d89a02ef5a1c597355c370af1b52fd8ae222b7ac3868518978e344398529a583d3e68b8ac8f39c575e20d930bc665c5849ce045ab9d5947ea694d5806021fef08cd67eb6f32fdbc6e5040c35fc05dc7bb490ee819d4c4552f3daabb05f6602fddd73c6b0ffc637e832acc96e5bfe9caa5c455c170d5c8b91150321c27aee308bc05268499d34acf0859b4f5a3e149db2fa79e26bdb33b4766e75e99dca305bbcd26b2c358590a771c6da8966de097cf420261b1988e09421a689456bf0511e695662a8704f759ea37622fa2d0bdb433b113c237f31f1c5f1372902c9cba3cc01e92b81dba4eca5f04108d4596e2fdb1877baca2b724614f7dd3e5b57e1eb5ad26cc5e56cd98e0ce906ae8a825562220158484d9cef707de872cc6a4668448767264875ec064fa8fdef725e189fd0be82ff87296b6ca38c0c6ce3ca4622a028207cc80bedc216bcd40990b774b0681dc50940e45089c671dcb8884d6c650106b23dc3e3cc7bdfccf514bbefe9530ce2317233a950035c8dc129219e97fffc3d3768b28e0f7d6d09465276d88853e603966a75258482bed6f0c9bbff9981c9a623f69f0a99c9d3c3568891108eb8ca28b0cf50b6cec091fcf235442e32a8d6c3a38ee2814a0228110608e8a17e1a7eed1e2ea15a77d0c2a04fb8a5d23fe0cc0cc2942cd65d62b9458182812915f543b87024f815520e5f3608b6b2e88a6dc4afd390ce86f616ddde92a942309763b93ca1d49beea7367fdaa7fe784244cc0926b6aec267feace31ed67ec83ad2a5cfa583cafa403cacc5757c41c623bdb7594e22c6930459d19d8f105be95076fe14a39f8067de73f8cc383dfce09bc0244a336f4de38510175bf49c70d2f5648de3abe063ba8c5b30228286034dcfc01e7a004030726ec814ee8faec64f005d3a23fa180847c48e4f67027a6feb3e0dbcdccd4ef0528ae65633a8f37a56d7ae8587c2e413718f63699edd5f964ce8247a9315115a4aa839490422ef3927eb5faf7e092c46465629bc4cb11d28716ba48370de03b177e234d68692afc1113e966f22225c3e027c9ec8203cc","signature":"0xa3984edd7d335c703d81ddd43c8ff6b27839832e21cd1fb0623aafdf834a774917cad37fcba1b9f61fb69ab0a583c366038faca1700973489907b09633bb67d9b19e36759bde9683a49c88988c0cb9fada722bc60127ce09f580f0c4d4cade69","withdrawalCredentials":"0x0100000000000000000000008968dfe1faf05f75b7dd7a67a4c12f2d66d1e562"},{"depositDataRoot":"0xfc41d1e5bb5a1123ee59e63dc20a9cfe747b20baa244f68b093b6bb976470cee","publicKey":"0x94769a92e1f1bfa24e7feefb3397a2ef267afe178d188f91d1a87193a1ad1ac9e0621dfd9fc3be2e79f0780847dc591d","operatorIds":[208,209,210,211],"shares":"0xa5a0173893b7078113e5edae301eb1ab41a89333d00f61741a628cde5427451343634c1bb58bd4fb06bdd47bfc91fae809f85f8d14122b192b610b2ac6abae67c9a9b6ea2f1db41bba116aea3ad3c5669d064fff55469bdd21272aa2420ec124aee7034a653803ce2805e3274fe3c13c19096e0b84bab08ddc2374b22330e77392189b75aa28668444eb55a26cf3dcfc98229d4ffca9055fa03b74671568cd8b22dfff9998ebfb3dc0027da1c0bfa7985b5aaf8abf7e751abab0f1dc7c345ec28c8b1655d2e3373ea7f59178fcb46a385ab66bddb842de0287721d8d4d16f160fb237b14a9fbe58a87fb091104fa5b6da2bb403937d9d6fd1ee7a3a599d8b53602211a6d8b501177f823e6d58901c45b8f8a95182d4f3b48e526078c6f32d79b2c9fa469f06567ab44af5d9b5f8158ab00c86a81291daf67f4c4e9f1a95e913dcc4d259911f0a7dfc1742aa96108abb7dd517731a1d5098cd32de08b447d037983779108fe79a2cd57b7d16dbd9ed1c0d456958815f2fb5cc00842a21bfab62eafe2fa6cabd4d8f42547760400d3b2d157bd9b84f2e319e203e54eaff52190e544eeeebff0cf044b6a271a04f6581cc04cb14e54fc89f233d2977c8d29829b39e0aade10c45cff3973bf32a8a41bba2d16246cc1c7da5634b88f826a12a25749ac949231d8cdb9b7d384400206ff59fb5b44f99da9d8af5bd45c6b494e5ed6ce1721f7171d963537abad051ea17c8d809b1cb2828917da165a7269ec23ca87a38de8a41e1625eae14cc6449df2a3207c1fa7aa9e5340c8cabd9db896acd92d34c43c0687de760a86534e4b74313798a7890369478f54e2073f2f83fd356b55461e514c8984acd14a2215972f63c772ec2bcd97669bd2ceb6cccd707a2e56ad6832bb7d075c8ce10e3522e704afcac1edd94037c407e968722aed47892a5efe7998b44134070bbbfc1f95662ddcf4713fd8096d95584123a1179563fa41df21026c2c87e74023250e7bf34fd570fe9eccb005c11f5a796303d710e784e91bbde1a4c1e73eedd4459cedd2cbcc3ea743a5a0f3dda0f0b929011dc051337a0d720438bcafc91d285692b80cce6dbfa7141d3226895bac45f98921c4bf795a78652210ac7be2ea223272e40777425f51727629357667fcf2ee1c6c2e59979e6a20d21799cc6456b65dbecf0066fed927c6d83721f4722a2b2535e2dd5fb9ca402091d56f5ed4918f428c16057999bf8c250cffc01e233c52493d7e2d0a3c46868ed488f64c8ca9359bfacad604b87d04f1f94663371f9628193a3e89a7c353b1f9ffd78236bf1cd9d412f6678b80afe076d3d8d1ceb0684ed067cf1fe876e3f5f55141a8e35535e43344674650bad44abf7ec6a4f427fe34d987bf53f6104eb54a0a357c182f968bd82731750e5759a6ec78c00ff1a72f164dde0ea5b0ba734dcc8cffc33251c3895fb2c5c290d38e8d9b470c549f421eb6a2baa0f4dfd3f76fe985833cfa614ed8e13dcae64bbeb2d6a324ae7969e89bb1b1253cb49da3bfa5fd1d52e52cdd474f97f4195aafc0da04f07c48be9112690fa95478d798fd57a6d292328b6233364d04411ca08ac7c9a6093aeb6ca58c8057d24f7a5240affb79610c662b3ea10eb83fed8d3958a6db7e36b5bd9b4219b0ebc37f0097e8ecf5b0afb76ac0ad00ad5101634720e1805ef76f1a21b039d56bcc98dece46436ae949bc6709f140551f6db6413d2ee48bf76ee56ac37b76da3b5c7b7bc9189b85f77d839607e9c38510883da9173abcf5b72aadffce873b36575ad320cd4f9c89af2cb5bd308ce6d1560c9b52d9c9c8675450fbbbc45449285b7af8244a4f62662443a8c5","signature":"0xb605fe68cf373bfe584cc752c24c499001752e6f8b9339fb1ad5aabf16cdc4b114f83b4c154bbdd9f0f95e1325306a970fe58874b13e5c803ae818d70e1823db1e061d4c5fa365260811e66db202a8f01caa908486d48cea1cc6ef25f3465888","withdrawalCredentials":"0x010000000000000000000000e5de8980262bf6428a78767ff52bd638467da317"},{"depositDataRoot":"0x3d066d2c54bb44baacd5865fe257b7ea08fcb665b34ee2e951cfe6694225a5f2","publicKey":"0x82735db3352ae919ad19becebacc3f9fb5322d65d3bdca6d0d9b2e8a77f772c6163512ec207c9ebe522d38505ac98867","operatorIds":[208,209,210,211],"shares":"0xb5993ae83aaf93eb6b00ea2650577989fd3ddea7469da71ef637e48e0c8bc901562e578303020a5e5c8e26e56974edf40d0799fd9043a40fdededbb362c4cf139ce496c581d569b6646c433b9e966170e77400d7e316d65fc94a6652489379c9989bf6c4e1e1975a456eecd66a559913bf612371e86ca2310861cbb23a3f63bdc4e23a6822db8fdd177a6ca06ff3f7f4b76156ca7676a8dcb66e6a57009fb9f7de812e7b5a38bbfaf404ac2eb65621aaa17d7f2356235d7f330dbe6d4b9eeb0c8f94f14fd857aa0dc928ad7a1ab5a86945d285c3edae20e2792fccb11d860d02568c5804da2e3501d2fef194426bd3a987f8d6a23e0d41cbcab10c558bab94865e4bc73cb59b8fb4d7154ab200068e8f36ef5756e9505adfb16d2d95ffeda345579e9d21d27fff75ecb4518dfcff7c570ddba3cada7d27807a5b4b95926b67f95e4dda5426ec43d1fdcdab58c0c2109c8c18c84b9dd21621f66ff0ac438e8b0c9c65eaf889f700674323fee841528244063f742a1c9062d123c60cb2a7d87aec9f390fb45b476e80888df49bfd642a2ae4b8ce8274052279c1be5c5666efbe4f5c6f6373dc71c10aab21709cca6a3ca0deaa0f8dc7df79dc886da4bd769ceac7e4d6767b2972a91cdce8ef13923e63924d38497bdab54f08511a6a3cd4f2fe05358f446b6eff9eb70f3f797316547f57b9c3da211af28779f3529f5d8f3779912f84877d35ff3bc67fd2cd4d9d8626a3fe4164337125ddeec0e5338a8191dd5a8f93db41e89505c11794e45a94d30cb50c841f1ad411ef84bcb26c2dee006d7330789a78040334503edbfe91dd87b62fb603b662d3db2f77b079851361a615c90de2285f4d973cb9f84fadce07899c4e9f4452846b25b2340aa33d522dbfd646f2696d9a6cc93fa427f12ac09a1463a4da29adfac2687a6a04b8ae35e6263ce95d7d63fae0019d9357373beafd04b9ff4bdbac731b79d77dc2f12bd934709ed635e4588283f327cd9ed174056c76741c383bdd551bbe6826cccc2728298565a551598fee93f1b6752cb4ec8fd921d371bedbdaad8fcaf40883d45307c3e328a3ec02c5fbe36243bd74e9f7b59872ea826c9b8694d9635d2ce6f49fa60694b35fcd6d8233b47822598bf7c3cca9c54f578114a9cdfa912cc299b290189592f1ac2d273b09ac5579115bcfe2d26cf04f3cba8ac44d06181c1fc2cb8df3041f856a182df1d3c14ce7b78ea4a74fda72c402b87ce5fd406636e6541f7cf5edb7e804a37259535ec8e9aa3e429a9c0c0ad45a6fc025d2be7783540cf89f2cba7b47902829358597eaf5ff8f58972de338215573c0e83ea28c332ae5cd02d1b7b172d65b10ff2e8d1698e844933093d57bb806081c5773bbd1b598e8047eef50c4ab32834ed0b312800e6ce2ba342f17c615c007deff8b74c3580723c2dabdec51c1833f4e392cb02cf9fbc1e3cd3ad4cd30c4e1b27c907ab275c9f24ff6ad0dfbcab05160c70af776b978a17c5c174a57a8d7e6f9b65057781c90d3fc6fbae104e83649a07ea12c5587696df64350fa4c93e9290d4bfcfc9777b2fe9d7319564d32f46ccccc50df2787d6fd761e6210bd8fbc76033b24c0665169f6ea9464e33e9b6c5c468e482e73fc24fbf2d01889cde3767414f2d3be1b26d930ad58796a951e62eb9d474a86e514b3075506de6615afa9ffc4b4f1c5e7fa62cadefd4be6e8b6da4790e15aee428a8c227e37d98efbcf148c7fae151ac27c8970c56909b86449892bc60ee413730b054d6edd7c5f63072c96b32ced62ed27deb49b4889aff5f9b23864e072917c78f51e746df99b0910429a1bab2d4bdcd3fe4e5a8a9f3e3acd8a","signature":"0x8f930a051312e8936530d4182e9d096091add912968fa9905c390a39c0f44f8157c6b6b02a60ce659cc3ad470bba0d9d068c9e52fa037ba332a259f09c9462f2a6621be70813afa018679197dd965db470189ee83f939c70dcab52674fae194d","withdrawalCredentials":"0x010000000000000000000000394c6ea30803eb70f2ef5544e312a00d25461c4b"}]} \ No newline at end of file diff --git a/contracts/ethereum/scripts/dev.ts b/contracts/ethereum/scripts/dev.ts index 203f151b1..8e450427b 100644 --- a/contracts/ethereum/scripts/dev.ts +++ b/contracts/ethereum/scripts/dev.ts @@ -179,7 +179,7 @@ void async function () { await functionsOracle.addAuthorizedSenders([donTransmitter.address, managerAddress]) const ssvViews = await ethers.getContractAt(ISSVViewsAbi, process.env.SSV_VIEWS_ADDRESS as string) as ISSVViews - const preregisteredOperatorIds = process.env.PREREGISTERED_OPERATOR_IDS?.split(',').map(id => parseInt(id)) || [208, 209, 210, 211, 212, 213, 214, 215] + const preregisteredOperatorIds = process.env.PREREGISTERED_OPERATOR_IDS?.split(',').map(id => parseInt(id)) || [208, 209, 210, 211/*, 212, 213, 214, 215*/] if (preregisteredOperatorIds.length < 4) throw new Error('Not enough operator ids provided') const preregisteredBalance = ethers.utils.parseEther('10') for (const operatorId of preregisteredOperatorIds) { diff --git a/contracts/ethereum/src/v1/dev/CasimirCore.sol b/contracts/ethereum/src/v1/dev/CasimirCore.sol new file mode 100644 index 000000000..d24fb99f1 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/CasimirCore.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./interfaces/ICasimirCore.sol"; + +/// @title Core shared methods +abstract contract CasimirCoreDev is ICasimirCoreDev { + /// @dev Validate an address is not the zero address + function onlyAddress(address checkAddress) internal pure { + if (checkAddress == address(0)) { + revert InvalidAddress(); + } + } +} diff --git a/contracts/ethereum/src/v1/dev/CasimirFactory.sol b/contracts/ethereum/src/v1/dev/CasimirFactory.sol new file mode 100644 index 000000000..41b1da0ff --- /dev/null +++ b/contracts/ethereum/src/v1/dev/CasimirFactory.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./CasimirCore.sol"; +import "./interfaces/ICasimirFactory.sol"; +import "./interfaces/ICasimirManager.sol"; +import "./interfaces/ICasimirRegistry.sol"; +import "./interfaces/ICasimirUpkeep.sol"; +import "./libraries/CasimirBeacon.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +/// @title Factory that deploys and configures managers +contract CasimirFactoryDev is ICasimirFactoryDev, CasimirCoreDev, Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable { + /** + * @inheritdoc ICasimirFactoryDev + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + address public immutable managerBeaconAddress; + /** + * @inheritdoc ICasimirFactoryDev + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + address public immutable poolBeaconAddress; + /** + * @inheritdoc ICasimirFactoryDev + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + address public immutable registryBeaconAddress; + /** + * @inheritdoc ICasimirFactoryDev + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + address public immutable upkeepBeaconAddress; + /** + * @inheritdoc ICasimirFactoryDev + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + address public immutable viewsBeaconAddress; + /// @dev Deployed manager addresses + mapping(uint32 => address) private managerAddresses; + /// @dev Deployed views addresses + mapping(uint32 => address) private viewsAddresses; + /// @dev Last manager ID + uint32 private lastManagerId; + /// @dev Storage gap + uint256[50] private __gap; + + /** + * @dev Constructor + * @param managerBeaconAddress_ Manager beacon address + * @param poolBeaconAddress_ Pool beacon address + * @param registryBeaconAddress_ Registry beacon address + * @param upkeepBeaconAddress_ Upkeep beacon address + * @param viewsBeaconAddress_ Views beacon address + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor( + address managerBeaconAddress_, + address poolBeaconAddress_, + address registryBeaconAddress_, + address upkeepBeaconAddress_, + address viewsBeaconAddress_ + ) { + onlyAddress(managerBeaconAddress_); + onlyAddress(poolBeaconAddress_); + onlyAddress(registryBeaconAddress_); + onlyAddress(upkeepBeaconAddress_); + onlyAddress(viewsBeaconAddress_); + managerBeaconAddress = managerBeaconAddress_; + poolBeaconAddress = poolBeaconAddress_; + registryBeaconAddress = registryBeaconAddress_; + upkeepBeaconAddress = upkeepBeaconAddress_; + viewsBeaconAddress = viewsBeaconAddress_; + _disableInitializers(); + } + + /** + * @notice Initialize the contract + */ + function initialize() public initializer { + __Ownable_init(); + __ReentrancyGuard_init(); + } + + /// @inheritdoc ICasimirFactoryDev + function deployManager( + address daoOracleAddress, + address functionsOracleAddress, + Strategy memory strategy + ) external onlyOwner { + onlyAddress(daoOracleAddress); + onlyAddress(functionsOracleAddress); + managerAddresses[++lastManagerId] = CasimirBeaconDev.createManager( + managerBeaconAddress, + daoOracleAddress, + functionsOracleAddress, + strategy + ); + ICasimirManagerDev manager = ICasimirManagerDev(managerAddresses[lastManagerId]); + viewsAddresses[lastManagerId] = CasimirBeaconDev.createViews(viewsBeaconAddress, address(manager)); + emit ManagerDeployed(lastManagerId); + } + + /// @inheritdoc ICasimirFactoryDev + function getManagerConfig(uint32 managerId) external view returns (ManagerConfig memory) { + ICasimirManagerDev manager = ICasimirManagerDev(managerAddresses[managerId]); + ICasimirRegistryDev registry = ICasimirRegistryDev(manager.getRegistryAddress()); + ICasimirUpkeepDev upkeep = ICasimirUpkeepDev(manager.getUpkeepAddress()); + return + ManagerConfig({ + managerAddress: managerAddresses[managerId], + registryAddress: address(registry), + upkeepAddress: address(upkeep), + viewsAddress: viewsAddresses[managerId], + strategy: Strategy({ + minCollateral: registry.minCollateral(), + lockPeriod: manager.lockPeriod(), + userFee: manager.userFee(), + compoundStake: upkeep.compoundStake(), + eigenStake: manager.eigenStake(), + liquidStake: manager.liquidStake(), + privateOperators: registry.privateOperators(), + verifiedOperators: registry.verifiedOperators() + }) + }); + } + + /// @inheritdoc ICasimirFactoryDev + function getManagerIds() external view returns (uint32[] memory) { + uint32[] memory managerIds = new uint32[](lastManagerId); + for (uint32 i; i < lastManagerId; i++) { + managerIds[i] = i + 1; + } + return managerIds; + } + + /// @inheritdoc ICasimirFactoryDev + function getOwner() external view returns (address) { + return owner(); + } +} diff --git a/contracts/ethereum/src/v1/dev/CasimirManager.sol b/contracts/ethereum/src/v1/dev/CasimirManager.sol new file mode 100644 index 000000000..369143314 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/CasimirManager.sol @@ -0,0 +1,880 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./CasimirCore.sol"; +import "./interfaces/ICasimirFactory.sol"; +import "./interfaces/ICasimirManager.sol"; +import "./interfaces/ICasimirPool.sol"; +import "./interfaces/ICasimirRegistry.sol"; +import "./interfaces/ICasimirUpkeep.sol"; +import "./libraries/CasimirArray.sol"; +import "./libraries/CasimirBeacon.sol"; +import "./vendor/interfaces/ISSVNetwork.sol"; +import "./vendor/interfaces/IWETH9.sol"; +import "./vendor/interfaces/IFunctionsBillingRegistry.sol"; +import "./vendor/interfaces/IKeeperRegistrar.sol"; +import "./vendor/interfaces/IAutomationRegistry.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/beacon/IBeaconUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol"; +import "@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolState.sol"; +import "@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol"; + +/// @title Manager that accepts and distributes deposits +contract CasimirManagerDev is ICasimirManagerDev, CasimirCoreDev, Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable { + using CasimirArrayDev for uint32[]; + using CasimirArrayDev for bytes[]; + using CasimirArrayDev for Withdrawal[]; + + /// @inheritdoc ICasimirManagerDev + uint256 public lockPeriod; + /// @inheritdoc ICasimirManagerDev + uint32 public userFee; + /// @inheritdoc ICasimirManagerDev + bool public eigenStake; + /// @inheritdoc ICasimirManagerDev + bool public liquidStake; + /// @inheritdoc ICasimirManagerDev + uint32 public reportPeriod; + /// @inheritdoc ICasimirManagerDev + uint64 public functionsId; + /// @inheritdoc ICasimirManagerDev + uint256 public upkeepId; + /// @inheritdoc ICasimirManagerDev + uint256 public latestBeaconBalance; + /// @inheritdoc ICasimirManagerDev + uint256 public finalizableActivations; + /// @inheritdoc ICasimirManagerDev + uint256 public finalizableCompletedExits; + /// @inheritdoc ICasimirManagerDev + uint256 public requestedWithdrawalBalance; + /// @inheritdoc ICasimirManagerDev + uint256 public reservedFeeBalance; + /// @inheritdoc ICasimirManagerDev + uint256 public requestedExits; + /** + * @dev Chainlink functions billing registry contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + IFunctionsBillingRegistry private immutable functionsBillingRegistry; + /** + * @dev LINK ERC-20 token contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + LinkTokenInterface private immutable linkToken; + /** + * @dev Keeper registrar contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + IKeeperRegistrar private immutable keeperRegistrar; + /** + * @dev Automation registry contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + IAutomationRegistry private immutable keeperRegistry; + /** + * @dev SSV clusters contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + ISSVClusters private immutable ssvClusters; + /** + * @dev SSV ERC-20 token contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + IERC20Upgradeable private immutable ssvToken; + /** + * @dev Uniswap factory contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + IUniswapV3Factory private immutable swapFactory; + /** + * @dev Uniswap router contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + ISwapRouter private immutable swapRouter; + /** + * @dev WETH9 ERC-20 token contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + IWETH9 private immutable wethToken; + /// @dev Compound minimum (0.1 ETH) + uint256 private constant COMPOUND_MINIMUM = 100000000 gwei; + /// @dev Scale factor for each rewards to stake ratio + uint256 private constant SCALE_FACTOR = 1 ether; + /// @dev Uniswap 0.3% fee tier + uint24 private constant UNISWAP_FEE_TIER = 3000; + /// @dev Pool capacity + uint256 private constant POOL_CAPACITY = 32 ether; + /// @dev DAO oracle address + address private daoOracleAddress; + /// @dev Factory contract + ICasimirFactoryDev private factory; + /// @dev Registry contract + ICasimirRegistryDev private registry; + /// @dev Upkeep contract + ICasimirUpkeepDev private upkeep; + /// @dev Last pool ID created + uint32 private lastPoolId; + /// @dev Latest beacon chain balance after fees + uint256 private latestBeaconBalanceAfterFees; + /// @dev Latest active rewards + int256 private latestActiveRewardBalance; + /// @dev Report finalizable exited balance + uint256 private finalizableExitedBalance; + /// @dev Report finalizable recovered balance + uint256 private finalizableRecoveredBalance; + /// @dev All users + mapping(address => User) private users; + /// @dev Sum of scaled rewards to balance ratios + uint256 private stakeRatioSum; + /// @dev Total pending withdrawals count + uint256 private requestedWithdrawals; + /// @dev Pending withdrawals + Withdrawal[] private requestedWithdrawalQueue; + /// @dev All pool addresses + mapping(uint32 => address) private poolAddresses; + /// @dev Validator tip balance + uint256 private tipBalance; + /// @dev Pool recovered balances + mapping(uint32 => uint256) private recoveredBalances; + /// @dev Total deposits not yet in pools + uint256 private prepoolBalance; + /// @dev Total exited deposits + uint256 private exitedBalance; + /// @dev IDs of pools ready for initiation + uint32[] private readyPoolIds; + /// @dev IDS of pools pending deposit confirmation + uint32[] private pendingPoolIds; + /// @dev IDs of pools staked + uint32[] private stakedPoolIds; + /// @dev Slashed pool count + uint256 private forcedExits; + /// @dev Storage gap + uint256[50] private __gap; + + /** + * @dev Constructor + * @param functionsBillingRegistry_ Chainlink functions billing registry contract + * @param keeperRegistrar_ Chainlink keeper registrar contract + * @param keeperRegistry_ Chainlink keeper registry contract + * @param linkToken_ Chainlink token contract + * @param ssvNetwork_ SSV network contract + * @param ssvToken_ SSV token contract + * @param swapFactory_ Uniswap factory contract + * @param swapRouter_ Uniswap router contract + * @param wethToken_ WETH9 token contract + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor( + IFunctionsBillingRegistry functionsBillingRegistry_, + IKeeperRegistrar keeperRegistrar_, + IAutomationRegistry keeperRegistry_, + LinkTokenInterface linkToken_, + ISSVClusters ssvNetwork_, + IERC20Upgradeable ssvToken_, + IUniswapV3Factory swapFactory_, + ISwapRouter swapRouter_, + IWETH9 wethToken_ + ) { + onlyAddress(address(functionsBillingRegistry_)); + onlyAddress(address(keeperRegistrar_)); + onlyAddress(address(keeperRegistry_)); + onlyAddress(address(linkToken_)); + onlyAddress(address(ssvNetwork_)); + onlyAddress(address(ssvToken_)); + onlyAddress(address(swapFactory_)); + onlyAddress(address(swapRouter_)); + onlyAddress(address(wethToken_)); + functionsBillingRegistry = functionsBillingRegistry_; + keeperRegistrar = keeperRegistrar_; + keeperRegistry = keeperRegistry_; + linkToken = linkToken_; + ssvClusters = ssvNetwork_; + ssvToken = ssvToken_; + swapFactory = swapFactory_; + swapRouter = swapRouter_; + wethToken = wethToken_; + _disableInitializers(); + } + + /** + * @notice Initialize the contract + * @param daoOracleAddress_ DAO oracle address + * @param functionsOracleAddress Chainlink functions oracle address + * @param strategy Staking strategy configuration + */ + function initialize( + address daoOracleAddress_, + address functionsOracleAddress, + Strategy memory strategy + ) public initializer { + __Ownable_init(); + __ReentrancyGuard_init(); + daoOracleAddress = daoOracleAddress_; + factory = ICasimirFactoryDev(msg.sender); + registry = ICasimirRegistryDev( + CasimirBeaconDev.createRegistry( + factory.registryBeaconAddress(), + strategy.minCollateral, + strategy.privateOperators, + strategy.verifiedOperators + ) + ); + upkeep = ICasimirUpkeepDev( + CasimirBeaconDev.createUpkeep( + factory.upkeepBeaconAddress(), + msg.sender, + functionsOracleAddress, + strategy.compoundStake + ) + ); + userFee = strategy.userFee; + eigenStake = strategy.eigenStake; + liquidStake = strategy.liquidStake; + stakeRatioSum = 1000 ether; + } + + /// @notice Receive and deposit validator tips + receive() external payable { + tipBalance += msg.value; + if (tipBalance >= COMPOUND_MINIMUM) { + depositTips(); + } + } + + /// @inheritdoc ICasimirManagerDev + function depositStake() external payable nonReentrant { + User storage user = users[msg.sender]; + uint256 depositAfterFees = subtractFees(msg.value); + reservedFeeBalance += msg.value - depositAfterFees; + if (user.stake0 > 0) { + user.stake0 = getUserStake(msg.sender); + } + user.stakeRatioSum0 = stakeRatioSum; + user.stake0 += depositAfterFees; + distributeStake(depositAfterFees); + emit StakeDeposited(msg.sender, depositAfterFees); + } + + /// @inheritdoc ICasimirManagerDev + function depositRewards(uint32 poolId) external payable { + if (msg.value == 0) { + revert InvalidAmount(); + } + onlyPool(poolAddresses[poolId]); + uint256 rewardsAfterFees = subtractFees(msg.value); + reservedFeeBalance += msg.value - rewardsAfterFees; + distributeStake(rewardsAfterFees); + emit RewardsDeposited(rewardsAfterFees); + } + + /// @inheritdoc ICasimirManagerDev + function depositExitedBalance(uint32 poolId) external payable { + onlyPool(poolAddresses[poolId]); + uint256 balance = msg.value + recoveredBalances[poolId]; + delete recoveredBalances[poolId]; + delete poolAddresses[poolId]; + exitedBalance += balance; + finalizableExitedBalance += balance; + finalizableCompletedExits++; + emit ExitedBalanceDeposited(poolId, msg.value); + } + + /// @inheritdoc ICasimirManagerDev + function depositRecoveredBalance(uint32 poolId) external payable { + if (msg.sender != address(registry)) { + revert Unauthorized(); + } + recoveredBalances[poolId] += msg.value; + finalizableRecoveredBalance += msg.value; + emit RecoveredBalanceDeposited(poolId, msg.value); + } + + /// @inheritdoc ICasimirManagerDev + function depositClusterBalance( + uint64[] memory operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint256 feeAmount, + uint256 minTokenAmount, + bool processed + ) external { + onlyOracle(); + uint256 ssvAmount = retrieveFees(feeAmount, minTokenAmount, address(ssvToken), processed); + ssvToken.approve(address(ssvClusters), ssvAmount); + ssvClusters.deposit(address(this), operatorIds, ssvAmount, cluster); + emit ClusterBalanceDeposited(ssvAmount); + } + + /// @inheritdoc ICasimirManagerDev + function depositFunctionsBalance(uint256 feeAmount, uint256 minTokenAmount, bool processed) external { + onlyOracle(); + uint256 linkAmount = retrieveFees(feeAmount, minTokenAmount, address(linkToken), processed); + if (functionsId == 0) { + functionsId = functionsBillingRegistry.createSubscription(); + functionsBillingRegistry.addConsumer(functionsId, address(upkeep)); + } + if (!linkToken.transferAndCall(address(functionsBillingRegistry), linkAmount, abi.encode(functionsId))) { + revert TransferFailed(); + } + emit FunctionsBalanceDeposited(linkAmount); + } + + /// @inheritdoc ICasimirManagerDev + function depositUpkeepBalance(uint256 feeAmount, uint256 minTokenAmount, bool processed) external { + onlyOracle(); + uint256 linkAmount = retrieveFees(feeAmount, minTokenAmount, address(linkToken), processed); + linkToken.approve(address(keeperRegistrar), linkAmount); + if (upkeepId == 0) { + upkeepId = keeperRegistrar.registerUpkeep( + IKeeperRegistrar.RegistrationParams({ + name: string("CasimirV1Upkeep"), + encryptedEmail: new bytes(0), + upkeepContract: address(upkeep), + gasLimit: 5000000, + adminAddress: address(this), + checkData: new bytes(0), + offchainConfig: new bytes(0), + amount: uint96(linkAmount) + }) + ); + } else { + keeperRegistry.addFunds(upkeepId, uint96(linkAmount)); + } + emit UpkeepBalanceDeposited(linkAmount); + } + + /// @inheritdoc ICasimirManagerDev + function depositReservedFees() external payable { + onlyFactoryOwner(); + reservedFeeBalance += msg.value; + emit ReservedFeesDeposited(msg.value); + } + + /// @inheritdoc ICasimirManagerDev + function withdrawReservedFees(uint256 amount) external { + onlyFactoryOwner(); + if (amount > reservedFeeBalance) { + revert InvalidAmount(); + } + reservedFeeBalance -= amount; + (bool success, ) = msg.sender.call{value: amount}(""); + if (!success) { + revert TransferFailed(); + } + emit ReservedFeesWithdrawn(amount); + } + + /// @inheritdoc ICasimirManagerDev + function rebalanceStake( + uint256 beaconBalance, + uint256 sweptBalance, + uint256 activatedDeposits, + uint256 completedExits + ) external { + onlyUpkeep(); + reportPeriod++; + uint256 expectedActivatedBalance = activatedDeposits * POOL_CAPACITY; + uint256 expectedExitedBalance = completedExits * POOL_CAPACITY; + uint256 expectedEffectiveBalance = stakedPoolIds.length * POOL_CAPACITY; + int256 rewards = int256(beaconBalance + sweptBalance + finalizableRecoveredBalance) - + int256(expectedEffectiveBalance + expectedExitedBalance); + int256 change = rewards - latestActiveRewardBalance; + if (change > 0) { + uint256 gain = uint256(change); + if (rewards > 0) { + uint256 gainAfterFees = subtractFees(gain); + stakeRatioSum += MathUpgradeable.mulDiv(stakeRatioSum, gainAfterFees, getTotalStake()); + latestBeaconBalanceAfterFees += gainAfterFees; + emit StakeRebalanced(gainAfterFees); + } else { + stakeRatioSum += MathUpgradeable.mulDiv(stakeRatioSum, gain, getTotalStake()); + latestBeaconBalanceAfterFees += gain; + emit StakeRebalanced(gain); + } + } else if (change < 0) { + uint256 loss = uint256(-change); + stakeRatioSum -= MathUpgradeable.mulDiv(stakeRatioSum, loss, getTotalStake()); + latestBeaconBalanceAfterFees -= loss; + emit StakeRebalanced(loss); + } + int256 sweptRewards = int256(sweptBalance + finalizableRecoveredBalance) - int256(finalizableExitedBalance); + if (sweptRewards > 0) { + latestBeaconBalanceAfterFees -= subtractFees(uint256(sweptRewards)); + } + latestBeaconBalanceAfterFees -= finalizableExitedBalance; + latestBeaconBalanceAfterFees += expectedActivatedBalance; + latestActiveRewardBalance = rewards - sweptRewards; + latestBeaconBalance = beaconBalance; + finalizableExitedBalance = 0; + finalizableRecoveredBalance = 0; + finalizableActivations = 0; + finalizableCompletedExits = 0; + } + + /// @inheritdoc ICasimirManagerDev + function compoundRewards(uint32[5] memory poolIds) external { + onlyUpkeep(); + for (uint256 i; i < poolIds.length; i++) { + uint32 poolId = poolIds[i]; + if (poolId == 0) { + break; + } + ICasimirPoolDev pool = ICasimirPoolDev(poolAddresses[poolId]); + pool.depositRewards(); + } + } + + /// @inheritdoc ICasimirManagerDev + function requestWithdrawal(uint256 amount) external nonReentrant { + User storage user = users[msg.sender]; + user.stake0 = getUserStake(msg.sender); + if (user.stake0 < amount) { + revert InvalidAmount(); + } + user.stakeRatioSum0 = stakeRatioSum; + user.stake0 -= amount; + if (amount <= getWithdrawableBalance()) { + if (amount <= exitedBalance) { + exitedBalance -= amount; + } else { + uint256 remainder = amount - exitedBalance; + exitedBalance = 0; + prepoolBalance -= remainder; + } + fulfillWithdrawal(msg.sender, amount); + } else { + requestedWithdrawalQueue.push(Withdrawal({userAddress: msg.sender, amount: amount, period: reportPeriod})); + requestedWithdrawalBalance += amount; + requestedWithdrawals++; + uint256 coveredExitBalance = requestedExits * POOL_CAPACITY; + if (requestedWithdrawalBalance > coveredExitBalance) { + uint256 exitsRequired = (requestedWithdrawalBalance - coveredExitBalance) / POOL_CAPACITY; + if ((requestedWithdrawalBalance - coveredExitBalance) % POOL_CAPACITY > 0) { + exitsRequired++; + } + requestExits(exitsRequired); + } + emit WithdrawalInitiated(msg.sender, amount); + } + } + + /// @inheritdoc ICasimirManagerDev + function fulfillWithdrawals(uint256 count) external { + onlyUpkeep(); + uint256 withdrawalAmount; + uint256 withdrawalCount; + while (count > 0) { + count--; + if (requestedWithdrawalQueue.length == 0) { + break; + } + Withdrawal memory withdrawal = requestedWithdrawalQueue[0]; + if (withdrawal.period > reportPeriod) { + break; + } + requestedWithdrawalQueue.removeWithdrawalItem(0); + withdrawalAmount += withdrawal.amount; + withdrawalCount++; + fulfillWithdrawal(withdrawal.userAddress, withdrawal.amount); + } + if (withdrawalAmount <= exitedBalance) { + exitedBalance -= withdrawalAmount; + } else { + uint256 remainder = withdrawalAmount - exitedBalance; + exitedBalance = 0; + prepoolBalance -= remainder; + } + requestedWithdrawalBalance -= withdrawalAmount; + requestedWithdrawals -= withdrawalCount; + } + + /// @inheritdoc ICasimirManagerDev + function initiatePool( + bytes32 depositDataRoot, + bytes memory publicKey, + bytes memory signature, + bytes memory withdrawalCredentials, + uint64[] memory operatorIds, + bytes memory shares + ) external { + onlyOracle(); + if (readyPoolIds.length == 0) { + revert NoReadyPools(); + } + uint32 poolId = readyPoolIds[0]; + readyPoolIds.removeUint32Item(0); + pendingPoolIds.push(poolId); + poolAddresses[poolId] = CasimirBeaconDev.createPool( + factory.poolBeaconAddress(), + address(registry), + operatorIds, + poolId, + publicKey, + shares + ); + { + ICasimirPoolDev(poolAddresses[poolId]).depositStake{value: POOL_CAPACITY}( + depositDataRoot, + signature, + withdrawalCredentials + ); + for (uint256 i; i < operatorIds.length; i++) { + registry.addOperatorPool(operatorIds[i], poolId); + } + } + emit PoolInitiated(poolId); + } + + /// @inheritdoc ICasimirManagerDev + function activatePool( + uint256 pendingPoolIndex, + ISSVNetworkCore.Cluster memory cluster, + uint256 feeAmount, + uint256 minTokenAmount, + bool processed + ) external { + onlyOracle(); + uint32 poolId = pendingPoolIds[pendingPoolIndex]; + ICasimirPoolDev pool = ICasimirPoolDev(poolAddresses[poolId]); + PoolRegistration memory poolRegistration = pool.getRegistration(); + if (poolRegistration.status != PoolStatus.PENDING) { + revert PoolNotPending(); + } + finalizableActivations++; + pool.setStatus(PoolStatus.ACTIVE); + uint256 ssvAmount = retrieveFees(feeAmount, minTokenAmount, address(ssvToken), processed); + ssvToken.approve(address(ssvClusters), ssvAmount); + ssvClusters.registerValidator( + poolRegistration.publicKey, + poolRegistration.operatorIds, + poolRegistration.shares, + ssvAmount, + cluster + ); + pendingPoolIds.removeUint32Item(pendingPoolIndex); + stakedPoolIds.push(poolId); + emit PoolActivated(poolId); + } + + /// @inheritdoc ICasimirManagerDev + function resharePool( + uint32 poolId, + uint64[] memory operatorIds, + uint64 newOperatorId, + uint64 oldOperatorId, + bytes memory shares, + ISSVNetworkCore.Cluster memory cluster, + ISSVNetworkCore.Cluster memory oldCluster, + uint256 feeAmount, + uint256 minTokenAmount, + bool processed + ) external { + onlyOracle(); + ICasimirPoolDev pool = ICasimirPoolDev(poolAddresses[poolId]); + PoolStatus poolStatus = pool.status(); + if (poolStatus != PoolStatus.ACTIVE && poolStatus != PoolStatus.PENDING) { + revert PoolNotActive(); + } + uint256 poolReshares = pool.reshares(); + if (poolReshares >= 2) { + revert PoolMaxReshared(); + } + bytes memory poolPublicKey = pool.publicKey(); + uint256 ssvAmount = retrieveFees(feeAmount, minTokenAmount, address(ssvToken), processed); + ssvToken.approve(address(ssvClusters), ssvAmount); + ssvClusters.removeValidator(poolPublicKey, pool.getOperatorIds(), oldCluster); + ssvClusters.registerValidator(poolPublicKey, operatorIds, shares, ssvAmount, cluster); + pool.setOperatorIds(operatorIds); + pool.setReshares(poolReshares + 1); + registry.removeOperatorPool(oldOperatorId, poolId, 0); + registry.addOperatorPool(newOperatorId, poolId); + emit PoolReshared(poolId); + } + + /// @inheritdoc ICasimirManagerDev + function reportForcedExits(uint32[] memory poolIds) external { + onlyOracle(); + uint256 newForcedExits; + uint256 newRequestedExits; + for (uint256 i; i < poolIds.length; i++) { + uint32 poolId = poolIds[i]; + ICasimirPoolDev pool = ICasimirPoolDev(poolAddresses[poolId]); + PoolStatus poolStatus = pool.status(); + if (poolStatus == PoolStatus.EXITING_FORCED) { + revert ForcedExitAlreadyReported(); + } + newForcedExits++; + if (poolStatus == PoolStatus.EXITING_REQUESTED) { + newRequestedExits++; + } + pool.setStatus(PoolStatus.EXITING_FORCED); + } + forcedExits += newForcedExits; + requestedExits -= newRequestedExits; + emit ForcedExitsReported(poolIds); + } + + /// @inheritdoc ICasimirManagerDev + function reportCompletedExit( + uint256 stakedPoolIndex, + uint32[] memory blamePercents, + ISSVNetworkCore.Cluster memory cluster + ) external { + onlyOracle(); + uint32 poolId = stakedPoolIds[stakedPoolIndex]; + ICasimirPoolDev pool = ICasimirPoolDev(poolAddresses[poolId]); + PoolStatus poolStatus = pool.status(); + if (poolStatus != PoolStatus.EXITING_FORCED && poolStatus != PoolStatus.EXITING_REQUESTED) { + revert PoolNotExiting(); + } + stakedPoolIds.removeUint32Item(stakedPoolIndex); + if (poolStatus == PoolStatus.EXITING_REQUESTED) { + requestedExits--; + } else if (poolStatus == PoolStatus.EXITING_FORCED) { + forcedExits--; + } + pool.withdrawBalance(blamePercents); + ssvClusters.removeValidator(pool.publicKey(), pool.getOperatorIds(), cluster); + emit ExitCompleted(poolId); + } + + /// @inheritdoc ICasimirManagerDev + function withdrawClusterBalance( + uint64[] memory operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint256 amount + ) external { + onlyOracle(); + ssvClusters.withdraw(operatorIds, amount, cluster); + } + + /// @inheritdoc ICasimirManagerDev + function cancelFunctions() external { + onlyFactoryOwner(); + functionsBillingRegistry.cancelSubscription(functionsId, address(this)); + functionsId = 0; + emit FunctionsCancelled(); + } + + /// @inheritdoc ICasimirManagerDev + function cancelUpkeep() external { + onlyFactoryOwner(); + keeperRegistry.cancelUpkeep(upkeepId); + upkeepId = 0; + emit UpkeepCancelled(); + } + + /// @inheritdoc ICasimirManagerDev + function withdrawLINKBalance(uint256 amount) external { + onlyFactoryOwner(); + if (!linkToken.transfer(msg.sender, amount)) { + revert TransferFailed(); + } + emit LINKBalanceWithdrawn(amount); + } + + /// @inheritdoc ICasimirManagerDev + function withdrawSSVBalance(uint256 amount) external { + onlyFactoryOwner(); + SafeERC20Upgradeable.safeTransfer(ssvToken, msg.sender, amount); + emit SSVBalanceWithdrawn(amount); + } + + /// @inheritdoc ICasimirManagerDev + function getPendingWithdrawalEligibility( + uint256 index, + uint256 period + ) external view returns (bool pendingWithdrawalEligibility) { + if (requestedWithdrawals > index) { + pendingWithdrawalEligibility = requestedWithdrawalQueue[index].period <= period; + } + } + + /// @inheritdoc ICasimirManagerDev + function getPendingPoolIds() external view returns (uint32[] memory) { + return pendingPoolIds; + } + + /// @inheritdoc ICasimirManagerDev + function getStakedPoolIds() external view returns (uint32[] memory) { + return stakedPoolIds; + } + + /// @inheritdoc ICasimirManagerDev + function getPoolAddress(uint32 poolId) external view returns (address poolAddress) { + poolAddress = poolAddresses[poolId]; + } + + /// @inheritdoc ICasimirManagerDev + function getRegistryAddress() external view returns (address registryAddress) { + registryAddress = address(registry); + } + + /// @inheritdoc ICasimirManagerDev + function getUpkeepAddress() external view returns (address upkeepAddress) { + upkeepAddress = address(upkeep); + } + + /// @inheritdoc ICasimirManagerDev + function getUserStake(address userAddress) public view returns (uint256 userStake) { + userStake = MathUpgradeable.mulDiv(users[userAddress].stake0, stakeRatioSum, users[userAddress].stakeRatioSum0); + } + + /// @inheritdoc ICasimirManagerDev + function getTotalStake() public view returns (uint256 totalStake) { + totalStake = getBufferedBalance() + latestBeaconBalanceAfterFees - requestedWithdrawalBalance; + } + + /// @inheritdoc ICasimirManagerDev + function getBufferedBalance() public view returns (uint256 bufferedBalance) { + bufferedBalance = getWithdrawableBalance() + readyPoolIds.length * POOL_CAPACITY; + } + + /// @inheritdoc ICasimirManagerDev + function getWithdrawableBalance() public view returns (uint256 withdrawableBalance) { + withdrawableBalance = prepoolBalance + exitedBalance; + } + + /// @notice Deposit the current tip balance + function depositTips() private { + uint256 tipsAfterFees = subtractFees(tipBalance); + reservedFeeBalance += tipBalance - tipsAfterFees; + tipBalance = 0; + distributeStake(tipsAfterFees); + emit TipsDeposited(tipsAfterFees); + } + + /** + * @dev Distribute stake to new pools + * @param amount Stake amount to distribute + */ + function distributeStake(uint256 amount) private { + while (amount > 0) { + uint256 remainingCapacity = POOL_CAPACITY - prepoolBalance; + if (remainingCapacity > amount) { + prepoolBalance += amount; + amount = 0; + } else { + prepoolBalance = 0; + amount -= remainingCapacity; + readyPoolIds.push(++lastPoolId); + emit InitiationRequested(lastPoolId); + } + } + } + + /** + * @notice Fulfill a user withdrawal + * @param userAddress User address + * @param amount Withdrawal amount + */ + function fulfillWithdrawal(address userAddress, uint256 amount) private { + (bool success, ) = userAddress.call{value: amount}(""); + if (!success) { + revert TransferFailed(); + } + emit WithdrawalFulfilled(userAddress, amount); + } + + /** + * @notice Request a given count of staked pool exits + * @param count Count of exits to request + */ + function requestExits(uint256 count) private { + uint256 index = 0; + while (count > 0) { + uint32 poolId = stakedPoolIds[index]; + ICasimirPoolDev pool = ICasimirPoolDev(poolAddresses[poolId]); + PoolStatus poolStatus = pool.status(); + if (poolStatus == PoolStatus.PENDING || poolStatus == PoolStatus.ACTIVE) { + count--; + index++; + pool.setStatus(PoolStatus.EXITING_REQUESTED); + requestedExits++; + emit ExitRequested(poolId); + } + } + } + + /** + * @dev Retrieve fees for a given amount of a given token + * @param amount Amount to retrieve + * @param minTokenAmount Minimum token amount out after processing fees + * @param token Token address + * @param processed Whether the amount is already processed + */ + function retrieveFees( + uint256 amount, + uint256 minTokenAmount, + address token, + bool processed + ) private returns (uint256 amountOut) { + if (processed) { + amountOut = amount; + } else { + reservedFeeBalance -= amount; + wethToken.deposit{value: amount}(); + wethToken.approve(address(swapRouter), wethToken.balanceOf(address(this))); + IUniswapV3PoolState swapPool = IUniswapV3PoolState( + swapFactory.getPool(address(wethToken), token, UNISWAP_FEE_TIER) + ); + if (swapPool.liquidity() < amount) { + revert InsufficientLiquidity(); + } + ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({ + tokenIn: address(wethToken), + tokenOut: token, + fee: UNISWAP_FEE_TIER, + recipient: address(this), + deadline: block.timestamp, + amountIn: amount, + amountOutMinimum: minTokenAmount, + sqrtPriceLimitX96: 0 + }); + amountOut = swapRouter.exactInputSingle(params); + } + } + + /** + * @dev Subtract fees from a given amount + * @param amount Original amount + * @return amountAfterFees Amount after fees + */ + function subtractFees(uint256 amount) private view returns (uint256 amountAfterFees) { + amountAfterFees = MathUpgradeable.mulDiv(amount, 100, 100 + userFee); + } + + /// @dev Validate the caller is the factory owner + function onlyFactoryOwner() private view { + if (msg.sender != factory.getOwner()) { + revert Unauthorized(); + } + } + + /// @dev Validate the caller is the oracle + function onlyOracle() private view { + if (msg.sender != daoOracleAddress) { + revert Unauthorized(); + } + } + + /// @dev Validate the caller is the pool + function onlyPool(address poolAddress) private view { + if (msg.sender != poolAddress) { + revert Unauthorized(); + } + } + + /// @dev Validate the caller is the upkeep + function onlyUpkeep() private view { + if (msg.sender != address(upkeep)) { + revert Unauthorized(); + } + } +} diff --git a/contracts/ethereum/src/v1/dev/CasimirPool.sol b/contracts/ethereum/src/v1/dev/CasimirPool.sol new file mode 100644 index 000000000..b6156b2ee --- /dev/null +++ b/contracts/ethereum/src/v1/dev/CasimirPool.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./CasimirCore.sol"; +import "./interfaces/ICasimirPool.sol"; +import "./interfaces/ICasimirManager.sol"; +import "./interfaces/ICasimirRegistry.sol"; +import "./vendor/interfaces/IDepositContract.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +/// @title Pool that accepts deposits and stakes a validator +contract CasimirPoolDev is ICasimirPoolDev, CasimirCoreDev, Initializable, OwnableUpgradeable, ReentrancyGuardUpgradeable { + /// @inheritdoc ICasimirPoolDev + bytes public publicKey; + /// @inheritdoc ICasimirPoolDev + uint256 public reshares; + /// @inheritdoc ICasimirPoolDev + PoolStatus public status; + /** + * @dev Beacon deposit contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + IDepositContract private immutable depositContract; + /// @dev Pool deposit capacity + uint256 private constant POOL_CAPACITY = 32 ether; + /// @dev Operator IDs + uint64[] private operatorIds; + /// @dev Pool ID + uint32 private poolId; + /// @dev Operator key shares + bytes private shares; + /// @dev Manager contract + ICasimirManagerDev private manager; + /// @dev Registry contract + ICasimirRegistryDev private registry; + + /// @dev Storage gap + uint256[50] private __gap; + + /** + * @dev Constructor + * @param depositContract_ Beacon deposit contract + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(IDepositContract depositContract_) { + onlyAddress(address(depositContract_)); + depositContract = depositContract_; + _disableInitializers(); + } + + /** + * @notice Initialize the contract + * @param registry_ Registry contract + * @param operatorIds_ The operator IDs + * @param poolId_ Pool ID + * @param publicKey_ The validator public key + */ + function initialize( + ICasimirRegistryDev registry_, + uint64[] memory operatorIds_, + uint32 poolId_, + bytes memory publicKey_, + bytes memory shares_ + ) public initializer { + __Ownable_init(); + __ReentrancyGuard_init(); + manager = ICasimirManagerDev(msg.sender); + registry = registry_; + poolId = poolId_; + operatorIds = operatorIds_; + publicKey = publicKey_; + shares = shares_; + } + + /// @inheritdoc ICasimirPoolDev + function depositStake( + bytes32 depositDataRoot, + bytes memory signature, + bytes memory withdrawalCredentials + ) external payable onlyOwner { + if (status != PoolStatus.READY) { + revert PoolAlreadyInitiated(); + } + if (msg.value != POOL_CAPACITY) { + revert InvalidDepositAmount(); + } + bytes memory computedWithdrawalCredentials = abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + if (keccak256(computedWithdrawalCredentials) != keccak256(withdrawalCredentials)) { + revert InvalidWithdrawalCredentials(); + } + status = PoolStatus.PENDING; + depositContract.deposit{value: msg.value}(publicKey, withdrawalCredentials, signature, depositDataRoot); + } + + /// @inheritdoc ICasimirPoolDev + function depositRewards() external onlyOwner { + if (status != PoolStatus.ACTIVE) { + revert PoolNotActive(); + } + uint256 balance = address(this).balance; + manager.depositRewards{value: balance}(poolId); + } + + /// @inheritdoc ICasimirPoolDev + function setOperatorIds(uint64[] memory newOperatorIds) external onlyOwner { + operatorIds = newOperatorIds; + emit OperatorIdsSet(newOperatorIds); + } + + /// @inheritdoc ICasimirPoolDev + function setReshares(uint256 newReshares) external onlyOwner { + reshares = newReshares; + emit ResharesSet(newReshares); + } + + /// @inheritdoc ICasimirPoolDev + function setStatus(PoolStatus newStatus) external onlyOwner { + status = newStatus; + emit StatusSet(newStatus); + } + + /// @inheritdoc ICasimirPoolDev + function withdrawBalance(uint32[] memory blamePercents) external onlyOwner { + if (status != PoolStatus.EXITING_FORCED && status != PoolStatus.EXITING_REQUESTED) { + revert PoolNotExiting(); + } + if (status == PoolStatus.WITHDRAWN) { + revert PoolAlreadyWithdrawn(); + } + status = PoolStatus.WITHDRAWN; + uint256 balance = address(this).balance; + int256 rewards = int256(balance) - int256(POOL_CAPACITY); + if (rewards > 0) { + manager.depositRewards{value: uint256(rewards)}(poolId); + } + for (uint256 i; i < blamePercents.length; i++) { + uint256 blameAmount; + if (rewards < 0) { + uint256 blamePercent = blamePercents[i]; + blameAmount = MathUpgradeable.mulDiv(uint256(-rewards), blamePercent, 100); + } + registry.removeOperatorPool(operatorIds[i], poolId, blameAmount); + } + manager.depositExitedBalance{value: balance}(poolId); + } + + /// @inheritdoc ICasimirPoolDev + function getOperatorIds() external view returns (uint64[] memory) { + return operatorIds; + } + + /// @inheritdoc ICasimirPoolDev + function getRegistration() external view returns (PoolRegistration memory) { + return PoolRegistration({operatorIds: operatorIds, publicKey: publicKey, shares: shares, status: status}); + } +} diff --git a/contracts/ethereum/src/v1/dev/CasimirRegistry.sol b/contracts/ethereum/src/v1/dev/CasimirRegistry.sol new file mode 100644 index 000000000..8ae34d968 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/CasimirRegistry.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./CasimirCore.sol"; +import "./interfaces/ICasimirRegistry.sol"; +import "./interfaces/ICasimirManager.sol"; +import "./vendor/interfaces/ISSVViews.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; + +/** + * @title Registry for pool operators + */ +contract CasimirRegistryDev is + ICasimirRegistryDev, + CasimirCoreDev, + Initializable, + OwnableUpgradeable, + ReentrancyGuardUpgradeable +{ + /// @inheritdoc ICasimirRegistryDev + uint256 public minCollateral; + /// @inheritdoc ICasimirRegistryDev + bool public privateOperators; + /// @inheritdoc ICasimirRegistryDev + bool public verifiedOperators; + /** + * @dev SSV views contract + * @custom:oz-upgrades-unsafe-allow state-variable-immutable + */ + ISSVViews private immutable ssvViews; + /// @dev Manager contract + ICasimirManagerDev private manager; + /// @dev Previously registered operator IDs + uint64[] private operatorIds; + /// @dev Operators by ID + mapping(uint64 => Operator) private operators; + /// @dev Operator pools by operator ID and pool ID + mapping(uint64 => mapping(uint32 => bool)) private operatorPools; + /// @dev Storage gap + uint256[50] private __gap; + + /** + * @dev Constructor + * @param ssvViews_ SSV views contract + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor(ISSVViews ssvViews_) { + onlyAddress(address(ssvViews_)); + ssvViews = ssvViews_; + _disableInitializers(); + } + + /** + * @notice Initialize the contract + * @param minCollateral_ Minimum collateral per operator per pool + * @param privateOperators_ Whether private operators are enabled + * @param verifiedOperators_ Whether verified operators are enabled + */ + function initialize(uint256 minCollateral_, bool privateOperators_, bool verifiedOperators_) public initializer { + __Ownable_init(); + __ReentrancyGuard_init(); + manager = ICasimirManagerDev(msg.sender); + minCollateral = minCollateral_; + privateOperators = privateOperators_; + verifiedOperators = verifiedOperators_; + } + + /// @inheritdoc ICasimirRegistryDev + function registerOperator(uint64 operatorId) external payable { + onlyOperatorOwner(operatorId); + if (privateOperators) { + onlyPrivateOperator(operatorId); + } + Operator storage operator = operators[operatorId]; + if (operator.id != 0) { + revert OperatorAlreadyRegistered(); + } + operatorIds.push(operatorId); + operator.id = operatorId; + operator.active = true; + operator.collateral = msg.value; + emit OperatorRegistered(operatorId); + } + + /// @inheritdoc ICasimirRegistryDev + function depositCollateral(uint64 operatorId) external payable { + onlyOperatorOwner(operatorId); + Operator storage operator = operators[operatorId]; + operator.collateral += msg.value; + operator.active = true; + emit CollateralDeposited(operatorId, msg.value); + } + + /// @inheritdoc ICasimirRegistryDev + function requestWithdrawal(uint64 operatorId, uint256 amount) external { + onlyOperatorOwner(operatorId); + Operator storage operator = operators[operatorId]; + if (operator.active || operator.resharing) { + revert CollateralInUse(); + } + if (operator.collateral < amount) { + revert InvalidAmount(); + } + operator.collateral -= amount; + (bool success, ) = msg.sender.call{value: amount}(""); + if (!success) { + revert TransferFailed(); + } + emit WithdrawalFulfilled(operatorId, amount); + } + + /// @inheritdoc ICasimirRegistryDev + function requestDeactivation(uint64 operatorId) external { + onlyOperatorOwner(operatorId); + Operator storage operator = operators[operatorId]; + if (!operator.active) { + revert OperatorNotActive(); + } + if (operator.resharing) { + revert OperatorResharing(); + } + if (operator.poolCount == 0) { + operator.active = false; + emit DeactivationCompleted(operatorId); + } else { + operator.resharing = true; + emit DeactivationRequested(operatorId); + } + } + + /// @inheritdoc ICasimirRegistryDev + function addOperatorPool(uint64 operatorId, uint32 poolId) external onlyOwner { + Operator storage operator = operators[operatorId]; + if (!operator.active) { + revert OperatorNotActive(); + } + if (operator.resharing) { + revert OperatorResharing(); + } + if (operatorPools[operatorId][poolId]) { + revert PoolAlreadyExists(); + } + uint256 eligiblePools = (operator.collateral / minCollateral) - operator.poolCount; + if (eligiblePools == 0) { + revert InsufficientCollateral(); + } + operatorPools[operatorId][poolId] = true; + operator.poolCount += 1; + emit OperatorPoolAdded(operatorId, poolId); + } + + /// @inheritdoc ICasimirRegistryDev + function removeOperatorPool(uint64 operatorId, uint32 poolId, uint256 blameAmount) external { + onlyOwnerOrPool(poolId); + Operator storage operator = operators[operatorId]; + if (!operatorPools[operatorId][poolId]) { + revert PoolDoesNotExist(); + } + if (blameAmount > minCollateral) { + revert InvalidAmount(); + } + operatorPools[operatorId][poolId] = false; + operator.poolCount -= 1; + if (operator.poolCount == 0 && operator.resharing) { + operator.active = false; + operator.resharing = false; + emit DeactivationCompleted(operatorId); + } + if (blameAmount > 0) { + operator.collateral -= blameAmount; + manager.depositRecoveredBalance{value: blameAmount}(poolId); + } + emit OperatorPoolRemoved(operatorId, poolId, blameAmount); + } + + /// @inheritdoc ICasimirRegistryDev + function getOperator(uint64 operatorId) external view returns (Operator memory operator) { + operator = operators[operatorId]; + } + + /// @inheritdoc ICasimirRegistryDev + function getOperatorIds() external view returns (uint64[] memory) { + return operatorIds; + } + + /// @dev Validate the caller is the owner of the operator + function onlyOperatorOwner(uint64 operatorId) private view { + (address operatorOwner, , , , , ) = ssvViews.getOperatorById(operatorId); + if (msg.sender != operatorOwner) { + revert Unauthorized(); + } + } + + /// @dev Validate the caller is the owner or the authorized pool + function onlyOwnerOrPool(uint32 poolId) private view { + if (msg.sender != owner() && msg.sender != manager.getPoolAddress(poolId)) { + revert Unauthorized(); + } + } + + /// @dev Validate the caller is a private operator + function onlyPrivateOperator(uint64 operatorId) private view { + (, uint256 fee, , , ,) = ssvViews.getOperatorById(operatorId); + if (fee != 0) { + revert OperatorNotPrivate(); + } + } +} diff --git a/contracts/ethereum/src/v1/dev/CasimirUpkeep.sol b/contracts/ethereum/src/v1/dev/CasimirUpkeep.sol new file mode 100644 index 000000000..50e58c713 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/CasimirUpkeep.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./CasimirCore.sol"; +import "./interfaces/ICasimirFactory.sol"; +import "./interfaces/ICasimirManager.sol"; +import "./interfaces/ICasimirUpkeep.sol"; +import "./vendor/FunctionsClient.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; + +/// @title Upkeep contract that automates reporting operations +contract CasimirUpkeepDev is + ICasimirUpkeepDev, + CasimirCoreDev, + Initializable, + OwnableUpgradeable, + ReentrancyGuardUpgradeable, + FunctionsClient +{ + using Functions for Functions.Request; + + /// @inheritdoc ICasimirUpkeepDev + bool public compoundStake; + /// @dev Report-to-report heartbeat duration + uint256 private constant REPORT_HEARTBEAT = 1 days; + /// @dev Factory contract + ICasimirFactoryDev private factory; + /// @dev Manager contract + ICasimirManagerDev private manager; + /// @dev Previous report timestamp + uint256 private previousReportTimestamp; + /// @dev Current report status + ReportStatus private reportStatus; + /// @dev Current report period + uint32 private reportPeriod; + /// @dev Current report remaining request count + uint256 private reportRemainingRequests; + /// @dev Current report block + uint256 private reportRequestBlock; + /// @dev Current report request timestamp + uint256 private reportTimestamp; + /// @dev Current report swept balance + uint256 private reportSweptBalance; + /// @dev Current report beacon chain balance + uint256 private reportBeaconBalance; + /// @dev Current report deposit activations + uint256 private reportActivatedDeposits; + /// @dev Current report unexpected exits + uint256 private reportForcedExits; + /// @dev Current report completed exits + uint256 private reportCompletedExits; + /// @dev Current report compoundable pools + uint32[5] private reportCompoundablePoolIds; + /// @dev Finalizable compoundable pools + uint32[5] private finalizableCompoundablePoolIds; + /// @dev Current report request + mapping(bytes32 => RequestType) private reportRequests; + /// @dev Current report response error + bytes private reportResponseError; + /// @dev Request source + string private requestSource; + /// @dev Default request arguments + string[] private defaultRequestArgs; + /// @dev Fulfillment gas limit + uint32 private fulfillGasLimit; + /// @dev Storage gap + uint256[50] private __gap; + + /** + * @dev Constructor + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor() FunctionsClient(address(0)) { + _disableInitializers(); + } + + /** + * Initialize the contract + * @param factoryAddress Factory address + * @param functionsOracleAddress Chainlink functions oracle address + * @param compoundStake_ Whether compound stake is enabled + */ + function initialize( + address factoryAddress, + address functionsOracleAddress, + bool compoundStake_ + ) public initializer { + __Ownable_init(); + __ReentrancyGuard_init(); + factory = ICasimirFactoryDev(factoryAddress); + manager = ICasimirManagerDev(msg.sender); + compoundStake = compoundStake_; + setOracle(functionsOracleAddress); + } + + /// @inheritdoc ICasimirUpkeepDev + function performUpkeep(bytes calldata) external override { + (bool upkeepNeeded, ) = checkUpkeep(""); + if (!upkeepNeeded) { + revert UpkeepNotNeeded(); + } + if (reportStatus == ReportStatus.FINALIZED) { + previousReportTimestamp = reportTimestamp; + reportStatus = ReportStatus.REQUESTING; + reportRequestBlock = block.number; + reportTimestamp = block.timestamp; + reportPeriod = manager.reportPeriod(); + Functions.Request memory request; + request.initializeRequest(Functions.Location.Inline, Functions.CodeLanguage.JavaScript, requestSource); + string[] memory requestArgs = defaultRequestArgs; + requestArgs[7] = StringsUpgradeable.toString(previousReportTimestamp); + requestArgs[8] = StringsUpgradeable.toString(reportTimestamp); + requestArgs[9] = StringsUpgradeable.toString(reportRequestBlock); + sendFunctionsRequest(request, requestArgs, RequestType.BALANCES); + sendFunctionsRequest(request, requestArgs, RequestType.DETAILS); + } else { + if ( + manager.requestedWithdrawalBalance() > 0 && + manager.getPendingWithdrawalEligibility(0, reportPeriod) && + manager.requestedWithdrawalBalance() <= manager.getWithdrawableBalance() + ) { + manager.fulfillWithdrawals(5); + } + if (!manager.getPendingWithdrawalEligibility(0, reportPeriod)) { + reportStatus = ReportStatus.FINALIZED; + manager.rebalanceStake({ + beaconBalance: reportBeaconBalance, + sweptBalance: reportSweptBalance, + activatedDeposits: reportActivatedDeposits, + completedExits: reportCompletedExits + }); + manager.compoundRewards(reportCompoundablePoolIds); + reportBeaconBalance = 0; + reportActivatedDeposits = 0; + reportForcedExits = 0; + reportCompletedExits = 0; + reportCompoundablePoolIds = [0, 0, 0, 0, 0]; + } + } + emit UpkeepPerformed(reportStatus); + } + + /// @inheritdoc ICasimirUpkeepDev + function setFunctionsOracle(address newFunctionsOracleAddress) external { + onlyFactoryOwner(); + setOracle(newFunctionsOracleAddress); + emit FunctionsOracleAddressSet(newFunctionsOracleAddress); + } + + /// @inheritdoc ICasimirUpkeepDev + function setFunctionsRequest( + string calldata newRequestSource, + string[] calldata newRequestArgs, + uint32 newFulfillGasLimit + ) external { + onlyFactoryOwner(); + requestSource = newRequestSource; + defaultRequestArgs = newRequestArgs; + fulfillGasLimit = newFulfillGasLimit; + emit FunctionsRequestSet(newRequestSource, newRequestArgs, newFulfillGasLimit); + } + + /// @inheritdoc ICasimirUpkeepDev + function checkUpkeep(bytes memory) public view override returns (bool upkeepNeeded, bytes memory checkData) { + if (reportStatus == ReportStatus.FINALIZED) { + bool checkActive = manager.getPendingPoolIds().length + manager.getStakedPoolIds().length > 0; + bool heartbeatLapsed = (block.timestamp - reportTimestamp) >= REPORT_HEARTBEAT; + upkeepNeeded = checkActive && heartbeatLapsed; + } else if (reportStatus == ReportStatus.PROCESSING) { + bool finalizeReport = reportActivatedDeposits == manager.finalizableActivations() && + reportCompletedExits == manager.finalizableCompletedExits(); + upkeepNeeded = finalizeReport; + } + return (upkeepNeeded, checkData); + } + + /** + * @dev Callback that is invoked once the DON has resolved the request or hit an error + * @param requestId Request ID, returned by sendRequest() + * @param response Aggregated response from the DON + * @param executionError Aggregated error from the code execution + */ + function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory executionError) internal override { + RequestType requestType = reportRequests[requestId]; + if (requestType == RequestType.NONE) { + revert InvalidRequest(); + } + reportResponseError = executionError; + if (executionError.length == 0) { + delete reportRequests[requestId]; + reportRemainingRequests--; + if (requestType == RequestType.BALANCES) { + (uint128 beaconBalance, uint128 sweptBalance) = abi.decode(response, (uint128, uint128)); + reportBeaconBalance = uint256(beaconBalance); + reportSweptBalance = uint256(sweptBalance); + } else { + ( + uint32 activatedDeposits, + uint32 forcedExits, + uint32 completedExits, + uint32[5] memory compoundablePoolIds + ) = abi.decode(response, (uint32, uint32, uint32, uint32[5])); + reportActivatedDeposits = activatedDeposits; + reportForcedExits = forcedExits; + reportCompletedExits = completedExits; + reportCompoundablePoolIds = compoundablePoolIds; + finalizableCompoundablePoolIds = compoundablePoolIds; + if (reportActivatedDeposits > 0) { + emit ActivationsRequested(activatedDeposits); + } + if (reportForcedExits > 0) { + emit ForcedExitReportsRequested(forcedExits); + } + if (reportCompletedExits > 0) { + emit CompletedExitReportsRequested(completedExits); + } + } + if (reportRemainingRequests == 0) { + reportStatus = ReportStatus.PROCESSING; + } + } + emit OCRResponse(requestId, response, executionError); + } + + /** + * @dev Send a Chainlink functions request + * @param request Chainlink functions request + * @param requestArgs Chainlink functions request arguments + * @param requestType Chainlink functions request type + */ + function sendFunctionsRequest( + Functions.Request memory request, + string[] memory requestArgs, + RequestType requestType + ) private { + requestArgs[10] = StringsUpgradeable.toString(uint256(requestType)); + request.addArgs(requestArgs); + bytes32 requestId = sendRequest(request, manager.functionsId(), fulfillGasLimit); + reportRequests[requestId] = requestType; + reportRemainingRequests++; + } + + /// @dev Validate the caller is the factory owner + function onlyFactoryOwner() private view { + if (msg.sender != factory.getOwner()) { + revert Unauthorized(); + } + } +} diff --git a/contracts/ethereum/src/v1/dev/CasimirViews.sol b/contracts/ethereum/src/v1/dev/CasimirViews.sol new file mode 100644 index 000000000..34dc88344 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/CasimirViews.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./interfaces/ICasimirManager.sol"; +import "./interfaces/ICasimirPool.sol"; +import "./interfaces/ICasimirRegistry.sol"; +import "./interfaces/ICasimirUpkeep.sol"; +import "./interfaces/ICasimirViews.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; + +/** + * @title Views contract that provides read-only access to the state + */ +contract CasimirViewsDev is ICasimirViewsDev, Initializable { + /// @dev Compound minimum (0.1 ETH) + uint256 private constant COMPOUND_MINIMUM = 100000000 gwei; + /// @dev Manager contract + ICasimirManagerDev private manager; + /// @dev Storage gap + uint256[50] private __gap; + + /** + * @dev Constructor + * @custom:oz-upgrades-unsafe-allow constructor + */ + constructor() { + _disableInitializers(); + } + + /** + * @notice Initialize the contract + * @param managerAddress Manager address + */ + function initialize(address managerAddress) public initializer { + manager = ICasimirManagerDev(managerAddress); + } + + /// @inheritdoc ICasimirViewsDev + function getCompoundablePoolIds( + uint256 startIndex, + uint256 endIndex + ) external view returns (uint32[5] memory compoundablePoolIds) { + uint32[] memory pendingPoolIds = manager.getPendingPoolIds(); + uint32[] memory stakedPoolIds = manager.getStakedPoolIds(); + uint256 count = 0; + for (uint256 i = startIndex; i < endIndex; i++) { + uint32 poolId; + if (i < pendingPoolIds.length) { + poolId = pendingPoolIds[i]; + } else { + poolId = stakedPoolIds[i - pendingPoolIds.length]; + } + if (manager.getPoolAddress(poolId).balance >= COMPOUND_MINIMUM) { + compoundablePoolIds[count] = poolId; + count++; + if (count == 5) { + break; + } + } + } + } + + /// @inheritdoc ICasimirViewsDev + function getDepositedPoolCount() external view returns (uint256 depositedPoolCount) { + depositedPoolCount = manager.getPendingPoolIds().length + manager.getStakedPoolIds().length; + } + + /// @inheritdoc ICasimirViewsDev + function getDepositedPoolPublicKeys(uint256 startIndex, uint256 endIndex) external view returns (bytes[] memory) { + bytes[] memory publicKeys = new bytes[](endIndex - startIndex); + uint32[] memory pendingPoolIds = manager.getPendingPoolIds(); + uint32[] memory stakedPoolIds = manager.getStakedPoolIds(); + uint256 count = 0; + for (uint256 i = startIndex; i < endIndex; i++) { + uint32 poolId; + if (i < pendingPoolIds.length) { + poolId = pendingPoolIds[i]; + } else { + poolId = stakedPoolIds[i - pendingPoolIds.length]; + } + publicKeys[count] = ICasimirPoolDev(manager.getPoolAddress(poolId)).publicKey(); + count++; + } + return publicKeys; + } + + /// @inheritdoc ICasimirViewsDev + function getDepositedPoolStatuses( + uint256 startIndex, + uint256 endIndex + ) external view returns (PoolStatus[] memory) { + PoolStatus[] memory statuses = new PoolStatus[](endIndex - startIndex); + uint32[] memory pendingPoolIds = manager.getPendingPoolIds(); + uint32[] memory stakedPoolIds = manager.getStakedPoolIds(); + uint256 count = 0; + for (uint256 i = startIndex; i < endIndex; i++) { + uint32 poolId; + if (i < pendingPoolIds.length) { + poolId = pendingPoolIds[i]; + } else { + poolId = stakedPoolIds[i - pendingPoolIds.length]; + } + statuses[count] = ICasimirPoolDev(manager.getPoolAddress(poolId)).status(); + count++; + } + return statuses; + } + + /// @inheritdoc ICasimirViewsDev + function getOperators( + uint256 startIndex, + uint256 endIndex + ) external view returns (Operator[] memory) { + Operator[] memory operators = new Operator[](endIndex - startIndex); + ICasimirRegistryDev registry = ICasimirRegistryDev(manager.getRegistryAddress()); + uint64[] memory operatorIds = registry.getOperatorIds(); + uint256 count = 0; + for (uint256 i = startIndex; i < endIndex; i++) { + uint64 operatorId = operatorIds[i]; + operators[count] = registry.getOperator(operatorId); + count++; + } + return operators; + } + + /// @inheritdoc ICasimirViewsDev + function getPoolConfig(uint32 poolId) external view returns (PoolConfig memory poolConfig) { + address poolAddress = manager.getPoolAddress(poolId); + ICasimirPoolDev pool = ICasimirPoolDev(poolAddress); + poolConfig = PoolConfig({ + poolAddress: poolAddress, + balance: poolAddress.balance, + operatorIds: pool.getOperatorIds(), + publicKey: pool.publicKey(), + reshares: pool.reshares(), + status: pool.status() + }); + } + + /// @inheritdoc ICasimirViewsDev + function getSweptBalance(uint256 startIndex, uint256 endIndex) external view returns (uint128 sweptBalance) { + uint32[] memory pendingPoolIds = manager.getPendingPoolIds(); + uint32[] memory stakedPoolIds = manager.getStakedPoolIds(); + for (uint256 i = startIndex; i <= endIndex; i++) { + uint32 poolId; + if (i < pendingPoolIds.length) { + poolId = pendingPoolIds[i]; + } else { + poolId = stakedPoolIds[i - pendingPoolIds.length]; + } + sweptBalance += uint128(manager.getPoolAddress(poolId).balance / 1 gwei); + } + } +} diff --git a/contracts/ethereum/src/v1/dev/interfaces/ICasimirCore.sol b/contracts/ethereum/src/v1/dev/interfaces/ICasimirCore.sol new file mode 100644 index 000000000..14a4e9d0d --- /dev/null +++ b/contracts/ethereum/src/v1/dev/interfaces/ICasimirCore.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +/// @title Core interface +interface ICasimirCoreDev { + /// @dev Manager configuration + struct ManagerConfig { + address managerAddress; + address registryAddress; + address upkeepAddress; + address viewsAddress; + Strategy strategy; + } + + /// @dev Registered operator + struct Operator { + uint64 id; + bool active; + uint256 collateral; + uint256 poolCount; + bool resharing; + } + + /// @dev Pool config + struct PoolConfig { + address poolAddress; + uint256 balance; + uint64[] operatorIds; + bytes publicKey; + uint256 reshares; + PoolStatus status; + } + + /// @dev Pool registration + struct PoolRegistration { + uint64[] operatorIds; + bytes publicKey; + bytes shares; + PoolStatus status; + } + + /// @dev Pool status + enum PoolStatus { + READY, + PENDING, + ACTIVE, + EXITING_FORCED, + EXITING_REQUESTED, + WITHDRAWN + } + + /// @dev Staking strategy + struct Strategy { + uint256 minCollateral; + uint256 lockPeriod; + uint32 userFee; + bool compoundStake; + bool eigenStake; + bool liquidStake; + bool privateOperators; + bool verifiedOperators; + } + + /// @dev User stake account + struct User { + uint256 stake0; + uint256 stakeRatioSum0; + } + + /// @dev User withdrawal request + struct Withdrawal { + address userAddress; + uint256 amount; + uint256 period; + } + + error InvalidAddress(); + error InvalidAmount(); + error PoolAlreadyInitiated(); + error PoolAlreadyWithdrawn(); + error PoolMaxReshared(); + error PoolNotActive(); + error PoolNotPending(); + error PoolNotExiting(); + error TransferFailed(); + error Unauthorized(); +} diff --git a/contracts/ethereum/src/v1/dev/interfaces/ICasimirFactory.sol b/contracts/ethereum/src/v1/dev/interfaces/ICasimirFactory.sol new file mode 100644 index 000000000..536bb8725 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/interfaces/ICasimirFactory.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./ICasimirCore.sol"; + +/// @title Factory interface +interface ICasimirFactoryDev is ICasimirCoreDev { + event FunctionsRequestSet( + uint32 indexed managerId, + string newRequestSource, + string[] newRequestArgs, + uint32 newFulfillGasLimit + ); + event FunctionsOracleSet(uint32 indexed managerId, address newFunctionsOracleAddress); + event ManagerDeployed(uint32 managerId); + event ReservedFeesWithdrawn(uint32 indexed managerId, uint256 amount); + + /** + * @notice Deploy a new manager + * @param daoOracleAddress DAO oracle address + * @param functionsOracleAddress Chainlink functions oracle address + * @param strategy Staking strategy configuration + */ + function deployManager(address daoOracleAddress, address functionsOracleAddress, Strategy memory strategy) external; + + /// @notice Manager beacon address + function managerBeaconAddress() external view returns (address); + + /// @notice Pool beacon address + function poolBeaconAddress() external view returns (address); + + /// @notice Registry beacon address + function registryBeaconAddress() external view returns (address); + + /// @notice Upkeep beacon address + function upkeepBeaconAddress() external view returns (address); + + /// @notice Views beacon address + function viewsBeaconAddress() external view returns (address); + + /// @notice Get manager config + function getManagerConfig(uint32 managerId) external view returns (ManagerConfig memory); + + /// @notice Get the manager IDs + function getManagerIds() external view returns (uint32[] memory); + + /// @notice Get the owner address + function getOwner() external view returns (address); +} diff --git a/contracts/ethereum/src/v1/dev/interfaces/ICasimirManager.sol b/contracts/ethereum/src/v1/dev/interfaces/ICasimirManager.sol new file mode 100644 index 000000000..05332edee --- /dev/null +++ b/contracts/ethereum/src/v1/dev/interfaces/ICasimirManager.sol @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./ICasimirCore.sol"; +import "../vendor/interfaces/ISSVNetworkCore.sol"; + +interface ICasimirManagerDev is ICasimirCoreDev { + event ClusterBalanceDeposited(uint256 amount); + event PoolActivated(uint32 indexed poolId); + event PoolInitiated(uint32 indexed poolId); + event InitiationRequested(uint32 indexed poolId); + event PoolReshared(uint32 indexed poolId); + event ExitRequested(uint32 indexed poolId); + event ForcedExitsReported(uint32[] poolIds); + event LINKBalanceWithdrawn(uint256 amount); + event ExitedBalanceDeposited(uint32 indexed poolId, uint256 amount); + event ExitCompleted(uint32 indexed poolId); + event StakeDeposited(address indexed sender, uint256 amount); + event StakeRebalanced(uint256 amount); + event RecoveredBalanceDeposited(uint32 indexed poolId, uint256 amount); + event ReservedFeesDeposited(uint256 amount); + event ReservedFeesWithdrawn(uint256 amount); + event RewardsDeposited(uint256 amount); + event SSVBalanceWithdrawn(uint256 amount); + event TipsDeposited(uint256 amount); + event FunctionsBalanceDeposited(uint256 amount); + event UpkeepBalanceDeposited(uint256 amount); + event FunctionsCancelled(); + event UpkeepCancelled(); + event WithdrawalFulfilled(address indexed sender, uint256 amount); + event WithdrawalRequested(address indexed sender, uint256 amount); + event WithdrawalInitiated(address indexed sender, uint256 amount); + + error ForcedExitAlreadyReported(); + error InsufficientLiquidity(); + error NoReadyPools(); + + /// @notice Deposit user stake + function depositStake() external payable; + + /** + * @notice Deposit pool rewards + * @param poolId Pool ID + */ + function depositRewards(uint32 poolId) external payable; + + /** + * @notice Deposit pool exited balance + * @param poolId Pool ID + */ + function depositExitedBalance(uint32 poolId) external payable; + + /** + * @notice Deposit pool operator recovered balance + * @param poolId Pool ID + */ + function depositRecoveredBalance(uint32 poolId) external payable; + + /// @notice Deposit reserved fees + function depositReservedFees() external payable; + + /** + * @notice Deposit to a cluster balance + * @param operatorIds Operator IDs + * @param cluster Cluster snapshot + * @param feeAmount Fee amount to deposit + * @param minTokenAmount Minimum SSV token amount out after processing fees + * @param processed Whether the fee amount is already processed + */ + function depositClusterBalance( + uint64[] memory operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint256 feeAmount, + uint256 minTokenAmount, + bool processed + ) external; + + /** + * @notice Deposit to the functions balance + * @param feeAmount Fee amount to deposit + * @param minTokenAmount Minimum LINK token amount out after processing fees + * @param processed Whether the fee amount is already processed + */ + function depositFunctionsBalance(uint256 feeAmount, uint256 minTokenAmount, bool processed) external; + + /** + * @notice Deposit to the upkeep balance + * @param feeAmount Fee amount to deposit + * @param minTokenAmount Minimum LINK token amount out after processing fees + * @param processed Whether the fee amount is already processed + */ + function depositUpkeepBalance(uint256 feeAmount, uint256 minTokenAmount, bool processed) external; + + /** + * @notice Rebalance the rewards to stake ratio and redistribute swept rewards + * @param beaconBalance Beacon chain balance + * @param sweptBalance Swept balance + * @param activatedDeposits Activated deposit count + * @param completedExits Withdrawn exit count + */ + function rebalanceStake( + uint256 beaconBalance, + uint256 sweptBalance, + uint256 activatedDeposits, + uint256 completedExits + ) external; + + /** + * @notice Compound pool rewards + * @param poolIds Pool IDs + */ + function compoundRewards(uint32[5] memory poolIds) external; + + /** + * @notice Request to withdraw user stake + * @param amount Withdrawal amount + */ + function requestWithdrawal(uint256 amount) external; + + /** + * @notice Fulfill pending withdrawals + * @param count Withdrawal count + */ + function fulfillWithdrawals(uint256 count) external; + + /** + * @notice Initiate the next ready pool + * @param depositDataRoot Deposit data root + * @param publicKey Validator public key + * @param signature Deposit signature + * @param withdrawalCredentials Validator withdrawal credentials + * @param operatorIds Operator IDs + * @param shares Operator shares + */ + function initiatePool( + bytes32 depositDataRoot, + bytes memory publicKey, + bytes memory signature, + bytes memory withdrawalCredentials, + uint64[] memory operatorIds, + bytes memory shares + ) external; + + /** + * @notice Withdraw reserved fees + * @param amount Amount to withdraw + */ + function withdrawReservedFees(uint256 amount) external; + + /** + * @notice Activate a pool + * @param pendingPoolIndex Pending pool index + * @param cluster SSV cluster + * @param feeAmount Fee amount + * @param minTokenAmount Minimum token amount + * @param processed Whether the fee has been processed + */ + function activatePool( + uint256 pendingPoolIndex, + ISSVNetworkCore.Cluster memory cluster, + uint256 feeAmount, + uint256 minTokenAmount, + bool processed + ) external; + + /** + * @notice Report a reshare + * @param poolId Pool ID + * @param operatorIds Operator IDs + * @param newOperatorId New operator ID + * @param oldOperatorId Old operator ID + * @param shares Operator shares + * @param cluster Cluster snapshot + * @param oldCluster Old cluster snapshot + * @param feeAmount Fee amount to deposit + * @param minTokenAmount Minimum SSV token amount out after processing fees + * @param processed Whether the fee amount is already processed + */ + function resharePool( + uint32 poolId, + uint64[] memory operatorIds, + uint64 newOperatorId, + uint64 oldOperatorId, + bytes memory shares, + ISSVNetworkCore.Cluster memory cluster, + ISSVNetworkCore.Cluster memory oldCluster, + uint256 feeAmount, + uint256 minTokenAmount, + bool processed + ) external; + + /** + * @notice Report forced exits + * @param poolIds Pool IDs + */ + function reportForcedExits(uint32[] memory poolIds) external; + + /** + * @notice Report a completed exit + * @param stakedPoolIndex Staked pool index + * @param blamePercents Operator blame percents (0 if balance is 32 ether) + * @param cluster Cluster snapshot + */ + function reportCompletedExit( + uint256 stakedPoolIndex, + uint32[] memory blamePercents, + ISSVNetworkCore.Cluster memory cluster + ) external; + + /** + * @notice Withdraw cluster balance + * @param operatorIds Operator IDs + * @param cluster Cluster snapshot + * @param amount Amount to withdraw + */ + function withdrawClusterBalance( + uint64[] memory operatorIds, + ISSVNetworkCore.Cluster memory cluster, + uint256 amount + ) external; + + /** + * @notice Withdraw LINK balance + * @param amount Amount to withdraw + */ + function withdrawLINKBalance(uint256 amount) external; + + /** + * @notice Withdraw SSV balance + * @param amount Amount to withdraw + */ + function withdrawSSVBalance(uint256 amount) external; + + /// @notice Cancel the Chainlink functions subscription + function cancelFunctions() external; + + /// @notice Cancel the Chainlink upkeep subscription + function cancelUpkeep() external; + + /// @notice User stake lock period + function lockPeriod() external view returns (uint256); + + /// @notice User stake fee percentage + function userFee() external view returns (uint32); + + /// @notice Whether eigen stake is enabled + function eigenStake() external view returns (bool); + + /// @notice Whether liquid stake is enabled + function liquidStake() external view returns (bool); + + /// @notice Chainlink functions subscription ID + function functionsId() external view returns (uint64); + + /// @notice Chainlink upkeep subscription ID + function upkeepId() external view returns (uint256); + + /// @notice Latest beacon chain balance + function latestBeaconBalance() external view returns (uint256); + + /// @notice Reserved fee balance + function reservedFeeBalance() external view returns (uint256); + + /// @notice Requested withdrawal balance + function requestedWithdrawalBalance() external view returns (uint256); + + /// @notice Requested exit count + function requestedExits() external view returns (uint256); + + /// @notice Fully reported activations in the current period + function finalizableActivations() external view returns (uint256); + + /// @notice Fully reported completed exits in the current period + function finalizableCompletedExits() external view returns (uint256); + + /// @notice Current report period + function reportPeriod() external view returns (uint32); + + /// @notice Get the total stake (buffered + beacon - requested withdrawals) + function getTotalStake() external view returns (uint256); + + /// @notice Get the pending pool IDs + function getPendingPoolIds() external view returns (uint32[] memory); + + /// @notice Get the staked pool IDs + function getStakedPoolIds() external view returns (uint32[] memory); + + /// @notice Get the buffered balance (prepool + exited + ready) + function getBufferedBalance() external view returns (uint256); + + /** + * @notice Get the eligibility of a pending withdrawal + * @param index Index of the pending withdrawal + * @param period Period to check + */ + function getPendingWithdrawalEligibility(uint256 index, uint256 period) external view returns (bool); + + /// @notice Get the withdrawable balance (prepool + exited) + function getWithdrawableBalance() external view returns (uint256); + + /** + * @notice Get user stake + * @param userAddress User address + */ + function getUserStake(address userAddress) external view returns (uint256); + + /** + * @notice Get a pool address + * @param poolId Pool ID + */ + function getPoolAddress(uint32 poolId) external view returns (address); + + /// @notice Get the registry address + function getRegistryAddress() external view returns (address); + + /// @notice Get the upkeep address + function getUpkeepAddress() external view returns (address); +} diff --git a/contracts/ethereum/src/v1/dev/interfaces/ICasimirPool.sol b/contracts/ethereum/src/v1/dev/interfaces/ICasimirPool.sol new file mode 100644 index 000000000..90729030e --- /dev/null +++ b/contracts/ethereum/src/v1/dev/interfaces/ICasimirPool.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "../interfaces/ICasimirCore.sol"; + +interface ICasimirPoolDev is ICasimirCoreDev { + event OperatorIdsSet(uint64[] operatorIds); + event ResharesSet(uint256 reshares); + event StatusSet(PoolStatus status); + + error InvalidDepositAmount(); + error InvalidWithdrawalCredentials(); + + /** + * @notice Deposit pool stake + * @param depositDataRoot Deposit data root + * @param signature Deposit signature + * @param withdrawalCredentials Validator withdrawal credentials + */ + function depositStake( + bytes32 depositDataRoot, + bytes memory signature, + bytes memory withdrawalCredentials + ) external payable; + + /// @notice Deposit pool rewards + function depositRewards() external; + + /** + * @notice Set the operator IDs + * @param newOperatorIds New operator IDs + */ + function setOperatorIds(uint64[] memory newOperatorIds) external; + + /** + * @notice Set the reshare count + * @param newReshares New reshare count + */ + function setReshares(uint256 newReshares) external; + + /** + * @notice Set the pool status + * @param newStatus New status + */ + function setStatus(PoolStatus newStatus) external; + + /** + * @notice Withdraw pool balance to the manager + * @param blamePercents Operator loss blame percents + */ + function withdrawBalance(uint32[] memory blamePercents) external; + + /// @notice Validator public key + function publicKey() external view returns (bytes memory); + + /// @notice Reshare count + function reshares() external view returns (uint256); + + /// @notice Pool status + function status() external view returns (PoolStatus); + + /// @notice Get the pool operator IDs + function getOperatorIds() external view returns (uint64[] memory); + + /// @notice Get the pool registration + function getRegistration() external view returns (PoolRegistration memory); +} diff --git a/contracts/ethereum/src/v1/dev/interfaces/ICasimirRegistry.sol b/contracts/ethereum/src/v1/dev/interfaces/ICasimirRegistry.sol new file mode 100644 index 000000000..a6ca9fc7f --- /dev/null +++ b/contracts/ethereum/src/v1/dev/interfaces/ICasimirRegistry.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./ICasimirCore.sol"; + +interface ICasimirRegistryDev is ICasimirCoreDev { + event CollateralDeposited(uint64 indexed operatorId, uint256 amount); + event DeactivationCompleted(uint64 indexed operatorId); + event DeactivationRequested(uint64 indexed operatorId); + event DeregistrationCompleted(uint64 indexed operatorId); + event OperatorPoolAdded(uint64 indexed operatorId, uint32 poolId); + event OperatorPoolRemoved(uint64 operatorId, uint32 poolId, uint256 blameAmount); + event OperatorRegistered(uint64 indexed operatorId); + event WithdrawalFulfilled(uint64 indexed operatorId, uint256 amount); + + error CollateralInUse(); + error InsufficientCollateral(); + error OperatorAlreadyRegistered(); + error OperatorNotActive(); + error OperatorNotPrivate(); + error OperatorResharing(); + error PoolAlreadyExists(); + error PoolDoesNotExist(); + + /** + * @notice Register an operator + * @param operatorId Operator ID + */ + function registerOperator(uint64 operatorId) external payable; + + /** + * @notice Deposit operator collateral + * @param operatorId Operator ID + */ + function depositCollateral(uint64 operatorId) external payable; + + /** + * @notice Request to withdraw operator collateral + * @param operatorId Operator ID + * @param amount Amount to withdraw + */ + function requestWithdrawal(uint64 operatorId, uint256 amount) external; + + /** + * @notice Request operator deactivation + * @param operatorId Operator ID + */ + function requestDeactivation(uint64 operatorId) external; + + /** + * @notice Add a pool to an operator + * @param operatorId Operator ID + * @param poolId Pool ID + */ + function addOperatorPool(uint64 operatorId, uint32 poolId) external; + + /** + * @notice Remove a pool from an operator + * @param operatorId Operator ID + * @param poolId Pool ID + * @param blameAmount Amount to recover from collateral + */ + function removeOperatorPool(uint64 operatorId, uint32 poolId, uint256 blameAmount) external; + + /** + * @notice Get an operator + * @param operatorId Operator ID + */ + function getOperator(uint64 operatorId) external view returns (Operator memory); + + /// @notice Get all previously registered operator IDs + function getOperatorIds() external view returns (uint64[] memory); + + /// @notice Minimum collateral per operator per pool + function minCollateral() external view returns (uint256); + + /// @notice Whether private operators are enabled + function privateOperators() external view returns (bool); + + /// @notice Whether verified operators are enabled + function verifiedOperators() external view returns (bool); +} diff --git a/contracts/ethereum/src/v1/dev/interfaces/ICasimirUpkeep.sol b/contracts/ethereum/src/v1/dev/interfaces/ICasimirUpkeep.sol new file mode 100644 index 000000000..5b2f0fb21 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/interfaces/ICasimirUpkeep.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./ICasimirCore.sol"; +import "@chainlink/contracts/src/v0.8/interfaces/AutomationCompatibleInterface.sol"; + +interface ICasimirUpkeepDev is ICasimirCoreDev, AutomationCompatibleInterface { + /// @dev Functions request type + enum RequestType { + NONE, + BALANCES, + DETAILS + } + + /// @dev Report status + enum ReportStatus { + FINALIZED, + REQUESTING, + PROCESSING + } + + event ActivationsRequested(uint256 count); + event ForcedExitReportsRequested(uint256 count); + event CompletedExitReportsRequested(uint256 count); + event OCRResponse(bytes32 indexed requestId, bytes result, bytes err); + event FunctionsRequestSet(string newRequestSource, string[] newRequestArgs, uint32 newFulfillGasLimit); + event FunctionsOracleAddressSet(address newFunctionsOracleAddress); + event UpkeepPerformed(ReportStatus indexed status); + + error InvalidRequest(); + error UpkeepNotNeeded(); + + /// @notice Perform the upkeep + function performUpkeep(bytes calldata) external; + + /** + * @notice Set a new Chainlink functions request + * @param newRequestSource New Chainlink functions source code + * @param newRequestArgs New Chainlink functions arguments + * @param newFulfillGasLimit New Chainlink functions fulfill gas limit + */ + function setFunctionsRequest( + string calldata newRequestSource, + string[] calldata newRequestArgs, + uint32 newFulfillGasLimit + ) external; + + /** + * @notice Set a new Chainlink functions oracle address + * @param newFunctionsOracleAddress New Chainlink functions oracle address + */ + function setFunctionsOracle(address newFunctionsOracleAddress) external; + + /// @notice Check if the upkeep is needed + function checkUpkeep(bytes calldata checkData) external view returns (bool upkeepNeeded, bytes memory); + + /// @notice Whether compound stake is enabled + function compoundStake() external view returns (bool); +} diff --git a/contracts/ethereum/src/v1/dev/interfaces/ICasimirViews.sol b/contracts/ethereum/src/v1/dev/interfaces/ICasimirViews.sol new file mode 100644 index 000000000..84a8c3156 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/interfaces/ICasimirViews.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "./ICasimirCore.sol"; + +interface ICasimirViewsDev is ICasimirCoreDev { + /** + * @notice Get the next five compoundable pool IDs + * @param startIndex Start index + * @param endIndex End index + */ + function getCompoundablePoolIds(uint256 startIndex, uint256 endIndex) external view returns (uint32[5] memory); + + /// @notice Get the deposited pool count + function getDepositedPoolCount() external view returns (uint256); + + /** + * @notice Get the deposited pool public keys + * @param startIndex Start index + * @param endIndex End index + */ + function getDepositedPoolPublicKeys(uint256 startIndex, uint256 endIndex) external view returns (bytes[] memory); + + /** + * @notice Get the deposited pool statuses + * @param startIndex Start index + * @param endIndex End index + */ + function getDepositedPoolStatuses(uint256 startIndex, uint256 endIndex) external view returns (PoolStatus[] memory); + + /** + * @notice Get operators + * @param startIndex Start index + * @param endIndex End index + */ + function getOperators(uint256 startIndex, uint256 endIndex) external view returns (Operator[] memory); + + /** + * @notice Get pool config + * @param poolId Pool ID + */ + function getPoolConfig(uint32 poolId) external view returns (PoolConfig memory); + + /** + * @notice Get the swept balance (in gwei) + * @param startIndex Start index + * @param endIndex End index + */ + function getSweptBalance(uint256 startIndex, uint256 endIndex) external view returns (uint128); +} diff --git a/contracts/ethereum/src/v1/dev/libraries/CasimirArray.sol b/contracts/ethereum/src/v1/dev/libraries/CasimirArray.sol new file mode 100644 index 000000000..7d7be4964 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/libraries/CasimirArray.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "../interfaces/ICasimirCore.sol"; + +/// @title Library to extend array functionality +library CasimirArrayDev { + error IndexOutOfBounds(); + error EmptyArray(); + + function removeUint32Item(uint32[] storage uint32Array, uint index) internal { + if (uint32Array.length == 0) { + revert EmptyArray(); + } + if (index >= uint32Array.length) { + revert IndexOutOfBounds(); + } + for (uint i = index; i < uint32Array.length - 1; i++) { + uint32Array[i] = uint32Array[i + 1]; + } + uint32Array.pop(); + } + + function removeBytesItem(bytes[] storage bytesArray, uint index) internal { + if (bytesArray.length == 0) { + revert EmptyArray(); + } + if (index >= bytesArray.length) { + revert IndexOutOfBounds(); + } + for (uint i = index; i < bytesArray.length - 1; i++) { + bytesArray[i] = bytesArray[i + 1]; + } + bytesArray.pop(); + } + + function removeWithdrawalItem(ICasimirCoreDev.Withdrawal[] storage withdrawals, uint index) internal { + if (withdrawals.length == 0) { + revert EmptyArray(); + } + if (index >= withdrawals.length) { + revert IndexOutOfBounds(); + } + for (uint i = index; i < withdrawals.length - 1; i++) { + withdrawals[i] = withdrawals[i + 1]; + } + withdrawals.pop(); + } +} diff --git a/contracts/ethereum/src/v1/dev/libraries/CasimirBeacon.sol b/contracts/ethereum/src/v1/dev/libraries/CasimirBeacon.sol new file mode 100644 index 000000000..785a98a6a --- /dev/null +++ b/contracts/ethereum/src/v1/dev/libraries/CasimirBeacon.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "../CasimirManager.sol"; +import "../CasimirPool.sol"; +import "../CasimirRegistry.sol"; +import "../CasimirUpkeep.sol"; +import "../CasimirViews.sol"; +import "../interfaces/ICasimirCore.sol"; +import "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; + +/// @title Library to create beacon proxy contracts +library CasimirBeaconDev { + /** + * @notice Deploy a new manager beacon proxy contract + * @param managerBeaconAddress Manager beacon address + * @param daoOracleAddress DAO oracle address + * @param functionsOracleAddress Chainlink functions oracle address + * @param strategy Staking strategy configuration + */ + function createManager( + address managerBeaconAddress, + address daoOracleAddress, + address functionsOracleAddress, + ICasimirCoreDev.Strategy memory strategy + ) public returns (address managerAddress) { + managerAddress = address( + new BeaconProxy( + managerBeaconAddress, + abi.encodeWithSelector( + CasimirManagerDev(payable(address(0))).initialize.selector, + daoOracleAddress, + functionsOracleAddress, + strategy + ) + ) + ); + } + + /** + * @notice Deploy a new pool beacon proxy contract + * @param poolBeaconAddress Pool beacon address + * @param registryAddress Registry contract address + * @param poolId Pool ID + * @param operatorIds Operator IDs + * @param publicKey Validator public key + * @param shares Operator key shares + * @return poolAddress Pool contract address + */ + function createPool( + address poolBeaconAddress, + address registryAddress, + uint64[] memory operatorIds, + uint32 poolId, + bytes memory publicKey, + bytes memory shares + ) public returns (address poolAddress) { + poolAddress = address( + new BeaconProxy( + poolBeaconAddress, + abi.encodeWithSelector( + CasimirPoolDev(address(0)).initialize.selector, + registryAddress, + operatorIds, + poolId, + publicKey, + shares + ) + ) + ); + } + + /** + * @notice Deploy a new registry beacon proxy + * @param registryBeaconAddress Registry beacon address + * @param minCollateral Minimum collateral per operator per pool + * @param privateOperators Whether private operators are enabled + * @param verifiedOperators Whether verified operators are enabled + * @return registryAddress Registry address + */ + function createRegistry( + address registryBeaconAddress, + uint256 minCollateral, + bool privateOperators, + bool verifiedOperators + ) public returns (address registryAddress) { + registryAddress = address( + new BeaconProxy( + registryBeaconAddress, + abi.encodeWithSelector( + CasimirRegistryDev(address(0)).initialize.selector, + minCollateral, + privateOperators, + verifiedOperators + ) + ) + ); + } + + /** + * @notice Deploy a new upkeep beacon proxy contract + * @param upkeepBeaconAddress Upkeep beacon address + * @param factoryAddress Factory contract address + * @param functionsOracleAddress Chainlink functions oracle address + * @param compoundStake Whether to compound stake + * @return upkeepAddress Upkeep contract address + */ + function createUpkeep( + address upkeepBeaconAddress, + address factoryAddress, + address functionsOracleAddress, + bool compoundStake + ) public returns (address upkeepAddress) { + upkeepAddress = address( + new BeaconProxy( + upkeepBeaconAddress, + abi.encodeWithSelector( + CasimirUpkeepDev(address(0)).initialize.selector, + factoryAddress, + functionsOracleAddress, + compoundStake + ) + ) + ); + } + + /** + * @notice Deploy a new views beacon proxy contract + * @param viewsBeaconAddress Views beacon address + * @param managerAddress Manager contract address + * @return viewsAddress Views contract address + */ + function createViews(address viewsBeaconAddress, address managerAddress) public returns (address viewsAddress) { + viewsAddress = address( + new BeaconProxy( + viewsBeaconAddress, + abi.encodeWithSelector(CasimirViewsDev(address(0)).initialize.selector, managerAddress) + ) + ); + } +} diff --git a/contracts/ethereum/src/v1/dev/mock/FunctionsBillingRegistry.sol b/contracts/ethereum/src/v1/dev/mock/FunctionsBillingRegistry.sol new file mode 100644 index 000000000..e2d69c0d0 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/mock/FunctionsBillingRegistry.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "@chainlink/contracts/src/v0.8/dev/functions/FunctionsBillingRegistry.sol"; \ No newline at end of file diff --git a/contracts/ethereum/src/v1/dev/mock/FunctionsOracle.sol b/contracts/ethereum/src/v1/dev/mock/FunctionsOracle.sol new file mode 100644 index 000000000..26fa6677f --- /dev/null +++ b/contracts/ethereum/src/v1/dev/mock/FunctionsOracle.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "@chainlink/contracts/src/v0.8/dev/functions/FunctionsOracle.sol"; diff --git a/contracts/ethereum/src/v1/dev/mock/FunctionsOracleFactory.sol b/contracts/ethereum/src/v1/dev/mock/FunctionsOracleFactory.sol new file mode 100644 index 000000000..ea9e7f9dd --- /dev/null +++ b/contracts/ethereum/src/v1/dev/mock/FunctionsOracleFactory.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +import "@chainlink/contracts/src/v0.8/dev/functions/FunctionsOracleFactory.sol"; \ No newline at end of file diff --git a/contracts/ethereum/src/v1/dev/vendor/FunctionsClient.sol b/contracts/ethereum/src/v1/dev/vendor/FunctionsClient.sol new file mode 100644 index 000000000..ea8fb2f61 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/FunctionsClient.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.6; + +import "@chainlink/contracts/src/v0.8/dev/functions/Functions.sol"; +import "@chainlink/contracts/src/v0.8/dev/interfaces/FunctionsClientInterface.sol"; +import "@chainlink/contracts/src/v0.8/dev/interfaces/FunctionsOracleInterface.sol"; + +/** + * @title The Chainlink Functions client contract + * @notice Contract writers can inherit this contract in order to create Chainlink Functions requests + */ +abstract contract FunctionsClient is FunctionsClientInterface { + FunctionsOracleInterface internal s_oracle; + mapping(bytes32 => address) internal s_pendingRequests; + + event RequestSent(bytes32 indexed id); + event RequestFulfilled(bytes32 indexed id); + + error SenderIsNotRegistry(); + error RequestIsAlreadyPending(); + error RequestIsNotPending(); + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address oracle) { + setOracle(oracle); + } + + /** + * @inheritdoc FunctionsClientInterface + */ + function getDONPublicKey() external view override returns (bytes memory) { + return s_oracle.getDONPublicKey(); + } + + /** + * @notice Estimate the total cost that will be charged to a subscription to make a request: gas re-imbursement, plus DON fee, plus Registry fee + * @param req The initialized Functions.Request + * @param subscriptionId The subscription ID + * @param gasLimit gas limit for the fulfillment callback + * @return billedCost Cost in Juels (1e18) of LINK + */ + function estimateCost( + Functions.Request memory req, + uint64 subscriptionId, + uint32 gasLimit, + uint256 gasPrice + ) public view returns (uint96) { + return s_oracle.estimateCost(subscriptionId, Functions.encodeCBOR(req), gasLimit, gasPrice); + } + + /** + * @notice Sends a Chainlink Functions request to the stored oracle address + * @param req The initialized Functions.Request + * @param subscriptionId The subscription ID + * @param gasLimit gas limit for the fulfillment callback + * @return requestId The generated request ID + */ + function sendRequest( + Functions.Request memory req, + uint64 subscriptionId, + uint32 gasLimit + ) internal returns (bytes32) { + bytes32 requestId = s_oracle.sendRequest(subscriptionId, Functions.encodeCBOR(req), gasLimit); + s_pendingRequests[requestId] = s_oracle.getRegistry(); + emit RequestSent(requestId); + return requestId; + } + + /** + * @notice User defined function to handle a response + * @param requestId The request ID, returned by sendRequest() + * @param response Aggregated response from the user code + * @param err Aggregated error from the user code or from the execution pipeline + * Either response or error parameter will be set, but never both + */ + function fulfillRequest(bytes32 requestId, bytes memory response, bytes memory err) internal virtual; + + /** + * @inheritdoc FunctionsClientInterface + */ + function handleOracleFulfillment( + bytes32 requestId, + bytes memory response, + bytes memory err + ) external override recordChainlinkFulfillment(requestId) { + fulfillRequest(requestId, response, err); + } + + /** + * @notice Sets the stored Oracle address + * @param oracle The address of Functions Oracle contract + */ + function setOracle(address oracle) internal { + s_oracle = FunctionsOracleInterface(oracle); + } + + /** + * @notice Gets the stored address of the oracle contract + * @return The address of the oracle contract + */ + function getChainlinkOracleAddress() internal view returns (address) { + return address(s_oracle); + } + + /** + * @notice Allows for a request which was created on another contract to be fulfilled + * on this contract + * @param oracleAddress The address of the oracle contract that will fulfill the request + * @param requestId The request ID used for the response + */ + function addExternalRequest(address oracleAddress, bytes32 requestId) internal notPendingRequest(requestId) { + s_pendingRequests[requestId] = oracleAddress; + } + + /** + * @dev Reverts if the sender is not the oracle that serviced the request. + * Emits RequestFulfilled event. + * @param requestId The request ID for fulfillment + */ + modifier recordChainlinkFulfillment(bytes32 requestId) { + if (msg.sender != s_pendingRequests[requestId]) { + revert SenderIsNotRegistry(); + } + delete s_pendingRequests[requestId]; + emit RequestFulfilled(requestId); + _; + } + + /** + * @dev Reverts if the request is already pending + * @param requestId The request ID for fulfillment + */ + modifier notPendingRequest(bytes32 requestId) { + if (s_pendingRequests[requestId] != address(0)) { + revert RequestIsAlreadyPending(); + } + _; + } +} diff --git a/contracts/ethereum/src/v1/dev/vendor/interfaces/IAutomationRegistry.sol b/contracts/ethereum/src/v1/dev/vendor/interfaces/IAutomationRegistry.sol new file mode 100644 index 000000000..e146a35f9 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/interfaces/IAutomationRegistry.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache +pragma solidity 0.8.18; + +struct UpkeepInfo { + address target; + uint32 executeGas; + bytes checkData; + uint96 balance; + address admin; + uint64 maxValidBlocknumber; + uint32 lastPerformBlockNumber; + uint96 amountSpent; + bool paused; + bytes offchainConfig; +} + +interface IAutomationRegistry { + function getUpkeep(uint256 id) external view returns (UpkeepInfo memory); + + function addFunds(uint256 id, uint96 amount) external; + + function cancelUpkeep(uint256 id) external; +} diff --git a/contracts/ethereum/src/v1/dev/vendor/interfaces/IDepositContract.sol b/contracts/ethereum/src/v1/dev/vendor/interfaces/IDepositContract.sol new file mode 100644 index 000000000..22d873b16 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/interfaces/IDepositContract.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +interface IDepositContract { + /// @notice A processed deposit event. + event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index); + + /// @notice Submit a Phase 0 DepositData object. + /// @param pubkey A BLS12-381 public key. + /// @param withdrawal_credentials Commitment to a public key for withdrawals. + /// @param signature A BLS12-381 signature. + /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object. + /// Used as a protection against malformed input. + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable; + + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external view returns (bytes32); + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external view returns (bytes memory); +} diff --git a/contracts/ethereum/src/v1/dev/vendor/interfaces/IFunctionsBillingRegistry.sol b/contracts/ethereum/src/v1/dev/vendor/interfaces/IFunctionsBillingRegistry.sol new file mode 100644 index 000000000..37af31218 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/interfaces/IFunctionsBillingRegistry.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +interface IFunctionsBillingRegistry { + function getSubscription( + uint64 subscriptionId + ) external view returns (uint96 balance, address owner, address[] memory consumers); + + function createSubscription() external returns (uint64); + + function addConsumer(uint64 subscriptionId, address consumer) external; + + function cancelSubscription(uint64 subscriptionId, address receiver) external; +} diff --git a/contracts/ethereum/src/v1/dev/vendor/interfaces/IKeeperRegistrar.sol b/contracts/ethereum/src/v1/dev/vendor/interfaces/IKeeperRegistrar.sol new file mode 100644 index 000000000..a8c6c515a --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/interfaces/IKeeperRegistrar.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +interface IKeeperRegistrar { + struct RegistrationParams { + string name; + bytes encryptedEmail; + address upkeepContract; + uint32 gasLimit; + address adminAddress; + bytes checkData; + bytes offchainConfig; + uint96 amount; + } + + function registerUpkeep(RegistrationParams calldata requestParams) external returns (uint256); +} diff --git a/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVNetwork.sol b/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVNetwork.sol new file mode 100644 index 000000000..42149debf --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVNetwork.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import "../../../../../lib/ssv-network/contracts/interfaces/ISSVNetwork.sol"; diff --git a/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVNetworkCore.sol b/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVNetworkCore.sol new file mode 100644 index 000000000..749bab70e --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVNetworkCore.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import "../../../../../lib/ssv-network/contracts/interfaces/ISSVNetworkCore.sol"; diff --git a/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVViews.sol b/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVViews.sol new file mode 100644 index 000000000..5a5abf17e --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/interfaces/ISSVViews.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.18; + +import "../../../../../lib/ssv-network/contracts/interfaces/ISSVViews.sol"; diff --git a/contracts/ethereum/src/v1/dev/vendor/interfaces/IWETH9.sol b/contracts/ethereum/src/v1/dev/vendor/interfaces/IWETH9.sol new file mode 100644 index 000000000..aef6ac5d5 --- /dev/null +++ b/contracts/ethereum/src/v1/dev/vendor/interfaces/IWETH9.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Interface for WETH9 +interface IWETH9 is IERC20 { + /// @notice Deposit ether to get wrapped ether + function deposit() external payable; + + /** + * @notice Withdraw wrapped ether to get ether + * @param amount Amount of wrapped ether to withdraw + */ + function withdraw(uint256 amount) external; +} diff --git a/contracts/ethereum/test/fixtures/shared.ts b/contracts/ethereum/test/fixtures/shared.ts index eb46efd4e..f95bee12b 100644 --- a/contracts/ethereum/test/fixtures/shared.ts +++ b/contracts/ethereum/test/fixtures/shared.ts @@ -22,8 +22,8 @@ export async function deploymentFixture() { if (!process.env.SWAP_ROUTER_ADDRESS) throw new Error('No swap router address provided') if (!process.env.WETH_TOKEN_ADDRESS) throw new Error('No weth token address provided') - const [, daoOracle, donTransmitter] = await ethers.getSigners() - + const [owner, daoOracle, donTransmitter] = await ethers.getSigners() + const functionsOracleFactoryFactory = await ethers.getContractFactory('FunctionsOracleFactory') const functionsOracleFactory = await functionsOracleFactoryFactory.deploy() as FunctionsOracleFactory await functionsOracleFactory.deployed() @@ -162,7 +162,47 @@ export async function deploymentFixture() { await functionsOracle.addAuthorizedSenders([donTransmitter.address, managerAddress]) const ssvViews = await ethers.getContractAt(ISSVViewsAbi, process.env.SSV_VIEWS_ADDRESS as string) as ISSVViews - const preregisteredOperatorIds = process.env.PREREGISTERED_OPERATOR_IDS?.split(',').map(id => parseInt(id)) || [208, 209, 210, 211, 212, 213, 214, 215] + + return { + managerBeacon, + poolBeacon, + registryBeacon, + upkeepBeacon, + viewsBeacon, + factory, + manager, + registry, + upkeep, + views, + functionsBillingRegistry, + functionsOracle, + ssvViews, + daoOracle, + donTransmitter + } +} + +/** Fixture to preregister operators */ +export async function preregistrationFixture() { + const { + managerBeacon, + poolBeacon, + registryBeacon, + upkeepBeacon, + viewsBeacon, + factory, + manager, + registry, + upkeep, + views, + functionsBillingRegistry, + functionsOracle, + ssvViews, + daoOracle, + donTransmitter + } = await loadFixture(deploymentFixture) + + const preregisteredOperatorIds = process.env.PREREGISTERED_OPERATOR_IDS?.split(',').map(id => parseInt(id)) || [208, 209, 210, 211/*, 212, 213, 214, 215*/] if (preregisteredOperatorIds.length < 4) throw new Error('Not enough operator ids provided') const preregisteredBalance = ethers.utils.parseEther('10') for (const operatorId of preregisteredOperatorIds) { @@ -179,12 +219,28 @@ export async function deploymentFixture() { await result.wait() } - return { manager, registry, upkeep, views, functionsBillingRegistry, ssvViews, daoOracle, donTransmitter } + return { + managerBeacon, + poolBeacon, + registryBeacon, + upkeepBeacon, + viewsBeacon, + factory, + manager, + registry, + upkeep, + views, + functionsBillingRegistry, + functionsOracle, + ssvViews, + daoOracle, + donTransmitter + } } /** Fixture to stake 16 for the first user */ export async function firstUserDepositFixture() { - const { manager, registry, upkeep, views, functionsBillingRegistry, ssvViews, daoOracle, donTransmitter } = await loadFixture(deploymentFixture) + const { manager, registry, upkeep, views, functionsBillingRegistry, ssvViews, daoOracle, donTransmitter } = await loadFixture(preregistrationFixture) const [, , , firstUser] = await ethers.getSigners() const depositAmount = round(16 * ((100 + await manager.userFee()) / 100), 10) diff --git a/contracts/ethereum/test/operators.ts b/contracts/ethereum/test/operators.ts index 51aee44bd..7cafcfa8f 100644 --- a/contracts/ethereum/test/operators.ts +++ b/contracts/ethereum/test/operators.ts @@ -1,24 +1,23 @@ import { ethers, network } from 'hardhat' import { loadFixture, setBalance, time } from '@nomicfoundation/hardhat-network-helpers' import { expect } from 'chai' -import { deploymentFixture, secondUserDepositFixture } from './fixtures/shared' +import { preregistrationFixture, secondUserDepositFixture } from './fixtures/shared' import { round } from '../helpers/math' import { initiatePoolHandler, reportCompletedExitsHandler } from '../helpers/oracle' import { fulfillReport, runUpkeep } from '../helpers/upkeep' -import { ICasimirCore } from '../build/@types' describe('Operators', async function () { - it('Registration of operators 1 through 8 creates 8 eligible operators', async function () { - const { registry, views } = await loadFixture(deploymentFixture) + it('Preregistration of operators 1 through 4 creates 4 eligible operators', async function () { + const { registry, views } = await loadFixture(preregistrationFixture) const operatorIds = await registry.getOperatorIds() const startIndex = 0 const endIndex = operatorIds.length const operators = await views.getOperators(startIndex, endIndex) - expect(operators.length).equal(8) + expect(operators.length).equal(4) }) it('First initiated deposit uses 4 eligible operators', async function () { - const { manager, registry, views, daoOracle } = await loadFixture(deploymentFixture) + const { manager, registry, views, daoOracle } = await loadFixture(preregistrationFixture) const [firstUser] = await ethers.getSigners() const depositAmount = round(32 * ((100 + await manager.userFee()) / 100), 10) @@ -29,7 +28,7 @@ describe('Operators', async function () { const operatorIds = await registry.getOperatorIds() const startIndex = 0 - const endIndex = operatorIds.length - 1 + const endIndex = operatorIds.length const operators = await views.getOperators(startIndex, endIndex) const operatorsWithPools = operators.filter(operator => Number(operator.poolCount.toString()) === 1) @@ -37,7 +36,7 @@ describe('Operators', async function () { }) it('Operator deregistration with 1 pool emits 1 reshare request', async function () { - const { manager, registry, ssvViews, daoOracle } = await loadFixture(deploymentFixture) + const { manager, registry, ssvViews, daoOracle } = await loadFixture(preregistrationFixture) const [firstUser] = await ethers.getSigners() const depositAmount = round(32 * ((100 + await manager.userFee()) / 100), 10) @@ -62,7 +61,7 @@ describe('Operators', async function () { }) it('Operator deregistration with 0 pools allows immediate collateral withdrawal', async function () { - const { registry, ssvViews } = await loadFixture(deploymentFixture) + const { registry, ssvViews } = await loadFixture(preregistrationFixture) const operatorIds = await registry.getOperatorIds() const deregisteringOperatorId = operatorIds[0] diff --git a/contracts/ethereum/test/upgrade.ts b/contracts/ethereum/test/upgrade.ts new file mode 100644 index 000000000..06174e763 --- /dev/null +++ b/contracts/ethereum/test/upgrade.ts @@ -0,0 +1,145 @@ +import { ethers, network, upgrades } from 'hardhat' +import { loadFixture, setBalance } from '@nomicfoundation/hardhat-network-helpers' +import { expect } from 'chai' +import { deploymentFixture } from './fixtures/shared' +import requestConfig from '@casimir/functions/Functions-request-config' +import ICasimirRegistryDevAbi from '../build/abi/ICasimirRegistryDev.json' +import ISSVViewsAbi from '../build/abi/ISSVViews.json' +import { CasimirRegistryDev, CasimirUpkeepDev, ISSVViews } from '../build/@types' + +describe('Upgrade', async function () { + it('Upgrade contracts with dev versions', async function () { + const { + functionsBillingRegistry, + functionsOracle, + managerBeacon, + poolBeacon, + registryBeacon, + upkeepBeacon, + viewsBeacon, + factory, + daoOracle, + donTransmitter + } = await loadFixture(deploymentFixture) + + const beaconDevLibraryFactory = await ethers.getContractFactory('CasimirBeaconDev') + const beaconDevLibrary = await beaconDevLibraryFactory.deploy() + + const managerDevBeaconFactory = await ethers.getContractFactory('CasimirManagerDev', { + libraries: { + CasimirBeaconDev: beaconDevLibrary.address + } + }) + const managerDevBeacon = await upgrades.upgradeBeacon(managerBeacon.address, managerDevBeaconFactory, { + constructorArgs: [ + functionsBillingRegistry.address, + process.env.KEEPER_REGISTRAR_ADDRESS as string, + process.env.KEEPER_REGISTRY_ADDRESS as string, + process.env.LINK_TOKEN_ADDRESS as string, + process.env.SSV_NETWORK_ADDRESS as string, + process.env.SSV_TOKEN_ADDRESS as string, + process.env.SWAP_FACTORY_ADDRESS as string, + process.env.SWAP_ROUTER_ADDRESS as string, + process.env.WETH_TOKEN_ADDRESS as string + ], + unsafeAllow: ['external-library-linking'] + }) + await managerDevBeacon.deployed() + + const poolDevBeaconFactory = await ethers.getContractFactory('CasimirPoolDev') + const poolDevBeacon = await upgrades.upgradeBeacon(poolBeacon.address, poolDevBeaconFactory, { + constructorArgs: [ + process.env.DEPOSIT_CONTRACT_ADDRESS as string + ] + }) + await poolDevBeacon.deployed() + + const registryDevBeaconFactory = await ethers.getContractFactory('CasimirRegistryDev') + const registryDevBeacon = await upgrades.upgradeBeacon(registryBeacon.address, registryDevBeaconFactory, { + constructorArgs: [ + process.env.SSV_VIEWS_ADDRESS as string + ] + }) + await registryDevBeacon.deployed() + + const upkeepDevBeaconFactory = await ethers.getContractFactory('CasimirUpkeepDev') + const upkeepDevBeacon = await upgrades.upgradeBeacon(upkeepBeacon.address, upkeepDevBeaconFactory) + await upkeepDevBeacon.deployed() + + const viewsDevBeaconFactory = await ethers.getContractFactory('CasimirViewsDev') + const viewsDevBeacon = await upgrades.upgradeBeacon(viewsBeacon.address, viewsDevBeaconFactory) + await viewsDevBeacon.deployed() + + const factoryDevFactory = await ethers.getContractFactory('CasimirFactoryDev', { + libraries: { + CasimirBeaconDev: beaconDevLibrary.address + } + }) + const factoryDev = await upgrades.upgradeProxy(factory.address, factoryDevFactory, { + constructorArgs: [ + managerBeacon.address, + poolBeacon.address, + registryBeacon.address, + upkeepBeacon.address, + viewsBeacon.address + ], + unsafeAllow: ['external-library-linking'] + }) + await factoryDev.deployed() + + const privateStrategy = { + minCollateral: ethers.utils.parseEther('1.0'), + lockPeriod: 0, + userFee: 5, + compoundStake: true, + eigenStake: false, + liquidStake: false, + privateOperators: true, + verifiedOperators: false + } + const deployPrivateStrategy = await factory.deployManager( + daoOracle.address, + functionsOracle.address, + privateStrategy + ) + await deployPrivateStrategy.wait() + const [defaultManagerId, privateManagerId] = await factory.getManagerIds() + const [privateManagerAddress, privateRegistryAddress, privateUpkeepAddress, privateViewsAddress] = await factory.getManagerConfig(privateManagerId) + // const privateManager = await ethers.getContractAt('CasimirManager', privateManagerAddress) as CasimirManagerDev + const privateRegistry = await ethers.getContractAt('CasimirRegistryDev', privateRegistryAddress) as CasimirRegistryDev + const privateUpkeep = await ethers.getContractAt('CasimirUpkeepDev', privateUpkeepAddress) as CasimirUpkeepDev + // const privateViews = await ethers.getContractAt('CasimirViews', privateViewsAddress) as CasimirViewsDev + + requestConfig.args[1] = privateViewsAddress + const fulfillGasLimit = 300000 + const setRequest = await privateUpkeep.setFunctionsRequest(requestConfig.source, requestConfig.args, fulfillGasLimit) + await setRequest.wait() + + await functionsBillingRegistry.setAuthorizedSenders([donTransmitter.address, functionsOracle.address]) + await functionsOracle.setRegistry(functionsBillingRegistry.address) + await functionsOracle.addAuthorizedSenders([donTransmitter.address, privateManagerAddress]) + + const ssvViews = await ethers.getContractAt(ISSVViewsAbi, process.env.SSV_VIEWS_ADDRESS as string) as ISSVViews + const operatorId = 208 + const registrationCollateral = ethers.utils.parseEther('10') + const [operatorOwnerAddress] = await ssvViews.getOperatorById(operatorId) + const operatorOwnerSigner = ethers.provider.getSigner(operatorOwnerAddress) + const currentBalance = await ethers.provider.getBalance(operatorOwnerAddress) + const nextBalance = currentBalance.add(registrationCollateral) + await setBalance(operatorOwnerAddress, nextBalance) + await network.provider.request({ + method: 'hardhat_impersonateAccount', + params: [operatorOwnerAddress] + }) + const registerOperator = privateRegistry.connect(operatorOwnerSigner).registerOperator(operatorId, { value: registrationCollateral }) + await expect(registerOperator).to.be.revertedWithCustomError(privateRegistry, 'OperatorNotPrivate') + + const [, defaultRegistryAddress] = await factory.getManagerConfig(defaultManagerId) + const defaultRegistry = await ethers.getContractAt(ICasimirRegistryDevAbi, defaultRegistryAddress) as CasimirRegistryDev + const registerOperatorDefault = await defaultRegistry.connect(operatorOwnerSigner).registerOperator(operatorId, { value: registrationCollateral }) + await registerOperatorDefault.wait() + + const operator = await defaultRegistry.getOperator(operatorId) + expect(operator.active).equal(true) + }) +}) \ No newline at end of file diff --git a/contracts/ethereum/test/users.ts b/contracts/ethereum/test/users.ts index d0ae8ea65..209026a45 100644 --- a/contracts/ethereum/test/users.ts +++ b/contracts/ethereum/test/users.ts @@ -1,14 +1,14 @@ import { ethers } from 'hardhat' import { loadFixture, setBalance, time } from '@nomicfoundation/hardhat-network-helpers' import { expect } from 'chai' -import { deploymentFixture } from './fixtures/shared' +import { preregistrationFixture } from './fixtures/shared' import { round } from '../helpers/math' import { activatePoolsHandler, depositFunctionsBalanceHandler, depositUpkeepBalanceHandler, initiatePoolHandler, reportCompletedExitsHandler } from '../helpers/oracle' import { fulfillReport, runUpkeep } from '../helpers/upkeep' describe('Users', async function () { it('User\'s 16.0 stake and half withdrawal updates total and user stake, and user balance', async function () { - const { manager } = await loadFixture(deploymentFixture) + const { manager } = await loadFixture(preregistrationFixture) const [firstUser] = await ethers.getSigners() const depositAmount = round(16 * ((100 + await manager.userFee()) / 100), 10) @@ -35,7 +35,7 @@ describe('Users', async function () { }) it('User\'s 64.0 stake and half withdrawal updates total and user stake, and user balance', async function () { - const { manager, upkeep, views, functionsBillingRegistry, daoOracle, donTransmitter } = await loadFixture(deploymentFixture) + const { manager, upkeep, views, functionsBillingRegistry, daoOracle, donTransmitter } = await loadFixture(preregistrationFixture) const [firstUser] = await ethers.getSigners() const depositAmount = round(64 * ((100 + await manager.userFee()) / 100), 10) diff --git a/scripts/ethereum/dev.ts b/scripts/ethereum/dev.ts index f82e35ba9..4f81bfc11 100644 --- a/scripts/ethereum/dev.ts +++ b/scripts/ethereum/dev.ts @@ -37,7 +37,7 @@ void async function () { const wallet = ethers.Wallet.fromMnemonic(process.env.BIP39_SEED) // Account for the mock, beacon, and library deployments - const walletNonce = await provider.getTransactionCount(wallet.address) + 17 + const walletNonce = await provider.getTransactionCount(wallet.address) + 13 if (!process.env.FACTORY_ADDRESS) { process.env.FACTORY_ADDRESS = ethers.utils.getContractAddress({ diff --git a/scripts/ethereum/test.ts b/scripts/ethereum/test.ts index b4bcb3582..e023b4d90 100755 --- a/scripts/ethereum/test.ts +++ b/scripts/ethereum/test.ts @@ -31,7 +31,7 @@ void async function () { const wallet = ethers.Wallet.fromMnemonic(process.env.BIP39_SEED) // Account for the mock, beacon, and library deployments - const walletNonce = await provider.getTransactionCount(wallet.address) + 17 + const walletNonce = await provider.getTransactionCount(wallet.address) + 13 if (!process.env.FACTORY_ADDRESS) { process.env.FACTORY_ADDRESS = ethers.utils.getContractAddress({ diff --git a/scripts/root/dev.ts b/scripts/root/dev.ts index b179e8d93..9b80a18fc 100644 --- a/scripts/root/dev.ts +++ b/scripts/root/dev.ts @@ -104,7 +104,7 @@ void async function () { const wallet = ethers.Wallet.fromMnemonic(process.env.BIP39_SEED) // Account for the mock, beacon, and library deployments - const walletNonce = await provider.getTransactionCount(wallet.address) + 17 + const walletNonce = await provider.getTransactionCount(wallet.address) + 13 if (!process.env.FACTORY_ADDRESS) { process.env.FACTORY_ADDRESS = ethers.utils.getContractAddress({ diff --git a/services/oracle/scripts/generate.ts b/services/oracle/scripts/generate.ts index a6480bb0a..561cf88ef 100644 --- a/services/oracle/scripts/generate.ts +++ b/services/oracle/scripts/generate.ts @@ -18,7 +18,7 @@ void async function () { if (!process.env.FACTORY_ADDRESS) throw new Error('No factory address provided') - const preregisteredOperatorIds = process.env.PREREGISTERED_OPERATOR_IDS?.split(',').map(id => parseInt(id)) || [208, 209, 210, 211, 212, 213, 214, 215] + const preregisteredOperatorIds = process.env.PREREGISTERED_OPERATOR_IDS?.split(',').map(id => parseInt(id)) || [208, 209, 210, 211/*, 212, 213, 214, 215*/] if (preregisteredOperatorIds.length < 4) throw new Error('Not enough operator ids provided') const accountPath = 'm/44\'/60\'/0\'/0/1'