From 430b5523d46f83b41aabe79952274130b7cf15c4 Mon Sep 17 00:00:00 2001 From: Huang-Ming Huang Date: Mon, 9 Nov 2026 17:26:37 +0000 Subject: [PATCH] Fix reentrant desyndication custody attestations (WIRE-441) Change-Id: I3c9ae88cbc907104bc27cbcceb042bc43db62b48 --- .../include/sysio.synd/sysio.synd.hpp | 4 + contracts/sysio.synd/src/sysio.synd.cpp | 27 ++-- contracts/sysio.synd/sysio.synd.abi | 15 +++ contracts/sysio.synd/sysio.synd.wasm | Bin 121084 -> 121717 bytes contracts/test_contracts/CMakeLists.txt | 1 + .../reenter_desyndicate/CMakeLists.txt | 4 + .../reenter_desyndicate.abi | 55 ++++++++ .../reenter_desyndicate.cpp | 46 +++++++ .../reenter_desyndicate.wasm | Bin 0 -> 10555 bytes contracts/tests/contracts.hpp.in | 8 ++ contracts/tests/sysio.synd_tests.cpp | 123 ++++++++++++++++++ 11 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 contracts/test_contracts/reenter_desyndicate/CMakeLists.txt create mode 100644 contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.abi create mode 100644 contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.cpp create mode 100644 contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.wasm diff --git a/contracts/sysio.synd/include/sysio.synd/sysio.synd.hpp b/contracts/sysio.synd/include/sysio.synd/sysio.synd.hpp index 1e7ad2476b..1ebc1df4c5 100644 --- a/contracts/sysio.synd/include/sysio.synd/sysio.synd.hpp +++ b/contracts/sysio.synd/include/sysio.synd/sysio.synd.hpp @@ -437,6 +437,10 @@ namespace sysio { /// `max_fee` caps the fee in shadow base units before any bucket tick or movement. [[sysio::action]] void desyndicate(name holder, asset quantity, uint64_t max_fee); + /// Self-inline continuation after the recorded return's transfer and burn. Reads live outstanding + /// shadow and queues its attestation without an intervening callback. Direct calls are refused. + [[sysio::action]] void emitdesynd(uint64_t request_id); + /// Current desyndication fee and amount released, both in shadow base units. struct desynd_quote { uint64_t fee; ///< Flat plus floored basis-point charge. diff --git a/contracts/sysio.synd/src/sysio.synd.cpp b/contracts/sysio.synd/src/sysio.synd.cpp index 111824e9f8..34bb5848de 100644 --- a/contracts/sysio.synd/src/sysio.synd.cpp +++ b/contracts/sysio.synd/src/sysio.synd.cpp @@ -69,6 +69,8 @@ constexpr name creditowed_action = "creditowed"_n; constexpr name transfer_action = "transfer"_n; /// `sysio.msgch`'s outbound queue. constexpr name queueout_action = "queueout"_n; +/// Final desyndication emission, after the holder's transfer notifications and the burn. +constexpr name emitdesynd_action = "emitdesynd"_n; /// `sysio.liq::mintyield`, which a released yield item sends. constexpr name mintyield_action = "mintyield"_n; /// `sysio.bond::hold`, which a challenge sends, and `sysio.bond::claim`, which pulls what a ruling awards @@ -1542,10 +1544,6 @@ void synd::desyndicate(name holder, asset quantity, uint64_t max_fee) { // quantity or more leaves nothing to release and is refused before the subtraction. const uint64_t fee = quote.fee; const uint64_t amount = quote.net; - // The depot's outstanding shadow once the burn below has run, read from `st` before it: the burn is - // inline, so nothing has moved supply yet. The net was part of the holder's balance, so it is within - // the supply and the difference cannot underflow. - const uint64_t total_syndicated = liq::outstanding_of(LIQ_ACCOUNT, st) - amount; // The fee pot's position settles before the transfer that backs the fee arrives; no distribution runs // between this action and its inline transfer, so both settle at one index. The quantity moves to this @@ -1572,12 +1570,23 @@ void synd::desyndicate(name holder, asset quantity, uint64_t max_fee) { .amount = amount, }); + // send() queues children: the transfer may re-enter desyndicate before either burn has run. + // Read outstanding only in the final child, after this request's transfer and burn finish. + action(active_of(get_self()), get_self(), emitdesynd_action, std::make_tuple(request_id)).send(); +} + +void synd::emitdesynd(uint64_t request_id) { + require_receipt_origin(get_self()); + const auto row = returns_t(get_self()).get(return_key{request_id}, outstanding_return_not_found_msg); + const auto st = liq::find_stat_by_token(LIQ_ACCOUNT, row.token_code); + check(st.has_value(), no_shadow_reason.data()); + opp::attestations::DesyndicateLIQ msg; - msg.chain_code = st.chain_code.value; - msg.user = opp::types::ChainAddress{*kind, *pubkey}; - msg.amount = opp::types::TokenAmount{st.token_code.value, static_cast(amount)}; + msg.chain_code = row.chain_code.value; + msg.user = opp::types::ChainAddress{row.chain_kind, row.pubkey}; + msg.amount = opp::types::TokenAmount{row.token_code.value, static_cast(row.amount)}; msg.request_id = request_id; - msg.total_syndicated = total_syndicated; + msg.total_syndicated = liq::outstanding_of(LIQ_ACCOUNT, *st); // `no_size{}`: raw protobuf bytes, the form the outpost decodes the attestation `data` field as // (the same encoding sysio.opreg's emitters use). std::vector encoded; @@ -1585,7 +1594,7 @@ void synd::desyndicate(name holder, asset quantity, uint64_t max_fee) { (void)out(msg); action(active_of(get_self()), MSGCH_ACCOUNT, queueout_action, - std::make_tuple(st.chain_code.value, AttestationType::ATTESTATION_TYPE_DESYNDICATE_LIQ, encoded)) + std::make_tuple(row.chain_code.value, AttestationType::ATTESTATION_TYPE_DESYNDICATE_LIQ, encoded)) .send(); } diff --git a/contracts/sysio.synd/sysio.synd.abi b/contracts/sysio.synd/sysio.synd.abi index 347ec4af6c..541dee32f9 100644 --- a/contracts/sysio.synd/sysio.synd.abi +++ b/contracts/sysio.synd/sysio.synd.abi @@ -193,6 +193,16 @@ } ] }, + { + "name": "emitdesynd", + "base": "", + "fields": [ + { + "name": "request_id", + "type": "uint64" + } + ] + }, { "name": "envelope_key", "base": "", @@ -1243,6 +1253,11 @@ "type": "dropenv", "ricardian_contract": "" }, + { + "name": "emitdesynd", + "type": "emitdesynd", + "ricardian_contract": "" + }, { "name": "finishreturn", "type": "finishreturn", diff --git a/contracts/sysio.synd/sysio.synd.wasm b/contracts/sysio.synd/sysio.synd.wasm index c29fccfb486c3c94d01de5b7abfa4761a2818c08..f11dbfcf76a615ece3b0fbf46fe7b4da5e1b3564 100755 GIT binary patch delta 13055 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 delta 12643 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 Px{nur1nrl8LZAN++x6La diff --git a/contracts/test_contracts/CMakeLists.txt b/contracts/test_contracts/CMakeLists.txt index f264d3e9db..f91afcb81e 100644 --- a/contracts/test_contracts/CMakeLists.txt +++ b/contracts/test_contracts/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(blockinfo_tester) add_subdirectory(sendinline) add_subdirectory(reenter_deposit) +add_subdirectory(reenter_desyndicate) add_subdirectory(block_transfer) add_subdirectory(epoch_stub) add_subdirectory(badtoken) diff --git a/contracts/test_contracts/reenter_desyndicate/CMakeLists.txt b/contracts/test_contracts/reenter_desyndicate/CMakeLists.txt new file mode 100644 index 0000000000..cac733ae9d --- /dev/null +++ b/contracts/test_contracts/reenter_desyndicate/CMakeLists.txt @@ -0,0 +1,4 @@ +# Like reenter_deposit, the regression consumes committed artifacts under the source tree. +if(BUILD_SYSTEM_CONTRACTS) + add_contract(reenter_desyndicate reenter_desyndicate reenter_desyndicate.cpp) +endif() diff --git a/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.abi b/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.abi new file mode 100644 index 0000000000..7c03cd8b2c --- /dev/null +++ b/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.abi @@ -0,0 +1,55 @@ +{ + "____comment": "This file was generated with sysio-abigen. DO NOT EDIT ", + "version": "sysio::abi/1.2", + "types": [], + "structs": [ + { + "name": "arm", + "base": "", + "fields": [ + { + "name": "quantity", + "type": "asset" + }, + { + "name": "max_fee", + "type": "uint64" + } + ] + }, + { + "name": "armed_request", + "base": "", + "fields": [ + { + "name": "quantity", + "type": "asset" + }, + { + "name": "max_fee", + "type": "uint64" + } + ] + } + ], + "actions": [ + { + "name": "arm", + "type": "arm", + "ricardian_contract": "" + } + ], + "tables": [ + { + "name": "armed", + "type": "armed_request", + "index_type": "i64", + "key_names": ["scope","primary_key"], + "key_types": ["name","uint64"], + "table_id": 49186 + } + ], + "ricardian_clauses": [], + "variants": [], + "action_results": [] +} \ No newline at end of file diff --git a/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.cpp b/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.cpp new file mode 100644 index 0000000000..3f2ed6a0a9 --- /dev/null +++ b/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include + +#include +#include + +namespace { +/// The real custody transfer whose holder notification exercises WIRE-441. +constexpr sysio::name synd_account{"sysio.synd"}; +constexpr sysio::name desyndicate_action{"desyndicate"}; +constexpr sysio::name active_permission{"active"}; +constexpr sysio::name armed_table{"armed"}; +} // namespace + +/// Re-enter desyndication once from the holder's outgoing LIQ transfer notification. +class [[sysio::contract]] reenter_desyndicate : public sysio::contract { +public: + using contract::contract; + + /// Amount and fee cap of the one nested request; removing the row prevents further recursion. + struct [[sysio::table("armed")]] armed_request { + sysio::asset quantity; + uint64_t max_fee; + }; + using armed_t = sysio::singleton; + + /// Arm a single callback before starting the outer desyndication directly on sysio.synd. + [[sysio::action]] void arm(sysio::asset quantity, uint64_t max_fee) { + require_auth(get_self()); + armed_t(get_self(), get_self().value).set(armed_request{quantity, max_fee}, get_self()); + } + + /// A genuine outgoing custody transfer triggers the self-authorized nested request exactly once. + [[sysio::on_notify("sysio.liq::transfer")]] + void on_transfer(sysio::name from, sysio::name to, sysio::asset quantity, std::string memo) { + if (from != get_self() || to != synd_account) return; + armed_t armed(get_self(), get_self().value); + if (!armed.exists()) return; + const auto request = armed.get(); + armed.remove(); + sysio::action(sysio::permission_level{get_self(), active_permission}, synd_account, desyndicate_action, + std::make_tuple(get_self(), request.quantity, request.max_fee)).send(); + } +}; diff --git a/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.wasm b/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.wasm new file mode 100644 index 0000000000000000000000000000000000000000..4d731c78b5cb78dad866cca1a4c90b96779bbc40 GIT binary patch literal 10555 zcmc(lUyNMWUB}No_uiR_?{4=xagwd=k~22}Y)Bw!leHTH*=uUMshcK2QB?7=p6rg} zowaw@vtuXb!RsOu8HiMac%bkSMM$JVMM5e9+6UYg5L8Hj1X79+3aB6|^&zSXiU28m zzQ1#4ch+$nwSB;U=H7emIluq^XIyXNY~q|tUQa%F(z%l^**xhtH{Ho}b8|D^^O;<7 zCleOio0nL&7p-{9OP5?a*|FvLrrq4zX*;O9OWXmI&wTR|P~6FkCqr~ehF zTOuq`;ZC;n&WO4M82|fzciLaQQ1%u_%WJC(OTAHVVPp9h`YzG()W*e)<+X+0#zucQ za$ZZVv;DIh{gF%U*s$MQT9`QNvhkk9^^0-O)Ubc<{PM8B&^tdm?Q*@(d(W={&*^@8 z<-)@9Xkn?pF&eI2bS*p2+6&-l>tQNxSOnFm?>bs47tare{ngRJu)o+}zR(}K+s4O4 znjPB}kBLCHOK*#f{_4`g^6Hu8)qZ59*|OSyY2*rR>2Rh$>}~WT<+~@jUszmQ>bt4* zNbROuZhE@aYGtkb16ekc&G-*wWj5_wzBRLZcRIcM_L)2O+SHF3* zN`o8JGlP2lT;=BcH;;1A*X1a<6FSa)GSL2zaMMi+UMKUe-aM|iqPa|pkQUw*b4jv! zFE~~z-pu(a4uf~$i_`%x^+kqW>rMbrV0If}*^=a??rex<>q4 zM=O7HIthNab5pJ;f-73UDesJzJL2W;B6nrW6<{QeEcbx%jgDp}kv=hX>_7dU|jW@b9A2{W%$`Ep}5RVTkGuDhO!cVA>H4fZ`CZq)mJPR2bSk zX1xQbzZp<(s*DhKz;VT|Cf;u*mmv5$Doh0wv6MON93E6zBVrlBgHh6wQa0xwmXz38 zkeDc!y>3a?A4*GhCgTMYWXQ{ky$=sUhDyqNxksglZvF@5<|;u;!hbNblGb5> zt6kQgYoGtlBjp2Ti|GUIfYb(EbACUPwSAt;{z3T@@{Hz9T2FBvJG3`H8PWs3RQ&ij zhh5p0GqQBwomI7wPee+ zaH!0jD`%^*k~e0U<5rm{_aPsh97p( z(1mPoS6aOx3Z%gTih>312pC`fh@THW#$~Aoopn$Six|<<1Zyo~1D}qIV|8R&&SF%! zmdGAHpCv6>X0UJDig5Luw4`t<#?v+l3@PMBs^>Nu#WygdTe~*IoQVS6&WGhv! z9N-%}*hMHY?I%R^4krk5$*|j!QD73osvR>zOPff5T-ufq*c)1NzAO-9D`fDhfZtL^ z;E_v}h18VQvRnXtO`;N$NDwtl=?}g=CF| zApDNWIgvcd@@}*88|3kRQ2xJYMk7YE3?^z*G9$0e`C04(SNDV{1>Kn#_sb8u;=7&- z0aKCV=gOxRXhZUII!ZA6)_4El`j_4~uE)H-ihi0zm`e|rlaNP$&gBy8!7it{pkw?# zum1xV+K+C5YY(Nx!dza_EF5gEKW2z=pIpXN>NO7Dg!25z<=4OQm4Eu;f4}i{^EsZ! zlqr=O+76feVEVL@fZfR_>$1GFYJ&0~e))4Bj>!#L#m;qzph_E5ZH0=weryopo$9i~ zEF2K=TswpE$L+DaUyK~^AFC{W<`zL>wIuoS=5X^yARZ%bv)a=7)ug;vz7ZYt#VB+N zM5S#nW1b4SnHrQ&St6!fERTb1GJ{(&rVk39l`5emqwd_9&hZ`sI=LJzlfi>3)`<*$ ze5+hYz?ODeU^GWsRvhzB|!a7a;|pAix*?&8`B?>ooP=&tYg%D>uXjO z!&>=JHBQWuxq)o7tHRX0H9kWC=9}$Ebg>30UQHqcq+DYFd)~=|h$rEyMizfw(J84n zEmnxFAWbK_yI&&b4iF`WL=tbxIPO!k$RQEbUCiNle90?T`#-D1WyL@K7Gb zA*GoZ6uEQiH53-Ppf7A785jDN@?Ohr$&0rkxPzru6D`g^(KX}^*2R5vtygrHm_|e z0c=}aQQOb^`TDiX*dVGjpmnMCs&-|k-uwhQF^OR73Lgk2ss>`vi(;)2<+q5!E+FbA z*wl2Xr${ETsnTwcmbaA;T79p8$41F#Y?J?Ty|Kxpd@D7<-?o>(JQ`6bv!L+DJJXdUdr2u9?)3r4fZqcH)TJ-@X<%p7*Oq^ zW`^a)E@X<_!nm|lPT8{5HdQ{N0x{-C^E%mba2Sm!!cnKHgoYIWU;1j(KutA6fXFm& zzA^bGB*ou(yv|3jd$lnv^)c^nSXaI>w$b{JxP7eK4iuJVU5y!8#F{WGt9B+#4?NMf zTq@hThHa7VCT$Dn8xZDhW?S!fX2P~)Qxf+TuH31KzN#ex6^+MEyMl;lSK1+U*HnVh zuKq1D+*VweMew}7;$t{-(HdSgYxvhD12pDfrMN0oaXv?NYu&R`+SW?_=5?vt3h_Gz z_XWwn;LREUq$Hksql>(1vaQkdrlQ(NmK6}VO#__Q!dIT#$9jx5q(uE<-`e6+^klwjMB zI$Tk$m@7`8zJ^Bx!BNa0rc61Z$QTcg1XpxvILHg6QT`NAxTP~jD4pW>G7Xgz{<>oo z74{Ug`)P-bs~oM;2i(+jViLg#J2rW$Vpp$?Vku56v`@Y~HDH<~3GiBO8!Seoal}Mq zw-ZL9D&(~5*k{y|5lw4jEL|L!W+#@@l30N2WBb$qi+wH~-ivR^Em9^nnLEG*c0s7t zs3KyF;)$dhP?-tjcNB#LNd6K0@!F3_HYT=)9xF<0dVUi|n-O(nRIw)QCXERiqdU-U z@z;%0)d|+5BjZk`&se!85h4y$+KC$&GvN!fSYWspzF+Rpxtt)14T-s9lAOsAk#uE> zPJz29enmmZRLeN33Cq@`pkFLT21=-#C>Ecs@_m*zWH4FU7(PkecSRbIPM1r@IQV$X1~Uqph=5pTLG$RVr2s)XPhhR;V%a)nShnTbOgwnnxkZrY+?`FCY z@p~SLb&T2;#V`V4q>?s}9+EUw-quaVET&CwU@R&Y#u2vmy_3|R$x6T166{SO@%oma zlyy^pU^l+zF!`fV{#)fc1HmmW6I?IPi##ehFiTC z(}yTXXHM}u#*08u4zSE#XebPm9JDSypRd|<|n6RD6~2nppgYsBZ7gFjR{YkjH;Z1fd41< z)$j19O*GUsePZ9Z&A!tLMrB6X1JM;Qkk4I<;+r}BHfb8ptC%&1S1e|i8ZNmUQ_GKo z7!%v0kFp{Jvo0swV^z#lEHCs`6WgU}M$oR@=ZbsK765_M$#kVW8o#FwslK8%tT2%k#V;%KutibthSQ9A*aBFjq?sO`f=B8b zQ1(hB!lMQymJUr;6$Q#n+SM`j#FBz_25E79O5T)2fkxEmrC73f%nrfDHk7de!(I8L z)koY1YeyOX0DXUztaP1{`pOq9O*#o5YL2|)p6desNLvHCB&q?2gYDu&2D5w|Scp*< zz@Om4_l%z`mbNCp%6+ItQsKj`)Y*BdpGE2MQ#Qttp@E$}rb?^;YJjP7U%FELs|}Xp zIoPDW8^+obNN&U+nVC}fVtRXW|9O8M0YW0pvATbkE}^iZ037RiEaXv= z@skeW_BedXe~^QO2nQO%3E}7;6^@!sc-;Mt9vi;AK-w=f@<^!W`BgRw=@3Q;mGW?J zGhNNPiD$8}b;T;iRqnR{23!KL5XM}6`SN!j(bq!>8u}1!-HZnHtfWp z|L1GtFO%%>AOFKI{ms8Wsl)n;rg>Z5R}@pO+zq{ixAN(tcq_+iE?;-WKj)`<%V+va zVYC){XU?oG@)M}A-g}|H;q*%_elpeXoejOwsDE~S6u0RIPi)y3h0*DL;3q@w#B*V| z_F`B(-CKQuJxk|@%d0PhUa+4=xy9b<>e@&rHnck|kN5@DXl*EzXV;dNPhA{uiBDne z`9Xhiv=Nq9!>ITCnSNM16?%X;-5-Xc4aY0w{yr(dpiZz2VFnR1ZV{rR9y$dvpEb5W07=z0@D}Pw{_sQS2_SUg(`!-ezx% z^ZZ61>bJS>or3?MAA#8^_=K48=J*@5?@0nLE{{%|CN_F!A#k{Swl};OR{9swrxb8; z%$e|=>5tY{!_rzmO38R0Jmz%@%ZgO%2Y$G2k-2-F`#&a3q)7k( literal 0 HcmV?d00001 diff --git a/contracts/tests/contracts.hpp.in b/contracts/tests/contracts.hpp.in index be96caed8e..6e60c20ae6 100644 --- a/contracts/tests/contracts.hpp.in +++ b/contracts/tests/contracts.hpp.in @@ -57,6 +57,14 @@ struct contracts { static std::vector reject_all_wasm() { return read_wasm("${CMAKE_SOURCE_DIR}/contracts/test_contracts/reject_all.wasm"); } static std::vector reenter_deposit_wasm() { return read_wasm("${CMAKE_SOURCE_DIR}/contracts/test_contracts/reenter_deposit/reenter_deposit.wasm"); } static std::vector reenter_deposit_abi() { return read_abi("${CMAKE_SOURCE_DIR}/contracts/test_contracts/reenter_deposit/reenter_deposit.abi"); } + /// One-shot holder callback for the desyndication custody-total regression. + static std::vector reenter_desyndicate_wasm() { + return read_wasm("${CMAKE_SOURCE_DIR}/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.wasm"); + } + /// ABI paired with the committed reentrant holder WASM. + static std::vector reenter_desyndicate_abi() { + return read_abi("${CMAKE_SOURCE_DIR}/contracts/test_contracts/reenter_desyndicate/reenter_desyndicate.abi"); + } static std::vector block_transfer_wasm() { return read_wasm("${CMAKE_SOURCE_DIR}/contracts/test_contracts/block_transfer/block_transfer.wasm"); } static std::vector block_transfer_abi() { return read_abi("${CMAKE_SOURCE_DIR}/contracts/test_contracts/block_transfer/block_transfer.abi"); } static std::vector epoch_stub_wasm() { return read_wasm("${CMAKE_SOURCE_DIR}/contracts/test_contracts/epoch_stub/epoch_stub.wasm"); } diff --git a/contracts/tests/sysio.synd_tests.cpp b/contracts/tests/sysio.synd_tests.cpp index 8078a5645c..4f9c209cb3 100644 --- a/contracts/tests/sysio.synd_tests.cpp +++ b/contracts/tests/sysio.synd_tests.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -1663,6 +1664,128 @@ sysio::opp::attestations::DesyndicateLIQ queued_desyndication(sysio_synd_tester& return msg; } +/// An ordinary, unprivileged holder contract may desyndicate once from its own LIQ notification. +struct reentrant_synd_tester : sysio_synd_tester { + abi_serializer holder_abi_ser; + + /// Give alice only her own code authority; sysio.synd is called directly at transaction depth zero. + reentrant_synd_tester() { + constexpr auto holder = "alice"_n; + deploy(holder, contracts::util::reenter_desyndicate_wasm(), contracts::util::reenter_desyndicate_abi(), + holder_abi_ser, false); + authority auth(get_public_key(holder, "active")); + auth.accounts.push_back(permission_level_weight{{holder, config::sysio_code_name}, 1}); + set_authority(holder, config::active_name, auth, config::owner_name); + produce_blocks(); + BOOST_REQUIRE_EQUAL(config::default_max_inline_action_depth, + control->get_global_properties().configuration.max_inline_action_depth); + } + + /// Arm exactly one nested request without adding a wrapper action around the outer request. + action_result arm(uint64_t quantity, uint64_t max_fee = std::numeric_limits::max()) { + return push("alice"_n, holder_abi_ser, "alice"_n, "arm"_n, + mvo()("quantity", asset(static_cast(quantity), LIQSOL_SYM))("max_fee", max_fee)); + } +}; + +/// Each FIFO message snapshots live supply after its own burn, including parked yield and retained fees. +BOOST_AUTO_TEST_CASE(desyndicate_reentry_emits_nested_then_outer_live_outstanding) try { + for (const uint32_t fee_bps : {0u, 250u}) { + reentrant_synd_tester t; + constexpr uint64_t initial = 100 * t.UNIT; + constexpr uint64_t parked = 7 * t.UNIT; + constexpr uint64_t outer = 40 * t.UNIT; + constexpr uint64_t nested = 20 * t.UNIT; + const uint64_t outer_fee = outer * fee_bps / BPS_DENOMINATOR; + const uint64_t nested_fee = nested * fee_bps / BPS_DENOMINATOR; + const uint64_t outer_net = outer - outer_fee; + const uint64_t nested_net = nested - nested_fee; + BOOST_REQUIRE_EQUAL(t.success(), t.liq_mint("alice"_n, t.LIQSOL, initial)); + BOOST_REQUIRE_EQUAL(t.success(), t.liq_mintyield(t.SOLANA, t.LIQSOL, parked)); + BOOST_REQUIRE_EQUAL(t.success(), t.link_svm("alice"_n, t.ed_signer())); + BOOST_REQUIRE_EQUAL(t.success(), t.setconfig(t.SOLANA, t.LIQSOL, + {.desynd_fee_bps = fee_bps, .desynd_burst = initial})); + BOOST_REQUIRE_EQUAL(t.success(), t.arm(nested, nested_fee)); + BOOST_REQUIRE_EQUAL(t.success(), t.desyndicate("alice"_n, outer, t.LIQSOL_SYM, name{}, outer_fee)); + + const auto inner_msg = queued_desyndication(t, 1); + const auto outer_msg = queued_desyndication(t, 2); + BOOST_REQUIRE_EQUAL(2u, inner_msg.request_id()); + BOOST_REQUIRE_EQUAL(1u, outer_msg.request_id()); + BOOST_REQUIRE_EQUAL(nested_net, inner_msg.amount().amount()); + BOOST_REQUIRE_EQUAL(outer_net, outer_msg.amount().amount()); + BOOST_REQUIRE_EQUAL(initial + parked - nested_net, inner_msg.total_syndicated()); + BOOST_REQUIRE_EQUAL(initial + parked - nested_net - outer_net, outer_msg.total_syndicated()); + BOOST_REQUIRE_EQUAL(initial - nested_net - outer_net, t.supply()); + BOOST_REQUIRE_EQUAL(parked, t.pending()); + BOOST_REQUIRE_EQUAL(initial - outer - nested, t.liq_balance("alice"_n)); + BOOST_REQUIRE_EQUAL(outer_fee + nested_fee, t.feepot_balance(t.LIQSOL)); + BOOST_REQUIRE_EQUAL(outer_fee + nested_fee, t.liq_balance(t.SYND_ACCOUNT)); + BOOST_REQUIRE_EQUAL(initial - outer - nested, t.desynd_bucket_row(t.SOLANA, t.LIQSOL)["level"].as_uint64()); + BOOST_REQUIRE_EQUAL(outer_net, t.return_row(1)["amount"].as_uint64()); + BOOST_REQUIRE_EQUAL(nested_net, t.return_row(2)["amount"].as_uint64()); + BOOST_REQUIRE_EQUAL("alice", t.return_row(1)["holder"].as_string()); + BOOST_REQUIRE_EQUAL("alice", t.return_row(2)["holder"].as_string()); + BOOST_REQUIRE_EQUAL(3u, t.counters_row()["next_request_id"].as_uint64()); + BOOST_REQUIRE(t.attestation_row(3).is_null()); + BOOST_REQUIRE_EQUAL(inner_msg.user().SerializeAsString(), outer_msg.user().SerializeAsString()); + + // Receipt global sequences reflect execution order; action-trace slots reflect scheduling order. + std::vector> ordered; + for (const auto& trace : t.last_trace->action_traces) { + if (trace.receiver == trace.act.account && + (trace.act.name == "burn"_n || trace.act.name == "emitdesynd"_n || trace.act.name == "queueout"_n)) { + BOOST_REQUIRE(trace.receipt.has_value()); + ordered.emplace_back(trace.receipt->global_sequence, trace.act.name); + } + } + std::sort(ordered.begin(), ordered.end()); + const std::vector expected{"burn"_n, "emitdesynd"_n, "queueout"_n, + "burn"_n, "emitdesynd"_n, "queueout"_n}; + BOOST_REQUIRE_EQUAL(ordered.size(), expected.size()); + for (size_t i = 0; i < expected.size(); ++i) BOOST_REQUIRE_EQUAL(expected[i], ordered[i].second); + + // The arm was consumed: a later ordinary request adds exactly one attestation. + BOOST_REQUIRE_EQUAL(t.success(), t.desyndicate("alice"_n, t.UNIT)); + BOOST_REQUIRE_EQUAL(3u, queued_desyndication(t, 3).request_id()); + BOOST_REQUIRE(t.attestation_row(4).is_null()); + } +} FC_LOG_AND_RETHROW() + +/// A nested refusal rolls back the outer transfer, fee, bucket debit, return and request allocation. +BOOST_FIXTURE_TEST_CASE(desyndicate_reentry_refusal_rolls_back_every_request, reentrant_synd_tester) try { + constexpr uint64_t initial = 100 * UNIT; + constexpr uint64_t outer = 40 * UNIT; + constexpr uint64_t nested = 20 * UNIT; + BOOST_REQUIRE_EQUAL(success(), liq_mint("alice"_n, LIQSOL, initial)); + BOOST_REQUIRE_EQUAL(success(), link_svm("alice"_n, ed_signer())); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {.desynd_fee_bps = 250, .desynd_burst = outer})); + BOOST_REQUIRE_EQUAL(success(), arm(nested)); + const auto before = desynd_state(); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("desyndication exceeds the current budget"), desyndicate("alice"_n, outer)); + BOOST_REQUIRE(before == desynd_state()); + BOOST_REQUIRE(return_row(1).is_null()); + BOOST_REQUIRE(attestation_row(1).is_null()); +} FC_LOG_AND_RETHROW() + +/// Neither a holder signature nor a direct system-contract signature can forge or replay an emission. +BOOST_FIXTURE_TEST_CASE(desyndicate_emission_requires_self_inline_origin, sysio_synd_tester) try { + BOOST_REQUIRE_EQUAL(success(), liq_mint("alice"_n, LIQSOL, 100 * UNIT)); + BOOST_REQUIRE_EQUAL(success(), link_svm("alice"_n, ed_signer())); + BOOST_REQUIRE_EQUAL(success(), setconfig(SOLANA, LIQSOL, {})); + BOOST_REQUIRE_EQUAL(success(), desyndicate("alice"_n, UNIT)); + const auto before = desynd_state(); + for (const uint64_t request_id : {uint64_t{0}, uint64_t{1}, std::numeric_limits::max()}) { + BOOST_REQUIRE(mentions(push(SYND_ACCOUNT, synd_abi_ser, "alice"_n, "emitdesynd"_n, + mvo()("request_id", request_id)), "missing authority of sysio.synd")); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("syndication receipts require a self inline action"), + push(SYND_ACCOUNT, synd_abi_ser, SYND_ACCOUNT, "emitdesynd"_n, + mvo()("request_id", request_id))); + BOOST_REQUIRE(before == desynd_state()); + } + BOOST_REQUIRE(attestation_row(2).is_null()); +} FC_LOG_AND_RETHROW() + // Review Focus 6: a DESYNDICATE_LIQ carries the depot's outstanding shadow after its own burn -- the // supply plus the yield parked in liqpending, net of the burned amount -- and `desyndlog` records the // same total. The fee stays outstanding: it is transferred into the fee pot, not burned. The carried