Skip to content

fix(WrappedM): address remaining ChainSecurity audit issues#134

Merged
PierrickGT merged 4 commits into
v2from
proto-969-fix-remaining-chainsecurity-issues
Jun 24, 2026
Merged

fix(WrappedM): address remaining ChainSecurity audit issues#134
PierrickGT merged 4 commits into
v2from
proto-969-fix-remaining-chainsecurity-issues

Conversation

@PierrickGT

Copy link
Copy Markdown
Member

Issues:

  • 010 - fix(WrappedM): prevent frozen claim recipient from blocking stopEarningFor
    An earner could point their claim recipient at a frozen address so that the permissionless stopEarningFor() reverts inside _claim() -> _transfer() -> _revertIfFrozen(recipient_), keeping the account earning indefinitely and defeating the deauthorization mechanism.
    Both stopEarningFor() overloads now fall back to skipTransfer when the resolved claim recipient is frozen, mirroring the _beforeFreeze path: the yield is crystallized onto the account's own balance instead of being routed to the frozen recipient, so deauthorization always succeeds.
  • 016 - fix(WrappedM): enforce sorted unique earners in migrator list
    The WrappedMTokenMigratorV1 migration corrupts accounting if an address appears twice in the earners array, since the first pass overwrites the lastIndex slot with the computed principal.
    ListOfEarnersToMigrate now requires strictly ascending addresses, enforcing sorted order, uniqueness and non-zero.

Improvement:

…ngFor

An earner could point their claim recipient at a frozen address so that the
permissionless stopEarningFor() reverts inside _claim() -> _transfer() ->
_revertIfFrozen(recipient_), keeping the account earning indefinitely and
defeating the deauthorization mechanism.

Both stopEarningFor() overloads now fall back to skipTransfer when the resolved
claim recipient is frozen, mirroring the _beforeFreeze path: the yield is
crystallized onto the account's own balance instead of being routed to the
frozen recipient, so deauthorization always succeeds.
The WrappedMTokenMigratorV1 migration corrupts accounting if an address
appears twice in the earners array, since the first pass overwrites the
lastIndex slot with the computed principal. ListOfEarnersToMigrate now
requires strictly ascending addresses, enforcing sorted order, uniqueness
and non-zero in a single O(n) pass without the gas cost of an O(n^2) check.

Integration test fixtures sort the in-memory earners copy before deploying
the migrator, mirroring the production generation script.
Make the earners list generation safe to rely on for the v1->v2 migration:

- Paginate holder fetches so earners are never silently truncated by the
  `first` cap, with a max-pages guard against a broken `skip`.
- Build the whole file in memory and write once after every network
  succeeds; abort without writing on any error, and exit non-zero, so a
  partial run never mixes fresh and stale lists.
- Throw on duplicate or zero earner addresses instead of relying solely on
  the on-chain check.
- Fix the GraphQL response typing (drop the unsound cast), add a request
  timeout, centralize the chain enum, and drop the unused balance field.
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

LCOV of commit b873e34 during Forge Coverage #654

Summary coverage rate:
  lines......: 99.4% (327 of 329 lines)
  functions..: 100.0% (68 of 68 functions)
  branches...: 87.5% (7 of 8 branches)

Files changed coverage rate:
                                 |Lines       |Functions  |Branches    
  Filename                       |Rate     Num|Rate    Num|Rate     Num
  =====================================================================
  src/ListOfEarnersToMigrate.sol |22.2%      9| 0.0%     2|    -      0
  src/WrappedMToken.sol          |21.9%    270| 0.0%    59|    -      0

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

Changes to gas cost

Generated at commit: a29a3c1138744919cd0db8b22c69f3d31a92fbd9, compared to commit: aa6dc9d2e8a7b5df7d583964253ae68190761ce2

🧾 Summary (20% most significant diffs)

Contract Method Avg (+/-) %
WrappedMTokenHarness stopEarningFor(address[])
transferFrom
unwrap
+38,205 ❌
-7,049 ✅
-1,164 ✅
+100.22%
-10.89%
-3.62%
MockSwapFacility swapOutM -7,254 ✅ -5.93%

Full diff report 👇
Contract Deployment Cost (+/-) Method Min (+/-) % Avg (+/-) % Median (+/-) % Max (+/-) % # Calls (+/-)
WrappedMTokenHarness 5,689,329 (+13,846) accruedYieldOf
approve
claimExcess
claimFor
claimRecipientFor
currentIndex
excess
freeze
initialize
projectedEarningSupply
setAccountOf(address,uint256)
setAccountOf(address,uint256,uint256,bool)
setEnableMIndex
setTotalEarningPrincipal
setTotalEarningSupply
setTotalNonEarningSupply
startEarningFor(address)
stopEarningFor(address)
stopEarningFor(address[])
totalAccruedYield
transfer
transferFrom
unwrap
wrap
2,722 (0)
2,754 (0)
2,456 (0)
2,561 (0)
4,891 (0)
2,645 (0)
14,973 (0)
31,270 (0)
24,398 (0)
7,252 (0)
5,190 (0)
8,179 (0)
5,339 (0)
2,563 (0)
2,551 (0)
2,595 (0)
2,564 (0)
2,698 (0)
31,112 (+5,751)
7,298 (0)
5,045 (0)
6,014 (0)
503 (0)
525 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
+22.68%
0.00%
0.00%
0.00%
0.00%
0.00%
7,015 (-49)
27,535 (-939)
27,519 (-992)
35,214 (+2,308)
6,013 (-225)
5,604 (-17)
16,473 (-84)
39,506 (-88)
168,992 (+29)
9,860 (-163)
24,652 (+51)
43,881 (-133)
6,755 (+29)
21,297 (-57)
20,589 (-198)
21,783 (-33)
94,956 (+52)
70,254 (+2,145)
76,325 (+38,205)
10,638 (+87)
30,409 (-198)
57,688 (-7,049)
31,015 (-1,164)
43,425 (-1,505)
-0.69%
-3.30%
-3.48%
+7.01%
-3.61%
-0.30%
-0.51%
-0.22%
+0.02%
-1.63%
+0.21%
-0.30%
+0.43%
-0.27%
-0.95%
-0.15%
+0.05%
+3.15%
+100.22%
+0.82%
-0.65%
-10.89%
-3.62%
-3.35%
7,659 (0)
29,139 (0)
20,198 (0)
15,076 (0)
4,891 (0)
8,260 (0)
17,834 (0)
31,270 (0)
172,873 (0)
7,252 (-5,488)
25,090 (0)
45,179 (0)
5,339 (0)
22,463 (0)
22,451 (0)
22,495 (0)
100,112 (0)
70,436 (+2,054)
58,378 (+20,258)
12,773 (0)
22,576 (0)
50,759 (-15,211)
35,498 (+4,800)
41,769 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
-43.08%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
+3.00%
+53.14%
0.00%
0.00%
-23.06%
+15.64%
0.00%
13,164 (0)
29,139 (0)
50,960 (0)
97,256 (0)
8,266 (0)
8,260 (0)
17,961 (0)
83,923 (0)
172,873 (0)
12,740 (0)
25,090 (0)
45,179 (0)
22,439 (0)
22,463 (0)
22,451 (0)
22,495 (0)
100,112 (0)
75,412 (+1,542)
139,486 (+88,607)
12,913 (0)
100,918 (+16,916)
89,470 (0)
35,498 (0)
75,969 (0)
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
0.00%
+2.09%
+174.15%
0.00%
+20.14%
0.00%
0.00%
0.00%
343 (0)
106 (0)
102 (0)
107 (0)
6 (+1)
2,158 (-24)
107 (0)
32 (+1)
135 (+1)
101 (0)
618 (+24)
364 (-22)
495 (+2)
550 (-23)
352 (-23)
700 (+24)
108 (0)
107 (0)
3 (+1)
127 (0)
215 (0)
104 (0)
94 (-2)
106 (0)
ListOfEarnersToMigrate 254,186 (+254,186) getEarners 2,617 (0) 0.00% 59,928 (-12,925) -17.74% 81,552 (0) 0.00% 81,552 (0) 0.00% 27 (+6)
MockRegistrar 180,239 (0) get 405 (0) 0.00% 2,007 (-376) -15.78% 2,405 (0) 0.00% 2,405 (0) 0.00% 483 (+112)
MockSwapFacility 441,322 (0) swapInM
swapOutM
48,557 (-14,193)
35,887 (0)
-22.62%
0.00%
84,290 (-437)
115,153 (-7,254)
-0.52%
-5.93%
82,825 (0)
115,515 (-6,746)
0.00%
-5.52%
151,225 (0)
142,759 (-9)
0.00%
-0.01%
309 (0)
108 (0)
WrappedMToken 5,296,761 (+13,822) stopEarningFor
transfer
transferFrom
58,175 (+1,542)
33,891 (0)
39,403 (0)
+2.72%
0.00%
0.00%
58,989 (+1,680)
59,995 (-763)
49,538 (-271)
+2.93%
-1.26%
-0.54%
58,989 (+1,680)
67,374 (0)
55,602 (0)
+2.93%
0.00%
0.00%
59,804 (+1,818)
83,913 (0)
72,886 (0)
+3.14%
0.00%
0.00%
2 (0)
327 (0)
227 (0)
MockM 522,934 (0) balanceOf
setBalanceOf
setCurrentIndex
setPrincipalBalanceOf
593 (0)
24,181 (0)
26,568 (0)
24,239 (0)
0.00%
0.00%
0.00%
0.00%
2,368 (+15)
43,416 (-42)
43,374 (+1)
43,818 (+399)
+0.64%
-0.10%
+0.00%
+0.92%
2,593 (0)
44,141 (+12)
43,692 (0)
44,187 (0)
0.00%
+0.03%
0.00%
0.00%
2,593 (0)
44,261 (0)
43,692 (0)
44,307 (0)
0.00%
0.00%
0.00%
0.00%
818 (-9)
427 (0)
866 (+1)
202 (0)
Proxy 104,329 (0) fallback 5,029 (0) 0.00% 27,345 (+66) +0.24% 13,047 (0) 0.00% 200,947 (0) 0.00% 12,705 (-79)
WrappedMTokenMigratorV1 1,624,603 (+8,049)
Foo 216,100 (-12)

* fix(deploy): sort earners before migrator construction

* refactor(test): pass earners storage array directly to migrator deploy

Drop the manual storage-to-memory copy loops in the integration test
fixtures; assigning or passing the storage array to a memory parameter
already performs the copy.

---------

Co-authored-by: Pierrick Turelier <pierrick@turelier.com>
const REQUEST_TIMEOUT_MS = 30_000;

const NETWORKS = [
"arbitrum",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the final list? Some networks like Monad are missing — is it due to a lack of wM earners on them?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's not the final one.

@toninorair toninorair left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm 👍

@PierrickGT PierrickGT merged commit 4716596 into v2 Jun 24, 2026
5 checks passed
@PierrickGT PierrickGT deleted the proto-969-fix-remaining-chainsecurity-issues branch June 24, 2026 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants